奇妙的流控制 Python中的迭代器与生成器

开发 后端 前端
文章介绍了在Python 2.2中引入的迭代器和生成器的原理和用法,对于Python程序员来说,非常有必要了解迭代器和生成器的来龙去脉。

在Python 2.2中引进了一种带有新关键字的新型构造。这种构造是生成器;关键字是yield。生成器使几个新型、强大和富有表现力的编程习惯用法成为可能,但初看,要理解生成器,还是有一点困难。

51CTO推荐阅读:深入了解Python暂缓列表生成器

由于迭代器比较容易理解,让我们先来看它。基本上, 迭代器是含有 .next() 方法的对象。唔,这样定义不十分正确,但非常接近。事实上,当迭代器应用新的 iter() 内置函数时,大多数迭代器的上下文希望得到一个可以生成迭代器的对象。为使用户定义的类(该类含有必不可少的 .next() 方法)返回迭代器,需要使 __iter__() 方法返回 self 。本文中的示例会清楚地说明这一点。如果迭代有一个逻辑终止,则迭代器的 .next() 方法可能决定抛出 StopIteration 异常。

生成器要稍微复杂和一般化一点。但生成器最典型的用途是用来定义迭代器;所以不值得总是为一些细微之处而担心。 生成器是这样一个函数,它记住上一次返回时在函数体中的位置。对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。

在某些方面,生成器就象本专栏前面文章讨论的函数型编程中的“终止”。象“终止”一样,生成器“记住”了它数据状态。但生成器比“终止”要更进一步:生成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。由于连续性使您在执行框架间任意跳转,而不总是返回到直接调用者的上下文(如同生成器那样),因此它仍是比较一般的。幸运的是,使用生成器比理解程序流和状态的所有概念性问题容易得多。实际上,稍加实践之后,就可以象普通函数那样容易地使用生成器。

随机遍历

让我们考虑一个相当简单的问题,可以用多种方法来解决它 ― 新方法和旧方法都可以。假设我们想要一串正的随机数字流,它比服从向后参考约束的数字流要小。明确的讲,我们希望每个后续数字比前一个数字至少大或小 0.4。而且,数字流本身不是无限的,在几个随机步骤后结束。这个示例中,当数字流中产生小于 0.1 的数字时,我们将简单地结束它。上述的约束有点象可以在“随机遍历”算法找到的约束,结束条件类似“统计”或“局部最小值”结果 ― 但当然,这要比大多数现实世界中简单。在 Python 2.1或更早的版本中,我们有几种方法来解决这个问题。一种方法是,简单地生成流中的数字列表并返回它。可能看起来象:

  1. RandomWalk_List.py  
  2. import  
  3. random  
  4. def  
  5. randomwalk_list  
  6. ():  
  7. last, rand = 1, random.random()   
  8. # init candidate elements  
  9. nums = []     
  10. # empty list  
  11. while  
  12. rand > 0.1:   
  13. # threshhold terminator  
  14. if  
  15. abs(last-rand) >= 0.4:     
  16. # accept the number  
  17.   last = rand 
  18.   nums.append(rand)     
  19. # add latest candidate to nums  
  20. else  
  21. :  
  22. print  
  23. '*',  
  24. # display the rejection  
  25. rand = random.random()    
  26. # new candidate  
  27. nums.append(rand)   
  28. # add the final small element  
  29. return  
  30. nums 

利用这个函数就象如下所示般简单:

  1. 随机遍历列表的迭代  
  2. for num in randomwalk_list():  
  3. print num, 

上面这种方法中有几个值得注意的局限性。这个特定的示例中极不可能产生庞大的数字列表,但只通过将阀值终结符定义得较严格,就可以创建任意大流(随机精确大小,但可以预见数量级)。在某种程度上,内存和性能问题可能使得这种方法不切实际,以及没有必要。同样是这个问题,使得 Python 较早的版本中添加了 xrange() 和 xreadlines() 。更重要的是,许多流取决于外部事件,并且当每个元素可用时,才处理这些流。例如,流可以侦听一个端口,或者等待用户输入。试图在流之外创建完整的列表并不就是这些情形中的某一种。

在 Python 2.1 和较早版本中,我们的诀窍是使用“静态”函数局部变量来记住关于函数的上一次调用的一些事情。显而易见,全局变量可以做同样的工作,但它们带来了大家熟知的全局性名称空间污染的问题,并会因非局部性而引起错误。这里,如果您不熟悉这个诀窍,可能会感到诧异 ― Python 没有“正式”的静态范围声明。然而,如果赋予了命名参数可变的缺省值,那么参数就可以,用作以前调用的持久存储器。明确的讲,列表是一些便利的可变对象,他们甚至可以方便地保留多个值。使用“静态”方法,可以编写如下的函数:

  1. RandomWalk_Static.py  
  2. import  
  3. random  
  4. def  
  5. randomwalk_static  
  6. (last=[1]):  
  7. # init the "static" var(s)  
  8. rand = random.random()  
  9. # init a candidate value  
  10. if  
  11. last[0] < 0.1:   
  12. # threshhold terminator  
  13. return  
  14. None     
  15. # end-of-stream flag  
  16. while  
  17. abs(last[0]-rand) < 0.4:    
  18. # look for usable candidate  
  19. print  
  20. '*',  
  21. # display the rejection  
  22. rand = random.random()    
  23. # new candidate  
  24. last[0] = rand  
  25. # update the "static" var  
  26. return  
  27. rand 

这个函数是十分友好的存储器。它只需要记住一个以前的值,返回一个单个数字(不是一个数字的大列表)。并且与此类似的一个函数可以返回取决于(部分地或完全地)外部事件的连续的值。不利的一面是,利用这个函数有点不够简练,且相当不灵活。

  1. 静态随机遍历的迭代  
  2. num = randomwalk_static()  
  3. while num is not None:  
  4. print num,  
  5. num = randomwalk_static() 

#p#

新的遍历方法

实质上,Python 2.2 序列都是迭代器。Python 常见的习惯用法 for elem in lst: 现在实际上让 lst 产生一个迭代器。然后, for 循环反复调用这个迭代器的 .next() 方法,直到它遇到 StopIteration 异常为止。幸运的是,由于所有常见的内置类型自动产生它们的迭代器,所以 Python 程序员不需要知道这里发生了什么。实际上,现在字典里有 .iterkeys() 、 .iteritems() 和 .itervalues() 方法来产生迭代器;首要的是在新的习惯用法 for key in dct: 中使用了什么。同样,通过调用 .readline() 迭代器支持新的习惯用法 for line in file: 。

但是如果实际所产生的是在 Python 解释器内,则显而易见要用定制类来产生它们自己的迭代器,而不是专使用内置类型的迭代器。定制类支持直接使用 randomwalk_list() 以及一次一个元素这种“极度节省”的 randomwalk_static ,它是简单易懂的:

  1. RandomWalk_Iter.py  
  2. import  
  3. random  
  4. class  
  5. randomwalk_iter  
  6. :  
  7. def  
  8. __init__  
  9. (self):  
  10. self.last = 1   
  11. # init the prior value  
  12. self.rand = random.random()   
  13. # init a candidate value  
  14. def  
  15. __iter__  
  16. (self):  
  17. return  
  18. self     
  19. # simplest iterator creation  
  20. def  
  21. next  
  22. (self):  
  23. if  
  24. self.rand < 0.1:   
  25. # threshhold terminator  
  26. raise  
  27. StopIteration   
  28. # end of iteration  
  29. else  
  30. :   
  31. # look for usable candidate  
  32. while  
  33. abs(self.last-self.rand) < 0.4: 
  34. print  
  35. '*',  
  36. # display the rejection  
  37. self.rand = random.random()   
  38. # new candidate  
  39. selfself.last = self.rand     
  40. # update prior value  
  41. return  
  42. self.rand 

这个定制迭代器看起来确实如同由函数生成的真实列表一样:

  1. 随机遍历类的迭代  
  2. for num in randomwalk_iter():  
  3. print num, 

事实上,即使支持习惯用法 if elem in iterator ,它仅尝试为确定真值所需要的那么多的迭代器的元素,(如果最终的值为 false,当然,它就需要测试所有元素)。

#p#

美中不足

上述方法对于手边的问题非常好用。但没有一种方法能很好地解决这样的情形:例程在运行中创建了大量的局部变量,并把它的运行简化为循环和条件的嵌套。如果带静态(或全局)变量的迭代器类或函数取决于多个数据状态,则出现两个问题。一个是一般性问题:创建多个实例属性或静态列表元素来保留每个数据值。更为重要的问题是计算如何确切地返回到与数据状态相符的流逻辑的相关部分。非常容易忘记不同数据间的相互作用和互相依存。

生成器完全绕过了整个问题。生成器“返回”时带关键字 yield ,但“记住”了它“返回”的所有确切执行位置。下次调用生成器时,它再接着上次的位置 — 包括函数流和变量值这两个方面。

在 Python 2.2+ 中,不直接 写生成器。相反,编写一个函数,当调用它时,返回生成器。这可能看起来有点古怪,但“函数工厂”是 Python 的常见特性,并且“生成器工厂”明显是这个概念性扩展。在 Python 2.2+ 中使函数成为生成器工厂是它主体某处的一个或多个 yield 语句。如果 yield 发生, return 一定只发生在没有伴随任何返回值的情况中。然而,一个较好的选择是,安排函数体以便于完成所有 yield 之后,执行就“跳转到结束”。但如果遇到 return ,它导致产生的生成器抛出 StopIteration 异常,而不是进一步生成值。

从我的观点来看,过去对生成器工厂的语法选择有点欠缺。 yield 语句可以非常好地存在于函数体中,您可能无法确定是否函数一定会在函数体最初 N 行内的某处作为生成器工厂而存在。当然,对于函数工厂,也存在这样的问题,但是由于函数工厂不改变函数体的实际 语法(并且有时允许函数体返回普通值,尽管这可能不是出自良好的设计)。对于我来说,新关键字 ― 比如 generator 代替 def ― 会是一个比较好的选择。

先不考虑语法,当调用生成器来担当迭代器时,生成器有良好的状况来自动担当迭代器。这里不需要象类的 .__iter__() 方法。遇到的每个 yield 都成为生成器的 .next() 方法的返回值。为了清楚起见,我们来看一个最简单的生成器:

  1. 最简单可行的 Python 2.2 生成器  
  2. >>>   
  3. from  
  4. __future__   
  5. import  
  6. generators  
  7. >>>   
  8. def  
  9. gen  
  10. ():  
  11. yield 1  
  12. >>> g = gen()  
  13. >>> g.next()  
  14. 1  
  15. >>> g.next()  
  16. Traceback (most recent call last):  
  17. File "<pyshell#15>", line 1,   
  18. in  
  19. ?  
  20. g.next()  
  21. StopIteration 

让我们使生成器工作在我们样本问题中:

  1. RandomWalk_Generator.py  
  2. from  
  3. __future__   
  4. import  
  5. generators     
  6. # only needed for Python 2.2  
  7. import  
  8. random  
  9. def  
  10. randomwalk_generator  
  11. ():  
  12. last, rand = 1, random.random()   
  13. # initialize candidate elements  
  14. while  
  15. rand > 0.1:   
  16. # threshhold terminator  
  17. print  
  18. '*',  
  19. # display the rejection  
  20. if  
  21. abs(last-rand) >= 0.4:     
  22. # accept the number  
  23.   last = rand     
  24. # update prior value  
  25.   yield rand  
  26. # return AT THIS POINT  
  27. rand = random.random()    
  28. # new candidate  
  29. yield rand    
  30. # return the final small element 

这个定义的简单性是吸引人的。可以手工或者作为迭代器来利用这个生成器。在手工情形下,生成器可以在程序中传递,并且无论在哪里以及无论何时需要(这非常灵活),都可以调用。手工情形的一个简单示例是:

  1. 随机遍历生成器的手工使用  
  2. gen = randomwalk_generator()  
  3. try:  
  4. while 1: print gen.next(),  
  5. except StopIteration:  
  6. pass 

然而,更多情况下,可能将生成器作为迭代器来使用,这样更为简练(并且看起来又象只是一个老式的序列):

  1. 作为迭代器的随机遍历生成器  
  2. for num in randomwalk_generator():  
  3. print_short(num) 

结束语

Python 程序员需要花一点时间来熟悉生成器的来龙去脉。最初这样一个简单构造所增加的能力是令人惊奇的;并且我预言,甚至熟练的程序员(象 Python 开发人员自己)也需要花一些时间来继续发现使用生成器过程中的一些微妙的新技术。

【编辑推荐】

  1. Python闭包的概念、形式与应用
  2. 手把手教您Python多线程应用技巧
  3. 对Python特色的详细介绍
  4. 加速程序开发 Python整合C语言模块

 

责任编辑:王晓东 来源: IBM DW
相关推荐

2023-03-01 00:07:32

JavaScript迭代器生成器

2017-06-26 16:26:15

Python迭代对象迭代器

2023-11-15 13:35:00

迭代器生成器Python

2023-09-02 20:15:48

迭代器前端生成器

2017-09-06 09:26:03

Python生成器协程

2023-07-21 17:08:30

2017-03-20 17:49:21

Java Web模板代码

2023-05-05 08:53:38

迭代器生成器Python

2009-07-01 17:30:14

样式生成器Visual Stud

2021-12-04 22:07:44

Python

2023-05-04 16:24:10

人工智能图像生成器

2023-02-07 16:11:41

2022-07-25 10:27:36

背景生成器工具前端

2010-03-26 13:03:23

Boost.Pytho

2021-06-06 16:31:57

PythonPython 3.7 编程语言

2021-11-28 08:03:41

Python迭代器对象

2022-02-15 10:30:58

UUID

2012-03-30 09:31:44

WEBCSS

2009-07-01 17:35:04

样式生成器Visual Stud

2010-09-07 16:31:17

SQL语句insert
点赞
收藏

51CTO技术栈公众号