某次面试问候选人:Python 中生成器是什么?答曰:有 yield 关键字的函数。而在我印象中此种函数返回的值是生成器,而函数本身不是。如下:

In [1]: def get_nums(n): 
   ...:     for i in range(n): 
   ...:         yield i 
In [2]: type(get_nums)
Out[2]: function
In [3]: nums = get_nums(10)   
In [4]: type(nums)
Out[4]: generator

但看候选人那么笃定,隐隐然感觉哪里不对,于是有了以下探究。

作者:木鸟杂记 www.qtmuniao.com, 转载请注明出处

要弄清楚 Python3 中这些概念的区别,最权威的当然是去看官方的术语对照表

generator

​ A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.

​ Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn’t clear, using the full terms avoids ambiguity.

generator iterator

​ An object created by a generator function.

​ Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator iterator resumes, it picks up where it left off (in contrast to functions which start fresh on every invocation).

generator expression

​ An expression that returns an iterator. It looks like a normal expression followed by a for clause defining a loop variable, range, and an optional if clause.

可以看出,Python generator 相关的概念主要有三个,分别是 generatorgenerator expressiongenerator iterator。上面也特别指出了, genrator 在有的上下文中指的是 generator function(如候选人所说),但在另外一些上下文中却指的是 generator iterator(如上面例子解释器告诉我们的)。为了避免歧义,大家在表达时可以尽量说全称。

结合我的一些经验,可以将其归纳为以下三个概念:

  • Generator Function:含有 yield 关键字的函数,会返回一系列值,可以使用 next() 对其返回值进行迭代。
  • Generator Iterator:generator function 返回的对象。可以进行一次性地迭代。
  • Generator Expression:可以求值为 generator iterator 的表达式。使用小括号和 for 来定义,如下面例子:
  • In [5]: a = (i*i for i in range(10))                                                                                                                                                                                                                                     
    In [6]: type(a)                                                                                                                                                                                                                                                          
    Out[6]: generator
    

    生成器 vs 迭代器

    从中文字面可能不好理解它们的关系,但是从上文提到的英文术语来分析:generator iterator。他们的关系就一目了然了:

    迭代器(Iterator)是一种更宽泛的概念,生成器(generator Iterator)是一种迭代器,但反过来不成立。

    迭代器是任何实现了 __next__ 方法的对象(object),可以通过 next(iterator) 对其进行迭代,迭代结束时会抛出 StopIteration 异常。

    while True:
      try:
        x = next(an_iterator)
        do_sth_with(x)
      except StopIteration:
        break
    

    通常我们会使用 for in 来对其进行简化迭代:

    for x in an_iterator:
      do_sth_with(x)
    

    yield 原理

    yield 是一个神奇的关键字,它会临时挂起当前函数,记下其上下文(包括局部变量、待决的 try catch 等),将控制权返回给函数调用者。当下一次再调用其所在 generator function 时,会恢复保存的上下文,继续执行剩下的语句,直到再遇到 yield 或者退出为止。

    我们常见的 return 是与之相对的关键字,但 return 会结束函数调用,销毁上下文(弹出栈帧),将控制权返回给调用者。

    因此,以 yield 进行执行流控制的函数称为 generator function,以 return 进行执行流控制的函数,就是普通的 function 喽~

    当然,由于可以临时挂起函数的执行,yield 还有更高阶的用法,即充当其调用者和被挂起函数间交互的桥梁:

    In [1]: def dynamic_step_seq(size, start=0, default_step=1): 
        ...:   x = start 
        ...:   for _ in range(size): 
        ...:     given_step = yield x 
        ...:     if given_step is not None: 
        ...:       x += given_step 
        ...:     else: 
        ...:       x += default_step 
    In [2]: for x in dynamic_step_seq(10): 
        ...:     print(x, end=' ') 
        ...: print()                                                                                                                                                                                                                                                         
    0 1 2 3 4 5 6 7 8 9 
    In [3]: dss = dynamic_step_seq(10)                                                                                                                                                                                                                                      
    In [4]: next(dss) # 注                                                                                                                                                                                                                                                
    Out[5]: 0
    In [6]: dynamic_step = 1 
        ...: while True: 
        ...:     try: 
        ...:         x = dss.send(dynamic_step) 
        ...:         dynamic_step += 1 
        ...:         print(x, end=' ') 
        ...:     except StopIteration: 
        ...:         print() 
        ...:         break 
    1 3 6 10 15 21 28 36 45 
    

    :此处初看有些奇怪,但是通过 yield 作用我们能推断出原理:需要首先调用 next 将函数运行至 yield 处,才能通过 generator.send 给 generator 传送对象。

    那么使用生成器有什么好处呢?简单来说,主要有两大好处:

    使用 yield 关键字或者生成器表达式可以很方便的生成一个迭代器对象。为了说明这一点,我们来比较一下对于一个需求的不同实现。该需求很简单:获取前 n 个自然数。

    最直观的方法为,构造一个数组然后返回:

    # Build and return a list
    def firstn(n):
        num, nums = 0, []
        while num < n:
            nums.append(num)
            num += 1
        return nums
    sum_of_first_n = sum(firstn(1000000))
    

    当 n 很小的时候,该实现没有什么问题,但是当 n 变得很大,你的机器内存是吃不消的:

    In [4]: a = firstn(10000000000000000000)                                                                                                                                                                                                                                 
    Killed
    

    IPython 直接内存爆掉被 kill 了。

    于是,我们很自然的想起可以用生成器模式,但是你仍然不想用 yield,于是你需要构造一个对象,并实现 __iter____next__ 方法:

    # Using the generator pattern (an iterable)
    class firstn(object):
        def __init__(self, n):
            self.n = n
            self.num, self.nums = 0, []
        def __iter__(self):
            return self
        def __next__(self):
            if self.num < self.n:
                cur, self.num = self.num, self.num+1
                return cur
            else:
                raise StopIteration()
    sum_of_first_n = sum(firstn(1000000))
    

    :在 Python3 中,如果你的类实现了__iter__ ,则成为一个可迭代对象,可以调用 iter(instance) 得到一个迭代器。如果你的类实现了 __next__,那么你的类实例本身就成为了一个迭代器,可以通过 next(instance) 来调用,进行迭代。

    这些代码终于实现了我们的要求。但是,它有如下问题:

  • 为了实现一个简单的需求却不得不构造冗长的代码。
  • 使用了很多语言约定,读起来不直观。
  • 逻辑表达的绕来绕去。
  • 于是,你忍不住了,终于使用了 yield:

    # a generator that yields items instead of returning a list
    def firstn(n):
        num = 0
        while num < n:
            yield num
            num += 1
    sum_of_first_n = sum(firstn(1000000))
    

    于是你使用了寥寥几行,构造出了一个生成器函数。

    其实[坏笑],Python 3 中,就有该函数: range(n)

    利用此函数以及 Iterator expression,你可以用很紧凑的代码构造出很多迭代数列

    squares = (x * x for x in range(n))
    evens = (2 * x for x in range(n))
    

    注意,他们都只能迭代一次。

    这一条主要是针对内存使用上来说的。因为迭代器不会保存所有值,而是在运行中动态的计算出数列的各个值,并将之前的数值扔掉,因此是很省内存的,这点在前面的例子也有体现。这里举另外一个更实际一点例子。

    假设我们有一个很大的文件(比如说 8G) ,但是你的电脑只有 4G 内存,你如何利用 Python 对其进行处理?

    答案是使用 yield 构造 generator Iterator:

    def read_by_chunks(file, chunk_size=1024):
        while True:
            data = file.read(chunk_size)
            if not data:
                break
            yield data
    f = open('your_big_file.dat')
    for chunk in read_by_chunks(f):
        process_chunk(chunk)
    

    这种通过构造 generator 逐块读取的方法又叫惰性加载,也叫流式读取,是处理大文件的一种常见方式。如果你的是文本文件,可以按行读取,那代码就更简单了:

    with open('your_big_file.txt') as f:
        for line in f: 
            process_line(line)
    

    Python3 文件类句柄就是一个迭代器,默认会将文件按行分割以惰性加载。

    Python 3 术语对照表:docs.python.org/3/glossary.…

    Python 3 wiki:wiki.python.org/moin/Genera…

    Iterator vs Iterable: stackoverflow.com/questions/5…

    Lazy read big file:stackoverflow.com/questions/5…

    我是青藤木鸟,一个喜欢摄影的分布式系统程序员,欢迎关注我的公众号:“木鸟杂记”

  •