PEP 255解读:为什么需要生成器
作者:互联网
#摘要
PEP 255 -- Simple Generators (opens new window)在Python引入了生成器(Generator)的概念,以及与生成器一起使用的一个新语句——yield语句。注意它是语句(statement)而不是表达式(expression) 初始版本的yield没有返回值, PEP 342才将其定义为表达式
#动机
当一个生产者函数在处理某些复杂任务时,它可能需要维持住生产完某个值时的状态(所以不能直接return给你),大多数编程语言都提供不了既易用又高效的方案,基本做法都是让消费者作为回调函数,然后每生产一个值时就去调用一下。
#Python词法解析器 tokenize
作者拿标准库中tokenize函数作为了例子。我们先去看下 Python 2.0 tokenize函数 (opens new window)的文档。
The tokenize module provides a lexical scanner for Python source code, implemented in Python.
即tokenize是python实现的python代码的词法解析器。
2.0版本的api是
tokenize(readline, tokeneater)
两个参数都是函数,调readline,读一行代码,解析出数个token, 对每个token都回调tokeneater函数
现在生成器版本的api是
tokenize(readline)
没tokeneater了。
我们先看下生成器版本的tokenize怎么用(为什么不看非生成器版本的? 因为我没python2.0的环境)
下面的代码用tokenize对自己进行词法解析(我解析我自己)
from tokenize import tokenize
f = open(__file__, 'rb')
def readline_wrapper():
print('Read a line, and parse it')
return f.readline()
g = tokenize(readline=readline_wrapper)
for token_info in g:
print(token_info)
f.close()
部分输出如下
Read a line, and parse it
TokenInfo(type=59 (ENCODING), string='utf-8', start=(0, 0), end=(0, 0), line='')
TokenInfo(type=1 (NAME), string='from', start=(1, 0), end=(1, 4), line='from tokenize import tokenize\r\n')
TokenInfo(type=1 (NAME), string='tokenize', start=(1, 5), end=(1, 13), line='from tokenize import tokenize\r\n')
TokenInfo(type=1 (NAME), string='import', start=(1, 14), end=(1, 20), line='from tokenize import tokenize\r\n')
TokenInfo(type=1 (NAME), string='tokenize', start=(1, 21), end=(1, 29), line='from tokenize import tokenize\r\n')
TokenInfo(type=4 (NEWLINE), string='\r\n', start=(1, 29), end=(1, 31), line='from tokenize import tokenize\r\n')
Read a line, and parse it
TokenInfo(type=58 (NL), string='\r\n', start=(2, 0), end=(2, 2), line='\r\n')
Read a line, and parse it
...
#Python缩进检查 tabnanny
标准库中的tabnanny是使用tokenize的一个例子, 是用来检查代码缩进是否正确的。
基于生成器版本tokenize的主要实现为
# ...
process_tokens(tokenize.generate_tokens(f.readline))
# ...
def process_tokens(tokens):
INDENT = tokenize.INDENT
DEDENT = tokenize.DEDENT
NEWLINE = tokenize.NEWLINE
JUNK = tokenize.COMMENT, tokenize.NL
indents = [Whitespace("")]
check_equal = 0
for (type, token, start, end, line) in tokens:
if type == NEWLINE:
check_equal = 1
elif type == INDENT:
check_equal = 0
thisguy = Whitespace(token)
if not indents[-1].less(thisguy):
witness = indents[-1].not_less_witness(thisguy)
msg = "indent not greater e.g. " + format_witnesses(witness)
raise NannyNag(start[0], msg, line)
indents.append(thisguy)
elif type == DEDENT:
check_equal = 1
del indents[-1]
elif check_equal and type not in JUNK:
check_equal = 0
thisguy = Whitespace(line)
if not indents[-1].equal(thisguy):
witness = indents[-1].not_equal_witness(thisguy)
msg = "indent not equal e.g. " + format_witnesses(witness)
raise NannyNag(start[0], msg, line)
tokenize生成token, process_token中对token进行处理,process_token的状态(indents,check_equal)都是局部变量。
这种写法有点像原文提到的
有一个替代方案是一次性对Python代码进行解析,将所有token放在一个list中。然后再用for循环遍历list,便可以用局部变量和局部控制流(例如循环和嵌套的 if 语句),来跟踪其状态。 然而这样并不实用:要进行解析的python代码特别大,没人知道把它一次性读进来需要用多少内存; 另外,有时我们仅仅想要查看某个特定的东西是否曾出现(例如,future 声明,或者像 IDLE 做的那样,只 是首个缩进的声明),因此解析整个程序就是严重地浪费时间。
不过生成器避免了一次性生成所有token,也就没有浪费内存和效率低的缺点。
作为对比,python2.0版本tabnanny状态保存在全局变量中, 在回调函数tokeneater使用和更新这些状态。
def tokeneater(type, token, start, end, line,
INDENT=tokenize.INDENT,
DEDENT=tokenize.DEDENT,
NEWLINE=tokenize.NEWLINE,
COMMENT=tokenize.COMMENT,
OP=tokenize.OP):
global nesting_level, indents, check_equal
另一个替代方案是为什么不把tokenize实现为迭代器,用next()获取下一个token, 一样没有浪费内存和效率低的缺点。作者给出的理由是:你调用tokenize是方便了, 但将tokenize实现为迭代器可不容易
这个方案也把 tokenize 的负担转化成记住 next() 的调用状态,读者只要瞄一眼 tokenize.tokenize_loop() ,就会意识到这是一件多么可怕的苦差事。或者想象一下,有一个用来生成一般树结构的节 点的算法, 若把它改成一个迭代器实现,就需要手动地移除递归状态并维护遍历的状态
#生成器
提供一种函数,它可以返回中间结果(“下一个值”)给它的调用者,同时还保存了函数的局部状态,以便在停止的位置恢复调用。
def fib():
a, b = 0, 1
while 1:
yield b
a, b = b, a+b
当 fib() 首次被调用时,它将 a 设为 0,将 b 设为 1,然后生成 b 给其调用者。调用者得到 1。当 fib 恢复时,从它的角度来看,yield 语句实际上跟 print 语句差不多:fib 继续执行,且所有局部状态完好无损。然后,a 和 b 的值变为 1,并且 fib 再次循环到 yield,生成 1 给它的调用者。以此类推。 从 fib 的角度来看,它只是提供一系列结果,就像用了回调一样。但是从调用者的角度来看,fib 的调用就是一个可随时恢复的可迭代对象。跟线程一样,这允许两边以最自然的方式进行编码;但与线程方法不同,这可以在所有平台上高效完成。事实上,恢复生成器应该不比函数调用昂贵。
#yield
- yield是一个语句(statement) (注: 过时了,最新版本的yield为expression)
- yield 语句只能在函数内部使用。包含 yield 语句的函数被称为生成器函数(generator function)
- 当调用生成器函数时,实际参数还是绑定到函数的局部变量空间,但不会执行代码。得到的是一个生成器迭代器对象(generator iterator);这符合迭代器协议,因此可用于 for 循环。
- 每次调用 generator-iterator 的 next() 方法时,才会执行 generator function 中的代码,直至遇到 yield 或 return 语句(见下文),或者直接迭代到generator function函数体尽头。
- 如果执行到 yield 语句,则函数的状态会被冻结,并将 expression_list (跟在yield后面的表达式) 的值返回给 next() 的调用者。“冻结”是指挂起所有本地状态,包括局部变量、指令指针和内部堆栈:保存足够的信息,以便在下次调用 next() 时,函数可以继续执行,仿佛 yield 语句只是一次普通的外部调用。
- yield 语句不能用于 try-finally 结构的 try 子句中,因为你不能保证生成器会被再次激活(resume),也就无法保证 finally 语句块会被执行;这与finally的用法相矛盾。
- 生成器正在running时不能resume (即generator function里的代码正在跑着,你还想进入)
- generator function中return不能带表达式 (注:过时了,现在可以带)
def g():
print("running")
i = next(me) # generator is running, can not call next
yield i
me = g()
next(me)
#异常传播
如果一个未捕获的异常——包括但不限于 StopIteration——由生成器函数引发或传递,则异常会以通常的方式传递给调用者,若试图重新激活生成器函数的话,则会引发StopIteration 。 换句话说,未捕获的异常终结了生成器的使用寿命。
def f():
return 1 / 0
def g():
while True:
yield f()
me = g()
try:
next(me)
except Exception as e:
print(type(e)) # <class 'ZeroDivisionError'>
try:
next(me)
except Exception as e:
print(type(e)) # <class 'StopIteration'>
标签:生成器,PEP,yield,token,tokenize,line,type,255 来源: https://www.cnblogs.com/mmyz-sysu-panjn/p/15365696.html