【2.1.2】列表生成器--yield

今天在微信上看到一个帖子:

下面的代码会报错,为什么?

class A(object):
	x = 1
	gen = (x for _ in xrange(10))  # gen=(x for _ in range(10))
if __name__ == "__main__":
	print(list(A.gen))

在回答这个问题之前,我们先来聊聊几个基本的问题

一、列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用range(1, 11)

	>>> range(1, 11)
	[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但如果要生成[1x1, 2x2, 3x3, …, 10x10]怎么做?方法一是循环:

	>>> L = []
	>>> for x in range(1, 11):
	...    L.append(x * x)
	...
	>>> L
	[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

但是循环太繁琐,而列表生成式则可以用一行语句代替循环生成上面的list:

	>>> [x * x for x in range(1, 11)]
	[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

	>>> [x * x for x in range(1, 11) if x % 2 == 0]
	[4, 16, 36, 64, 100]

还可以使用两层循环,可以生成全排列:

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

其他例子:

	>>> import os # 导入os模块,模块的概念后面讲到
	>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
	['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']
	

	>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
	>>> [k + '=' + v for k, v in d.iteritems()]
	['y=B', 'x=A', 'z=C']
	
	>>> L = ['Hello', 'World', 'IBM', 'Apple']
	>>> [s.lower() for s in L]
	['hello', 'world', 'ibm', 'apple']

将一个列表中的所有字符串变成小写生成一个列表:

L = ['Hello','World',18,'Apple',None]
k = [x.lower() for x in L if isinstance(x,str)]
print k

内建的isinstance函数可以判断一个变量是不是字符串

isinstance(x, str)

二、生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器(Generator)。

要创建一个generator,有很多种方法。

方法一:把一个列表生成式的[]改成()

	>>> L = [x * x for x in range(10)]
	>>> L
	[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
	>>> g = (x * x for x in range(10))
	>>> g
	 at 0x104feab40>

创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。 我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢? 如果要一个一个打印出来,可以通过generator的next()方法:

	>>> g.next()
	0
	>>> g.next()
	1
	>>> g.next()
	4
	>>> g.next()
	9
	...
	>>> g.next()
	Traceback (most recent call last):
	  File "", line 1, in 
	StopIteration

我们讲过,generator保存的是算法,每次调用next(),就计算出下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。 当然,上面这种不断调用next()方法实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:

	>>> g = (x * x for x in range(10))
	>>> for n in g:
	...     print n
	...
	0
	1
	4

方法二:yield

著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到: 1, 1, 2, 3, 5, 8, 13, 21, 34, …

def fib(max):
	n, a, b = 0, 0, 1
	while n < max:
		print b
		a, b = b, a + b
		n = n + 1
	
	>>> fib(6)
	1
	1
	2
	3
	5
	8

要把fib函数变成generator,只需要把print b改为yield b就可以了:

def fib(max):
	n, a, b = 0, 0, 1
	while n < max:
		yield b
		a, b = b, a + b
		n = n + 1

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

	>>> fib(6)

	>>> def odd():
	...     print 'step 1'
	...     yield 1
	...     print 'step 2'
	...     yield 3
	...     print 'step 3'
	...     yield 5
	...
	>>> o = odd()
	>>> o.next()
	step 1
	1
	>>> o.next()
	step 2
	3
	>>> o.next()
	step 3
	5
	>>> o.next()
	Traceback (most recent call last):
	  File "", line 1, in 
	StopIteration

可以看到,odd不是普通函数,而是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next()就报错。

同样的,把函数改成generator后,我们基本上从来不会用next()来调用它,而是直接使用for循环来迭代:

	>>> for n in fib(6):
	...     print n
	...
	1
	1
	2
	3
	5
	8

三、变量作用域

回到最开始的问题。这个问题是变量作用域问题,在 gen =( x for _ in xrange (10)) 中 gen 是一个 generator ,在 generator 中变量有自己的一套作用域,与其余作用域空间相互隔离。因此,将会出现这样的 NameError:name ' x ' is not defined 的问题,那么解决方案是什么呢?答案是:用 lambda 。

class A(object):
	x = 1
	gen = (lambda x: (x for _ in xrange(10)))(
		x)  # gen=(x for _ in range(10))
if __name__ == "__main__":
	print(list(A.gen))

或者,也可以这样:

class A(object):
	x = 1
	gen = (A.x for _ in xrange(10))  # gen=(x for _ in range(10))
if __name__ == "__main__":
	print(list(A.gen))

参考资料

廖雪峰–列表生成式 

廖雪峰–生成器 http://blog.csdn.net/yejianyun1/article/details/52640838

药企,独角兽,苏州。团队长期招人,感兴趣的都可以发邮件聊聊:tiehan@sina.cn
个人公众号,比较懒,很少更新,可以在上面提问题,如果回复不及时,可发邮件给我: tiehan@sina.cn