python yield 进阶(二)

PS:接上一篇 本文摘自OSchina Linuxqueen

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

1 def get_primes(start):
2     for element in magical_infinite_range(start):
3         if is_prime(element):
4             return element

假设这样去调用get_primes:

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

显然,在get_primes中,一上来就会碰到输入等于3的,并且在函数的第4行返回。与直接返回不同,我们需要的是在退出时可以为下一次请求准备一个值。

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

 

走进生成器

这类问题极其常见以至于Python专门加入了一个结构来解决它:生成器。一个生成器会“生成”值。创建一个生成器几乎和生成器函数的原理一样简单。

一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。除了以上内容,创建一个生成器没有什么多余步骤了。

生成器函数返回生成器的迭代器。这可能是你最后一次见到“生成器的迭代器”这个术语了, 因为它们通常就被称作“生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是__next__()。如同迭代器一样,我们可以使用next()函数来获取下一个值。

 

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

(next()会操心如何调用生成器的__next__()方法)。既然生成器是一个迭代器,它可以被用在for循环中。

每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。**

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

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

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

这里有两个简单的方法来使用它:

01 >>> for value in simple_generator_function():
02 >>>     print(value)
03 1
04 2
05 3
06 >>>
our_generator 
= simple_generator_function()
07 >>> next(our_generator)
08 1
09 >>> next(our_generator)
10 2
11 >>> next(our_generator)
12 3
 

魔法?

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

我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。

1 def get_primes(number):
2     while True:
3         if is_prime(number):
4             yield number
5         number += 1
 

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

01 >>>
our_generator 
= simple_generator_function()
02 >>> for value in our_generator:
03 >>>     print(value)
04  
05 >>> #
我们的生成器没有下一个值了...
06 >>> print(next(our_generator))
07 Traceback
(most recent call last):
08   File "<ipython-input-13-7e48a609051a>",
line 
1in <module>
09     next(our_generator)
10 StopIteration
11  
12 >>> #
然而,我们总可以再创建一个生成器
13 >>> #
只需再次调用生成器函数即可
14  
15 >>>
new_generator 
= simple_generator_function()
16 >>> print(next(new_generator)) #
工作正常
17 1

因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。

 

执行流程

让我们回到调用get_primes的地方:solve_number_10。

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

我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素是如何创建的有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。

  1. 进入第三行的while循环
  2. 停在if条件判断(3是素数)
  3. 通过yield将3和执行控制权返回给solve_number_10

接下来,回到insolve_number_10:

  1. for循环得到返回值3
  2. for循环将其赋给next_prime
  3. total加上next_prime
  4. for循环从get_primes请求下一个值

这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。

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

最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方,同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。

 

更给力点

PEP 342中加入了将值传给生成器的支持。PEP
342
加入了新的特性,能让生成器在单一语句中实现,生成一个值(像从前一样),接受一个值,或同时生成一个值并接受一个值。

我们用前面那个关于素数的函数来展示如何将一个值传给生成器。这一次,我们不再简单地生成比某个数大的素数,而是找出比某个数的等比级数大的最小素数(例如10, 我们要生成比10,100,1000,10000 ... 大的最小素数)。我们从get_primes开始:

01 def print_successive_primes(iterations,
base
=10):
02     #
像普通函数一样,生成器函数可以接受一个参数
03     
04     prime_generator = get_primes(base)
05     #
这里以后要加上点什么
06     for power in range(iterations):
07         #
这里以后要加上点什么
08  
09 def get_primes(number):
10     while True:
11         if is_prime(number):
12         #
这里怎么写?

get_primes的后几行需要着重解释。yield关键字返回number的值,而像 other = yield foo 这样的语句的意思是,"返回foo的值,这个值返回给调用者的同时,将other的值也设置为那个值"。你可以通过send方法来将一个值”发送“给生成器。

1 def get_primes(number):
2     while True:
3         if is_prime(number):
4             number = yield number
5         number += 1
 

通过这种方式,我们可以在每次执行yield的时候为number设置不同的值。现在我们可以补齐print_successive_primes中缺少的那部分代码:

1 def print_successive_primes(iterations,
base
=10):
2     prime_generator = get_primes(base)
3     prime_generator.send(None)
4     for power in range(iterations):
5         print(prime_generator.send(base ** power))

这里有两点需要注意:首先,我们打印的是generator.send的结果,这是没问题的,因为send在发送数据给生成器的同时还返回生成器通过yield生成的值(就如同生成器中yield语句做的那样)。

第二点,看一下prime_generator.send(None)这一行,当你用send来“启动”一个生成器时(就是从生成器函数的第一行代码执行到第一个yield语句的位置),你必须发送None。这不难理解,根据刚才的描述,生成器还没有走到第一个yield语句,如果我们发生一个真实的值,这时是没有人去“接收”它的。一旦生成器启动了,我们就可以像上面那样发送数据了。

综述

在本系列文章的后半部分,我们将讨论一些yield的高级用法及其效果。yield已经成为Python最强大的关键字之一。现在我们已经对yield是如何工作的有了充分的理解,我们已经有了必要的知识,可以去了解yield的一些更“费解”的应用场景。

不管你信不信,我们其实只是揭开了yield强大能力的一角。例如,send确实如前面说的那样工作,但是在像我们的例子这样,只是生成简单的序列的场景下,send几乎从来不会被用到。下面我贴一段代码,展示send通常的使用方式。对于这段代码如何工作以及为何可以这样工作,在此我并不打算多说,它将作为第二部分很不错的热身。

01 import random
02  
03 def get_data():
04     """返回0到9之间的3个随机数"""
05     return random.sample(range(10), 3)
06  
07 def consume():
08     """显示每次传入的整数列表的动态平均值"""
09     running_sum = 0
10     data_items_seen = 0
11  
12     while True:
13         data = yield
14         data_items_seen += len(data)
15         running_sum += sum(data)
16         print(‘The
running average is {}‘
.format(running_sum / float(data_items_seen)))
17  
18 def produce(consumer):
19     """产生序列集合,传递给消费函数(consumer)"""
20     while True:
21         data = get_data()
22         print(‘Produced
{}‘
.format(data))
23         consumer.send(data)
24         yield
25  
26 if __name__ == ‘__main__‘:
27     consumer = consume()
28     consumer.send(None)
29     producer = produce(consumer)
30  
31     for in range(10):
32         print(‘Producing...‘)
33         next(producer)
 

请谨记……

我希望您可以从本文的讨论中获得一些关键的思想:

  • generator是用来产生一系列值的
  • yield则像是generator函数的返回结果
  • yield唯一所做的另一件事就是保存一个generator函数的状态
  • generator就是一个特殊类型的迭代器(iterator)
  • 和迭代器相似,我们可以通过使用next()来从generator中获取下一个值
  • 通过隐式地调用next()来忽略一些值

我希望这篇文章是有益的。如果您还从来没有听说过generator,我希望现在您可以理解它是什么以及它为什么是有用的,并且理解如何使用它。如果您已经在某种程度上比较熟悉generator,我希望这篇文章现在可以让您扫清对generator的一些困惑。

python yield 进阶(二)

时间: 2024-10-24 00:36:38

python yield 进阶(二)的相关文章

Python爬虫进阶二之PySpider框架安装配置

关于 首先,在此附上项目的地址,以及官方文档 PySpider 官方文档 安装 1. pip 首先确保你已经安装了pip,若没有安装,请参照 pip安装 2. phantomjs PhantomJS 是一个基于 WebKit 的服务器端 JavaScript API.它全面支持web而不需浏览器支持,其快速.原生支持各种Web标准:DOM 处理.CSS 选择器.JSON.Canvas 和 SVG. PhantomJS 可以用于页面自动化.网络监测.网页截屏以及无界面测试等. 安装 以上附有官方安

python yield 进阶(一)

PS:硬说原创 我只能说自己太不要脸了 就当是个搬运工吧 希望对您有帮助 先来看看基础的---重头戏在后面: yield的英文单词意思是生产,刚接触Python的时候感到非常困惑,一直没弄明白yield的用法. 只是粗略的知道yield可以用来为一个函数返回值塞数据,比如下面的例子: def addlist(alist): for i in alist: yield i + 1 取出alist的每一项,然后把i + 1塞进去.然后通过调用取出每一项: alist = [1, 2, 3, 4] f

Python 面向对象进阶(二)

1. 垃圾回收 小整数对象池 Python对小整数的定义是 [-5, 257),这些整数对象是提前建立好的; 在一个Python程序中,所有位于这个范围内的整数,使用的都是同一个对象; 单个字符共用对象,常驻内存; 大整数对象池 每一个大整数,均创建一个新的对象; intern机制 单个单词,不可修改,默认开启intern机制,共用对象,当引用计数为0时,则销毁; 字符串(含有空格),不可修改,没有开启intern机制,不共用对象; # 示例: a1 = "Helloworld" a2

Python之路【第三篇】:Python基础(二)

Python之路[第三篇]:Python基础(二) 内置函数 一 详细见python文档,猛击这里 文件操作 操作文件时,一般需要经历如下步骤: 打开文件 操作文件 一.打开文件 1 文件句柄 = file('文件路径', '模式') 注:python中打开文件有两种方式,即:open(...) 和  file(...) ,本质上前者在内部会调用后者来进行文件操作,推荐使用 open. 打开文件时,需要指定文件路径和以何等方式打开文件,打开后,即可获取该文件句柄,日后通过此文件句柄对该文件操作.

Python文件遍历二种方法

分享下有关Python文件遍历的两种方法,使用的OS模块的os.walk和os.listdir实现. 关于Python的文件遍历,大概有两种方法,一种是较为便利的os.walk(),还有一种是利用os.listdir()递归遍历.方法一:利用os.walkos.walk可以自顶向下或者自底向上遍历整个文件树,然后返回一个含有3个元素的tuple,(dirpath, dirnames, filenames).注意,os.walk()会返回一个generater,所以调用的时候一定要放到for循环中

Python面向对象进阶和socket网络编程-day08

写在前面 上课第八天,打卡: 为什么坚持?想一想当初: 一.面向对象进阶 - 1.反射补充 - 通过字符串去操作一个对象的属性,称之为反射: - 示例1: class Chinese: def __init__(self,name): self.name=name p = Chinese('standby') # 实例化一个对象 print(p) # 打印这个对象 --- <__main__.Chinese object at 0x0000000000B3A978> - 示例2: >&g

十分钟学习Python的进阶语法

(0)目录 VMware 下安装Ubuntu的吐血经历 零基础学习Shell编程 Linux下的makefile的妙用 Linux调试神器 -- gdb 十分钟学会Python的基本类型 分布式版本管理神器--GIT GIT文件的三种状态 & Git SSH秘钥问题 十分钟学习Python的进阶语法 配置SSH无密码访问及Linux热键.重启.kill进程 Java的不定长参数和Python的不定长参数对比 一:起因 (1)作为胶水语言的Python,可谓无处不在,快速开发原型网站:大数据处理等

Python 全栈开发:python函数进阶

python函数进阶 函数嵌套 函数对象 命名空间与作用域 闭包函数 装饰器 一.函数嵌套 函数的嵌套定义 def func1(): print('from func1') def func2(): #func2=内存地址 print('from func2') print(func2) func1() 函数的嵌套调用 # 比较两个数的大小 def max2(x,y): if x > y: return x else: return y # 比较三个数的大小 def max3(x,y,z): r

Python基础(二)

Python基础(二) Python 运算符(算术运算.比较运算.赋值运算.逻辑运算.成员运算) 基本数据类型(数字.布尔值.字符串.列表.元组.字典.set集合) for 循环 enumrate range和xrange 编码与进制转换 Python 运算符 1.算术运算: 2.比较运算: 3.赋值运算: 4.逻辑运算:  5.成员运算: 基本数据类型 1.数字 int(整型) 在32位机器上,整数的位数为32位,取值范围为-2**31-2**31-1,即-2147483648-2147483