有时,当你在处理几个不同的字典时,你需要把它们作为一个单一的字典来分组和管理。在其他情况下,你可以有代表不同范围或上下文的多个字典,并需要将它们作为一个单一的字典来处理,允许你按照给定的顺序或优先级访问底层数据。在这些情况下,你可以利用 Python 的 ChainMap collections 模块的优势。
ChainMap 将多个字典和映射分组在一个具有类似字典行为的、可更新的视图中。此外,ChainMap 提供的功能允许你有效地管理各种字典,定义关键的查询优先级,以及更多。
在本教程中,你将学习如何。
- 在你的 Python 程序中创建**
ChainMap实例** - 探索
ChainMap和 之间的差异dict - 使用
ChainMap,将几个字典作为一个整体来处理 - 管理钥匙查询的优先级
ChainMap
为了从本教程中获得最大的收获,你应该知道在 Python 中使用字典和列表的基本知识。
在旅程结束时,你会发现一些实际的例子,这些例子将帮助你更好地理解ChainMap 的最相关的功能和用例。
开始使用Python的ChainMap
Python的 ChainMap在Python 3.3中被添加到collections ,作为管理多个作用域和上下文的一个方便工具。这个类允许你把几个字典和其他映射组合在一起,使它们在逻辑上表现为一个整体。它创建了一个单一的可更新的视图,其工作方式类似于普通的字典,但有一些内部差异。
ChainMap 并不将其映射合并在一起。相反,它把它们保存在一个内部的映射列表中。然后ChainMap 在这个列表上重新实现常见的字典操作。由于内部列表持有对原始输入映射的引用,这些映射的任何变化都会影响整个ChainMap 对象。
将输入映射存储在一个列表中,允许你在一个给定的链映射中拥有重复的键。如果你执行一个键的查找,那么ChainMap ,搜索映射的列表,直到找到目标键的第一次出现。如果键丢失,那么你会得到一个 KeyError和平常一样。
当你需要管理嵌套的作用域时,将映射存储在一个列表中,其中每个映射代表一个特定的作用域或上下文,这才是真正的闪光点。
为了更好地理解作用域和上下文是什么,想想 Python 是如何解析名字的。当 Python 寻找一个名字时,它在下面搜索 locals(), globals(),最后是 builtins直到找到目标名字的第一次出现。如果这个名字不存在,那么你会得到一个NameError 。处理作用域和上下文是最常见的一种问题,你可以用ChainMap 来解决。
当你使用ChainMap ,你可以把几个键不相交或相交的字典连在一起。
在第一种情况下,ChainMap 允许你把所有的字典当作一个。所以,你可以访问键值对,就像你在处理一个字典一样。在第二种情况下,除了把你的字典作为一个整体来管理外,你还可以利用内部的映射列表,为你的字典中的重复键定义某种访问优先级。这就是为什么ChainMap 对象很适合处理多语境。
ChainMap 的一个奇怪的行为是,突变,如更新、添加、删除、清除和弹出键,只作用于内部映射列表中的第一个映射。下面是对ChainMap 的主要特点的总结。
- 从几个输入映射建立一个可更新的视图
- 提供与字典几乎相同的接口,但有一些额外的功能
- 不合并输入映射,而是将它们保存在一个内部公共列表中
- 可以看到输入映射中的外部变化
- 可以包含具有不同值的重复键
- 在内部映射列表中按顺序搜索按键
- 当搜索整个映射列表后缺少一个键时,抛出一个
KeyError。 - 只对内部列表中的第一个映射进行突变
在本教程中,你将了解到更多关于ChainMap 的所有这些很酷的功能。下面的部分将指导你如何在你的代码中创建新的ChainMap 实例。
实例化ChainMap
要在你的Python代码中创建ChainMap ,你首先需要从collections 中导入这个类,然后像往常一样调用它。类的初始化器可以接受零个或多个映射作为参数。在没有参数的情况下,它初始化一个链图,里面是一个空的字典。
>>>
>>> from collections import ChainMap
>>> from collections import OrderedDict, defaultdict
>>> # Use no arguments
>>> ChainMap()
ChainMap({})
>>> # Use regular dictionaries
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> ChainMap(numbers, letters)
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> ChainMap(numbers, {"a": "A", "b": "B"})
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> # Use other mappings
>>> numbers = OrderedDict(one=1, two=2)
>>> letters = defaultdict(str, {"a": "A", "b": "B"})
>>> ChainMap(numbers, letters)
ChainMap(
OrderedDict([('one', 1), ('two', 2)]),
defaultdict(<class 'str'>, {'a': 'A', 'b': 'B'})
)
这里,你使用不同的映射组合创建了几个ChainMap 对象。在每种情况下,ChainMap 都会返回所有输入映射的一个类似字典的视图。注意,你可以使用任何类型的映射,如 OrderedDict和 defaultdict.
你也可以使用类方法 .fromkeys() 创建ChainMap 对象。这个方法可以接受一个可迭代的键和所有键的可选缺省值。
>>>
>>> from collections import ChainMap
>>> ChainMap.fromkeys(["one", "two","three"])
ChainMap({'one': None, 'two': None, 'three': None})
>>> ChainMap.fromkeys(["one", "two","three"], 0)
ChainMap({'one': 0, 'two': 0, 'three': 0})
如果你在ChainMap 上调用.fromkeys() ,用一个键的迭代表作为参数,那么你会得到一个带有单个字典的链式映射。键来自于输入的迭代器,而值则默认为 None.另外,你可以给.fromkeys() 传递第二个参数,为每个键提供一个合理的默认值。
运行类似字典的操作
ChainMap 支持与普通字典相同的 API 来访问现有的键。一旦你有了一个ChainMap 对象,你就可以用字典式的键查找来检索现有的键,或者你可以使用 .get():
>>>
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_num = ChainMap(numbers, letters)
>>> alpha_num["two"]
2
>>> alpha_num.get("a")
'A'
>>> alpha_num["three"]
Traceback (most recent call last):
...
KeyError: 'three'
一个键的查找会搜索目标链图中的所有映射,直到找到所需的键。如果该键不存在,那么你会得到通常的KeyError 。现在,当你有重复的键时,查找操作是如何表现的?在这种情况下,你得到的是目标键的第一次出现。
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets["dogs"]
10
>>> pets.get("cats")
7
>>> pets["turtles"]
1
当你访问一个重复的键,比如"dogs" 和"cats" ,链图只返回该键的第一次出现。在内部,查找操作是按照输入映射在内部映射列表中出现的顺序来搜索的,这也是你把它们传入类的初始化器的确切顺序。
这个一般的行为也适用于迭代。
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> for key, value in pets.items():
... print(key, "->", value)
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3
for 循环遍历pets 中的字典,并打印出每个键值对的第一次出现。你也可以直接迭代字典,或者用 .keys()和 .values()来迭代,正如你对任何字典所做的那样。
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "cats": 3, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> for key in pets:
... print(key, "->", pets[key])
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3
>>> for key in pets.keys():
... print(key, "->", pets[key])
...
dogs -> 10
cats -> 7
turtles -> 1
pythons -> 3
>>> for value in pets.values():
... print(value)
...
10
7
1
3
同样,行为也是一样的。每次迭代都要经过底层链图中每个键、项和值的第一次出现。
ChainMap 也支持突变。换句话说,它允许你更新、添加、删除和弹出键-值对。在这种情况下的区别是,这些操作只作用于第一个映射。
>>>
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_num = ChainMap(numbers, letters)
>>> alpha_num
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> # Add a new key-value pair
>>> alpha_num["c"] = "C"
>>> alpha_num
ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'})
>>> # Update an existing key
>>> alpha_num["b"] = "b"
>>> alpha_num
ChainMap({'one': 1, 'two': 2, 'c': 'C', 'b': 'b'}, {'a': 'A', 'b': 'B'})
>>> # Pop keys
>>> alpha_num.pop("two")
2
>>> alpha_num.pop("a")
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'a'"
>>> # Delete keys
>>> del alpha_num["c"]
>>> alpha_num
ChainMap({'one': 1, 'b': 'b'}, {'a': 'A', 'b': 'B'})
>>> del alpha_num["a"]
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'a'"
>>> # Clear the dictionary
>>> alpha_num.clear()
>>> alpha_num
ChainMap({}, {'a': 'A', 'b': 'B'})
突变给定链图内容的操作只影响第一个映射,即使你试图突变的键存在于列表中的其他映射。例如,当你试图在第二个映射中更新"b" 时,真正发生的是你在第一个字典中添加了一个新的键。
你可以利用这种行为来创建可更新的链式映射,而不修改你的原始输入字典。在这种情况下,你可以使用一个空的 dictionary 作为ChainMap 的第一个参数。
>>>
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_num = ChainMap({}, numbers, letters)
>>> alpha_num
ChainMap({}, {'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> alpha_num["comma"] = ","
>>> alpha_num["period"] = "."
>>> alpha_num
ChainMap(
{'comma': ',', 'period': '.'},
{'one': 1, 'two': 2},
{'a': 'A', 'b': 'B'}
)
在这里,你使用一个空字典 ({}) 来创建alpha_num 。这样可以保证你对alpha_num 所做的修改永远不会影响你的两个原始输入字典numbers 和letters ,而只会影响列表开头的空字典。
合并字典与连锁字典
作为用ChainMap 链接多个字典的替代方法,你可以考虑用以下方法将它们合并起来 dict.update():
>>>
>>> from collections import ChainMap
>>> # Chain dictionaries with ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"hamsters": 2, "turtles": 1}
>>> ChainMap(for_adoption, vet_treatment)
ChainMap(
{'dogs': 10, 'cats': 7, 'pythons': 3},
{'hamsters': 2, 'turtles': 1}
)
>>> # Merge dictionaries with .update()
>>> pets = {}
>>> pets.update(for_adoption)
>>> pets.update(vet_treatment)
>>> pets
{'dogs': 10, 'cats': 7, 'pythons': 3, 'hamsters': 2, 'turtles': 1}
在这个具体的例子中,当你从两个具有唯一键的现有字典中建立一个链式地图和一个等价字典时,你会得到类似的结果。
与用.update() 合并词典相比,用ChainMap 链式词典有优点和缺点。第一个也是最重要的缺点是,你丢掉了使用多个作用域或上下文来管理和优先访问重复键的能力。使用.update() ,你为一个给定的键提供的最后一个值将总是优先的。
>>>
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 2, "dogs": 1}
>>> # Merge dictionaries with .update()
>>> pets = {}
>>> pets.update(for_adoption)
>>> pets.update(vet_treatment)
>>> pets
{'dogs': 1, 'cats': 2, 'pythons': 3}
普通字典不能存储重复键。每次你用一个现有键的值调用.update() ,该键就会用新的值更新。在这种情况下,你失去了使用不同作用域优先访问重复键的能力。
**注意:**从Python 3.5 开始,你也可以使用字典解包操作符 (**) 将字典合并在一起。此外,如果你使用的是Python 3.9,那么你可以使用 dictionary union 操作符 (|) 将两个字典合并成一个新的字典。
现在假设你有_n 个_不同的映射,每个映射最多有_m 个_键。从它们中创建一个ChainMap 对象需要O(n) 的执行时间,而检索一个键在最坏的情况下需要_O__(n_),在这种情况下,目标键在内部映射列表的最后一个字典中。
另外,在一个循环中使用.update() 创建一个普通的字典将花费_O__(nm_),而从最后的字典中检索一个键将花费_O_(1)。
结论是,如果你经常创建一连串的字典,并且每次只执行几个键的查找,那么你应该使用ChainMap 。如果是相反的情况,那么就使用普通的字典,除非你需要重复的键或多个作用域。
合并字典和链式字典的另一个区别是,当你使用ChainMap ,输入字典的外部变化会影响到底层的链,而合并字典则不是这样的。
探讨其他功能ChainMap
ChainMap 提供的 API 和功能大多与普通的 Python 字典相同,有一些你已经知道的细微差别。ChainMap 还支持一些额外的功能,这些功能是它的设计和目标所特有的。
在这一节中,你将了解所有这些附加功能。你将学习它们如何在你访问字典中的键值对时帮助你管理不同的作用域和上下文。
管理映射列表的方法.maps
ChainMap 在一个内部列表中存储所有的输入映射。这个列表可以通过一个公共的实例属性访问,这个属性叫做 .maps的公共实例属性来访问,并且它是用户可更新的。.maps 中映射的顺序与你将它们传入ChainMap 的顺序相匹配。这个顺序定义了你执行键查找操作时的搜索顺序。
下面是一个关于如何访问.maps 的例子。
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"dogs": 4, "turtles": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {'dogs': 4, 'turtles': 1}]
在这里,你用.maps 来访问pets 持有的内部映射列表。这个列表是一个常规的 Python 列表,所以你可以手动添加和删除映射,遍历这个列表,改变映射的顺序,等等。
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets.maps.append({"hamsters": 2})
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {"cats": 1}, {'hamsters': 2}]
>>> del pets.maps[1]
>>> pets.maps
[{'dogs': 10, 'cats': 7, 'pythons': 3}, {'hamsters': 2}]
>>> for mapping in pets.maps:
... print(mapping)
...
{'dogs': 10, 'cats': 7, 'pythons': 3}
{'hamsters': 2}
在这些例子中,你首先用.maps 添加一个新的字典。 .append().然后你使用del 关键字来删除位置1 的字典。你可以像管理任何常规 Python 列表一样管理.maps 。
注意:映射的内部列表,.maps ,总是包含至少一个映射。例如,如果你使用ChainMap() 创建一个没有参数的空链图,那么这个列表将存储一个空字典。
你可以使用.maps 遍历你的所有映射,同时对它们执行操作。遍历映射列表的可能性使你可以对每个映射执行不同的操作。有了这个选项,你可以绕过默认行为,即只对列表中的第一个映射进行突变。
一个有趣的例子是,你可以用下面的方法颠倒当前列表中映射的顺序 .reverse():
>>>
>>> from collections import ChainMap
>>> for_adoption = {"dogs": 10, "cats": 7, "pythons": 3}
>>> vet_treatment = {"cats": 1}
>>> pets = ChainMap(for_adoption, vet_treatment)
>>> pets
ChainMap({'dogs': 10, 'cats': 7, 'pythons': 3}, {'cats': 1})
>>> pets.maps.reverse()
>>> pets
ChainMap({'cats': 1}, {'dogs': 10, 'cats': 7, 'pythons': 3})
颠倒内部映射列表允许你在链图中查找一个特定的键时颠倒搜索顺序。现在当你查找"cats" ,你得到的是正在接受兽医治疗的猫的数量,而不是准备被收养的猫。
添加新的子上下文与.new_child()
ChainMap 也实现了 .new_child().这个方法可以选择接受一个映射作为参数,并返回一个新的ChainMap 实例,其中包含输入映射,后面是底层链图中所有的当前映射。
>>>
>>> from collections import ChainMap
>>> mom = {"name": "Jane", "age": 31}
>>> dad = {"name": "John", "age": 35}
>>> family = ChainMap(mom, dad)
>>> family
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})
>>> son = {"name": "Mike", "age": 0}
>>> family = family.new_child(son)
>>> for person in family.maps:
... print(person)
...
{'name': 'Mike', 'age': 0}
{'name': 'Jane', 'age': 31}
{'name': 'John', 'age': 35}
这里,.new_child() 返回一个新的ChainMap 对象,包含一个新的映射,son ,后面是旧的映射,mom 和dad 。注意,新的映射现在在内部映射列表中占据了第一个位置,.maps 。
通过.new_child() ,你可以创建一个子上下文,你可以在不改变任何现有映射的情况下更新它。例如,如果你在没有参数的情况下调用.new_child() ,那么它就会使用一个空的字典,并把它放在.maps 的开头。在这之后,你可以对你的新的空映射进行任何突变,保持映射的其余部分为只读。
跳过子上下文用.parents
ChainMap 的另一个有趣的特征是 .parents.这个属性返回一个新的ChainMap 实例,其中包含底层链图中的所有映射,除了第一个映射。当你在一个给定的链图中搜索键时,这个特性对于跳过第一个映射很有用。
>>>
>>> from collections import ChainMap
>>> mom = {"name": "Jane", "age": 31}
>>> dad = {"name": "John", "age": 35}
>>> son = {"name": "Mike", "age": 0}
>>> family = ChainMap(son, mom, dad)
>>> family
ChainMap(
{'name': 'Mike', 'age': 0},
{'name': 'Jane', 'age': 31},
{'name': 'John', 'age': 35}
)
>>> family.parents
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})
在这个例子中,你用.parents 来跳过包含儿子数据的第一个字典。在某种程度上,.parents 做的是.new_child() 的逆向操作。前者删除了一个字典,而后者在列表的开头添加了一个新的字典。在这两种情况下,你都得到一个新的链图。
管理作用域和上下文的方法ChainMap
可以说,ChainMap 的主要用例是提供一种有效的方法来管理多个作用域或上下文,并处理重复键的访问优先级。当你有几个存储重复键的字典,并且你想定义你的代码访问它们的顺序时,这个功能就很有用。
在ChainMap 文档中,你会发现一个经典的例子,它模拟了Python如何解决不同命名空间中的变量名。
当 Python 寻找一个名字时,它依次搜索局部、全局和内置作用域,按照同样的顺序直到找到目标名字。Python 的作用域是将名字映射到对象的字典。
为了模拟 Python 的内部查找链,你可以使用一个链图。
>>>
>>> import builtins
>>> # Shadow input with a global name
>>> input = 42
>>> pylookup = ChainMap(locals(), globals(), vars(builtins))
>>> # Retrieve input from the global namespace
>>> pylookup["input"]
42
>>> # Remove input from the global namespace
>>> del globals()["input"]
>>> # Retrieve input from the builtins namespace
>>> pylookup["input"]
<built-in function input>
在这个例子中,你首先创建了一个名为input 的全局变量,该变量将内置的 input()函数在 builtins作用域中的内置函数。然后你创建了pylookup 作为一个链图,包含了容纳每个 Python 作用域的三个字典。
当你从pylookup 检索input 时,你从全局作用域得到值42 。如果你从globals() 字典中删除input 键并再次访问它,那么你从builtins 作用域得到内置的input() 函数,它在 Python 的查找链中具有最低的优先级。
同样地,你可以使用ChainMap 来定义和管理重复键的查找顺序。这允许你优先访问所需的重复键的实例。
在标准库中跟踪ChainMap
ChainMap 的起源与一个性能问题密切相关,在 ConfigParser中的一个性能问题,该问题存在于 configparser模块中。有了ChainMap ,Python 的核心开发者通过优化下面的实现,极大地提高了这个模块的整体性能 ConfigParser.get().
你也可以在ChainMap 中找到作为一部分的 Template中的一部分。 string模块中的一部分。这个类将一个字符串模板作为参数,并允许你执行PEP 292中描述的字符串替换。输入的字符串模板包含嵌入的标识符,你可以随后用实际的值来替换。
>>>
>>> import string
>>> greeting = "Hey $name, welcome to $place!"
>>> template = string.Template(greeting)
>>> template.substitute({"name": "Jane", "place": "the World"})
'Hey Jane, welcome to the World!'
当你通过一个字典为name 和place 提供值时。 .substitute()在模板字符串中替换它们。此外,.substitute() 可以将值作为关键字参数(**kwargs),这在某些情况下会导致名称冲突。
>>>
>>> import string
>>> greeting = "Hey $name, welcome to $place!"
>>> template = string.Template(greeting)
>>> template.substitute(
... {"name": "Jane", "place": "the World"},
... place="Real Python"
... )
'Hey Jane, welcome to Real Python!'
在这个例子中,.substitute() 用你作为关键字参数提供的值而不是输入字典中的值替换了place 。如果你稍微挖掘一下这个方法的代码,那么你会发现它使用ChainMap ,在发生名称碰撞时有效地管理输入值的优先级。
下面是一个来自.substitute() 的源代码片段。
# string.py
# Snip...
from collections import ChainMap as _ChainMap
_sentinel_dict = {}
class Template:
"""A string class for supporting $-substitutions."""
# Snip...
def substitute(self, mapping=_sentinel_dict, /, **kws):
if mapping is _sentinel_dict:
mapping = kws
elif kws:
mapping = _ChainMap(kws, mapping)
# Snip...
在这里,突出显示的那一行发挥了神奇的作用。它使用了一个链图,将两个字典,kws 和mapping ,作为参数。通过将kws 作为第一个参数,该方法设定了输入数据中重复标识符的优先级。
将 Python 的ChainMap 付诸行动
到目前为止,你已经学会了如何使用ChainMap 将多个字典作为一个整体进行处理。你还了解了ChainMap 的特点以及这个类与普通字典的不同之处。ChainMap 的使用情况是相当具体的。它们包括。
- 在一个单一的视图中有效地分组多个字典
- 以一定的优先级在多个字典中进行搜索
- 提供一连串的默认值并管理其优先级
- 提高经常计算字典子集的代码的性能
在本节中,你将编码一些实际的例子,这些例子将帮助你更好地了解如何使用ChainMap 来解决现实世界的问题。
将多个库存作为一个整体进行访问
你要编码的第一个例子使用ChainMap 在一个视图中有效地搜索多个字典。在这种情况下,你会假设你有一堆独立的字典,它们之间有唯一的键。
假设你正在经营一家销售水果和蔬菜的商店。你已经编写了一个Python应用程序来管理你的库存。该程序从数据库中读取并返回两个字典,分别包含水果和蔬菜的价格数据。你需要一种有效的方法在一个字典中分组和管理这些数据。
经过一些研究,你最终使用了ChainMap 。
>>>
>>> from collections import ChainMap
>>> fruits_prices = {"apple": 0.80, "grape": 0.40, "orange": 0.50}
>>> veggies_prices = {"tomato": 1.20, "pepper": 1.30, "onion": 1.25}
>>> prices = ChainMap(fruits_prices, veggies_prices)
>>> order = {"apple": 4, "tomato": 8, "orange": 4}
>>> for product, units in order.items():
... price = prices[product]
... subtotal = units * price
... print(f"{product:6}: ${price:.2f} × {units} = ${subtotal:.2f}")
...
apple : $0.80 × 4 = $3.20
tomato: $1.20 × 8 = $9.60
orange: $0.50 × 4 = $2.00
在这个例子中,你用一个ChainMap 来创建一个类似于字典的对象,将来自fruits_prices 和veggies_prices 的数据分组。这允许你访问底层数据,就像你有效地拥有一个单一的字典一样。for 循环遍历一个给定的order 中的产品。然后,它计算出每类产品要支付的小计,并将其打印在屏幕上。
你可能会考虑在一个新的字典中对数据进行分组,在一个循环中使用.update() 。当你有有限的产品种类和少量的库存时,这可以很好地工作。但是,如果你管理许多不同类型的产品,那么与ChainMap 相比,使用.update() 来建立一个新的字典可能是低效的。
使用ChainMap 来解决这类问题,还可以帮助你为不同批次的产品定义优先级,让你以先进先出(FIFO)的方式管理库存。
确定命令行应用程序设置的优先次序
ChainMap 在管理你的应用程序中的默认配置值方面特别有帮助。正如你已经知道的,ChainMap 的主要特点之一是它允许你为关键的查找操作设置优先级。这听起来像是解决你的应用程序中的配置管理问题的正确工具。
例如,假设你正在开发一个命令行界面(CLI)应用程序。该应用程序允许用户指定一个代理服务来连接到互联网。设置的优先级是。
- 命令行选项 (
--proxy,-p) - 用户主目录中的本地配置文件
- 全系统的代理配置
如果用户在命令行中提供了一个代理,那么应用程序必须使用该代理。否则,应用程序应该使用下一个配置对象中提供的代理,以此类推。这是ChainMap 的最常见的使用情况之一。在这种情况下,你可以做以下事情。
>>>
>>> from collections import ChainMap
>>> cmd_proxy = {} # The user doesn't provide a proxy
>>> local_proxy = {"proxy": "proxy.local.com"}
>>> system_proxy = {"proxy": "proxy.global.com"}
>>> config = ChainMap(cmd_proxy, local_proxy, system_proxy)
>>> config["proxy"]
'proxy.local.com'
ChainMap 允许你为应用程序的代理配置定义适当的优先级。一个键查询搜索cmd_proxy ,然后是local_proxy ,最后是system_proxy ,返回手头的键的第一个实例。在这个例子中,用户没有在命令行中提供一个代理,所以应用程序从local_proxy ,它是列表中的下一个设置提供者。
管理默认参数值
ChainMap 的另一个用例是管理方法和函数中的默认参数值。假设你正在编写一个应用程序来管理你公司员工的数据。你有下面这个类,它代表一个普通的用户。
class User:
def __init__(self, name, user_id, role):
self.name = name
self.user_id = user_id
self.role = role
# Snip...
在某些时候,你需要添加一个功能,允许员工访问CRM系统的不同组件。你的第一个想法是修改User ,以增加新的功能。然而,这可能会使这个类变得过于复杂,所以你决定创建一个子类,CRMUser ,以提供所需的功能。
该类将接受一个用户name 和CRMcomponent 作为参数。它也会接受一些 **kwargs.你想以一种方式实现CRMUser ,允许你为基类的初始化器提供合理的默认值,而不失去**kwargs 的灵活性。
下面是你如何使用ChainMap 来解决这个问题。
from collections import ChainMap
class CRMUser(User):
def __init__(self, name, component, **kwargs):
defaults = {"user_id": next(component.user_id), "role": "read"}
super().__init__(name, **ChainMap(kwargs, defaults))
在这个代码示例中,你创建了一个User 的子类。在类的初始化器中,你把name,component, 和**kwargs 作为参数。然后为user_id 和role 创建一个带有默认值的本地字典。然后你调用父类的 .__init__()方法,使用 super().在这个调用中,你将name 直接传递给父类的初始化器,并使用链图为其余的参数提供默认值。
请注意,ChainMap 对象接受kwargs ,然后是defaults 作为参数。这个顺序保证了在你实例化这个类时,手动提供的参数 (kwargs) 优先于defaults 的值。
结论
Python 的ChainMap 来自collections 模块,它提供了一个有效的工具,可以将几个字典作为一个整体来管理。当你有多个代表不同范围或上下文的字典,并且需要对底层数据设置访问优先级时,这个类就很方便。
ChainMap 在一个可更新的视图中对多个字典和映射进行分组,其工作方式与字典相当。你可以使用ChainMap 对象来有效地处理几个字典,定义键查找优先级,并在 Python 中管理多个上下文。
在本教程中,你学会了如何。
- 在你的 Python 程序中创建**
ChainMap实例 - 探索
ChainMap和 之间的差异dict - 将几个字典作为一个整体来管理
ChainMap - 为键查找操作设置优先级
ChainMap
在本教程中,你还编码了一些实际的例子,帮助你更好地理解何时以及如何在你的Python代码中使用ChainMap 。