什么是迭代器
Python 中任意对象,只要定义了next方法,它就是一个迭代器。也可以说 只要是for循环的 都是可迭代对象?为什么说只要可以for就是。因为for语句会调用容器对象中的iter函数
>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
什么是生成器
我们知道 在函数体内包含yield 关键字后就不再是一个普通 函数。这种函数叫做生成器
生成器也是一种迭代器,但是你只能通过对其迭代一次。需要的时候再迭代一次。他们的数据不是已经在内存中的,而是在运行的时候生成的值。
其表达形式也很简单 只是将[] 换成() 就可以了。所以我们可以说 生成器表达式可以认为是一种特殊的生成器函数
def read_file_upper(path):
# lines = []
with open(path) as f:
for line in f:
# lines.append(line.upper())
yield line.upper()
# return lines
if __name__ == '__main__':
lines = []
linesUpper = read_file_upper("./test.txt")
# print(linesUpper) # 这个时候生成器处于暂停状态。没有我们指令是不会启动的
# print(next(linesUpper)) # 当使用next后启动状态,结束后等待下一个指令。它会记住自己当前进度
for l in linesUpper:
lines.append(l)
print(lines)
# 这样就将一个简单的函数变成 生成器 将内存使用量降低到最低。
# 单次调用后,next 调用 知道没有数据 会报出 StopIteration 异常
因此 上述代码后我们可以理解迭代器为我们做的事情:
- 不断调用 next 函数让生成器产出数据;
- 直到生成器抛出 StopIteration 异常;
思考?为什么生成器可以暂停。使用了什么原理
我们知道,生成器可以利用 yield 语句,将执行权归还给调用者。因此,生成器暂停执行的秘密就隐藏在 yield 语句中
生成器执行进度被保存在 f_lasti 字段
import dis, inspect
frame = None
def foo():
bar()
bar1()
def bar():
global frame
frame = inspect.currentframe()
pass
def bar1():
global frame
frame = inspect.currentframe()
pass
def gen_func():
yield 1
name = 'admin'
yield 2
gender = 'male'
return 3
if __name__ == '__main__':
foo()
print(frame.f_code.co_name)
gen = gen_func()
print(gen.gi_frame.f_lasti) # -1 ,在没有调用next方法迭代时,f_lasti 等于-1, 表示还没开始呢
print(gen.gi_frame.f_locals)
next(gen)
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)
next(gen)
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)
- 生成器函数编译后代码对象带有 CO_GENERATOR 标识;
- 如果函数代码对象带 CO_GENERATOR 标识,被调用时 Python 将创建生成器对象;
- 生成器创建的同时,Python 还创建一个栈帧对象,用于维护代码对象执行上下文;
- 调用 next/send 驱动生成器执行,Python 将生成器栈帧对象接入调用链,开始执行字节码;
- 执行到 yield 语句时,Python 将 yield 右边的值放入栈顶,并结束字节码执行循环,执行权回到上一个栈帧;
- yield 值最终作为 next 函数或 send 方法的返回值,被调用者取得;
- 再次调用 next/send ,Python 重新将生成器栈帧对象接入调用链恢复执行,通过 send 发送的值被放在栈s顶;
- 生成器函数重新启动后,从 YIELD_VALUE 后的字节码恢复执行,可从栈顶获得调用者发来的值;
- 代码执行权就这样在调用者和生成器间来回切换,而生成器栈顶被用来传值;
总结:
执行过程;
首先 LOADCONST 将需要带给调用者的 yield 右边的表达式值 加载到运行栈栈顶。
然后 YIELD_VALUE 开始放大招 ,它先从栈顶弹出 yield 的值 这个值作为底层函数 __PyEval_EvalFrameDefault 返回值
紧接着,_PyEval_EvalFrameDefault 函数将当前栈帧 (也就是生成器的栈帧) 从调用链中解开。