ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

PEP 255解读:为什么需要生成器

2021-10-04 08:33:34  阅读:201  来源: 互联网

标签:生成器 PEP yield token tokenize line type 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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有