小甲鱼 发表于 2023-6-8 04:28:37

collections -- 高阶容器类型



collections -- 高阶容器类型

collections 模块封装了一些非常好用的容器类型,它们可以作为 Python 标准容器(如 dict、list、set、tuple)的高级扩展,甚至完全替代它们。

下面按照出场顺序,给大家简单列一个说明表格:


页码 内容 描述
2 namedtuple() 创建命名元组的工厂函数
3 Counter 一个用于计数的类
4 deque 一个实现双端队列的容器
5 OrderedDict 字典(dict)的子类,不过它可以确保字典有序
6 defaultdict 字典(dict)的子类,当访问不存在的键时,返回一个默认值而不是抛出 KeyError 异常
7 ChainMap 类似字典(dict)的容器类,实现了将多个字典或映射链接在一起的方式
8 UserDict 封装了字典(dict)类,当需要创建自定义的字典类时,继承 UserDict 相比直接继承 dict 要更加便捷和安全
9 UserList 封装了列表(list)类,当需要创建自定义的列表类时,继承 UserList 相比直接继承 list 要更加便捷和安全
10 UserString 封装了字符串(str)类,当需要创建自定义的列表类时,继承 UserList 相比直接继承 str 要更加便捷和安全

请点击【页码】查询相应的详细文档

namedtuple() -- 创建命名元组的工厂函数

视频讲解:

https://www.bilibili.com/video/BV1m14y1R7Fc?p=1


collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)

参数讲解


参数 描述
typename 新的元组名称,它必须是一个有效的 python 标识符。
field_names 一个字符串序列,用于定义元组每个字段的名称。可以是一个空格或逗号分隔的字符串,或者是一个字符串列表。例如 'name age','name,age' 和 ['name', 'age'] 都是有效的。
rename (可选,默认值是 False)如果为 True,则无效的 field_names 参数将自动替换为位置名称。例如,如果 field_names 是 ['abc', 'def', 'ghi', 'abc'],并且 rename=True,那么 'def' 和第二个 'abc' 将会被替换为 '_1' 和 '_3'。
defaults (可选)一个可迭代的对象,用于设置字段的默认值。默认值从右向左应用。例如,如果 field_names 是 ['a', 'b', 'c', 'd'],并且 defaults 是 ,那么 'c' 的默认值将是 1,'d' 的默认值将是 2。注:这个参数是 Python 3.7 新添加的
module (可选)如果设置,那么生成的元组类的 __module__ 属性将被设置为此值。注:这个参数是 Python 3.6 新添加的

namedtuple() 用于创建一个新的类似于元组的数据类型,这个新的数据类型同时也支持通过属性名来访问元素。

与普通的元组相比,namedtuple() 创建的对象更具可读性,因为不需要记住元素的索引或者正确的解包顺序。

给大家举个简单的例子:

from collections import namedtuple

# 定义一个名为 'Person' 的命名元组,具有 'name' 和 'age' 两个字段(元素)
Person = namedtuple('Person', ['name', 'age'])

# 创建一个 'Person' 对象,并为两个字段赋值
man = Person(name='小甲鱼', age=18)

# 访问元素
print(man.name)# 小甲鱼
print(man.age)# 18

# 由于命名元组仍然是元组,所以试图修改其元素值的操作仍然是不被允许的
man.age = 28# 报错
下面是一个官方示例:

>>> # 基础示例
>>> Point = namedtuple('Point', ['x', 'y'])# 定义一个名为 'Point' 的命名元组,包含 'x' 和 'y' 两个字段
>>> p = Point(11, y=22)   # 使用位置参数或关键字参数实例化
>>> p + p             # 可以像普通元组(11, 22)一样通过索引访问
33
>>> x, y = p                # 可以像普通元组一样解包
>>> x, y
(11, 22)
>>> p.x + p.y               # 字段也可以通过名称访问
33
>>> p                     # 可读的 __repr__,以 name=value 的形式展示
Point(x=11, y=22)

三个方法和两个属性

除了继承 Python 元组的所有方法之外,命名元组还支持额外的三个方法和两个属性。

为了防止命名冲突,这些方法和属性均以下划线开头。

classmethod somenamedtuple._make(iterable)

_make(iterable) 是一个类方法,用于从一个可迭代对象创建一个命名元组实例。

用法如下:

>>> from collections import namedtuple
>>> Person = namedtuple('Person', ['name', 'age'])
>>> man = Person._make(['小甲鱼', 18])
>>> man.name
'小甲鱼'
>>> man.age
18

somenamedtuple._asdict()

_asdict() 方法返回一个字典,该字典的键是命名元组的字段名,值是对应的元素值。

用法如下:

>>> man._asdict()
{'name': '小甲鱼', 'age': 18}

somenamedtuple._replace(**kwargs)

_replace(**kwargs) 方法返回一个新的命名元组实例,并将替换指定字段的值。

用法如下:

>>> man._replace(age=28)
Person(name='小甲鱼', age=28)

somenamedtuple._fields

_fields 是一个包含命名元组所有字段名的元组。

用法如下:

>>> man._fields
('name', 'age')

somenamedtuple._field_defaults

_field_defaults 是一个包含命名元组所有字段名及其对应默认值的字典。

用法如下:

>>> Employee = namedtuple('Employee', ['name', 'age', 'job'], defaults=['Unknown', 0, 'Unknown'])
>>> Employee._field_defaults
{'name': 'Unknown', 'age': 0, 'job': 'Unknown'}

进一步讲解

将一个字典转换为命名元组,我们可以使用 ** 进行解包操作(忘记的童鞋可以看一下这一节课程 -> 传送门):

>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(**{'x':11, 'y':22})
>>> p
Point(x=11, y=22)
获取命名元组其中一个字段的值,我们可以使用 getattr() 函数:

>>> getattr(p, 'x')
11
>>> getattr(p, 'y')
22
由于命名元组是一个正常的 Python 类,因此它可以被轻松地继承并升级其功能。

下面我们演示一个计算 Point 到原点 (0, 0) 距离的代码:

>>> class Point(namedtuple('Point', ['x', 'y'])):
...   __slots__ = ()
...   @property
...   def hypot(self):
...         return (self.x ** 2 + self.y ** 2) ** 0.5
...   def __str__(self):
...         return "Point: x=%6.3fy=%6.3fhypot=%6.3f" % (self.x, self.y, self.hypot)
...
>>> for p in Point(3, 4), Point(14, 5/7):
...   print(p)
...
Point: x= 3.000y= 4.000hypot= 5.000
Point: x=14.000y= 0.714hypot=14.018
注1:上面的子类设置 __slots__ 为一个空元组,通过阻止创建实例字典保持了较低的内存开销(忘记的童鞋可以看一下这一节课程 -> 传送门)。

注2:@property 装饰器的作用是让这个类一旦被访问就自动去调用 hypot() 方法(忘记的童鞋可以看一下这一节课程 -> 传送门)。

如果需要添加一个新的字段,我们应该通过 _fields 创建一个新的命名元组来实现:

>>> Point3D = namedtuple('Point3D', Point._fields + ('z',))
文档字符串可以自定义,通过直接赋值给 __doc__ 属性:

>>> Book = namedtuple('Book', ['id', 'title', 'authors'])
>>> Book.__doc__ += ': Hardcover book in active collection'
>>> Book.id.__doc__ = '13-digit ISBN'
>>> Book.title.__doc__ = 'Title of first printing'
>>> Book.authors.__doc__ = 'List of authors sorted by last name'
>>> book = Book("978-7-302-51408-4", "《零基础入门学习Pyhon》", "小甲鱼")
>>> help(Book.id)
Help on _tuplegetter:

    13-digit ISBN

>>> book.id
'978-7-302-51408-4

csv 模块和 sqlite3 模块返回的结果可以被赋值给命名元组:

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print(emp.name, emp.title)
这段代码首先定义了一个名为 EmployeeRecord 的命名元组类型,该类型有五个字段:'name'、'age'、'title'、'department' 和 'paygrade'。

然后,代码中使用了两种不同的方式来从数据源创建 EmployeeRecord 对象,并打印每个员工的名字和职位。

从 CSV 文件中读取数据:

这里使用了 csv.reader() 来打开并读取名为 "employees.csv" 的文件,

然后使用 map() 函数和 EmployeeRecord._make() 方法来将每一行数据转换为 EmployeeRecord 对象。

_make() 是一个类方法,可以从一个序列中创建一个命名元组对象。

最后,代码打印出每个员工的名字和职位。

从 SQLite 数据库中读取数据:

这里首先连接到名为 "/companydata" 的 SQLite 数据库,然后使用 cursor.execute() 方法执行 SQL 查询,

从 'employees' 表中选择所有员工的名字、年龄、职位、部门和薪酬等级。

然后,同样使用 map() 函数和 EmployeeRecord._make() 方法来将查询结果转换为 EmployeeRecord 对象。

最后,代码打印出每个员工的名字和职位。

这段代码就很好地展示了命名元组的一个重要用途 —— 作为一个简单的数据类来存储和操作一组字段。

Counter -- 一个用于计数的类

视频讲解:

https://www.bilibili.com/video/BV1m14y1R7Fc?p=2


class collections.Counter()

Counter 是 collections 模块中用于计数类,它是字典(dict)的子类,用于将输入的序列或映射类型的数据进行统计,并提供给其它函数使用。

Counter 会存储每个输入元素(键)及该元素出现的次数。因此,Counter 非常类似于字典,但是字典存储的是键值对,而 Counter 存储的是元素及其对应的计数。

毫不夸张地说,Counter 在数据分析这块非常实用的,让我们先来看一个例子:

from collections import Counter

text = "In a world where technology is rapidly advancing, it is essential for individuals to stay updated with the latest trends and developments. As a coding enthusiast, I am always on the lookout for new learning resources that can help me hone my skills. One such platform that I have grown to love is FishC.com."

Counter(text).most_common(5)
# 输出:[(' ', 53), ('e', 24), ('t', 22), ('a', 21), ('s', 20)]
朴实无华的一个基本操作,立马就把这一大段字符串中出现频率最高的 5 个字符给打印了出来~

如果不使用 most_common(5) 方法,打印的是所有字符及对应的计数。

初始化 Counter 通常有下面几种方法:

>>> c = Counter()                     # 空对象
>>> c = Counter("I love FishC.com")   # 由迭代器创建
>>> c = Counter({'FishC': 5})         # 由映射(字典)创建
>>> c = Counter(cats = 6, dogs = 8)   # 由关键字参数创建
Counter 对象有一个字典操作的接口,如果访问的键没有相应的记录,会返回 0,而非抛出 KeyError 异常:

c = Counter(['FishA', 'FishC'])
c['FishB']
0
3.7 版修改:由于 Python 3.7 版之后确保了字典的插入顺序,因此 Counter 也继承了该特征。


Counter 对象除了继承大多数的字典方法之外,还额外附加了以下这些方法:

elements()

elements() 方法返回一个迭代器,其中每个元素将重复出现计数值所代表的次数。

元素会按首次出现的顺序返回。如果一个元素的计数值小于 1,将被忽略。

用法如下:

c = Counter(a=4, b=2, c=0, d=-2)
sorted(c.elements())
['a', 'a', 'a', 'a', 'b', 'b']

most_common()

most_common() 返回一个列表,其中包含 n 个频次最高的元素及其相应的次数,按常见程度由高到低排序。

如果 n 被省略或设置为 None,most_common() 将返回所有元素及其次数。

计数值相等的元素按首次出现的顺序排序:

>>> c = Counter("FiiissshhCCCCC")
>>> c.most_common(3)
[('C', 5), ('i', 3), ('s', 3)]

subtract()

从可迭代对象或映射对象中减去元素的计数,有点类似于 dict.update(),但这里是减去计数值,而不是替换。

输入和输出都可以是 0 或者负数:

>>> c = Counter("FiiissshhCCCCC")
>>> d = Counter("FFFishCCC")
>>> c.subtract(d)
>>> c
Counter({'i': 2, 's': 2, 'C': 2, 'h': 1, 'F': -2})

total()

total() 用于计算总计数值。

用法如下:

>>> c = Counter("FFFishCCC")
>>> c.total()
9
3.10 版新功能

通常字典方法都可用于 Counter 对象,除了下面这两个方法的工作方式与字典并不相同:

fromkeys(iterable)

这个类方法没有在 Counter 中实现。


update()

Counter 类的 update() 方法可以用于更新计数,可以接受一个可迭代对象或者一个字典作为参数:

>>> c = Counter()
>>> c.update(["FishA", "FishB", "FishC", "FishC"])
>>> c
Counter({'FishC': 2, 'FishA': 1, 'FishB': 1})
>>> c.update({"FishC":3, "FishB":2})
>>> c
Counter({'FishC': 5, 'FishB': 3, 'FishA': 1})# 注意,它是累加上去的

运算符支持

Counter 还支持多种数学运算来合并计数器!

加法和减法运算通过增加或减少相应元素的计数值来合并计数器。

它们可以处理带有负数计数的输入,但输出结果中将不包含小于或等于零的计数值:

>>> x = Counter(a=3, b=2, c=1)
>>> y = Counter(a=1, b=2, c=3)
>>> x + y
Counter({'a': 4, 'b': 4, 'c': 4})
>>> x - y
Counter({'a': 2})
交集运算返回两个计数器中相应元素的最小计数值,输出结果中将不包含小于或等于零的计数值:

>>> x & y
Counter({'b': 2, 'a': 1, 'c': 1})
并集运算返回两个计数器中相应元素的最大计数值,输出结果中将不包含小于或等于零的计数值:

>>> x | y
Counter({'a': 3, 'c': 3, 'b': 2})
相等和包含运算通过比较相应的计数值,来确定两个计数器是否相等,或一个计数器是否包含另一个计数器:

>>> x == y
False
>>> x >= y
False
另外值得一提的还有单目 + 和 - 运算符也可以用于 Counter 对象,它们分别表示一元正(不变)和一元负(取负数)操作。

这些运算符将遍历计数器中的所有元素,并根据运算符执行相应的操作:

>>> c = Counter(a=3, b=-2, c=1)
>>> +c
Counter({'a': 3, 'c': 1})
>>> -c
Counter({'b': 2})

重要更改

3.10 版更改:在相等性检测中,不存在的元素会被当作计数值为零。 在此之前,Counter(a=3) 和 Counter(a=3, b=0) 会被视为不同。

deque -- 一个实现双端队列的容器

视频讲解:

录制中~


class collections.deque(])

返回一个新的 deque 对象,该对象从左到右(使用 append() 方法)使用来自 iterable 参数指定的数据进行初始化。

如果没有指定 iterable 参数,则新的 deque 对象为空。

双端队列是栈和队列的泛化(名称读作 “deck”,是 “双端队列” 的简称)。

双端队列支持线程安全,并且内存高效地从 deque 的任一端添加和弹出元素,无论哪个方向,其性能大约为 O(1)。

虽然 list 对象支持类似的操作,但它针对快速固定长度的操作进行了优化,并且对改变底层数据表示的大小和位置的 pop(0) 和 insert(0, v) 操作产生了 O(n) 的内存移动成本。

如果没有指定 maxlen 参数或者将它设置为 None,那么 deque 可能会增长到任意长度(否则,deque 的长度限制为指定的最大长度)。

一旦有界长度的 deque 满了,在添加新的项目时,将会从另一端丢弃相应数量的项目。有界长度的 deque 提供了类似于 Unix 中的 tail 过滤器的功能。

例如,如果你有一个最大长度为 10 的 deque,当你向其中添加第 11 个元素时,第一个元素将被移除。这样,不论何时,deque 中始终保持最近添加的 10 个元素。因此,这个特性使得 deque 对于跟踪最近的活动(例如,最近的交易或者最新的日志条目)非常有用。


deque 对象支持以下方法:

append(x)

添加 x 到 deque 的右侧。


appendleft(x)

添加 x 到 deque 的左侧。


clear()

从 deque 中删除所有元素,使其长度为 0。


copy()

创建 deque 的浅拷贝。

3.5 版新功能


count(x)

计算 deque 中,值为 x 的元素数量。

3.2 版新功能


extend(iterable)

将 iterable 参数指定的内容添加扩展到 deque 的右侧位置。


extendleft(iterable)

将 iterable 参数指定的内容添加扩展到 deque 的左侧位置。

注意:左侧添加的系列操作会导致 iterable 参数中元素的顺序倒置。


index(x[, start[, stop]])

返回 x 在 deque 中的位置(在 start 索引之后和 stop 索引之前)。如果找到,返回第一个匹配项,如果未找到,抛出 ValueError 异常。

3.5 版新功能


insert(i, x)

在 deque 的位置 i 处插入 x 元素。

如果插入会导致有界 deque 超出 maxlen(最大长度),抛出 IndexError 异常。

3.5 版新功能


pop()

从 deque 的右侧删除并返回一个元素。

如果 deque 中没有元素了,抛出 IndexError 异常。


popleft()

从 deque 的左侧删除并返回一个元素。

如果 deque 中没有元素了,抛出 IndexError 异常。


remove(value)

删除第一个值为 value 的元素。如果未找到,抛出 ValueError 异常。


reverse()

将 deque 中的元素就地反转,返回值为 None。

3.2 版新功能


rotate(n=1)

将 deque 向右旋转 n 步。如果 n 为负数,则向左旋转。

当 deque 不为空时,向右旋转一步等价于 d.appendleft(d.pop()),向左旋转一步等价于 d.append(d.popleft())。


deque 对象还提供一个只读属性:

maxlen

表示 deque 的最大大小,如果没有大小限制,则该值为 None。

3.1 版新功能


除了以上内容,双端队列还支持迭代,pickle 存储,len(d),reversed(d),copy.copy(d),copy.deepcopy(d)。

使用 in 操作符进行成员测试,以及如 d 这样的下标引用以访问第一个元素。在两端的索引访问都是 O(1),但在中间会变慢到 O(n)。对于快速随机访问,应使用列表。

小甲鱼:可以参考 -> Python 列表、双端队列、集合、字典的时间复杂度

从 3.5 版本开始,支持 __add__(), __mul__(), 和 __imul__() 方法。

示例:

>>> from collections import deque
>>> d = deque('ish')               # 创建一个包含三个元素的新 deque
>>> for elem in d:                   # 遍历 deque 的元素
...   print(elem.upper())
I
S
H

>>> d.append('C')                  # 在右侧添加一个新元素
>>> d.appendleft('F')                # 在左侧添加一个新元素
>>> d                              # 显示 deque 的表示
deque(['F', 'i', 's', 'h', 'C'])

>>> d.pop()                        # 返回并移除最右侧的元素
'C'
>>> d.popleft()                      # 返回并移除最左侧的元素
'F'
>>> list(d)                        # 列出 deque 的内容
['i', 's', 'h']
>>> d                           # 查看最左侧的元素
'i'
>>> d[-1]                            # 查看最右侧的元素
'h'

>>> list(reversed(d))                # 以反向顺序列出 deque 的内容
['h', 's', 'i']
>>> 'h' in d                         # 在 deque 中搜索元素
True
>>> d.extend('jkl')                  # 同时添加多个元素
>>> d
deque(['i', 's', 'h', 'j', 'k', 'l'])
>>> d.rotate(1)                      # 向右旋转
>>> d
deque(['l', 'i', 's', 'h', 'j', 'k'])
>>> d.rotate(-1)                     # 向左旋转
>>> d
deque(['i', 's', 'h', 'j', 'k', 'l'])

>>> deque(reversed(d))               # 创建一个反向排序的新 deque
deque(['l', 'k', 'j', 'h', 's', 'i'])
>>> d.clear()                        # 清空 deque
>>> d.pop()                        # 不能从空的 deque 中弹出元素
Traceback (most recent call last):
    File "<pyshell#6>", line 1, in -toplevel-
      d.pop()
IndexError: pop from an empty deque

>>> d.extendleft('abc')            # extendleft() 将反转输入的顺序
>>> d
deque(['c', 'b', 'a'])

双端队列在实操中的使用

下面这个部分展示了在实际操作中,双端队列的各种使用方法。

有界长度 deque 提供了类似于 Unix 中的 tail 过滤器的功能:

def tail(filename, n=10):
    '返回文件的最后n行'
    with open(filename) as f:
      return deque(f, n)

另一种使用 deque 的方法,是通过向右添加和向左弹出操作,来维持一个最近添加元素的序列:

def moving_average(iterable, n=3):
    # moving_average() --> 40.0 42.0 45.0 43.0
    # http://en.wikipedia.org/wiki/Moving_average
    it = iter(iterable)
    d = deque(itertools.islice(it, n-1))
    d.appendleft(0)
    s = sum(d)
    for elem in it:
      s += elem - d.popleft()
      d.append(elem)
      yield s / n
小甲鱼:这段代码是用来计算移动平均(Moving Average)的,移动平均是作为技术分析中一种分析时间序列的常用工具,通常用于对财务数据进行技术分析,如股票价格、收益率或交易量等(看不懂代码的话,可以参考一下视频讲解哦~)。


使用存储在 deque 中的输入迭代器可以实现轮询调度器。

值从活动迭代器中的 0 号索引位置产生。

如果该迭代器已耗尽,可以使用 popleft() 将其移除;

否则,可以使用 rotate() 方法将其循环回到队尾:

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    iterators = deque(map(iter, iterables))
    while iterators:
      try:
            while True:
                yield next(iterators)
                iterators.rotate(-1)
      except StopIteration:
            # 删除已耗尽的迭代器
            iterators.popleft()
小甲鱼:轮询调度(Round Robin),是一种常见的调度算法,通常被作为操作系统进程或任务调度的分配策略,它的主要优点是其公平性和简单性。


rotate() 方法提供了一种实现 deque 切片和删除的方式。

例如,del d 的纯 Python 实现依赖于 rotate() 方法来定位要弹出的元素:

def delete_nth(d, n):
    d.rotate(-n)
    d.popleft()
    d.rotate(n)
要实现 deque 切片,使用类似的方法应用 rotate() 将目标元素带到 deque 的左侧。

使用 popleft() 删除旧条目,使用 extend() 添加新条目,然后反转旋转。

在该方法上进行微小的变化,就可以轻松实现 Forth 风格的堆栈操作,如 dup、drop、swap、over、pick、rot 和 roll。

还是挺好玩的,详情请见课后作业~

{:10_282:}

OrderedDict -- 字典(dict)的子类,不过它可以确保字典有序






更新中~

















歌者文明清理员 发表于 2023-6-8 17:37:07

支持甲鱼哥!

不二如是 发表于 2023-6-9 11:15:20

{:10_254:}{:10_254:}真香

不是黄瓜是傻瓜 发表于 2023-6-11 16:05:31

耶新板块哎

慢慢即漫漫 发表于 2023-7-5 14:55:59

{:10_257:}

琅琊王朝 发表于 2023-7-7 11:29:35

快加油肝啊?

编程追风梦小号 发表于 2023-8-5 12:34:32

甲鱼哥申请精华^o^--->通过!真香,值得学习!

killerhb02 发表于 2024-2-18 16:36:27

好好学习,天天向上
页: [1]
查看完整版本: collections -- 高阶容器类型