Courses
迭代器是可以被迭代的对象。它们是 Python 编程语言中的常见特性,常用于循环与列表推导式。任何能够派生出迭代器的对象称为可迭代对象(iterable)。
构造一个迭代器需要做不少工作。例如,每个迭代器对象的实现必须包含 __iter__() 和 __next__() 方法。除了上述前提条件,实现还必须能够跟踪对象的内部状态,并在没有更多值可返回时抛出 StopIteration 异常。这些规则被称为迭代器协议。
自行实现迭代器是一个冗长的过程,而且并非总是必要。一个更简单的替代方案是使用生成器对象。生成器是一种特殊的函数类型,使用 yield 关键字返回一个可以被迭代的迭代器,每次产生一个值。
能分辨在何种场景下实现迭代器、在何种场景下使用生成器,将提升您作为 Python 程序员的能力。在本教程的其余部分,我们将强调这两类对象的区别,帮助您在不同情境中做出最佳选择。
术语表
|
术语 |
定义 |
|
Iterable(可迭代对象) |
一种可以在循环中被遍历的 Python 对象。常见的可迭代对象包括列表、集合、元组、字典、字符串等。 |
|
Iterator(迭代器) |
一种可以被迭代的对象。因此,迭代器包含可数数量的值。 |
|
Generator(生成器) |
一种特殊的函数类型,不返回单个值;它返回一个包含一系列值的迭代器对象。 |
|
Lazy Evaluation(惰性求值) |
一种求值策略,只有在需要时才生成某些对象。因此,在某些开发者圈子中,惰性求值也被称为“按需调用(call-by-need)”。 |
|
Iterator Protocol(迭代器协议) |
在 Python 中定义迭代器时必须遵循的一组规则。 |
|
next() |
一个内置函数,用于返回迭代器中的下一个项目。 |
|
iter() |
一个内置函数,用于将可迭代对象转换为迭代器。 |
|
yield() |
一个与 return 类似的 Python 关键字,但 |
Python 迭代器与可迭代对象
可迭代对象能够一次返回其成员——也就是说,它们可以被迭代。常见的内置Python 数据结构(如列表、元组和集合)都是可迭代对象。其他数据结构如字符串和字典也被视为可迭代对象:字符串可以按字符迭代,字典的键也可以被迭代。经验法则:凡是在 for 循环中可以被迭代的对象,都可视为可迭代对象。
通过示例探索 Python 可迭代对象
根据定义,我们可以得出结论:所有迭代器也是可迭代对象。然而,并非每个可迭代对象都是迭代器。可迭代对象只有在被迭代时才会产生一个迭代器。
为演示这一点,我们将实例化一个列表(它是可迭代对象),并通过对该列表调用内置函数 iter() 来生成一个迭代器。
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
尽管列表本身不是迭代器,但调用 iter() 函数会将其转换为迭代器并返回该迭代器对象。
为了说明并非所有可迭代对象都是迭代器,我们将实例化相同的列表对象,并尝试调用用于返回迭代器中下一个项目的 next() 函数。
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
在上面的代码中,您可以看到对列表调用 next() 函数会引发 TypeError——了解更多请参阅Python 中的异常与错误处理。出现这种行为,是因为列表对象是可迭代对象而不是迭代器。
通过示例探索 Python 迭代器
因此,如果目标是遍历列表,那么必须先生成一个迭代器对象。只有这样,我们才能通过迭代器来管理对列表值的迭代。
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
每当您尝试遍历可迭代对象时,Python 都会自动生成一个迭代器对象。
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
当捕获到 StopIteration 异常时,循环结束。
从迭代器获取的值只能从左到右依次检索。Python 没有 previous() 函数来让开发者在迭代器中向后移动。
迭代器的惰性特性
可以基于同一个可迭代对象定义多个迭代器。每个迭代器都会维护自己的进度状态。因此,通过为一个可迭代对象定义多个迭代器实例,可以在一个实例迭代到末尾的同时,另一个实例仍然停留在开头。
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
请注意,iterator_b 打印的是序列的第一个元素。
因此,我们可以说迭代器具有惰性特性:创建迭代器时,元素并不会立刻产出,只有在被请求时才会返回。换言之,只有当我们显式调用 next(iter(list_instance)) 时,列表实例的元素才会被返回。
不过,也可以通过在迭代器对象上调用内置的可迭代数据结构容器(如 list()、set()、tuple())来一次性提取迭代器中的所有值,从而强制迭代器一次性生成其所有元素。
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
对于大型迭代器不建议这样做,因为它会强制生成并同时在内存中保存每个元素,从而违背惰性求值的目的。
当数据集过大而难以在内存中舒适地容纳,或当您希望以惰性方式迭代而不想编写完整的迭代器类时,通常生成器更合适。
Python 生成器
实现迭代器最省事的替代方案是使用生成器。虽然生成器看起来像普通的Python 函数,但它们有所不同。首先,生成器对象并不直接返回项目。相反,它使用 yield 关键字按需生成项目。因此,我们可以说生成器是一种利用惰性求值的特殊函数。
生成器不会像典型的可迭代对象那样将内容存储在内存中。举例来说,如果目标是找到一个正整数的所有因子,通常我们会实现一个传统函数(关于Python 函数的更多内容请参阅本教程)如下所示:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
上述代码返回了完整的因子列表。然而,请注意当使用生成器而不是传统 Python 函数时的差异:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
由于我们使用了 yield 而非 return,函数在运行后并不会退出。实质上,我们告诉 Python 去创建一个生成器对象而不是传统函数,从而可以跟踪该生成器对象的状态。
因此,可以对这个惰性迭代器调用 next() 函数,以一次显示序列中的一个元素。
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
创建生成器的另一种方式是使用生成器推导式。生成器表达式的语法与列表推导式相似,但使用圆括号而不是方括号。
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
探索 Python 的 yield 关键字
yield 关键字控制生成器函数的执行流。与使用 return 时会退出函数不同,yield 会返回函数但记住其局部变量的状态。
由 yield 调用返回的生成器可以被赋值给变量,并通过 next() 函数进行迭代——这会执行函数,直到遇到第一个 yield 关键字。一旦触达 yield,函数的执行被挂起。此时,函数的状态会被保存。因此,我们可以在需要时恢复函数执行。
函数会从上一次 yield 的位置继续执行。例如:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
在上述代码中,我们的生成器有四次 yield 调用,但我们尝试对其调用了五次 next,结果引发了 StopIteration 异常。出现这种行为,是因为我们的生成器并非无限序列,超出预期次数的调用会耗尽生成器。
总结
回顾一下:迭代器是可以被迭代的对象,生成器是利用惰性求值的特殊函数。实现自定义迭代器意味着您必须创建 __iter__() 和 __next__() 方法,而生成器则可以通过在 Python 函数或推导式中使用 yield 关键字来实现。
当您需要一个具备复杂状态维护行为的对象,或者希望暴露除 __next__()、__iter__() 与 __init__() 之外的其他方法时,您可能更倾向于使用自定义迭代器。另一方面,在处理大型数据集(因为它们不将内容存储在内存中)或不需要实现完整迭代器时,生成器可能更为合适。
FAQS
What is the difference between an iterator and a generator in Python?
迭代器是任何实现了 __iter__() 和 __next__() 的对象。生成器是一种更简单的方式,使用带有 yield 关键字的函数来创建迭代器。所有生成器都是迭代器,但并非所有迭代器都是生成器。
When should I use a generator instead of a list in Python?
当处理大型或无限序列,或内存效率很重要时,请使用生成器。列表会一次性将所有元素保存在内存中,而生成器一次只产生一个值。对于体量较小且需要重复使用的数据集,通常使用列表更合适。
What does the yield keyword do in Python?
yield 关键字会将函数转换为生成器。与返回并退出不同,yield 会暂停函数、返回一个值,并记住其状态,以便下一次调用时继续执行。
How do you create a generator in Python?
要么编写一个使用 yield 而非 return 的函数,要么使用生成器表达式——语法与列表推导式相同,但使用圆括号,例如 (x * 2 for x in range(10))。
Are generators faster than iterators in Python?
严格来说并不会更快,但它们更省内存,因为按需生成值。对于大型数据集,这通常意味着更好的整体性能;对于小数据集,差异可以忽略不计。