【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

个人公众号,比较懒,很少更新,可以在上面提问题:

更多精彩,请移步公众号阅读:

Sam avatar
About Sam
专注生物信息 专注转化医学