# 一、Python 迭代器
迭代器在 Python
中无处不在。它们在 for
循环,理解,生成器等中优雅地实现,但却隐藏在眼皮底下。
Python
中的 Iterator
只是一个可以迭代的对象。一个将返回数据的对象,一次返回一个元素。
从技术上讲, Python
迭代器对象必须实现两个特殊方法, __iter__()
和 __next__()
统称为迭代器协议。
如果我们可以从对象获得迭代器,则该对象称为可迭代。 Python
中的大多数内置容器(例如: list
, tuple
, string
等)都是可迭代的。
iter()
函数(也就是 __iter__()
方法)从它们返回一个迭代器。
# 1.1 通过 Python 中的迭代器进行迭代
我们使用该 next()
函数手动遍历迭代器的所有项目。 当我们到达末尾并且没有更多数据要返回时,它将引发 StopIteration
。 以下是一个示例:
# 定义一个列表 | |
my_list = [4, 7, 0, 3] | |
# 使用 iter () 获得迭代器 | |
my_iter = iter(my_list) | |
## 使用 iter () 获得迭代器 | |
#输出 4 | |
print(next(my_iter)) | |
#输出 7 | |
print(next(my_iter)) | |
## next(obj)与 obj .__ next __() 相同 | |
#输出 0 | |
print(my_iter.__next__()) | |
#输出 3 | |
print(my_iter.__next__()) | |
## 这将引起错误,没有项目剩下 | |
next(my_iter) |
# 1.2 for 循环实际上如何工作?
实际上,for 循环可以迭代任何可迭代的对象。让我们仔细看看如何 for 在 Python 中实际实现循环。
for element in iterable: | |
# 对元素做点什么 |
实际上的实现为:
# 创建一个迭代器对象 iterable | |
iter_obj = iter(iterable) | |
# 无限循环 | |
while True: | |
try: | |
# 获取下一项 | |
element = next(iter_obj) | |
# 对元素做点什么 | |
except StopIteration: | |
# 如果引发 StopIteration,则从循环中断 | |
break |
因此,在内部, for
循环通过在 iterable
上调用 iter()
创建一个迭代器对象 iter_obj
。
在循环内部,它调用 next()
来获取下一个元素,并使用这个值执行 for
循环的主体。当所有的项都用完后, StopIteration
被抛出,它在内部被捕获,循环结束。注意,任何其他类型的异常都会通过。
# 二、Python 生成器
用 Python
构建迭代器有很多开销;我们必须使用 __iter__()
和 __next__()
方法实现一个类,跟踪内部状态,在没有要返回的值时触发 StopIteration
等等。这既冗长又违反直觉。生成器在这种情况下可以派上用场。
Python
生成器是创建迭代器的简单方法。我们上面提到的所有开销都由 Python
的生成器自动处理。简而言之,生成器是一个函数,它返回一个对象(迭代器),我们可以对其进行迭代(一次一个值)。
# 2.1 在 Python 中创建生成器
在 Python 中创建生成器非常简单。 就像使用 yield
语句而不是 return
语句定义普通函数一样容易。如果一个函数包含至少一个 yield
语句 (它可能包含其他 yield
或 return
语句),那么它就成为一个生成器函数。 yield
和 return
都将从函数返回一些值。不同之处在于,当 return
语句完全终止一个函数时, yield
语句会暂停该函数保存其所有状态,然后在后续调用时继续执行。
生成器函数与常规函数的不同之处:
- 生成器函数包含一个或多个
yield
语句; - 调用时,它返回一个对象(迭代器),但不会立即开始执行;
- 像
__iter__()
和__next__()
这样的方法会自动实现。因此,我们可以使用next()
来遍历项目; - 一旦函数产生了结果,函数就会暂停,控制就会转移给调用者;
- 局部变量及其状态在连续调用之间被记住;
- 最后,当函数终止时,在进一步调用时会自动引发
StopIteration
。
这是一个示例,用于说明上述所有要点。我们有一个 my_gen()
由几个 yield
语句命名的生成器函数。
# 一个简单的生成器函数 | |
def my_gen(): | |
n = 1 | |
print('这是第一次打印') | |
# 生成器函数包含 yield 语句 | |
yield n | |
n += 1 | |
print('这是第二次打印') | |
yield n | |
n += 1 | |
print('这是最后一次打印') | |
yield n |
解释器中的交互式运行如下所示。在 Python Shell
中运行这些命令以查看输出。
>>> # 它返回一个对象,但不立即开始执行. | |
>>> a = my_gen() | |
>>> # 我们可以使用 next () 遍历这些项. | |
>>> next(a) | |
这是第一次打印 | |
1 | |
>>> # 一旦函数产生了结果,函数就会暂停,控制就会转移给调用者。 | |
>>> # 局部变量及其状态在连续调用之间被记住。 | |
>>> next(a) | |
这是第二次打印 | |
2 | |
>>> next(a) | |
这是最后一次打印 | |
3 | |
>>> # 最后,当函数终止时,在进一步调用时将自动引发 StopIteration。 | |
>>> next(a) | |
Traceback (most recent call last): | |
... | |
StopIteration |
在上面的示例中要注意的一件有趣的事情是,每次调用之间都会记住变量 n
的值。与普通函数不同,局部变量在函数产生时不会被破坏。此外,生成器对象只能迭代一次。要重新启动该过程,我们需要使用 = my_gen()
之类的东西来创建另一个生成器对象。
# 2.2 生成器与 for 循环一起使用
def rev_str(my_str): | |
length = len(my_str) | |
for i in range(length - 1,-1,-1): | |
yield my_str[i] | |
# For 循环以反转字符串 | |
# 输出: | |
# o | |
# l | |
# l | |
# e | |
# h | |
for char in rev_str("hello"): | |
print(char) |
# 2.3 Python 生成器表达式
使用生成器表达式可以轻松地动态创建简单的生成器。它使建造生成器变得容易。与 lambda
函数创建匿名函数相同,生成器表达式创建匿名生成器函数。生成器表达式的语法类似于 Python
中的列表理解语法。但是将方括号替换为圆括号。列表理解与生成器表达式之间的主要区别在于,虽然列表理解生成整个列表,但生成器表达式一次生成一个项目。他们有点懒,只在需要时才生成项目。由于这个原因,生成器表达式比等价的列表理解的内存效率要高得多。
# 初始化列表 | |
my_list = [1, 3, 6, 10] | |
# 使用列表理解对每个项目进行平方 | |
# 输出: [1, 9, 36, 100] | |
[x**2 for x in my_list] | |
# 同样的事情可以使用生成器表达式来完成 | |
# 生成器表达式没有立即产生所需的结果。相反,它返回了一个生成器对象 | |
# 输出: <generator object <genexpr> at 0x0000000002EBDAF8> | |
(x**2 for x in my_list) |
生成器表达式可以在函数内部使用。以这种方式使用时,可以删除圆括号。
>>> my_list = [1, 3, 6, 10] | |
[1, 3, 6, 10] | |
>>> sum(x**2 for x in my_list) | |
146 | |
>>> max(x**2 for x in my_list) | |
100 |
# 2.3 Python 生成器优点
- 易于实施,与它们的迭代器类对应项相比,生成器可以以一种清晰而简洁的方式实现。
- 节省内存,一个普通的返回序列的函数会在返回结果之前在内存中创建整个序列。如果序列中的项目数量很大,会影响效率。而这种序列的生成器实现对内存友好,因此是首选的,因为它一次只能生成一项。
- 表示无限流,生成器是表示无限数据流的绝佳媒介。无限流无法存储在内存中,并且由于生成器一次只生成一项,因此它可以表示无限数据流。(至少在理论上)