其他分享
首页 > 其他分享> > PEP 255解读:为什么需要生成器

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

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