当前位置: 首页 > Linux

[翻译]PEP255--SimpleGenerators

时间:2023-04-06 23:34:25 Linux

本来打算写一个Python生成器的,但是查资料的时候发现介绍生成器的PEP没人翻译,所以花了点时间翻译它。如果你在阅读的时候有不懂的地方,不要怀疑,很可能是我翻译的不到位。如果出现这种情况,建议大家直接看原文,最好把错误的地方告诉我,以便我修改。原文:https://www.python.org/dev/pe...创建日期:2001-05-18并入Python版本:2.2译者:豌豆花猫(Python猫公众号作者)PEP背景知识:学Python,怎么能不懂PEP呢?总结本PEP想要介绍Python中生成器的概念,以及一个新的表达式,即yield表达式。Motivation当一个生产者函数在处理一些困难的任务时,可能需要维护它产生一定值时的状态。大多数编程语言无法提供舒适高效的解决方案,除了在参数列表函数中添加回调,然后在每次产生值时调用它。例如,标准库中的tokenize.py就使用了这种方法:调用者必须传递一个tokeneater函数给tokenize(),然后在tokenize()找到下一个token时调用它。这允许以自然的方式编写tokenize,但程序调用tokenize将变得极其复杂,因为它需要记住在每次回调之前最后看到的是哪个token(s)。tabnanny.py中的tokeneater函数是一个更好的例子。它在全局变量中维护了一个状态机,用来记录已经出现的token和期望出现的token。很难正确,也很难理解。不幸的是,它已经是最标准的解决方法。另一种方法是立即生成Python程序的整个解析并将其存储在一个非常大的列表中。这允许标记化客户端使用局部变量和局部控制流(例如循环和嵌套if语句)以自然方式跟踪其状态。然而,这是不切实际的:程序会变得臃肿,因此不可能对实现整个解析所需的内存进行先验限制;一些tokenize客户只是想看看是否发生过特定的事情(例如,一个futurestatement,或者像IDLE那样只是第一个缩进的语句),所以解析整个程序是一种严重的时间浪费。另一种选择是将tokenize变成迭代器[注1],并在每次调用其next()方法时传递下一个标记。这对调用者来说非常方便,就像之前的解决方案将结果存储在一个大列表中一样,没有内存和“如果我想提前退出怎么办”的缺点。然而,这个解决方案也将标记化的负担转移到记住next()调用的状态,读者只需要看一眼tokenize.tokenize_loop()就会意识到这是多么可怕的苦差事。或者想象一下用递归算法生成一个普通树结构的节点:如果你将它投影到一个迭代器框架实现中,你需要手动去除递归状态并保持遍历状态。第四种选择是在不同的线程中运行生产者和消费者。这允许双方以自然的方式保持他们的状态,因此双方都会感到舒适。事实上,Python源代码分发中的Demo/threads/Generator.py提供了一个可用于常见任务的同步通信类。但是,这不会在没有线程的平台上工作,并且如果它工作的话会很慢(与没有线程可以实现的相比)。最后一个选项是使用支持轻量级协程的Python变体Stackless[注2-3]。它具有与上述线程方案相同的编程优势,但效率更高。然而,Stackless在Python的核心层面上存在争议,而Jython可能不会实现相同的语义。本PEP不是讨论这些问题的地方,但可以肯定地说,生成器是当前CPython中Stackless相关功能的一个子集的简单实现,可以说其他Python实现起来相对简单。以上分析完成了现有方案。其他一些高级语言也提供了很好的解决方案,特别是Sather的迭代器,它的灵感来自于CLU的迭代器[注4];Icon的生成器,一种生成每个表达式的新颖语言[注释5]。虽然不同,但基本思想是一样的:提供一个函数,返回一个中间结果(“下一个值”)给它的调用者,同时也保存函数的局部状态,以便在停止位置调用Resume(译注:简历,以下也译为激活)打电话。一个很简单的例子:deffib():a,b=0,1while1:yieldba,b=b,a+b当第一次调用fib()时,它设置a为0,b为1,然后生成b给它的调用者。调用者得到1。当fib恢复时,从它的角度来看,yield语句实际上与print语句相同:fib继续执行,所有本地状态完好无损。然后,a和b的值变成1,fib再次循环yield,把1yield给它的调用者。等等。从fib的角度来看,它只是提供一系列结果,就好像使用回调一样。但是从调用者的角度来看,fib的调用是一个可以随时恢复的可迭代对象。与线程一样,这允许双方以最自然的方式进行编码;但与线程方法不同,这可以在所有平台上高效地完成。事实上,恢复一个生成器应该不会比一个函数调用更昂贵。同样的方法适用于许多生产者/消费者功能。例如,tokenize.py可以生成下一个标记而不是调用回调函数并将其作为参数,并且标记化客户端可以以自然的方式迭代标记:Python生成器是一种迭代器,但非常强大。设计规范:yield引入了一个新的表达式:yield_stmt:"yield"expression_listyield是一个新的关键字,所以需要future声明[注8]引入:在早期版本中,如果要使用生成器的模块,必须包含以下内容头部附近的行(有关详细信息,请参阅PEP236):from__future__importgenerators如果在没有导入future模块的情况下使用yield关键字,将发出警告。在后续版本中,yield将成为语言关键字,不再需要future语句。yield语句只能在函数内部使用。包含yield语句的函数称为生成器函数。生成器函数在所有方面都只是一个普通函数,但在其代码对象的co_flags中设置了新的“CO_GENERATOR”标志。调用生成器函数时,实际参数仍绑定到函数的局部变量空间,但不执行任何代码。结果是一个生成器-迭代器对象;这符合迭代器协议[注释6],因此可以在for循环中使用。请注意,非限定名称“generator”可以指生成器函数和生成器迭代器,其中上下文是明确的。每次调用生成迭代器的next()方法时,都会执行生成器函数主体中的代码,直到遇到yield或return语句(见下文),或者迭代结束。如果到达yield语句,则函数的状态被冻结,expression_list的值返回给next()的调用者。“冻结”意味着暂停所有局部状态,包括局部变量、指令指针和内部堆栈:保存足够的信息以便下次调用next()时,函数可以继续执行,就好像yield语句只是一个普通语句一样外部调用。限制:yield语句不能用在try-finally结构的try子句中。困难在于不能保证生成器将被恢复,因此不能保证finally块将被执行;那会破坏最终太多的目的。限制:生成器在激活时不能被重新激活:>>>defg():...i=me.next()...yieldi>>>me=g()>>>me.next()Traceback(最近调用最后):...文件“”,第2行,在gValueError中:生成器已在执行设计规范:返回生成器函数还可以包含以下形式的返回语句:return正文不允许使用expression_list(当然它们可以嵌套在生成器内部的非生成器函数中)。当到达return语句时,程序正常返回,继续执行适当的finally子句(如果有的话)。然后引发StopIteration异常,表明迭代器已用完。如果程序执行到生成器末尾而没有显式返回,也会引发StopIteration异常。请注意,对于生成器和非生成器函数,返回意味着“我完成了,没有什么有趣的东西可以返回”。请注意,return不一定会触发StopIteration:关键是如何处理封闭的try-except构造。例如:>>>deff1():...try:...return...except:...yield1>>>printlist(f1())[]因为,就像在任何函数中一样,return只是退出,但是:>>>deff2():...try:...raiseStopIteration...except:...yield42>>>printlist(f2())[42]因为StopIterationCaught通过一个简单的例外,就像任何例外一样。设计规范:生成器和异常传播如果生成器函数引发或传递未捕获的异常(包括但不限于StopIteration),则异常将以通常的方式传递给调用者,并尝试重新激活生成器函数如果是这样,StopIteration将被提升。换句话说,未捕获的异常结束了生成器的生命周期。例子(非语言,只是一个例子):>>>deff():...return1/0>>>defg():...yieldf()#零除异常传播...yield42#我们永远不会到达这里>>>k=g()>>>k.next()Traceback(最近一次调用最后):文件“”,第1行,在?File"",line2,ingFile"",line2,infZeroDivisionError:integerdivisionormodulobyzero>>>k.next()#生成器无法恢复回溯(最近调用last):File"",line1,in?StopIteration>>>Designspecification:Try/Exception/Finally如前所述,try-finally结构的try子句中不能使用yield语句。这样做的结果是生成器非常小心地分配关键资源。但在其他地方,yield语句不受限制,比如finally子句,except子句,或者try-except结构的try子句:>>>deff():...try:...yield1..。try:...yield2...1/0...yield3#永远不会到达这里...exceptZeroDivisionError:...yield4...yield5...raise...except:...yield6...yield7#上面的“raise”停止了这个...除了:...yield8...yield9...try:...x=12...finally:...yield10...yield11>>>printlist(f())[1,2,4,5,8,9,10,11]>>>Example#BinarytreeclassTree:def__init__(self,label,left=None,right=None):self.label=labelself.left=leftself.right=rightdef__repr__(self,level=0,indent=""):s=level*indent+`self.label`如果self.left:s=s+"\n"+self.left.__repr__(level+1,indent)如果self.right:s=s+"\n"+self.right.__repr__(level+1,indent)returnsdef__iter__(self):returninorder(self)#从列表创建Treedeftree(list):n=len(list)ifn==0:return[]i=n/2returnTree(list[i],tree(list[:i]),tree(list[i+1:]))#递归生成器,依次生成树labelsdefinorder(t):ift:forxininorder(t.left):yieldxyieldt.labelforxininorder(t.right):yieldx#演示:创建一棵树t=tree("ABCDEFGHIJKLMNOPQRSTUVWXYZ")#按顺序打印树的节点forxint:printx,print#non-recursivegeneratordefinorder(node):stack=[]whilenode:whilenode.left:stack.append(node)node=node.leftyieldnode.labelwhilenotnode.right:try:node=stack.pop()exceptIndexError:returnyieldnode.labelnode=node.right#练习非递归生成器forxint:printx,print两个输出块显示:ABCDEFGHIJKLMNOPQRSTUVWXYZQ&A为什么重用def而不是new关键字?请参阅下面的BDFL声明部分为什么使用新关键字yield而不是内置函数?在Python中通过关键字可以更好的表达控制流,即yield是一个控制结构。为了高效地实现Jython,编译器需要在编译时识别潜在的挂起点,而new关键字将使这变得容易。CPython实现还大量使用它来检测哪些函数是生成器函数(尽管用new关键字代替def可以解决CPython的问题,但当人们问“为什么要使用新关键字?”关键字时,他们并不想要新关键字)。为什么不用new关键字的其他特殊语法呢?例如,为什么不使用yield3而不是:return3andcontinuereturnandcontinue3returngenerating3continuereturn3return>>,3fromgeneratorreturn3return>>3return<<3>>3<<3*3我没有错过任何一个眨眼酒吧?在数百条消息中,我为每个替代方案计算了三个建议并得出了上述结论。不需要新关键字会很好,但使用yield会更好——我个人认为yield在一堆无意义的关键字或运算符序列中更具表现力。尽管如此,如果这引起了足够的兴趣,支持者应该发起一项提案,让Guido进行裁决。为什么允许返回,但不强制执行StopIteration?“StopIteration”的机制是一个底层细节,就像Python2.1中的“IndexError”机制一样:实现需要做一些预定义的事情,Python为高级用户开放了这些机制。虽然并不是每个人都必须在这个级别工作。任何一种函数中的“返回”都表示“我完成了”,这很容易解释和使用。请注意,return并不总是等同于try-except构造中的raiseStopIteration(请参阅“设计规范:返回”部分)。那为什么不允许返回一个表达式呢?也许有一天它会被允许。在Icon中,returnexpr表示“我完成了”和“但我还有最后一个有用的值要返回,仅此而已”。在初始阶段,使用yield直接传递值而不强制使用returnexpr是很直接的。BDFL声明Issue引入了另一个新关键字(例如gen或generator)来替换def,或者以其他方式更改语法以区分生成器函数和非生成器函数。实际上(你如何看待它们),生成器是函数,但它们具有可恢复性。设置它们的机制是一个相对较小的技术问题,引入新关键字无助于强调生成器启动机制(生成器生命中至关重要但很小的一部分)。Pro实际上(你如何看待它们),生成器函数实际上是工厂函数,可以像魔术一样生成生成器迭代器。在这方面,它们与非生成器函数有很大不同,更像是构造函数而不是函数,因此重用def肯定会造成混淆。隐藏的yield语句不足以警告它们的语义如此不同。BDFLdef保留。任何一方的论点都不能完全令人信服,所以我咨询了我的语言设计者的直觉。它告诉我PEP中提出的语法是完全正确的——不太热,也不太冷。但是,就像希腊神话中的Delphi神谕一样,它并没有告诉我原因,所以我不会反驳反对这个PEP语法的论点。我能想到的最好的(除了我同意做出的反驳)是“FUD”。如果这从第一天起就成为语言的一部分,我非常怀疑它很久以前就会成为AndrewKuchling的“PythonWarts”页面。(译注:wart是疣,一种难看的皮肤病。这是一个wiki页面,列出了用Python找茬的建议)。ReferenceImplementation当前的实现(注释:2001)处于初步状态(未记录,但经过充分测试且可靠),是Python的CVS开发树[注释9]的一部分。使用它需要您从源代码构建Python。这是来自NeilSchenmauer[注释7]的早期补丁。脚注和参考文献[1]PEP-234、迭代器、Yee、VanRossumhttp://www.python.org/dev/pep...[2]http://www.stackless.com/[3]PEP-219,StacklessPython,McMillanhttp://www.python.org/dev/pep...[4]“Sather中的迭代抽象”Murer、Omohundro、Stoutamire和Szyperskihttp://www.icsi.berkeley.edu/。..[5]http://www.cs.arizona.edu/icon/[6]PEP234中描述了迭代器的概念。请参见上面的[1]。[7]http://python.ca/nas/python/g...[8]PEP236,回到__future__,Petershttp://www.python.org/dev/pep...[9]要试验此实现,请根据http://sf.net/cvs/?group_id=5470上的说明从CVS中检查Python,请注意标准测试Lib/test/test_generators.py包含许多示例,包括所有这些在这个PEP中。版权信息本文档已置于公共领域。源文档:https://github.com/python/peps/blob/master/pep-0255.txt(翻译完)PS:官方PEP有近500个,保守估计被翻译的不到20个成中文(在去重的情况下)。我很好奇,有多少人有兴趣翻译一些重要的PEP?现将此问题抛出探路,欢迎留言交流。---------------本文翻译发表于微信公众号【蟒猫】,后台回复“爱学习”,送你20+精选e-免费书籍。