# 倒数计时器
class DownCounter:
def __init__(self, start):
self.start = start
def __iter__(self):
return self
def __next__(self):
if self.start > 0:
self.start -= 1
return self.start
else:
raise StopIteration
for x in DownCounter(5):
print(x)
可以看见这里的代码有点啰嗦:
class
,按照代码规范名字要大写self
上__iter__
中 return self
StopIteration
异常yield
关键字的作用yield
重构迭代器yield
关键字,哪怕永远无法被执行到,函数都会发生变异。def gen():
print('hello')
if 0:
yield
g = gen()
print(g)
<generator object gen at 0x0000024EFF455AC0>
调用函数 gen()
没有像普通函数那样执行其中的代码,而是返回了一个 generator
对象。
print(type(gen))
print(type(g))
<class 'function'> <class 'generator'>
import inspect
# 是函数,也是生成器函数
print(inspect.isfunction(gen))
print(inspect.isgeneratorfunction(gen))
# 生成器函数不是 generator
print(inspect.isgenerator(gen)) # False
print(inspect.isgenerator(g)) # True
True True False True
通常,我们把含有 yield
的函数称之为「生成器函数 generator function」,把调用生成器函数返回的结果称为「生成器 generator」。
但是根据 Python 官方文档:
generator 生成器
A function which returns a generator iterator. ... 一个返回生成器迭代器的函数
Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn’t clear, using the full terms avoids ambiguity. 通常指的是生成器函数,但是在一定的语境下也可以指代生成器迭代器。为了避免歧义,推荐使用完整的术语。
generator iterator 生成器迭代器
An object created by a generator function.
一个由生成器函数创建的对象。
在不会产生歧义的情况下,提到「生成器」可能指的是函数,也可能是函数生成的对象,具体需要根据上下文判断。
为了区分,我一般将「生成器函数」的返回结果称为「生成器对象」。
这两者之间的关系将在稍后的技术内幕中详细解释。
generator iterator
这个名字非常拗口,实际很少这么说,但是它却指出了:
「生成器对象」是迭代器
既然是迭代器,那么肯定要满足「迭代器协议」:
__iter__
,返回迭代器对象自身__next__
,每次返回一个迭代数据,如果没有数据,则要抛出 StopIteration
异常g = gen()
hasattr(g, '__next__') and hasattr(g, '__iter__')
True
g is iter(g)
True
关于迭代器的性质我们已经有所了解,现在要理解的是 yield
在其中是如何发挥作用的。
在 Python 文档里 yield
词条有两个:一个是 yield 语句
,一个是 yield 表达式
。
yield
语句
yield
表达式
刚开始,yield
关键字是作为一个语句(statement)被引入的,引入的目的正是为了构建生成器。
后来为了更好地实现协程,增强了语法,变成了表达式(expression),但是仍然完全兼容原有用法。
本章中,我们只介绍 yield
语句的用法,在下一章中再介绍 yield
表达式语法所增强的部分。
yield
对函数做了什么¶yield
关键字最根本的作用是改变了函数的性质:
从这一点上说,生成器函数的作用和类(class)是差不多的。
生成器对象就是迭代器,所以它的运行方式和迭代器是一致的:
next()
函数来调用next()
都会在遇到 yield
后返回结果(作为 next()
的返回值)return
)则抛出 StopIteration
异常使用生成器对象图示
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-6-931ba23e54c9> in <module> ----> 1 使用生成器对象图示 NameError: name '使用生成器对象图示' is not defined
# 定义一个生成器函数
def gen_666(meet_yield):
print('hello')
if meet_yield: # 是否遇到 yield 由参数控制
print('yield!')
yield 666
print('back!')
print('bye.')
return 'result'
稍微修改上面的例子,通过参数来控制是否要执行 yield
语句。
传入两个不同的参数调用该函数,分别得到两个生成器对象。
g1 = gen_666(False) # 不会遇到 yield
x1 = next(g1)
print(x1)
hello bye.
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-9-8d0e28400d94> in <module> ----> 1 x1 = next(g1) 2 print(x1) StopIteration: result
next(g1)
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-10-b6af764d1389> in <module> ----> 1 next(g1) StopIteration:
因为生成器就是迭代器,所以直接使用 next()
函数来驱动它。
此时,生成器对象会开始执行对应的函数中的代码,就和普通函数没什么两样。
由于这个生成器函数中根本都没机会运行到 yield
所在的语句,所以它将直接就运行到结束的地方。
但是其中的 return
语句并不会像普通函数那样返回,而是抛出 StopIteration
异常。因为这才是迭代器该有的结束姿势。
注意,跟在 return
语句后面的返回值将作为异常的参数一并抛出。
再次调用,将直接报错。
接着来看遇到 yield
会怎样:
# 定义一个生成器函数
def gen_666(meet_yield):
print('hello')
if meet_yield: # 是否遇到 yield 由参数控制
print('yield!')
yield 666
print('back!')
print('bye.')
return 'result'
g2 = gen_666(True) # 会遇到 yield
这一次没有报错,代码执行到了 yield
所在的位置。
并且 yield
语句后面的对象作为 next
函数的返回值被赋值给了变量 x
。
x2 = next(g2)
hello yield!
print(x2)
666
next(g2)
back! bye.
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-15-a6dbba519a9b> in <module> ----> 1 next(g2) StopIteration: result
继续通过 next
函数调用生成器对象,函数执行到结束。
yield
¶只遭遇一次 yield
语句的生成器就是只能迭代一次的迭代器,通常没什么实用价值。
要想能迭代多次,可以在函数内多次使用 yield
语句:
def gen_func():
print('--- yield 1 ---')
yield 1
print('--- yield 2 ---')
yield 2
print('--- yield 3 ---')
yield 3
更多的时候,迭代器基本都是在循环中使用:
for x in gen_func():
print(x)
--- yield 1 --- 1 --- yield 2 --- 2 --- yield 3 --- 3
相对应地,yield
一般也是搭配循环使用的:
# itertools.count 示例
def count(start=0, step=1):
# count(10) --> 10 11 12 13 14 ...
# count(2.5, 0.5) -> 2.5 3.0 3.5 ...
n = start
while True:
yield n
n += step
通过这些例子可以看出来,使用 yield
来实现一个迭代器实在是太简洁了。
next()
调用生成器对象,对应的生成器函数代码开始运行yield
语句,next()
返回时yield
语句右边的对象作为 next()
的返回值yield
语句所在的位置暂停,当再次使用 next()
时继续从该位置继续运行StopIteration
异常return
语句显式地返回值,或者默认返回 None
值,返回值都只能作为异常的值一并抛出next()
,直接抛出 StopIteration
异常,并且不含返回值yield
重构迭代器¶class
定义的迭代器进行对比¶动作 | class 实现的迭代器 | yield 生成器 |
---|---|---|
定义迭代器 | class Iterator: def __init__(self, *args): |
def iter_fun(*args): ... |
构建迭代器 | Iterator(args) |
iter_fun(args) |
next(iterator) |
def __next__(self): return value |
yield value |
StopIteration |
raise StopIteration |
return |
iter(iterator) |
def __iter__(self): return self |
自动实现 |
在没有 yield
的时候,我们必须用 class
来定义迭代器类,代码非常的啰嗦。
现在只需要定义生成器函数即可实现同样的功能。
甚至有时候连 yield
都不用,往往只需要一行代码就可以搞定。
__iter__
接口__iter__
接口¶在上一章中,我们介绍了如何自定义迭代器,即同时包含 __iter__
和 __next__
方法的类。
有时候我们还需要自定义的是容器类型的可迭代对象,即只包含 __iter__
方法的类。
如果没有 yield
,则需要额外定义一个迭代器类,实现 __iter__
和 __next__
方法;最后在容器类的 __iter__
方法中返回迭代器的实例。
# 迭代器类
class MyCustomDataIterator:
def __init__(self, data):
self.data = data
self.index = -1
def __iter__(self):
return self
def __next__(self):
self.index += 1
if self.index < self.data.size:
return self.data.get_value(self.index)
else:
raise StopIteration
# 可迭代数据类
class MyCustomData:
# 其余部分代码不重要都略过
...
@property
def size(self): # 假设可以得到数据的大小
return self.size
def get_value(self, index): # 假设可通过索引按顺序得到数据
return index
def __iter__(self):
return MyCustomDataItertor(self) # 构建迭代器
现在有了 yield
,不需要定义迭代器类,只要在容器类的 __iter__
方法中使用 yield
即可。
# 无需定义这样的类
# class MyCustomDataIterator:
# def __init__(self, data):
# self.data = data
# self.index = -1
#
# def __iter__(self):
# return self
#
# def __next__(self):
# self.index += 1
# if self.index < self.data.size:
# return self.data.get_value(self.index)
# else:
# raise StopIteration
class MyCustomData:
# 其余部分代码不重要都略过
...
@property
def size(self): # 假设可以得到数据的大小
return self.size
def get_value(self, index): # 假设可通过索引按顺序得到数据
return index
def __iter__(self):
index = -1 # 注意,必须是局部变量
while index < 2: # 设定迭代完成的条件
index += 1
yield self.get_value(index)
mydata = MyCustomData() # 注意:mydata 是可迭代对象,但不是迭代器
把原来应该在 __next__
中的代码逻辑稍加修改:
if
判断迭代完成的语句改成 while
循环判断(当然也可能是 for
循环)return
值改为 yield
值StopIteration
异常,现在直接跳出循环即可需要注意的地方是:
迭代的进度总是由迭代器来保存的,容器类型的可迭代对象不应该有感知。
在迭代器类中,进度变量是通过 self
绑定到迭代器的实例上的。改为使用 yield
的方法后,应该避免和可迭代对象的实例绑定到一起。
for a in mydata:
for b in mydata:
print(a, b)
0 0 0 1 0 2 1 0 1 1 1 2 2 0 2 1 2 2
这里的代码主要是为了讲解,事实上绝大多数情况下,也无需这么麻烦。
因为自定义的数据结构基本都会依赖某个内置数据结构,比如列表,直接利用已有的迭代器就可以了。
在上一章中,我们定义了一个迭代器类:
BLACK_LIST = ['白嫖', '取关']
class SuzhiIterator:
def __init__(self, actions):
self.actions = actions
self.index = 0 # 初始化索引下标
def __next__(self):
while self.index < len(self.actions):
action = self.actions[self.index]
self.index += 1 # 更新索引下标
if action in BLACK_LIST:
continue
elif '币' in action:
return action * 2
else:
return action
raise StopIteration
def __iter__(self):
return self
重构如下:
BLACK_LIST = ['白嫖', '取关']
def suzhi(actions):
for action in actions:
if action in BLACK_LIST:
continue
elif '币' in action:
yield action * 2
else:
yield action
actions = ['点赞', '投币', '取关']
for x in suzhi(actions):
print(x)
点赞 投币投币
可以看到,整体代码非常的简洁,基本上只有业务逻辑部分的代码是要保留的。
事实上,这里我有意把逻辑复杂了,让它既有过滤,又有选择性地处理。
如果处理逻辑简单,还可以使用更加简洁的写法,这里就不展开讨论了。
最后来看一下用来生产数据的迭代器。
# 倒数计时器
class DownCounter:
def __init__(self, start):
self.start = start
def __iter__(self):
return self
def __next__(self):
if self.start > 0:
self.start -= 1
return self.start
else:
raise StopIteration()
使用 yield
实现同样的功能:
def countdown(start):
while start > 0:
start -= 1
yield start
for x in countdown(5):
print(x)
4 3 2 1 0
每当定义了一个函数之后,就得到了一个「函数对象」:
def func():
x = 1
print(x)
func
函数中的代码是保存在「代码对象(Code object)」中的:
func.__code__
代码对象随着函数对象一起创建,是函数对象的一个重要属性。
Code objects represent byte-compiled executable Python code, or bytecode.
代码对象中重要的属性以 co_
开头:
func_code = func.__code__
for attr in dir(func_code):
if attr.startswith('co_'):
print(f'{attr}\t: {getattr(func_code, attr)}')
函数对象和代码对象保存了函数的基本信息,当函数运行的时候,还需要一个对象来保存运行时的状态。
这个对象就是 「帧对象(Frame object)」。
Frame objects represent execution frames.
每一次调用函数,都会自动创建一个帧对象,记录当次运行的状态。
帧对象是 Python 内部使用的对象,平常不会去主动操控它,在函数执行完毕会自动清理掉。
一般只有在程序运行出错或者 Debug 的时候,我们才会通过 Traceback 对象来追溯其中的信息。
可以通过 inspect
模块来获取当前的帧对象。
import inspect
def foo():
# 获取到函数的运行帧并返回
return inspect.currentframe()
f1 = foo() # 由于被变量所引用,所以帧不会被垃圾回收
f1
# 再调用一次,得到另一个帧
f2 = foo()
## 函数对象、代码对象和帧对象之间的关系
show_backrefs(foo.__code__)
print(dir(f1))
帧对象中重要的属性以 f_
开头:
f_code
:执行的代码对象f_back
:指向上一个帧,也就是调用者的帧f_locals
:局部变量f_globals
:全局变量f_lineno
:当前对应的行号通常,帧对象的生命周期是随着函数的返回而结束。
当一个函数中调用了另一个函数,此时前一个函数还没有结束,所以这两个函数的帧对象是同时存在的。
比如,我们的程序一般都始于一个 main
函数,然后又调用其它函数,以此类推。
因此,一个程序的运行期,同时存在很多个帧对象。
函数之间的调用关系是先执行的后退出,所以帧对象之间的关系也是先入后出,正好以栈的形式保存。
因此,函数的运行帧又称为栈帧。
注意:一个线程只有一个函数运行栈。
## 展示发生函数调用时的栈
def foo():
return inspect.currentframe()
def bar():
return foo() # 返回 foo 函数运行时的帧对象
f1 = bar()
show_refs(f1)
有兴趣的可以继续利用 inspect
模块强大的自省能力来探究一番。
每次新调用一个函数,就在栈顶压入新的栈帧,而每当一个函数执行完,就从栈顶弹出该函数栈帧,回到了上一个函数。
因此,栈里面保存着所有正在运行的函数的上下文数据。
所以普通函数的运行有这些特点:
对普通函数的运行有了基本的认知之后,我们就能更好地来理解生成器函数的运行了。
生成器函数仍然是函数对象,当然也包括了代码对象。
import inspect
def gen_foo():
for _ in range(10):
yield inspect.currentframe() # 每一次迭代都返回当前帧
show_refs(gen_foo)
调用生成器函数不会直接运行(也就是说,不像普通函数那样创建帧对象并且压入函数栈),而是得到一个生成器对象。
那么秘密自然是藏在生成器对象里:
gf = gen_foo()
show_refs(gf)
可以看到,生成器对象中自带了一个帧对象。
当每次使用 next()
对生成器进行迭代时,都用这个帧对象(gi_frame
)来保存状态:
## 演示目的:展示生成器迭代的过程中都是同一个 frame 对象
gf = gen_foo()
# 存为变量,因为迭代结束后会清空
gi_frame = gf.gi_frame
# 保存所有迭代的结果
frames = list(gf)
print(gf.gi_frame) # None
for f in frames:
print(f is gi_frame)
生成器的 frame 对象在暂停状态下看不到调用的关系图:
gf = gen_foo()
# f = next(gf)
show_refs(gf.gi_frame)
想要在生成器运行的过程中观察运行栈的关系图也是可以的:
def gen_frame_graph():
for _ in range(10):
# 在运行时把图形生成
graph = show_refs(inspect.currentframe())
yield graph
gfg = gen_frame_graph()
def fun_a(g):
return next(g)
fun_a(gfg)
当多次调用 use_gen
并打印关系图时,会发现其中 use_gen
对应的 frame
对象也是没有变化的。
这是因为多次调用的时候 Python 复用了同一块内存,和生成器的帧一直不变是有本质区别的。
所以这里强制把它返回,让 Python 不能重复利用这个地址。
def fun_b(g):
return next(g)
fun_b(gfg)
综上,我们可以总结出:
next()
调用生成器时,就是将生成器引用的帧对象入栈yield
暂停的时候,就是将帧出栈学过 C/C++ 可能产生的疑惑
我们用的主流 Python 都是 CPython,其中的内存管理是依赖于底层 C 语言的。
如果学过 C/C++ 语言的同学可能记得,栈内存中的数据,退出后就没了,Python 是如何做到出栈后还能继续访问的呢?
关于 Python 解释器是如何管理内存的,知道以上 3 个结论基本就足够了。
更多的细节,有兴趣的可以去看 CPython 的源码。
现在,我们可以更好地理解所谓的 generator iterator
是什么了:
「生成器对象」是一个用来迭代执行「生成器函数」的迭代器
是不是有异曲同工之妙?
让一个函数可以多次迭代运行其中的代码才是生成器对象最最根本的作用,而不仅是字面意思上的生成数据的东西。
迭代产出数据只是迭代执行代码的自然结果而已。
当用生成器来实现迭代器的时候,我们关注的是重点是:yield <value>
返回出来的数据。
如果把焦点集中到「被迭代执行的代码」上,就能对生成器有个全新的视角,那就是「协程」。
# 列表解析
lst = [x*2 for x in range(3)]
print(lst)
# 生成器表达式
iter_exp = (x*2 for x in range(3))
print(iter_exp)
# 方法3:生成器表达式,推荐!
good_actions = (x for x in actions if x in GOOD_ACTIONS)
可能从直觉上,用 []
包围的结果是列表,所以会想当然的以为 ()
的结果是元组,实际此处的结果是一个生成器对象。
# 方法2:使用 filter 函数
good_actions = filter(lambda x: x in GOOD_ACTIONS, actions)
迭代相关的内置函数:
以下代码用来自动画图。
import inspect
import objgraph as og
def show_extra_info(x):
return f'id: {id(x)}'
def generator_related(x):
if inspect.isfunction(x) or inspect.iscode(x) or inspect.isframe(x) or inspect.isgenerator(x):
return True
return False
def show_refs(obj):
return og.show_refs(obj, filter=generator_related, extra_info=show_extra_info)
def show_backrefs(obj):
return og.show_backrefs(obj, max_depth=3, filter=generator_related, extra_info=show_extra_info)
show_refs(show_refs)
import graphviz as gv
graph_attr = {'rankdir': 'LR'}
dot = gv.Digraph('Coroutine', graph_attr=graph_attr, node_attr={'shape': 'box'})
dot.node('user', '调用者')
dot.node('gen', '生成器对象')
dot.edge('user', 'gen', label='next()', color='blue')
dot.edge('gen', 'user', label='yield', color='blue')
dot.edge('gen', 'user', label='StopIteration', color='red')
dot
使用生成器对象图示 = dot
import graphviz as gv
graph_attr = {'rankdir': 'LR'}
dot = gv.Digraph('Coroutine', graph_attr=graph_attr, node_attr={'shape': 'box'})
dot.node('user', '调用者')
dot.node('gen', '生成器对象')
dot.edge('user', 'gen', label='next()', color='blue')
dot.edge('gen', 'user', label='yield', color='blue')
dot.edge('gen', 'user', label='StopIteration', color='red')
dot.node('code', '<f1> #第 1 段代码 \\n yield|<f2> #第 2 段代码 \\n yield|<f3> #第 3 段代码 \\n yield | <f4> return', shape='record')
dot.edge('gen', 'code', style='invis', weights='100')
dot.edge('gen:e', 'code:f1', color='blue')
dot.edge('gen:e', 'code:f2', color='blue')
dot.edge('gen:e', 'code:f3', color='blue')
dot.edge('gen:e', 'code:f4', color='blue')
dot
import time
from IPython.display import display, clear_output
def display_list(iterable, delay=2, clear_before=True):
for obj in iterable:
if clear_before:
clear_output()
display(obj)
time.sleep(delay)
import graphviz as gv
graph_list = []
def create_graph(step=0):
graph_attr = {'rankdir': 'LR'}
dot = gv.Digraph('Coroutine', graph_attr=graph_attr, node_attr={'shape': 'box'})
dot.node('user', '调用者')
dot.node('gen', '生成器对象')
dot.edge('user', 'gen', label=f'第 {step+1} 次next()', color='blue')
dot.edge('gen', 'user', label='yield', color='blue', style='invis' if step==3 else None)
dot.edge('gen', 'user', label='StopIteration', color='red', style= None if step==3 else 'invis')
dot.node('code', '<f0> #第 1 段代码 \\n yield|<f1> #第 2 段代码 \\n yield|<f2> #第 3 段代码 \\n yield | <f3> #第 4 段代码 \\n return', shape='record')
dot.edge('gen', 'code', style='invis', weights='100')
dot.edge('gen:e', 'code:f0', color='blue' if step==0 else 'invis')
dot.edge('gen:e', 'code:f1', color='blue' if step==1 else 'invis')
dot.edge('gen:e', 'code:f2', color='blue' if step==2 else 'invis')
dot.edge('gen:e', 'code:f3', color='blue' if step==3 else 'invis')
return dot
for step in range(4):
graph_list.append(create_graph(step))
display_list(graph_list)