努力, 努力, 再努力
全部博文(220)
分类: Python/Ruby
2016-07-12 16:19:08
在开始课程之前,我要求学生们填写一份调查表,这个调查表反映了它们对Python中一些概念的理解情况。一些话题("if/else控制流" 或者 "定义和使用函数")对于大多数学生是没有问题的。但是有一些话题,大多数学生只有很少,或者完全没有任何接触,尤其是“生成器和yield关键字”。我 猜这对大多数新手Python程序员也是如此。
有事实表明,在我花了大功夫后,有些人仍然不能理解生成器和yield关键字。我想让这个问题有所改善。在这篇文章中,我将解释yield关键字到底是什么,为什么它是有用的,以及如何来使用它。
注意:最近几年,生成器的功能变得越来越强大,它已经被加入到了PEP。在我的下一篇文章中,我会通过协程(coroutine),协同 式多任务处理(cooperative multitasking),以及异步IO(asynchronous I/O)(尤其是GvR正在研究的 原型的实现)来介绍yield的真正威力。但是在此之前,我们要对生成器和yield有一个扎实的理解.
|
翻译于 3年前 5人顶 翻译的不错哦! |
协程与子例程我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None)。 一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。 对于在计算机编程中所讨论的函数,这是很标准的流程。这样的函数只能返回一个值,不过,有时可以创建能产生一个序列的函数还是有帮助的。要做到这一点,这种函数需要能够“保存自己的工作”。 我说过,能够“产生一个序列”是因为我们的函数并没有像通常意义那样返回。return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而"yield"的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。 |
翻译于 3年前 5人顶 翻译的不错哦! |
在Python中,拥有这种能力的“函数”被称为生成器,它非常的有用。生成器(以及yield语句)最初的引入是为了让程序员可以更简单的编写用来产生 值的序列的代码。 以前,要实现类似随机数生成器的东西,需要实现一个类或者一个模块,在生成数据的同时保持对每次调用之间状态的跟踪。引入生成器之后,这变得非常简单。 为了更好的理解生成器所解决的问题,让我们来看一个例子。在了解这个例子的过程中,请始终记住我们需要解决的问题:生成值的序列。 注意:在Python之外,最简单的生成器应该是被称为协程()的东西。在本文中,我将使用这个术语。请记住,在Python的概念中,这里提到的协程就是生成器。Python正式的术语是生成器;协程只是便于讨论,在语言层面并没有正式定义。 |
翻译于 3年前 2人顶 翻译的不错哦! |
例子:有趣的素数假设你的老板让你写一个函数,输入参数是一个int的list,返回一个可以迭代的包含素数1 的结果。 记住, 只是对象每次返回特定成员的一种能力。 你肯定认为"这很简单",然后很快写出下面的代码:
上面 is_prime 的实现完全满足了需求,所以我们告诉老板已经搞定了。她反馈说我们的函数工作正常,正是她想要的。 |
翻译于 3年前 3人顶 翻译的不错哦! |
处理无限序列噢,真是如此吗?过了几天,老板过来告诉我们她遇到了一些小问题:她打算把我们的get_primes函数用于一个很大的包含数字的list。实际上,这 个list非常大,仅仅是创建这个list就会用完系统的所有内存。为此,她希望能够在调用get_primes函数时带上一个start参数,返回所有 大于这个参数的素数(也许她要解决 )。 我们来看看这个新需求,很明显只是简单的修改get_primes是不可能的。 自然,我们不可能返回包含从start到无穷的所有的素数的列表 (虽然有很多有用的应用程序可以用来操作无限序列)。看上去用普通函数处理这个问题的可能性比较渺茫。 |
翻译于 3年前 2人顶 翻译的不错哦! |
在我们放弃之前,让我们确定一下最核心的障碍,是什么阻止我们编写满足老板新需求的函数。通过思考,我们得到这样的结论:函数只有一次返回结果的机会,因而必须一次返回所有的结果。得出这样的结论似乎毫无意义;“函数不就是这样工作的么”,通常我们都这么认为的。可是,不学不成,不问不知,“如果它们并非如此呢?” 想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要创建列表。没有列表,就没有内存的问题。由于老板告诉我们的是,她只需要遍历结果,她不会知道我们实现上的区别。 |
翻译于 3年前 2人顶 翻译的不错哦! |
不幸的是,这样做看上去似乎不太可能。即使是我们有神奇的函数,可以让我们从n遍历到无限大,我们也会在返回第一个值之后卡住:
显然,在get_primes中,一上来就会碰到输入等于3的,并且在函数的第4行返回。与直接返回不同,我们需要的是在退出时可以为下一次请求准备一个值。 不过函数做不到这一点。当函数返回时,意味着全部完成。我们保证函数可以再次被调用,但是我们没法保证说,“呃,这次从上次退出时的第4行开始执行,而不是常规的从第一行开始”。函数只有一个单一的入口:函数的第1行代码。 |
翻译于 3年前 2人顶 翻译的不错哦! |
走进生成器
|
翻译于 3年前 2人顶 翻译的不错哦! |
为了从生成器获取下一个值,我们使用next()函数,就像对付迭代器一样。
(next()会操心如何调用生成器的__next__()方法)。既然生成器是一个迭代器,它可以被用在for循环中。 每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。** yield就是专门给生成器用的return(加上点小魔法)。 下面是一个简单的生成器函数:
|
翻译于 3年前 2人顶 翻译的不错哦! |
魔法?那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保留下来,下一行要 执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用 next(),yield保存的状态就被无视了。 我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。
|
翻译于 3年前 1人顶 翻译的不错哦! |
如
果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。
这会通知next()的调用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while循环在我们的get_primes()函数出现
的原因。如果没有这个while,当我们第二次调用next()的时候,生成器函数会执行到函数末尾,触发StopIteration异常。一旦生成器的
值用完了,再调用next()就会出现错误,所以你只能将每个生成器的使用一次。下面的代码是错误的:
因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。 |
翻译于 3年前 1人顶 翻译的不错哦! |
执行流程让我们回到调用get_primes的地方:solve_number_10。
我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素是如何创建的有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。
接下来,回到insolve_number_10:
这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。
最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方, 同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把 number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于 2,000,000)。 |
翻译于 3年前 2人顶 翻译的不错哦! |
更给力点
|
1
2
3
4
5
6
7
8
9
10
11
12
|
def print_successive_primes(iterations, base=10):
# 像普通函数一样,生成器函数可以接受一个参数
prime_generator = get_primes(base)
# 这里以后要加上点什么
for power in range(iterations):
# 这里以后要加上点什么
def get_primes(number):
while True:
if is_prime(number):
# 这里怎么写?
|
1
2
3
4
5
|
def get_primes(number):
while True:
if is_prime(number):
number = yield number
number += 1
|
4人顶
翻译的不错哦!
通过这种方式,我们可以在每次执行yield的时候为number设置不同的值。现在我们可以补齐print_successive_primes中缺少的那部分代码:
这里有两点需要注意:首先,我们打印的是generator.send的结果,这是没问题的,因为send在发送数据给生成器的同时还返回生成器通过yield生成的值(就如同生成器中yield语句做的那样)。 第二点,看一下prime_generator.send(None)这一行,当你用send来“启动”一个生成器时(就是从生成器函数的第一行代码执行 到第一个yield语句的位置),你必须发送None。这不难理解,根据刚才的描述,生成器还没有走到第一个yield语句,如果我们发生一个真实的值, 这时是没有人去“接收”它的。一旦生成器启动了,我们就可以像上面那样发送数据了。 |
翻译于 3年前 2人顶 翻译的不错哦! |
综述在本系列文章的后半部分,我们将讨论一些yield的高级用法及其效果。yield已经成为Python最强大的关键字之一。现在我们已经对yield是如何工作的有了充分的理解,我们已经有了必要的知识,可以去了解yield的一些更“费解”的应用场景。 不管你信不信,我们其实只是揭开了yield强大能力的一角。例如,send确实如前面说的那样工作,但是在像我们的例子这样,只是生成简单的序列的场景下,send几乎从来不会被用到。下面我贴一段代码,展示send通常的使用方式。对于这段代码如何工作以及为何可以这样工作,在此我并不打算多说,它将作为第二部分很不错的热身。
|
翻译于 3年前 3人顶 翻译的不错哦! |
我希望您可以从本文的讨论中获得一些关键的思想:
我希望这篇文章是有益的。如果您还从来没有听说过generator,我希望现在您可以理解它是什么以及它为什么是有用的,并且理解如何使用它。如果您已经在某种程度上比较熟悉generator,我希望这篇文章现在可以让您扫清对generator的一些困惑。
同往常一样,如果某一节的内容不是很明确(或者某节内容更重要,亦或某些内容包含错误),请尽一切办法让我知晓。您可以在下面留下您的评论、给jeff@jeffknupp.com发送电子邮件或在Twitter中@jeffknupp。