深入表明yield和Generators(生成器)
当前位置:以往代写 > Python教程 >深入表明yield和Generators(生成器)
2019-06-14

深入表明yield和Generators(生成器)

生成器和yield要害字大概是Python内里最强大的最难领略的观念之一(或者没有之一), 可是并不故障yield成为Python内里最强大的要害字,对付初学者来讲确实很是难于领略,来看一篇关于yield的海外大牛写的文章,让你快速领略yield。 文章有点长,请耐性读完, 进程中有些例子, 循序渐进,让你不以为枯燥。

生成器

生成器是通过一个或多个yield表达式组成的函数,每一个生成器都是一个迭代器(可是迭代器不必然是生成器)。

假如一个函数包括yield要害字,这个函数就会变为一个生成器。

生成器并不会一次返回所有功效,而是每次碰着yield要害字后返回相应功效,并保存函数当前的运行状态,期待下一次的挪用。

由于生成器也是一个迭代器,那么它就应该支持next要领来获取下一个值。(也可以利用.__next__()属性, 在python2 中是.next())

协程与子例程

我们挪用一个普通的Python函数时,一般是从函数的第一行代码开始执行,竣事于return语句、异常可能函数竣事(可以看作隐式的返回None)。一旦函数将节制权交还给挪用者,就意味着全部竣事。函数中做的所有事情以及生存在局部变量中的数据都将丢失。再次挪用这个函数时,一切都将从新建设。 

对付在计较机编程中所接头的函数,这是很尺度的流程。这样的函数只能返回一个值,不外,有时可以建设能发生一个序列的函数照旧有辅佐的。要做到这一点,这种函数需要可以或许“生存本身的事情”。 

我说过,可以或许“发生一个序列”是因为我们的函数并没有像凡是意义那样返回。return隐含的意思是函数正将执行代码的节制权返回给函数被挪用的处所。而"yield"的隐含意思是节制权的转移是姑且和自愿的,我们的函数未来还会收回节制权。

在Python中,拥有这种本领的“函数”被称为生成器,它很是的有用。生成器(以及yield语句)最初的引入是为了让措施员可以更简朴的编写用来发生值的序列的代码。 以前,要实现雷同随机数生成器的对象,需要实现一个类可能一个模块,在生成数据的同时保持对每次挪用之间状态的跟踪。引入生成器之后,这变得很是简朴。

为了更好的领略生成器所办理的问题,让我们来看一个例子。在相识这个例子的进程中,请始终记着我们需要办理的问题:生成值的序列。

留意:在Python之外,最简朴的生成器应该是被称为协程(coroutines)的对象。在本文中,我将利用这个术语。请记着,在Python的观念中,这里提到的协程就是生成器。Python正式的术语是生成器;协程只是便于接头,在语言层面并没有正式界说。

例子:有趣的素数

假设你的老板让你写一个函数,输入参数是一个int的list,返回一个可以迭代的包括素数1 的功效。

记着,迭代器(Iterable) 只是工具每次返回特定成员的一种本领。

你必定认为"这很简朴",然后很快写出下面的代码:

def get_primes(input_list):
    result_list = list()
    for element in input_list:
        if is_prime(element):
            result_list.append()
    return result_list
# 可能更好一些的...
def get_primes(input_list):
    return (element for element in input_list if is_prime(element))
# 下面是 is_prime 的一种实现...
def is_prime(number):
    if number > 1:
        if number == 2:
            return True
        if number % 2 == 0:
            return False
        for current in range(3, int(math.sqrt(number) + 1), 2):
            if number % current == 0: 
                return False
        return True
    return False

上面 is_prime 的实现完全满意了需求,所以我们汇报老板已经搞定了。她反馈说我们的函数事情正常,正是她想要的。

处理惩罚无限序列

#p#分页标题#e#

噢,真是如此吗?过了几天,老板过来汇报我们她碰着了一些小问题:她规划把我们的get_primes函数用于一个很大的包括数字的list。实际上,这个list很是大,仅仅是建设这个list就会用完系统的所有内存。为此,她但愿可以或许在挪用get_primes函数时带上一个start参数,返回所有大于这个参数的素数(也许她要办理 Project Euler problem 10)。

我们来看看这个新需求,很明明只是简朴的修改get_primes是不行能的。 自然,我们不行能返回包括从start到无穷的所有的素数的列表 (固然有许多有用的应用措施可以用来操纵无限序列)。看上去用普通函数处理惩罚这个问题的大概性较量迷茫。

在我们放弃之前,让我们确定一下最焦点的障碍,是什么阻止我们编写满意老板新需求的函数。通过思考,我们获得这样的结论:函数只有一次返回功效的时机,因而必需一次返回所有的功效。得出这样的结论好像毫无意义;“函数不就是这样事情的么”,凡是我们都这么认为的。但是,不学不成,不问不知,“假如它们并非如此呢?”

想象一下,假如get_primes可以只是简朴返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要建设列表。没有列表,就没有内存的问题。由于老板汇报我们的是,她只需要遍历功效,她不会知道我们实现上的区别。

不幸的是,这样做看上去好像不太大概。纵然是我们有神奇的函数,可以让我们从n遍历到无限大,我们也会在返回第一个值之后卡住:

def get_primes(start):
    for element in magical_infinite_range(start):
        if is_prime(element):
            return element

假设这样去挪用get_primes:

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

显然,在get_primes中,一上来就会遇到输入便是3的,而且在函数的第4行返回。与直接返回差异,我们需要的是在退出时可觉得下一次请求筹备一个值。

不外函数做不到这一点。当函数返回时,意味着全部完成。我们担保函数可以再次被挪用,可是我们没法担保说,“呃,这次从上次退出时的第4行开始执行,而不是通例的从第一行开始”。函数只有一个单一的进口:函数的第1行代码。

走进生成器

这类问题极其常见以至于Python专门插手了一个布局来办理它:生成器。一个生成器会“生成”值。建设一个生成器险些和生成器函数的道理一样简朴。

一个生成器函数的界说很像一个普通的函数,除了当它要生成一个值的时候,利用yield要害字而不是return。假如一个def的主体包括yield,这个函数会自动酿成一个生成器(纵然它包括一个return)。除了以上内容,建设一个生成器没有什么多余步调了。

生成器函数返回生成器的迭代器。这大概是你最后一次见到“生成器的迭代器”这个术语了, 因为它们凡是就被称作“生成器”。要留意的是生成器就是一类非凡的迭代器。作为一个迭代器,生成器必需要界说一些要领(method),个中一个就是__next__()【留意: 在python2中是: next() 要领】。如同迭代器一样,我们可以利用next()函数来获取下一个值。

为了从生成器获取下一个值,我们利用next()函数,就像搪塞迭代器一样。

(next()会劳神如何挪用生成器的__next__()要领)。既然生成器是一个迭代器,它可以被用在for轮回中。

每当生成器被挪用的时候,它会返回一个值给挪用者。在生成器内部利用yield来完成这个行动(譬喻yield 7)。为了记着yield到底干了什么,最简朴的要领是把它看成专门给生成器函数用的非凡的return(加上点小邪术)。**

yield就是专门给生成器用的return(加上点小邪术)。

下面是一个简朴的生成器函数:

>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

#p#分页标题#e#

这里有两个简朴的要领来利用它:

>>> for value in simple_generator_function():
>>>     print(value)
1
2
3
>>> our_generator = simple_generator_function()
>>> next(our_generator)
1
>>> next(our_generator)
2
>>> next(our_generator)
3

邪术?

那么神奇的部门在那边?我很兴奋你问了这个问题!当一个生成器函数挪用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保存下来,下一行要执行的代码的位置也会被记录,直到再次挪用next()。一旦next()再次被挪用,生成器函数会从它上次分开的处所开始。假如永远不挪用next(),yield生存的状态就被无视了。

我们来重写get_primes()函数,这次我们把它写作一个生成器。留意我们不再需要magical_infinite_range函数了。利用一个简朴的while轮回,我们缔造了本身的无穷串列。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

假如生成器函数挪用了return,可能执行到函数的末端,会呈现一个StopIteration异常。 这会通知next()的挪用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while轮回在我们的get_primes()函数呈现的原因。假如没有这个while,当我们第二次挪用next()的时候,生成器函数会执行到函数末端,触发StopIteration异常。一旦生成器的值用完了,再挪用next()就会呈现错误,所以你只能将每个生成器的利用一次。下面的代码是错误的:

>>> our_generator = simple_generator_function()
>>> for value in our_generator:
>>>     print(value)
>>> # 我们的生成器没有下一个值了...
>>> print(next(our_generator))
Traceback (most recent call last):
  File "<ipython-input-13-7e48a609051a>", line 1, in <module>
    next(our_generator)
StopIteration
>>> # 然而,我们总可以再建设一个生成器
>>> # 只需再次挪用生成器函数即可
>>> new_generator = simple_generator_function()
>>> print(next(new_generator)) # 事情正常
1

因此,这个while轮回是用来确保生成器函数永远也不会执行到函数末端的。只要挪用next()这个生成器就会生成一个值。这是一个处理惩罚无穷序列的常见要领(这类生成器也是很常见的)。

执行流程

让我们回到挪用get_primes的处所:solve_number_10。

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

我们来看一下solve_number_10的for轮回中对get_primes的挪用,调查一下前几个元素是如何建设的有助于我们的领略。当for轮回从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。

进入第三行的while轮回

停在if条件判定(3是素数)

通过yield将3和执行节制权返回给solve_number_10

接下来,回到insolve_number_10:

for轮回获得返回值3

for轮回将其赋给next_prime

total加上next_prime

for轮回从get_primes请求下一个值

这次,进入get_primes时并没有从开头执行,我们从第5行继承执行,也就是上次分开的处所。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1 # <<<<<<<<<<

#p#分页标题#e#

最要害的是,number还保持我们上次挪用yield时的值(譬喻3)。记着,yield会将值传给next()的挪用方,同时还会生存生成器函数的“状态”。接下来,number加到4,回到while轮回的开始处,然后继承增加直到获得下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for轮回。这个周期会一直执行,直到for轮回竣事(获得的素数大于2,000,000)。

总结

要害点:

generator是用来发生一系列值的

yield则像是generator函数的返回功效

yield独一所做的另一件事就是生存一个generator函数的状态

generator就是一个非凡范例的迭代器(iterator)

和迭代器相似,我们可以通过利用next()来从generator中获取下一个值

通过隐式地挪用next()来忽略一些值

    关键字:

在线提交作业