49 KiB
Python 的集合:专门化数据类型的自助餐
Python 的 collections 模块提供了一组丰富的专用容器数据类型,这些数据类型经过精心设计,以 python 化且高效的方式处理特定的编程问题。该模块还提供了包装类,使得创建行为类似于内置类型dict、list和str的定制类更加安全。
学习collections中的数据类型和类将允许你用一套有价值的可靠而有效的工具来扩充你的编程工具包。
在本教程中,您将学习如何:
- 用
namedtuple编写可读和显式代码 - 使用
deque构建高效队列和堆栈 - 用
Counter快速计数物体 - 用
defaultdict处理缺失的字典键 - 用
OrderedDict保证插入顺序 - 使用
ChainMap将多个字典作为一个单元进行管理
为了更好地理解collections中的数据类型和类,你应该知道使用 Python 内置数据类型的基础知识,比如列表、元组和字典。另外,文章的最后一部分需要一些关于 Python 中面向对象编程的基础知识。
免费下载: 从 Python 技巧中获取一个示例章节:这本书用简单的例子向您展示了 Python 的最佳实践,您可以立即应用它来编写更漂亮的+Python 代码。
Python 的collections 入门
回到 Python 2.4 , Raymond Hettinger 为标准库贡献了一个名为 collections 的新模块。目标是提供各种专门的集合数据类型来解决特定的编程问题。
当时,collections只包含一个数据结构, deque ,专门设计为一个双端队列,支持序列两端高效的追加和弹出操作。从这一点开始,标准库中的几个模块利用了deque来提高它们的类和结构的性能。一些突出的例子是 queue 和 threading 。
随着时间的推移,一些专门的容器数据类型填充了该模块:
| 数据类型 | Python 版本 | 描述 |
|---|---|---|
T2deque |
2.4 | 一个类似序列的集合,支持从序列的任意一端有效地添加和移除项 |
T2defaultdict |
2.5 | 字典子类,用于为缺失的键构造默认值,并自动将它们添加到字典中 |
T2namedtuple() |
2.6 | 一个用于创建tuple子类的工厂函数,提供命名字段,允许通过名称访问项目,同时保持通过索引访问项目的能力 |
T2OrderedDict |
2.7 , 3.1 | 字典子类,根据插入键的时间保持键-值对的顺序 |
T2Counter |
2.7 , 3.1 | 字典子类,支持对序列或可重复项中的唯一项进行方便的计数 |
T2ChainMap |
3.3 | 一个类似字典的类,允许将多个映射作为单个字典对象处理 |
除了这些专门的数据类型,collections还提供了三个基类来帮助创建定制列表、字典和字符串:
| 班级 | 描述 |
|---|---|
T2UserDict |
围绕字典对象的包装类,便于子类化dict |
T2UserList |
围绕列表对象的包装类,便于子类化list |
T2UserString |
一个围绕字符串对象的包装类,便于子类化string |
对这些包装类的需求部分被相应的标准内置数据类型的子类化能力所掩盖。但是,有时使用这些类比使用标准数据类型更安全,也更不容易出错。
有了对collections的简要介绍以及本模块中的数据结构和类可以解决的具体用例,是时候更仔细地研究它们了。在此之前,需要指出的是,本教程整体上是对collections的介绍。在接下来的大部分章节中,您会发现一个蓝色的警告框,它会引导您找到关于这个类或函数的专门文章。
提高代码可读性:namedtuple()
Python 的namedtuple()是一个工厂函数,允许你用命名字段创建tuple子类。这些字段使用点符号让您直接访问给定命名元组中的值,就像在obj.attr中一样。
之所以需要这个特性,是因为使用索引来访问常规元组中的值很烦人,难以阅读,而且容易出错。如果您正在处理的元组有几个项,并且是在远离您使用它的地方构造的,这一点尤其正确。
**注:**查看使用 namedtuple 编写 Python 和 Clean 代码,深入了解如何在 Python 中使用namedtuple。
在 Python 2.6 中,开发人员可以用点符号访问带有命名字段的 tuple 子类,这似乎是一个理想的特性。这就是namedtuple()的由来。如果与常规元组相比,用这个函数构建的元组子类在代码可读性方面是一大优势。
为了正确看待代码可读性问题,考虑一下 divmod() 。这个内置函数接受两个(非复杂的)数字,并返回一个元组,该元组具有输入值的整数除法的商和余数:
>>> divmod(12, 5)
(2, 2)
它工作得很好。然而,这个结果是否具有可读性?你能说出输出中每个数字的含义吗?幸运的是,Python 提供了一种改进方法。您可以使用namedtuple编写带有显式结果的自定义版本的divmod():
>>> 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()创建新的 tuple 子类,需要两个必需的参数:
typename是您正在创建的类的名称。它必须是一个带有有效 Python 标识符的字符串。field_names是字段名列表,您将使用它来访问结果元组中的项目。它可以是:- 一个可迭代的字符串,比如
["field1", "field2", ..., "fieldN"] - 由空格分隔的字段名组成的字符串,例如
"field1 field2 ... fieldN" - 用逗号分隔字段名的字符串,如
"field1, field2, ..., fieldN"
- 一个可迭代的字符串,比如
例如,以下是使用namedtuple()创建具有两个坐标(x和y)的样本 2D Point的不同方法:
>>> 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)
在这些例子中,首先使用字段名的list创建Point。然后你实例化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')
这里,首先使用namedtuple()创建一个Person类。这一次,您使用一个名为defaults的可选参数,它接受元组字段的一系列默认值。注意namedtuple()将默认值应用于最右边的字段。
在第二个例子中,您使用 ._asdict() 从现有的命名元组创建一个字典。该方法返回一个使用字段名作为键的新字典。
最后,你用 ._replace() 替换job的原始值。这个方法不更新 tuple 的位置,而是返回一个新命名的 tuple,其新值存储在相应的字段中。你知道为什么._replace()返回一个新的命名元组吗?
构建高效的队列和堆栈:deque
Python 的 deque 是collections中第一个数据结构。这种类似序列的数据类型是对堆栈和队列的概括,旨在支持数据结构两端的高效内存和快速追加和弹出操作。
注:字deque读作“deck”,代表ddouble-eenddqueUE。
在 Python 中,在list对象的开头或左侧进行追加和弹出操作效率很低,时间复杂度O(n)。如果处理大型列表,这些操作的开销会特别大,因为 Python 必须将所有项目移到右边,以便在列表的开头插入新项目。
另一方面,列表右侧的 append 和 pop 操作通常是高效的( O (1)),除非 Python 需要重新分配内存来增加底层列表以接受新项。
Python 的deque就是为了克服这个问题而产生的。在一个deque对象两侧的追加和弹出操作是稳定的和同样有效的,因为 deques 被实现为一个双向链表。这就是为什么 deques 对于创建堆栈和队列特别有用。
以一个队列为例。它以先进/先出 ( 先进先出)的方式管理项目。它就像一个管道,你在管道的一端推入新的项目,从另一端弹出旧的项目。将一个项目添加到队列的末尾被称为入队操作。从队列的前面或开始处移除一个项目称为出列。
**注:**查看 Python 的 dequee:implementing Efficient queue and Stacks以深入探究如何在 Python 代码中使用deque。
现在假设你正在为一个排队买电影票的人建模。你可以用一个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持有一个作为初始化器的 iterable。maxlen保存一个指定deque最大长度的整数。
如果你不提供一个iterable,那么你会得到一个空的队列。如果您为 maxlen 提供一个值,那么您的 deque 将只存储最多maxlen个项目。
拥有一个maxlen是一个方便的特性。例如,假设您需要在一个应用程序中实现一个最近文件的列表。在这种情况下,您可以执行以下操作:
>>> 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)
一旦 dequeue 达到其最大大小(本例中为三个文件),在 dequeue 的一端添加新文件会自动丢弃另一端的文件。如果您不为maxlen提供一个值,那么 deque 可以增长到任意数量的项目。
到目前为止,您已经学习了 deques 的基本知识,包括如何创建 deques 以及如何从给定的 deques 的两端追加和弹出项目。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])
在这些例子中,您首先使用不同类型的 iterables 创建 deques 来初始化它们。deque和list的一个区别是deque.pop()不支持弹出给定索引处的项目。
注意,deque为.append()、、.pop()、、.extend()、提供了姊妹方法,并带有后缀left来表示它们在底层 deque 的左端执行相应的操作。
Deques 也支持序列操作:
| 方法 | 描述 |
|---|---|
T2.clear() |
从队列中删除所有元素 |
T2.copy() |
创建一个 deque 的浅层副本 |
T2.count(x) |
计算等于x的双队列元素的数量 |
T2.remove(value) |
删除第一次出现的value |
deques 的另一个有趣的特性是能够使用.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'])
该方法向右旋转 deque n步骤。n的默认值为1。如果给n提供一个负值,那么旋转向左。
最后,您可以使用索引来访问 dequee 中的元素,但是您不能对 dequee 进行切片:
>>> 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'
Deques 支持索引,但有趣的是,它们不支持切片。当您试图从现有的队列中检索一个切片时,您会得到一个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()并没有为你创建新的密匙。
由于处理字典中丢失的键是一种常见的需求,Python 的collections也为此提供了一个工具。defaultdict类型是dict的子类,旨在帮助你解决丢失的键。
**注意:**查看使用 Python defaultdict 类型处理丢失的键,深入了解如何使用 Python 的defaultdict。
defaultdict的构造函数将一个函数对象作为它的第一个参数。当您访问一个不存在的键时,defaultdict自动调用该函数,不带参数,为手边的键创建一个合适的默认值。
为了提供其功能,defaultdict将输入函数存储在 .default_factory 中,然后覆盖 .__missing__() 以在您访问任何丢失的键时自动调用该函数并生成默认值。
你可以使用任何可调用来初始化你的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']
在这个例子中,您有关于宠物及其品种的原始数据,您需要按照宠物对它们进行分组。为此,在创建defaultdict实例时,使用list()作为.default_factory。这使您的字典能够自动创建一个空列表([])作为您访问的每个缺失键的默认值。然后你用这个列表来存储你的宠物的品种。
最后,你应该注意到由于defaultdict是dict的子类,它提供了相同的接口。这意味着你可以像使用普通字典一样使用你的defaultdict对象。
保持字典有序:OrderedDict
有时,您需要字典来记住键值对的插入顺序。多年来,Python 的常规字典是无序的数据结构。所以,回到 2008 年, PEP 372 引入了给collections添加一个新字典类的想法。
新的类会根据钥匙插入的时间记住项目的顺序。这就是 OrderedDict 的由来。
OrderedDict在 Python 3.1 中引入。其应用编程接口(API)与dict基本相同。然而,OrderedDict按照键被第一次插入字典的顺序遍历键和值。如果为现有键分配一个新值,则键-值对的顺序保持不变。如果一个条目被删除并重新插入,那么它将被移动到字典的末尾。
**注:**查看Python 中的 OrderedDict vs dict:工作的正确工具以深入了解 Python 的OrderedDict以及为什么应该考虑使用它。
有几种方法可以创建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 : 向后兼容至少还有一个原因。在运行 than 3.6 之前版本的环境中,依靠常规的dict对象来保持项目的顺序会破坏您的代码。
好了,现在是时候看看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
对象计数是编程中常见的操作。假设你需要计算一个给定的条目在列表或 iterable 中出现了多少次。如果你的清单很短,那么计算清单上的项目会很简单快捷。如果你有一个很长的清单,那么计算清单会更有挑战性。
为了计数对象,你通常使用一个计数器,或者一个初始值为零的整数变量。然后递增计数器以反映给定对象出现的次数。
在 Python 中,你可以使用字典一次计算几个不同的对象。在这种情况下,键将存储单个对象,值将保存给定对象的重复次数,或对象的计数。
这里有一个例子,用一个普通的字典和一个 for循环来计算单词"mississippi"中的字母:
>>> 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 objects 在计数的时候很方便,因为你不需要检查键是否存在。字典保证任何丢失的键都有适当的默认值:
>>> 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子类。
以下是使用Counter编写"mississippi"示例的方法:
>>> from collections import Counter
>>> Counter("mississippi")
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
哇!真快!一行代码就完成了。在这个例子中,Counter遍历"mississippi",生成一个字典,将字母作为键,将它们的频率作为值。
**注:**查看 Python 的计数器:计算对象的 Python 方式深入了解Counter以及如何使用它高效地计算对象。
有几种不同的方法来实例化Counter。您可以使用列表、元组或任何具有重复对象的 iterables。唯一的限制是你的对象必须是可散列的 T4:
>>> 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中的,经过高度优化的 C 函数提供计数功能。如果这个函数由于某种原因不可用,那么这个类使用一个等效的但是效率较低的 Python 函数。
由于Counter是dict的子类,所以它们的接口大多相同。但是,也有一些微妙的区别。第一个区别是Counter没有实现 .fromkeys() 。这避免了不一致,比如Counter.fromkeys("abbbc", 2),其中每个字母都有一个初始计数2,而不管它在输入 iterable 中的实际计数。
第二个区别是 .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和dict的另一个区别是,访问丢失的键会返回0,而不是引发KeyError:
>>> from collections import Counter
>>> letters = Counter("mississippi")
>>> letters["a"]
0
这种行为表明计数器中不存在的对象的计数为零。在这个例子中,字母"a"不在原始单词中,所以它的计数是0。
在 Python 中,Counter也可以用来模拟一个多重集或包。多重集类似于集,但是它们允许给定元素的多个实例。一个元素的实例数量被称为它的多重性。例如,您可以有一个类似{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()来加减计数或重数。您也可以使用加法(+)和减法(-)运算符。
在 Python 中,您可以将Counter对象作为多重集来做更多的事情,所以请大胆尝试吧!
将字典链接在一起:ChainMap
Python 的ChainMap将多个字典和其他映射组合在一起,创建一个单一对象,其工作方式非常类似于常规字典。换句话说,它接受几个映射,并使它们在逻辑上表现为一个映射。
ChainMap对象是可更新的视图,这意味着任何链接映射的变化都会影响到整个ChainMap对象。这是因为ChainMap没有将输入映射合并在一起。它保留了一个映射列表,并在该列表的顶部重新实现了公共字典操作。例如,关键字查找会连续搜索映射列表,直到找到该关键字。
**注意:**查看 Python 的 ChainMap:有效管理多个上下文,深入了解如何在 Python 代码中使用ChainMap。
当你使用ChainMap对象时,你可以有几个字典,或者是唯一的或者是重复的键。
无论哪种情况,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"}
>>> 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),后跟当前实例中的所有地图。作为第一个参数传递的映射成为映射列表中的第一个映射。如果没有传递 map,那么这个方法使用一个空字典。
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和UserDictT3
有时您需要定制内置类型,如字符串、列表和字典,以添加和修改某些行为。从 Python 2.2 开始,你可以通过直接子类化这些类型来实现。但是,这种方法可能会遇到一些问题,您马上就会看到。
Python 的collections提供了三个方便的包装类,模拟内置数据类型的行为:
UserStringUserListUserDict
通过常规和特殊方法的组合,您可以使用这些类来模拟和定制字符串、列表和字典的行为。
现在,开发人员经常问自己,当他们需要定制内置类型的行为时,是否有理由使用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属性分别拥有一个list和一个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的行为的定制类时,这些类非常方便。*******