Skip to main content

数据容器

面向对象的迭代器

我们在前文已经多次提及迭代器了,并且介绍了使用生成器创建迭代器的方法。这一节,我们深入讨论一下迭代器内部内部构造和工作原理,以及如何使用面向对象的编程方式实现迭代器。

迭代器协议

迭代器协议规定了两个特殊的方法:__iter__()__next__(),这意味着实现了这两个方法的对象,就可以被当做迭代器。

  • __iter__() 方法: 返回迭代器对象本身。针对迭代器运行 iter() 函数,或是在 for 循环中去迭代一个迭代器的时候,调用的就是这个方法。
  • __next__() 方法: 返回迭代器中的下一个元素。针对迭代器运行 next() 函数时,调用的就是这个方法。如果没有更多元素可用,__next__() 应该抛出 StopIteration 异常。每次调用 __next__() 方法时,迭代器都会记住当前的状态,以便在下次调用时从上次离开的地方继续。

从协议中可以看出,迭代器是惰性求值的,它不会在一开始就计算出所有元素,而是在每次调用 next() 时才产生一个元素。这使得迭代器特别适用于处理大量或无限的数据集合。迭代器只能向前移动,不能回退到之前的元素,也不能复制迭代器的状态。

下面是一个自定义迭代器的示例,该迭代器能够返回一个递增的数列,直到达到一个上限:

class CountUpTo:
def __init__(self, max):
self.max = max
self.num = 0 # 初始化 self.num

def __iter__(self):
return self

def __next__(self):
if self.num < self.max:
result = self.num
self.num += 1
return result
else:
raise StopIteration

# 创建迭代器
counter = CountUpTo(5)
# 测试
print(next(counter)) # 输出: 0
print(next(counter)) # 输出: 1
print(next(counter)) # 输出: 2
print(next(counter)) # 输出: 3
print(next(counter)) # 输出: 4
# print(next(counter)) # 已经到头,再调用 next 会产生 StopIteration 异常

上面程序实现的 CountUpTo 类与我们前文介绍的生成器函数 count_up_to() 函数的功能是完全相同的。

通过可迭代对象实例化迭代器

程序中经常使用迭代器,很多都不是我们自己写的,而是从从可迭代对象那里获得的。比如最常见的列表,使用 iter() 函数可以从可迭代对象那里获得一个迭代器:

my_list = [1, 2, 3]
my_iter = iter(my_list)

print(next(my_iter)) # 输出 1
print(next(my_iter)) # 输出 2

在这个例子中,my_list 是一个可迭代对象,my_iter 是从 my_list 创建的迭代器。

itertools 库

Python 在 itertools 标准库模块中提供了一系列的迭代器,可以帮助我们进行复杂的迭代操作,比如组合数据、过滤数据等。使用 itertools 可以使代码更加简洁、高效、易于阅读。其实我们在前面的内容中已经介绍过好几个 itertools 库中的迭代器了,这里再集中介绍几个最常用的迭代器。

无限迭代器

  • count(start=0, step=1): 从 start 开始,无限地产生等差数列。
  • cycle(iterable): 无限地重复给定的序列。
  • repeat(object[, times]): 重复一个对象,无限次或指定次数。

生成素数序列时,我们曾经利用过这个 count 迭代器。

from itertools import count, cycle, repeat

# 从 10 开始的无限等差数列,步长为 2
for num in count(10, 2):
if num > 20: # 为了避免无限循环,添加一个退出条件
break
print(num)

# 无限重复列表 [1, 2, 3]
count = 0
for item in cycle([1, 2, 3]):
if count > 8: # 为了避免无限循环,添加一个退出条件
break
print(item)
count += 1

# 重复字符串 "Hello" 5 次
for item in repeat("Hello", 5):
print(item)

在使用这类可以无限产生元素的迭代器时,要小心避免造成死循环。也要避免把无限迭代器作为参数传递给函数,比如 print(*count()) 会尝试解包生成器能生成的所有的值,造成内存耗尽。

使用无限迭代器时,需要使用条件判断或限制迭代次数来避免死循环,内存耗尽等问题。

有限迭代器

有限迭代器种类繁多,我们使用示例分别介绍一下:

累积结果

accumulate(iterable[, func, *, initial=None]): 返回累积总和或其他二元函数的累积结果。

from itertools import accumulate
import operator

# 累加
data = [1, 2, 3, 4, 5]
result = list(accumulate(data))
print(result) # 输出: [1, 3, 6, 10, 15]

# 累积乘积
result = list(accumulate(data, operator.mul))
print(result) # 输出: [1, 2, 6, 24, 120]

accumulate 的功能与 reduce() 函数有些类似。最主要的区别在于 reduce() 函数只返回最终的累积结果,而 accumulate 会把中间累积运算的每一步过程都返回回来。

连接

chain(*iterables): 连接多个迭代器为一个长序列。

from itertools import chain

# 连接多个列表
result = list(chain([1, 2, 3], ['a', 'b', 'c']))
print(result) # 输出: [1, 2, 3, 'a', 'b', 'c']

chain() 函数在效果上与 + 运算符非常类似,它们都可以将多个序列连接在一起。区别在于, chain() 函数可以接收任何迭代对象,返回的是一个迭代器;而 + 运算符要求操作的对象类型相同(例如两个列表或两个元组),其返回结果是与输入类型相同的一个新的完整序列,而不是迭代器。chain() 函数有更高的内存效率,适合处理大数据。

过滤

接下来介绍的几个函数,都与过滤数据相关。与前文介绍的 filter() 函数有相似功能。

  • compress(data, selectors): 根据 selectors 中的布尔值筛选 data 中的元素。
from itertools import compress

# 根据布尔值筛选元素
data = [1, 2, 3, 4, 5]
selectors = [True, False, True, False, True]
result = list(compress(data, selectors))
print(result) # 输出: [1, 3, 5]

compress() 根据另一个布尔序列来筛选元素,而 filter() 函数根据一个函数的返回值来筛选元素。

  • dropwhile(predicate, iterable): 当 predicate 为真时跳过元素,然后返回剩下的元素。
from itertools import dropwhile

# 当元素小于 3 时跳过
result = list(dropwhile(lambda x: x < 3, [1, 2, 3, 4, 5, 2, 1]))
print(result) # 输出: [3, 4, 5, 2, 1]

dropwhile() 和 filter() 函数的区别: dropwhile 只在序列的起始进行条件判断,一旦条件不满足就停止判断并包含剩余的所有元素。而 filter() 会对序列中的每个元素进行条件判断。

  • takewhile(predicate, iterable): 当 predicate 为真时产生元素,一旦为假就停止。 与 dropwhile 相对应。
from itertools import takewhile

# 当元素小于 3 时产生元素
result = list(takewhile(lambda x: x < 3, [1, 2, 3, 4, 5]))
print(result) # 输出: [1, 2]
  • filterfalse(predicate, iterable): 产生使得 predicate 返回假的元素。
from itertools import filterfalse

# 产生使得 lambda 返回假的元素
result = list(filterfalse(lambda x: x % 2, [1, 2, 3, 4, 5]))
print(result) # 输出: [2, 4]

filterfalse() 与 filter() 函数的筛选逻辑正相反: filter() 返回那些使测试函数为真的元素,而 filterfalse() 返回那些使测试函数为假的元素。

分割

  • islice(iterable, start, stop[, step]): 从序列中切片返回选定元素。

islice 在功能上相当于切片操作,但是切片操作只能应用于序列类型的数据,列表、元组、字符串,而 islice 应用于任何可迭代对象,并且它返回的是一个迭代器,而不是序列类型的数据。

from itertools import islice

# 从序列中切片
result = islice(range(10), 2, 8, 2))
print(list(result)) # 输出: [2, 4, 6]
  • groupby(iterable, key=None): 按照 key 函数的返回值对序列中相邻元素进行分组。

groupby 主要用于对复杂数据结构进行分组操作,特别是当想要根据某个特定的属性或条件对数据集进行分组时。

from itertools import groupby

# 根据长度分组
data = ['abc', 'de', 'fgh', 'i', 'jk']
for k, g in groupby(data, key=len):
print(k, list(g)) # 输出: 3 ['abc', 'fgh'] 2 ['de', 'jk'] 1 ['i']

另一个示例,按班级对学生进行分组:

from itertools import groupby

students = [
{"name": "张三", "class": "A"},
{"name": "李四", "class": "B"},
{"name": "王五", "class": "A"},
{"name": "赵六", "class": "B"}
]

# 首先按照班级排序
students.sort(key=lambda x: x["class"])

# 然后使用 groupby
for key, group in groupby(students, key=lambda x: x["class"]):
print(key, list(group))

打包、拆包

  • starmap(function, iterable): 使用参数从 iterable 中解包得到,并将它们应用到 function。

map() 函数功能相同,但参数格式不同。如果每次操作的所有参数已经被组织成元组或列表,那么使用 starmap 更为合适;如果参数是分开的几个列表,或者每次操作只有一个参数,那么使用 map 更为方便。

from itertools import starmap

# 使用参数解包应用函数
result = list(starmap(pow, [(2, 3), (3, 2)]))
print(result) # 输出: [8, 9]
  • zip_longest(*iterables, fillvalue=None): 与内置的 zip() 函数类似,但以最长的可迭代对象为准。
from itertools import zip_longest

# 以最长的可迭代对象为准的 zip
result = list(zip_longest('ABCD', 'xy', fillvalue='-'))
print(result) # 输出: [('A', 'x'), ('B', 'y'), ('C', '-'), ('D', '-')]

排列组合

这一组函数,都是用来产生排列组合结果的。

  • product(*iterables, repeat=1): 计算笛卡尔积(Cartesian product)。笛卡尔积是来自两个或多个集合的所有可能的有序对组合。它会从每个集合中取出一个元素,组合成所有可能的元组。
from itertools import product

# 计算两个列表的笛卡尔积
result = list(product([1, 2], ['a', 'b']))
print(result) # 输出: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
  • permutations(iterable, r=None): 产生序列的所有可能排列。r 指定排列的长度,默认是全部元素。
from itertools import permutations

# 产生所有可能的三元素排列
result = list(permutations([1, 2, 3], 3))
print(result) # 输出: [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
  • combinations(iterable, r): 产生序列的所有组合,不考虑元素的顺序。r 指定组合的长度。
from itertools import combinations

# 产生所有可能的两元素组合
result = list(combinations([1, 2, 3], 2))
print(result) # 输出: [(1, 2), (1, 3), (2, 3)]
  • combinations_with_replacement(iterable, r): 产生序列的所有组合,考虑元素的顺序。r 指定组合的长度。
from itertools import combinations_with_replacement

# 产生所有可能的两元素组合,允许重复
result = list(combinations_with_replacement([1, 2, 3], 2))
print(result) # 输出: [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]

枚举

枚举在很多编程语言里都是基本的数据类型。在 Python 中,枚举不是基础数据类型,而是一个表示固定集合中元素的类。这些元素都是常量,不应该被更改。使用枚举可以为一组相关的常量提供有意义的名称,使代码更具可读性。

创建枚举

要在 Python 中创建枚举,需要使用 enum 模块中的 Enum 类:

from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

在这里,Color 是一个枚举,它有三个成员:RED、GREEN 和 BLUE。

枚举成员有名称和值,每个成员都是唯一的。枚举成员是不可变的,一旦定义就不能更改。

访问枚举成员

可以通过成员的名称或值来访问枚举成员:

print(Color.RED)        # 输出: Color.RED
print(Color.RED.name) # 输出: RED
print(Color.RED.value) # 输出: 1

遍历枚举

可以遍历枚举的所有成员:

for color in Color:
print(color.name, color.value)

使用自动分配值

如果不为枚举成员指定值,它们会自动分配整数值,从 1 开始递增:

from enum import auto, Enum

class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()

此时,RED 的值为 1,GREEN 的值为 2,BLUE 的值为 3。

检查枚举成员

可以检查某个值或名称是否是枚举中的成员:

print(Color(1))       # 输出: Color.RED
print(Color['RED']) # 输出: Color.RED

枚举的比较

枚举成员不能进行大小比较,但可以进行身份和等值比较:

print(Color.RED is Color.RED)   # 输出: True
print(Color.RED == Color.GREEN) # 输出: False

类型检查

在使用枚举数据的时候,可以进行类型检查,防止使用无效的值。比如:

from enum import Enum

# 定义一个枚举类
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3

def print_color(color):
if not isinstance(color, Color):
raise ValueError("不是一个有效的 Color 枚举成员")
print("选中的颜色是:", color.name)

# 正确使用枚举
print_color(Color.RED) # 输出: 选中的颜色是: RED

# 错误使用枚举
try:
print_color(1) # 尝试传入一个非枚举值
except ValueError as e:
print(e) # 输出: 不是一个有效的 Color 枚举成员

上面程序中的 print_color() 函数,对输入数据的类型进行了检查,看看输入参数是否是 Color 枚举类型,如果不是,它会抛出一个异常。

定义更复杂的枚举

枚举作为一个类,也可以具有一些方法。下面的程序为 Color 枚举类添加了打印描述的方法,和混合不同颜色的方法:

from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
PURPLE = 4

def describe(self):
return f"颜色名:{self.name};颜色值:{self.value}"

# 混合颜色
@classmethod
def mix(cls, color1, color2, ratio=0.5):
if color1 == cls.RED and color2 == cls.BLUE or color1 == cls.BLUE and color2 == cls.RED:
if ratio == 0.5: # 这里是一个简化的示例,假设只有 0.5 的比例是 PURPLE
return cls.PURPLE
return f"按照比例 {ratio} 混合 {color1.name}{color2.name} 无法产生已定义的颜色"

print(Color.RED.describe()) # 输出: 颜色名:RED;颜色值:1

# 演示颜色组合
result = Color.mix(Color.RED, Color.BLUE)
if isinstance(result, Color):
print(f"混合后的颜色是 {result.name}") # 输出: 混合后的颜色是 PURPLE
else:
print(result)

result = Color.mix(Color.RED, Color.BLUE, 0.3)
print(result) # 输出: 按照比例 0.3 混合 RED 和 BLUE 无法产生已定义的颜色

命名元组

Python 中可以给元组的元素命名,这样的元组就是命名元组(Namedtuples)。相比于普通的元组,命名元组可读性更好,它可以通过名称(而不是索引)获取元组的元素。Python 中使用 namedtuple 函数来创建命名元组。比如:

from collections import namedtuple

# 定义一个命名元组
Person = namedtuple("Person", ["name", "age", "gender"])

# 创建一个Person对象
p1 = Person(name="阮奇桢", age=40, gender="男")

print(p1.name) # 输出: 阮奇桢
print(p1.age) # 输出: 40
print(p1.gender) # 输出: 男

# 使用索引
print(p1[0]) # 输出: 阮奇桢

# 将命名元组转化为字典
print(p1._asdict()) # 输出: OrderedDict([('name', '阮奇桢'), ('age', 40), ('gender', '男')])

# 替换命名元组的字段值
p2 = p1._replace(name="男")
print(p2) # 输出: Person(name='男', age=40, gender='男')

# 获取所有字段名称
print(Person._fields) # 输出: ('name', 'age', 'gender')

虽然命名元组提供了类似于类的功能,但它们仍然是元组,因此它们的值是不可变的。这意味着你不能更改已创建的命名元组的字段值,但可以使用 ._replace() 方法返回一个新的命名元组,其某些字段值已更改。