Python 的集合:专业数据类型的自定义
目录
- Python 集合入门
- 提高代码可读性:namedtuple()
- 构建高效的队列和堆栈:deque
- 处理丢失的键:defaultdict
- 保持字典有序:OrderedDict
- 一次计数对象:计数器
- 将字典链接在一起:ChainMap
- 自定义内置函数:UserString、UserList 和 UserDict
- 结论
Python 的collections
模块提供了一组丰富的专用容器数据类型,这些数据类型经过精心设计,以 Pythonic 和高效的方式解决特定的编程问题。该模块还提供包装类,使之更安全创建,其行为类似于内建类型的自定义类dict
,list
和str
。
了解 中的数据类型和类collections
将使您能够使用一组有价值的可靠和高效的工具来扩展您的编程工具包。
在本教程中,您将学习如何:
- 编写可读且显式的代码
namedtuple
- 建立高效的队列,栈与
deque
- 快速计数对象
Counter
- 处理缺少字典键与
defaultdict
- 保证键的插入顺序
OrderedDict
- 将多个字典作为一个单元管理
ChainMap
为了更好地理解 中的数据类型和类collections
,您应该了解使用 Python 内置数据类型(例如列表、元组和字典)的基础知识。此外,本文的最后一部分需要有关Python 中面向对象编程的一些基本知识。
Python 入门 collections
早在Python 2.4中,雷蒙德赫廷杰贡献了一个所谓的新模块collections
的标准库。目标是提供各种专门的集合数据类型来解决特定的编程问题。
当时collections
只包含了一个数据结构 ,deque
专门设计成一个双端队列,支持在序列的任一端进行高效的append和pop操作。从那时起,标准库中的几个模块利用deque
来提高它们的类和结构的性能。一些突出的例子是queue
和threading
。
随着时间的推移,一些专门的容器数据类型填充了模块:
Data type | Python version | Description | 描述 |
---|---|---|---|
deque |
2.4 | A sequence-like collection that supports efficient addition and removal of items from either end of the sequence | 一个类似序列的集合,支持从序列的任一端有效地添加和删除项目 |
defaultdict |
2.5 | A dictionary subclass for constructing default values for missing keys and automatically adding them to the dictionary | 一个字典子类,用于为缺失的键构建默认值并自动将它们添加到字典中 |
namedtuple() |
2.6 | A factory function for creating subclasses of tuple that provides named fields that allow accessing items by name while keeping the ability to access items by index |
用于创建其子类的工厂函数tuple ,提供命名字段,允许按名称访问项目,同时保持按索引访问项目的能力 |
OrderedDict |
2.7, 3.1 | A dictionary subclass that keeps the key-value pairs ordered according to when the keys are inserted | 根据插入键的时间保持键值对排序的字典子类 |
Counter |
2.7, 3.1 | A dictionary subclass that supports convenient counting of unique items in a sequence or iterable | 一个字典子类,支持方便地对序列或可迭代中的唯一项进行计数 |
ChainMap |
3.3 | A dictionary-like class that allows treating a number of mappings as a single dictionary object | 一个类似字典的类,允许将多个映射视为单个字典对象 |
除了这些专门的数据类型之外,collections
还提供了三个基类来促进自定义列表、字典和字符串的创建:
Class | Description | 描述 |
---|---|---|
UserDict |
A wrapper class around a dictionary object that facilitates subclassing dict |
一个围绕字典对象的包装类,便于子类化 dict |
UserList |
A wrapper class around a list object that facilitates subclassing list |
一个围绕列表对象的包装类,便于子类化 list |
UserString |
A wrapper class around a string object that facilitates subclassing string |
一个围绕字符串对象的包装类,便于子类化 string |
将相应的标准内置数据类型进行子类化的能力使对这些包装器类的需求部分黯然失色。但是,有时使用这些类比使用标准数据类型更安全且不易出错。
通过collections
对本模块中的数据结构和类可以解决的具体用例的简要介绍,是时候仔细研究它们了。在此之前,重要的是要指出本教程是collections
一个整体的介绍。在以下大部分内容中,您会找到一个蓝色警告框,它会引导您阅读有关当前类或函数的专门文章。
提高代码可读性: namedtuple()
Pythonnamedtuple()
是一个工厂函数,它允许您创建tuple
具有命名字段的子类。这些字段使您可以使用点表示法直接访问给定命名元组中的值,例如 in obj.attr
。
之所以需要此功能,是因为使用索引访问常规元组中的值很烦人、难以阅读且容易出错。如果您正在使用的元组有多个项目并且在远离您使用它的地方构建,则尤其如此。
注意:查看使用 namedtuple 编写 Pythonic 和 Clean Code 以更深入地了解如何namedtuple
在 Python 中使用。
开发人员可以使用点表示法访问具有命名字段的元组子类,这在 Python 2.6 中似乎是一个理想的功能。这就是namedtuple()
. 如果将它们与常规元组进行比较,您可以使用此函数构建的元组子类在代码可读性方面是一个巨大的胜利。
为了正确看待代码可读性问题,请考虑divmod()
. 这个内置函数接受两个(非复数)数字并返回一个元组,其中包含由输入值的整数除法产生的商和余数:
>>> divmod(12, 5)
(2, 2)
它工作得很好。但是,这个结果可读吗?你能说出输出中每个数字的含义吗?幸运的是,Python 提供了一种改进方法。您可以使用以下代码对divmod()
具有显式结果的自定义版本进行编码namedtuple
:
>>> from collections import namedtuple
>>> def custom_divmod(x, y):
... DivMod = namedtuple("DivMod", "quotient remainder")
... return DivMod(*divmod(x, y))
...
>>> result = custom_divmod(12, 5)
>>> result
DivMod(quotient=2, remainder=2)
>>> result.quotient
2
>>> result.remainder
2
现在您知道结果中每个值的含义了。您还可以使用点表示法和描述性字段名称访问每个独立值。
要使用 创建新的元组子类namedtuple()
,您需要两个必需的参数:
typename
是您正在创建的类的名称。它必须是具有有效 Python 标识符的字符串。field_names
是您将用于访问生成的元组中的项目的字段名称列表。有可能:- 一个可迭代的字符串,例如
["field1", "field2", ..., "fieldN"]
- 具有以空格分隔的字段名称的字符串,例如
"field1 field2 ... fieldN"
- 具有逗号分隔字段名称的字符串,例如
"field1, field2, ..., fieldN"
- 一个可迭代的字符串,例如
例如,以下是使用以下方法创建Point
具有两个坐标 (x
和y
)的示例 2D 的不同方法namedtuple()
:
>>> from collections import namedtuple
>>> # Use a list of strings as field names
>>> Point = namedtuple("Point", ["x", "y"])
>>> point = Point(2, 4)
>>> point
Point(x=2, y=4)
>>> # Access the coordinates
>>> point.x
2
>>> point.y
4
>>> point[0]
2
>>> # Use a generator expression as field names
>>> Point = namedtuple("Point", (field for field in "xy"))
>>> Point(2, 4)
Point(x=2, y=4)
>>> # Use a string with comma-separated field names
>>> Point = namedtuple("Point", "x, y")
>>> Point(2, 4)
Point(x=2, y=4)
>>> # Use a string with space-separated field names
>>> Point = namedtuple("Point", "x y")
>>> Point(2, 4)
Point(x=2, y=4)
在这些示例中,您首先Point
使用list
字段名称创建。然后实例化Point
以创建一个point
对象。请注意,您可以通过字段名称和索引访问x
和y
。
其余示例展示了如何使用逗号分隔的字段名称字符串、生成器表达式和空格分隔的字段名称字符串创建等效的命名元组。
命名元组还提供了一系列很酷的功能,允许您为字段定义默认值、从给定的命名元组创建字典、替换给定字段的值等等:
>>> from collections import namedtuple
>>> # Define default values for fields
>>> Person = namedtuple("Person", "name job", defaults=["Python Developer"])
>>> person = Person("Jane")
>>> person
Person(name='Jane', job='Python Developer')
>>> # Create a dictionary from a named tuple
>>> person._asdict()
{'name': 'Jane', 'job': 'Python Developer'}
>>> # Replace the value of a field
>>> person = person._replace(job="Web Developer")
>>> person
Person(name='Jane', job='Web Developer')
在这里,您首先Person
使用namedtuple()
. 这一次,您使用一个名为的可选参数defaults
,该参数接受元组字段的一系列默认值。请注意,namedtuple()
将默认值应用于最右侧的字段。
在第二个示例中,您使用._asdict()
. 此方法返回一个使用字段名称作为键的新字典。
最后,您使用._replace()
替换 的原始值job
。此方法不会就地更新元组,而是返回一个新命名的元组,新值存储在相应的字段中。你知道为什么要._replace()
返回一个新命名的元组吗?
构建高效的队列和堆栈: deque
Pythondeque
是collections
. 这种类似序列的数据类型是堆栈和队列的泛化,旨在支持数据结构两端的高效内存和快速追加和弹出操作。
注:这个词deque
的发音是“甲板”和代表d ouble- é nded阙UE。
在 Python 中,list
对象开头或左侧的 append 和 pop 操作效率低下,时间复杂度为O ( n )。如果您使用大型列表,这些操作尤其昂贵,因为 Python 必须将所有项目向右移动才能在列表的开头插入新项目。
另一方面,列表右侧的 append 和 pop 操作通常是有效的(O (1)),除了 Python 需要重新分配内存以增长底层列表以接受新项目的情况。
Pythondeque
就是为了解决这个问题而创建的。deque
对象两侧的追加和弹出操作是稳定且同样高效的,因为双端队列是作为双向链表实现的。这就是为什么双端队列对于创建堆栈和队列特别有用。
以队列为例。它以先进/先出( FIFO ) 方式管理项目。它用作管道,您可以在管道的一端插入新项目,然后从另一端弹出旧项目。将项目添加到队列的末尾称为入队操作。从队列的前面或开头删除项目称为dequeue。
现在假设您正在模拟排队等候购买电影票的人。您可以使用deque
. 每次有新人到达时,您都会将他们排入队列。当队列前面的人拿到他们的票时,你就让他们出队。
以下是使用deque
对象模拟该过程的方法:
>>> from collections import deque
>>> ticket_queue = deque()
>>> ticket_queue
deque([])
>>> # People arrive to the queue
>>> ticket_queue.append("Jane")
>>> ticket_queue.append("John")
>>> ticket_queue.append("Linda")
>>> ticket_queue
deque(['Jane', 'John', 'Linda'])
>>> # People bought their tickets
>>> ticket_queue.popleft()
'Jane'
>>> ticket_queue.popleft()
'John'
>>> ticket_queue.popleft()
'Linda'
>>> # No people on the queue
>>> ticket_queue.popleft()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: pop from an empty deque
在这里,您首先创建一个空deque
对象来表示人员队列。要使一个人入队,您可以使用.append()
,它将项目添加到双端队列的右端。要使一个人出队,您可以使用.popleft()
,它删除并返回双端队列左端的项目。
注意:在 Python 标准库中,您会发现queue
. 该模块实现了多生产者、多消费者队列,可用于安全地在多个线程之间交换信息。
在deque
初始化有两个可选参数:
iterable
持有一个用作初始化程序的可迭代对象。maxlen
保存一个整数,用于指定 的最大长度deque
。
如果您不提供iterable
,则会得到一个空的双端队列。如果您为 提供值maxlen
,那么您的双端队列将最多存储maxlen
项目。
拥有 amaxlen
是一个方便的功能。例如,假设您需要在您的一个应用程序中实现最近文件列表。在这种情况下,您可以执行以下操作:
>>> from collections import deque
>>> recent_files = deque(["core.py", "README.md", "__init__.py"], maxlen=3)
>>> recent_files.appendleft("database.py")
>>> recent_files
deque(['database.py', 'core.py', 'README.md'], maxlen=3)
>>> recent_files.appendleft("requirements.txt")
>>> recent_files
deque(['requirements.txt', 'database.py', 'core.py'], maxlen=3)
一旦双端队列达到其最大大小(在本例中为三个文件),在双端队列的末尾添加一个新文件会自动丢弃另一端的文件。如果您不向 提供值maxlen
,则双端队列可以增长到任意数量的项目。
到目前为止,您已经学习了双端队列的基础知识,包括如何创建它们以及如何从给定双端队列的两端追加和弹出项目。Deques 提供了一些具有类似列表界面的附加功能。这里是其中的一些:
>>> from collections import deque
>>> # Use different iterables to create deques
>>> deque((1, 2, 3, 4))
deque([1, 2, 3, 4])
>>> deque([1, 2, 3, 4])
deque([1, 2, 3, 4])
>>> deque("abcd")
deque(['a', 'b', 'c', 'd'])
>>> # Unlike lists, deque doesn't support .pop() with arbitrary indices
>>> deque("abcd").pop(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pop() takes no arguments (1 given)
>>> # Extend an existing deque
>>> numbers = deque([1, 2])
>>> numbers.extend([3, 4, 5])
>>> numbers
deque([1, 2, 3, 4, 5])
>>> numbers.extendleft([-1, -2, -3, -4, -5])
>>> numbers
deque([-5, -4, -3, -2, -1, 1, 2, 3, 4, 5])
>>> # Insert an item at a given position
>>> numbers.insert(5, 0)
>>> numbers
deque([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5])
在这些示例中,您首先使用不同类型的可迭代对象创建双端队列来初始化它们。deque
和之间的一个区别list
是deque.pop()
不支持在给定索引处弹出项目。
请注意,deque
为、 和提供了姐妹方法.append()
,后缀表示它们在底层双端队列的左端执行相应的操作。.pop()
.extend()
left
Deques 还支持序列操作:
方法 | 描述 |
---|---|
.clear() |
从双端队列中删除所有元素 |
.copy() |
创建双端队列的浅拷贝 |
.count(x) |
计算 deque 元素的数量等于 x |
.remove(value) |
删除第一次出现 value |
双端队列的另一个有趣功能是能够使用.rotate()
以下方法旋转其元素:
>>> from collections import deque
>>> ordinals = deque(["first", "second", "third"])
>>> ordinals.rotate()
>>> ordinals
deque(['third', 'first', 'second'])
>>> ordinals.rotate(2)
>>> ordinals
deque(['first', 'second', 'third'])
>>> ordinals.rotate(-2)
>>> ordinals
deque(['third', 'first', 'second'])
>>> ordinals.rotate(-1)
>>> ordinals
deque(['first', 'second', 'third'])
此方法将双端队列n
向右旋转。默认值n
是1
。如果为 提供负值n
,则向左旋转。
最后,您可以使用索引访问双端队列中的元素,但不能对双端队列进行切片:
>>> from collections import deque
>>> ordinals = deque(["first", "second", "third"])
>>> ordinals[1]
'second'
>>> ordinals[0:2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence index must be integer, not 'slice'
双端队列支持索引,但有趣的是,它们不支持切片。当您尝试从现有双端队列中检索切片时,您会得到一个TypeError
. 这是因为对链表执行切片操作效率低下,因此该操作不可用。
处理丢失的键: defaultdict
当您在 Python 中使用字典时,您将面临的一个常见问题是如何处理丢失的键。如果您尝试访问给定字典中不存在的键,则会得到KeyError
:
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> favorites["fruit"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'fruit'
有几种方法可以解决此问题。例如,您可以使用.setdefault()
. 此方法将键作为参数。如果键存在于字典中,则返回相应的值。否则,该方法插入键,为其分配一个默认值,并返回该值:
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> favorites.setdefault("fruit", "apple")
'apple'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}
>>> favorites.setdefault("pet", "cat")
'dog'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}
在此示例中,您使用.setdefault()
为 生成默认值fruit
。由于该键在 中不存在favorites
,请.setdefault()
创建它并为其分配 值apple
。如果您.setdefault()
使用现有键调用,则调用不会影响字典,并且您的键将保存原始值而不是默认值。
.get()
如果缺少给定的键,您还可以使用返回合适的默认值:
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> favorites.get("fruit", "apple")
'apple'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python'}
在这里,.get()
返回apple
是因为底层字典中缺少键。但是,.get()
不会为您创建新密钥。
由于处理字典中缺失的键是一个常见的需求,Pythoncollections
也为此提供了一个工具。该defaultdict
类型是dict
旨在帮助您解决丢失键的子类。
注意:查看使用 Python defaultdict 类型处理丢失的键以深入了解如何使用 Python 的defaultdict
.
的构造defaultdict
函数接受一个函数对象作为它的第一个参数。当您访问一个不存在的键时,defaultdict
自动调用该函数而不带参数来为手头的键创建合适的默认值。
您可以使用任何可调用defaultdict
对象来初始化您的对象。例如,int()
您可以创建一个合适的计数器来计算不同的对象:
>>> from collections import defaultdict
>>> counter = defaultdict(int)
>>> counter
defaultdict(<class 'int'>, {})
>>> counter["dogs"]
0
>>> counter
defaultdict(<class 'int'>, {'dogs': 0})
>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["cats"] += 1
>>> counter["cats"] += 1
>>> counter
defaultdict(<class 'int'>, {'dogs': 3, 'cats': 2})
在这个例子中,你创建一个空defaultdict
与int()
作为第一个参数。当您访问一个不存在的键时,字典会自动调用int()
,它将0
作为手头键的默认值返回。defaultdict
在 Python 中计算事物时,这种对象非常有用。
的另一个常见用例defaultdict
是对事物进行分组。在这种情况下,方便的工厂函数是list()
:
>>> from collections import defaultdict
>>> pets = [
... ("dog", "Affenpinscher"),
... ("dog", "Terrier"),
... ("dog", "Boxer"),
... ("cat", "Abyssinian"),
... ("cat", "Birman"),
... ]
>>> group_pets = defaultdict(list)
>>> for pet, breed in pets:
... group_pets[pet].append(breed)
...
>>> for pet, breeds in group_pets.items():
... print(pet, "->", breeds)
...
dog -> ['Affenpinscher', 'Terrier', 'Boxer']
cat -> ['Abyssinian', 'Birman']
在此示例中,您有关于宠物及其品种的原始数据,您需要按宠物对它们进行分组。为此,请在创建实例时使用list()
as 。这使您的字典能够自动创建一个空列表 ( ) 作为您访问的每个缺失键的默认值。然后您使用该列表来存储您的宠物的品种。.default_factory
defaultdict
[]
最后,您应该注意,由于defaultdict
是 的子类dict
,因此它提供了相同的接口。这意味着您可以defaultdict
像使用常规字典一样使用您的对象。
保持字典有序: OrderedDict
有时您需要字典来记住插入键值对的顺序。多年来,Python 的常规词典都是无序的 数据结构。因此,早在 2008 年,PEP 372 就引入了向collections
.
新类将根据插入键的时刻记住项目的顺序。那就是OrderedDict
.
OrderedDict
在Python 3.1中引入。它的应用程序编程接口 (API) 与dict
. 但是,OrderedDict
以相同的顺序对键和值进行迭代,键首先被插入到字典中。如果为现有键分配新值,则键值对的顺序保持不变。如果一个条目被删除并重新插入,那么它将被移动到字典的末尾。
注意:查看Python 中的 OrderedDict 与 dict:工作的正确工具,以更深入地了解 PythonOrderedDict
以及您应该考虑使用它的原因。
有几种方法可以创建OrderedDict
对象。它们中的大多数与您创建常规字典的方式相同。例如,您可以通过实例化不带参数的类来创建一个空的有序字典,然后根据需要插入键值对:
>>> from collections import OrderedDict
>>> life_stages = OrderedDict()
>>> life_stages["childhood"] = "0-9"
>>> life_stages["adolescence"] = "9-18"
>>> life_stages["adulthood"] = "18-65"
>>> life_stages["old"] = "+65"
>>> for stage, years in life_stages.items():
... print(stage, "->", years)
...
childhood -> 0-9
adolescence -> 9-18
adulthood -> 18-65
old -> +65
在这个例子中,你通过OrderedDict
不带参数的实例化来创建一个空的有序字典。接下来,像使用常规字典一样将键值对添加到字典中。
当您遍历字典, 时life_stages
,您会按照将它们插入字典中的相同顺序获得键值对。保证物品的顺序是主要解决的问题OrderedDict
。
Python的3.6引入了一个新的实现dict
。这个实现提供了一个意想不到的新功能:现在常规词典将它们的项目按照它们第一次插入的顺序保存。
最初,该功能被认为是一个实现细节,文档建议不要依赖它。但是,从Python 3.7 开始,该功能正式成为语言规范的一部分。那么,使用的意义OrderedDict
何在?
有一些特点OrderedDict
仍然使它有价值:
- 意图通信:使用
OrderedDict
,您的代码将清楚地表明字典中项目的顺序很重要。您清楚地传达了您的代码需要或依赖于底层字典中项目的顺序。 - 控制项目的顺序:使用
OrderedDict
,您可以访问.move_to_end()
,这是一种允许您操纵字典中项目顺序的方法。您还将拥有一个增强的变体,.popitem()
它允许从底层字典的任一端删除项目。 - 相等测试行为:使用
OrderedDict
,字典之间的相等测试会考虑项目的顺序。因此,如果您有两个具有相同项目组但顺序不同的有序字典,那么您的字典将被视为不相等。
至少还有一个使用的理由OrderedDict
:向后兼容。在dict
运行 Python 3.6 之前版本的环境中,依靠常规对象来保留项目的顺序会破坏您的代码。
好的,现在是时候看看这些很酷的功能了OrderedDict
:
>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)
>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])
>>> # Move b to the right end
>>> letters.move_to_end("b")
>>> letters
OrderedDict([('d', 4), ('a', 1), ('c', 3), ('b', 2)])
>>> # Move b to the left end
>>> letters.move_to_end("b", last=False)
>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])
>>> # Sort letters by key
>>> for key in sorted(letters):
... letters.move_to_end(key)
...
>>> letters
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
在这些示例中,您使用.move_to_end()
来移动项目和重新排序letters
。请注意,.move_to_end()
接受一个名为的可选参数last
,该参数允许您控制要将项目移动到字典的哪一端。当您需要对字典中的项目进行排序或需要以任何方式操纵它们的顺序时,此方法非常方便。
OrderedDict
和普通字典之间的另一个重要区别是它们如何比较相等性:
>>> from collections import OrderedDict
>>> # Regular dictionaries compare the content only
>>> letters_0 = dict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
True
>>> # Ordered dictionaries compare content and order
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = OrderedDict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
False
>>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_0 == letters_2
True
在这里,letters_1
具有与 不同的项目顺序letters_0
。当您使用常规词典时,这种差异无关紧要,两个词典比较相等。另一方面,当您使用有序字典时,letters_0
和letters_1
不相等。这是因为有序字典之间的相等性测试考虑内容和项目的顺序。
一次计数对象: Counter
计数对象是编程中的常见操作。假设您需要计算给定项目出现在列表或可迭代对象中的次数。如果您的清单很短,那么计算其项目可能会简单快捷。如果您有一个很长的清单,那么计算项目将更具挑战性。
要对对象进行计数,通常使用counter或初始值为 0的整数变量。然后增加计数器以反映给定对象出现的次数。
在 Python 中,您可以使用字典一次计算多个不同的对象。在这种情况下,键将存储单个对象,值将保存给定对象的重复次数或对象的计数。
这是一个"mississippi"
使用常规字典和for
循环计算单词中字母的示例:
>>> word = "mississippi"
>>> counter = {}
>>> for letter in word:
... if letter not in counter:
... counter[letter] = 0
... counter[letter] += 1
...
>>> counter
{'m': 1, 'i': 4, 's': 4, 'p': 2}
循环遍历 中的字母word
。该条件语句检查,如果字母是不是已经在字典中并初始化字母的数量相应为零。最后一步是随着循环的进行增加字母的计数。
正如您已经知道的那样,defaultdict
对象在计算事物时很方便,因为您不需要检查键是否存在。字典保证任何缺失的键都有适当的默认值:
>>> from collections import defaultdict
>>> counter = defaultdict(int)
>>> for letter in "mississippi":
... counter[letter] += 1
...
>>> counter
defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})
在此示例中,您创建一个defaultdict
对象并使用 对其进行初始化int()
。随着int()
作为工厂的功能,基本默认的字典会自动创建丢失的钥匙,方便他们初始化为零。然后增加当前键的值以计算 中字母的最终计数"mississippi"
。
就像其他常见的编程问题一样,Python 也有一个解决计数问题的有效工具。在 中collections
,您会发现Counter
,这是一个dict
专为计算对象而设计的子类。
以下是如何"mississippi"
使用Counter
以下方法编写示例:
>>> from collections import Counter
>>> Counter("mississippi")
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
哇!那很快!一行代码就完成了。在这个例子中,Counter
迭代"mississippi"
,生成一个以字母为键、频率为值的字典。
注意:查看Python 的计数器:计算对象的 Pythonic 方法以深入了解Counter
以及如何使用它来有效地计算对象。
有几种不同的实例化方法Counter
。您可以使用列表、元组或任何具有重复对象的可迭代对象。唯一的限制是您的对象需要是可散列的:
>>> from collections import Counter
>>> Counter([1, 1, 2, 3, 3, 3, 4])
Counter({3: 3, 1: 2, 2: 1, 4: 1})
>>> Counter(([1], [1]))
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
整数是可散列的,因此Counter
可以正常工作。另一方面,列表不可散列,因此Counter
失败并带有TypeError
.
可散列意味着您的对象必须具有在其生命周期中永远不会改变的散列值。这是一个要求,因为这些对象将用作字典键。在 Python 中,不可变对象也是可散列的。
由于Counter
是 的子类dict
,它们的接口大体相同。但是,存在一些细微的差异。第一个区别是Counter
没有实现.fromkeys()
. 这避免了不一致,例如Counter.fromkeys("abbbc", 2)
,每个字母都有一个初始计数,2
而不管它在输入迭代中的实际计数如何。
第二个区别是.update()
不会用新的计数替换现有对象(键)的计数(值)。它将两个计数加在一起:
>>> from collections import Counter
>>> letters = Counter("mississippi")
>>> letters
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
>>> # Update the counts of m and i
>>> letters.update(m=3, i=4)
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2})
>>> # Add a new key-count pair
>>> letters.update({"a": 2})
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2, 'a': 2})
>>> # Update with another counter
>>> letters.update(Counter(["s", "s", "p"]))
>>> letters
Counter({'i': 8, 's': 6, 'm': 4, 'p': 3, 'a': 2})
在这里,您更新计数m
和i
。现在,这些字母包含它们初始计数的总和加上您通过 传递给它们的值.update()
。如果您使用原始计数器中不存在的键,则.update()
创建具有相应值的新键。最后,.update()
接受可迭代对象、映射、关键字参数以及其他计数器。
注意:由于Counter
是 的子类dict
,因此对可以存储在计数器的键和值中的对象没有限制。键可以存储任何可散列的对象,而值可以存储任何对象。但是,要在逻辑上用作计数器,值应该是表示计数的整数。
Counter
and之间的另一个区别dict
是访问丢失的键返回0
而不是提高 a KeyError
:
>>> from collections import Counter
>>> letters = Counter("mississippi")
>>> letters["a"]
0
此行为表示计数器中不存在的对象的计数为零。在此示例中,该字母"a"
不在原始单词中,因此其计数为0
。
在 Python 中,Counter
也可用于模拟multiset或bag。Multisets 类似于set,但它们允许给定元素的多个实例。一个元素的实例数被称为它的多重性。例如,您可以有一个像 {1, 1, 2, 3, 3, 3, 4, 4} 这样的多重集。
当你Counter
用来模拟多重集时,键代表元素,值代表它们各自的多重性:
>>> from collections import Counter
>>> multiset = Counter({1, 1, 2, 3, 3, 3, 4, 4})
>>> multiset
Counter({1: 1, 2: 1, 3: 1, 4: 1})
>>> multiset.keys() == {1, 2, 3, 4}
True
在这里, 的键multiset
相当于一个 Python 集。这些值包含集合中每个元素的多重性。
Python'Counter
提供了一些附加功能,可帮助您将它们作为多集使用。例如,您可以使用元素及其多重性的映射来初始化计数器。您还可以对元素的多重性等执行数学运算。
假设您在当地的宠物收容所工作。你有一定数量的宠物,你需要记录每天有多少宠物被领养,有多少宠物进出收容所。在这种情况下,您可以使用Counter
:
>>> from collections import Counter
>>> inventory = Counter(dogs=23, cats=14, pythons=7)
>>> adopted = Counter(dogs=2, cats=5, pythons=1)
>>> inventory.subtract(adopted)
>>> inventory
Counter({'dogs': 21, 'cats': 9, 'pythons': 6})
>>> new_pets = {"dogs": 4, "cats": 1}
>>> inventory.update(new_pets)
>>> inventory
Counter({'dogs': 25, 'cats': 10, 'pythons': 6})
>>> inventory = inventory - Counter(dogs=2, cats=3, pythons=1)
>>> inventory
Counter({'dogs': 23, 'cats': 7, 'pythons': 5})
>>> new_pets = {"dogs": 4, "pythons": 2}
>>> inventory += new_pets
>>> inventory
Counter({'dogs': 27, 'cats': 7, 'pythons': 7})
那很整齐!现在,您可以使用 来记录您的宠物Counter
。请注意,您可以使用.subtract()
和.update()
来减去和添加计数或多重性。您还可以使用加法 ( +
) 和减法 ( -
) 运算符。
Counter
在 Python 中将对象作为多重集还有很多事情可以做,所以继续尝试吧!
将字典链接在一起: ChainMap
Python 将ChainMap
多个字典和其他映射组合在一起以创建一个与常规字典非常相似的对象。换句话说,它需要多个映射并使它们在逻辑上表现为一个。
ChainMap
对象是可更新的视图,这意味着任何链接映射的更改都会影响整个ChainMap
对象。这是因为ChainMap
不会将输入映射合并在一起。它保留了一个映射列表,并在该列表的顶部重新实现了常见的字典操作。例如,键查找会连续搜索映射列表,直到找到键为止。
注意:查看Python 的 ChainMap:有效管理多个上下文以更深入地了解ChainMap
在 Python 代码中的使用。
当您使用ChainMap
对象时,您可以拥有多个带有唯一键或重复键的字典。
在任何一种情况下,都ChainMap
允许您将所有字典视为一个。如果您的字典中具有唯一键,则可以像使用单个字典一样访问和更新这些键。
如果您的字典中有重复的键,除了将您的字典作为一个来管理之外,您还可以利用映射的内部列表来定义某种访问优先级。由于此功能,ChainMap
对象非常适合处理多个上下文。
例如,假设您正在开发一个命令行界面 (CLI)应用程序。该应用程序允许用户使用代理服务连接到 Internet。设置优先级是:
- 命令行选项 (
--proxy
,-p
) - 用户主目录中的本地配置文件
- 全局代理配置
如果用户在命令行提供代理,则应用程序必须使用该代理。否则,应用程序应使用下一个配置对象中提供的代理,依此类推。这是最常见的用例之一ChainMap
。在这种情况下,您可以执行以下操作:
>>> from collections import ChainMap
>>> cmd_proxy = {} # The user doesn't provide a proxy
>>> local_proxy = {"proxy": "proxy.local.com"}
>>> global_proxy = {"proxy": "proxy.global.com"}
>>> config = ChainMap(cmd_proxy, local_proxy, global_proxy)
>>> config["proxy"]
'proxy.local.com'
ChainMap
允许您为应用程序的代理配置定义适当的优先级。键查找搜索cmd_proxy
,然后local_proxy
,最后global_proxy
,返回手头键的第一个实例。在此示例中,用户未在命令行提供代理,因此您的应用程序使用local_proxy
.
通常,ChainMap
对象的行为类似于常规dict
对象。但是,它们具有一些附加功能。例如,它们有一个.maps
保存内部映射列表的公共属性:
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums.maps
[{'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'}]
实例属性.maps
使您可以访问内部映射列表。此列表可更新。您可以手动添加和删除映射、遍历列表等。
此外,ChainMap
提供了一个.new_child()
方法和一个.parents
属性:
>>> from collections import ChainMap
>>> dad = {"name": "John", "age": 35}
>>> mom = {"name": "Jane", "age": 31}
>>> 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}
>>> family.parents
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})
使用.new_child()
,您可以创建一个ChainMap
包含新地图 ( son
)的新对象,后跟当前实例中的所有地图。作为第一个参数传递的地图成为地图列表中的第一个地图。如果您不传递地图,则该方法使用空字典。
该parents
属性返回一个ChainMap
包含当前实例中除第一个之外的所有地图的新对象。当您需要在键查找中跳过第一个映射时,这很有用。
最后一个要强调的功能ChainMap
是变异操作,例如更新键、添加新键、删除现有键、弹出键和清除字典,作用于内部映射列表中的第一个映射:
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> # Add a new key-value pair
>>> alpha_nums["c"] = "C"
>>> alpha_nums
ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'})
>>> # Pop a key that exists in the first dictionary
>>> alpha_nums.pop("two")
2
>>> alpha_nums
ChainMap({'one': 1, 'c': 'C'}, {'a': 'A', 'b': 'B'})
>>> # Delete keys that don't exist in the first dict but do in others
>>> del alpha_nums["a"]
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'a'"
>>> # Clear the dictionary
>>> alpha_nums.clear()
>>> alpha_nums
ChainMap({}, {'a': 'A', 'b': 'B'})
这些示例表明对ChainMap
对象的变异操作仅影响内部列表中的第一个映射。这是您在使用ChainMap
.
棘手的部分是,乍一看,似乎可以改变给定ChainMap
. 但是,您只能.maps
更改第一个映射中的键值对,除非您使用直接访问和更改列表中的其他映射。
定制的内置插件:UserString
,UserList
,和UserDict
有时您需要自定义内置类型,例如字符串、列表和字典,以添加和修改某些行为。从Python 2.2 开始,您可以通过直接对这些类型进行子类化来实现。但是,您可能会遇到这种方法的一些问题,稍后您将看到。
Pythoncollections
提供了三个方便的包装类来模拟内置数据类型的行为:
UserString
UserList
UserDict
结合常规方法和特殊方法,您可以使用这些类来模仿和自定义字符串、列表和字典的行为。
如今,开发人员经常问自己是否有理由使用UserString
, UserList
, 以及UserDict
何时需要自定义内置类型的行为。答案是肯定的。
内置类型的设计和实现考虑到了开闭原则。这意味着它们对扩展开放,但对修改关闭。允许修改这些类的核心特性可能会破坏它们的不变量。因此,Python 核心开发人员决定保护它们免受修改。
例如,假设您需要一个在插入键时自动将键小写的字典。您可以子类化dict
和覆盖,.__setitem__()
以便每次插入键时,字典都会小写键名:
>>> class LowerDict(dict):
... def __setitem__(self, key, value):
... key = key.lower()
... super().__setitem__(key, value)
...
>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> ordinals.update({"FOURTH": 4})
>>> ordinals
{'FIRST': 1, 'SECOND': 2, 'third': 3, 'FOURTH': 4}
>>> isinstance(ordinals, dict)
True
当您使用带有方括号 ( []
) 的字典样式分配插入新键时,此字典可以正常工作。但是,当您将初始字典传递给类构造函数或使用.update()
. 这意味着您需要覆盖.__init__()
、.update()
和其他一些方法才能使您的自定义字典正常工作。
现在看看同一个字典,但UserDict
用作基类:
>>> from collections import UserDict
>>> class LowerDict(UserDict):
... def __setitem__(self, key, value):
... key = key.lower()
... super().__setitem__(key, value)
...
>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> ordinals.update({"FOURTH": 4})
>>> ordinals
{'first': 1, 'second': 2, 'third': 3, 'fourth': 4}
>>> isinstance(ordinals, dict)
False
有用!您的自定义字典现在会将所有新键转换为小写字母,然后再将它们插入字典中。请注意,由于您不dict
直接继承自,因此您的类不会返回dict
上面示例中的实例。
UserDict
将常规字典存储在名为 的实例属性中.data
。然后它围绕该字典实现所有方法。UserList
并UserString
以相同的方式工作,但它们的.data
属性分别包含 alist
和一个str
对象。
如果您需要自定义这些类中的任何一个,那么您只需要覆盖适当的方法并根据需要更改它们的操作。
通常,当您需要一个与底层包装的内置类几乎相同的类并且您想要自定义其标准功能的某些部分时,您应该使用UserDict
, UserList
, 和UserString
。
使用这些类而不是内置的等效类的另一个原因是访问底层.data
属性以直接操作它。
从内置类型直接在很大程度上取代了使用的能力,继承UserDict
,UserList
以及UserString
。然而,内置类型的内部实现使得在不重写大量代码的情况下很难安全地继承它们。在大多数情况下,使用来自collections
. 它会让你免于几个问题和奇怪的行为。
结论
在 Python 的collections
模块中,您有几种专门的容器数据类型,可用于解决常见的编程问题,例如计数对象、创建队列和堆栈、处理字典中丢失的键等等。
中的数据类型和类collections
被设计为高效和 Pythonic。它们对您的 Python 编程之旅非常有帮助,因此学习它们非常值得您花时间和精力。
在本教程中,您学习了如何:
- 使用编写可读和显式的代码
namedtuple
- 建立高效的队列和堆栈使用
deque
- 使用有效地计数对象
Counter
- 处理缺少字典键与
defaultdict
- 记住键的插入顺序
OrderedDict
- 在一个视图中链接多个字典
ChainMap
您还了解了三种简便的包装类:UserDict
,UserList
,和UserString
。这些类是方便当你需要创建一个模仿的行为内置类型的自定义类dict
,list
和str
。
- 点赞
- 收藏
- 关注作者
评论(0)