Skip to content

Latest commit

 

History

History
202 lines (159 loc) · 7.58 KB

functional_programming4.md

File metadata and controls

202 lines (159 loc) · 7.58 KB

Python函数式(四)——生成器

迭代器回顾

我们在面向对象系列中介绍了迭代器这个概念。迭代器是指具有特殊方法__next____iter__的类。迭代器对象可以一次一个地输出结果:

class LetterIter:
    def __init__(self, start, end):
        self.start = ord(start)
        self.end = ord(end)
    def __iter__(self):
        return self
    def __next__(self):
        if self.start != self.end + 1:
            cur = self.start
            self.start += 1
            return chr(cur)
        else:
            raise StopIteration

for l in LetterIter('A', 'Z'):
    print(l)
    
# A
# B
# ...
# Z

上述代码一个最大的不足是过长,所以今天我们给出一个精简的解决方案:生成器。

生成器

生成器(generator)是一类特殊的函数。该类函数具有yield关键字,返回的是一个“生成器迭代器对象”(generator iterator)。这里需要明确两个概念:(1) 生成器指的是函数,而(2)生成器迭代器对象指的是生成器返回的对象。之所以称其为生成器迭代器对象是为了避免歧义(生成器本身就是一个函数对象),不过为了方便,本文后续均称呼其为生成器对象。例如,下面一个函数就是生成器,它的返回值就是一个生成器(迭代器)对象:

def gen_letter(start, end):
    start, end = ord(start), ord(end)
    while start != end + 1:
        yield chr(start)
        start += 1

generator_iterator = gen_letter('A', 'Z')
print(generator_iterator)
# <generator object gen_letter at 0x000002B152FCA258>

对于普通函数而言,利用小括号可以运行一个函数,且运行到return语句就会返回一个值:

def normal():
    return 1

print(normal())
# 1

而对于生成器而言,小括号不会调用函数,而是告诉解释器,生成一个生成器对象(很像通过一个类生成一个对象)。那么,这个对象要怎样使用呢?

与普通的函数不同生成器对象需要调用send()__next__()方法后才会执行生成器本身。函数在执行到yield语句时,会将yield后面的值返回出去,并且函数会挂起在yield的位置,直到外部给出了继续执行(__next__()send())的指令:

def gen_letter(start, end):
    print('Start to execute')
    start, end = ord(start), ord(end)
    while start != end + 1:
        print('Before yield')
        yield chr(start)
        print('After yield')
        start += 1
gen = gen_letter('A', 'Z')
print(gen.__next__())
# Start to execute
# Before yield
# A
print(gen.send(None)) # send() 需要接收一个参数
# After yield
# Before yield
# B

从运行结果看到,gen_letter('A', 'Z')并没有执行函数体(无任何打印信息),第一次调用__next__时,函数体开始执行,从打印结果可以清楚看到,函数执行到了yield语句,返回了字母A后就停止了。而当继续调用send(None)时,函数又从yield语句开始执行。循环回到yield后,返回了字母B,函数又停止了。这便是生成器最基础的运行流程。

VS 迭代器

生成器对象具有__next__方法,那么它如果拥有__iter__方法,按照迭代器的定义,它就属于迭代器:

print(hasattr(gen, '__iter__'))
# True

果然!原来生成器对象本身就是一种迭代器!只不过这类迭代器是通过函数与yield关键字形式定义的,而不是以类形式。既然是迭代器,那么它就可以利用for语句来循环迭代:

def gen_letter(start, end):
    start, end = ord(start), ord(end)
    while start != end + 1:
        yield chr(start)
        start += 1

generator_iterator = gen_letter('A', 'Z')
for l in generator_iterator:
    print(l)
# 'A'
# 'B'
# ...
# 'Z'

send

__next__可以让一个生成器对象产生一个值,那么send又是做什么用的呢?为什么send还需要一个参数?

其实,yield关键字不止生成一个值,还可以从外部接收一个值,而接收值的方式就是使用send方法传递:

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        print('Get a value {} from outside the generator'.format(val))
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1
            
c = counter(10)
print(c.__next__()) # 必须调用一次next才开始执行
# 0
print(next(c))
# Get a value None from outside the generator
# 1
print(c.send(2))
# Get a value 2 from outside the generator
# 2
print(c.send(None))
# Get a value None from outside the generator
# 3

在本例中,我们利用一个变量保存了yield语句的返回值,在外部,当我们调用__next__时,我们发现接收到的值是None。后续我们利用send方法传递了两个值,从打印结果可以看到,两个值都成功接收到了。从这里我们看到:

  1. send可以向生成器传递数据;
  2. next相当于send(None)

这里存在一个比较重要的问题:生成器的启动。前面的例子都是以__next__,也就是send(None)的方式启动的,如果我们使用send一个值的方法启动会有什么效果呢?

c1 = counter(10)
c1.send(10)
# TypeError: can't send non-None value to a just-started generator

解释器报错了,错误说明说无法为一个刚刚开始的生成器对象发送一个非None的值。这个错误的意思源自生成器的执行方式。在启动时,生成器函数从第一行开始执行到yield行,也就是val = (yield i)这一行。而这一行不是完全执行的,根据生成器的说明,它执行完yield i之后就立刻停下来了,直到下一次sendnext再执行val = 这半句话。所以,我们在生成器对象启动时就send一个值,根本无法赋给val。Python在这里的处理方式是只允许None发送进来,其他的值全部报错。因而,我们只能利用send(None)next()两种方式启动一个生成器。

throwclose

除去send之外,生成器对象还存在两个特殊的方法:throwclosethrow用于在生成器中抛出异常,而close用于提前结束生成器对象的循环:

def counter(maximum):
    i = 0
    while i < maximum:
        try:
            val = (yield i)
        except ValueError:
            print('Error catched')
            
c = counter(10)
c.__next__()
c.throw(ValueError)
# 'Error catched'

c.close()
c.__next__()
# StopIteration

斐波那契数列

在Python生成器应用中一个比较经典的例子就是斐波那契数列(Fibonacci Numbers)的生成。斐波那契数列是一个无穷数列,它的特点是从第三项开始,**每一项都是前面两项的和。**Python的迭代器和生成器很适合处理这类无穷数列问题。我们来看一下如何利用生成器实现一个斐波那契数列生成器。

def fib():
    a = b = 1
    while True:
        yield a
        a, b = b, a + b

f = fib()
for n in f:
    print('{} '.format(n), end='')
    if n > 100:
        f.close()
        break

# 1 1 2 3 5 8 13 21 34 55 89 144

这里稍作一点解释,yield a我们都知道是将a返回并暂停,而a, b = b, a + b的作用是将b赋给a同时a + b赋给b。相当于以两个数来看这个数组的话,一开始是ab,而下一时刻则变成ba + b。这样我们就获得了一个动态的斐波那契增长方式。是不是很简单?