深入理解python的yield和generator

什么是生成器

  • 生成器是一个特殊的程序,可以被用作控制循环的迭代行为
  • 生成器类似于返回值为数组的一个函数,这个函数可以接收参数,可以被调用,但是,不同于一般的函数会一次性返回包含了所有数值的数组,生成器一次只产生一个值,这样消耗的内粗数量大大减少,而且允许调用函数可以很快的开始处理前几个返回值。因此,生成器看起来像一个函数但是表现的却像一个迭代器。

python中的生成器

python提供了两种基本的方式。

  • 生成器函数:也是用def来定义,利用关键字yield一次返回一个结果,阻塞,重新开始
  • 生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果

生成器函数

为什么叫生成器函数?因为他随着时间的推移生成了一个数值队列。一般的函数在执行完毕之后会返回一个值然后退出,但是生成器函数会自动挂起,然后重新拾起继续执行,他会利用 yield 关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行。生成器和迭代协议是密切相关的,可迭代的对象都有一个 `next()` 成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。
为了支持迭代协议,拥有 yield 语句的函数被编译为生成器,这类函数被调用时返回一个生成器对象,返回的对象支持迭代接口,即成员方法 `next()` 继续从中断处执行执行。
看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# codes
def create_counter(n):
print "create counter"
while True:
yield n
print 'increment n'
n += 1

cnt = create_counter(2)
print cnt
print next(cnt)
print next(cnt)

# output
<generator object create_counter at 0x0000000001D141B0>
create counter
2
increment n
3

分析一下这个例子:

  • 在create_counter函数中出现了关键字yield,预示着这个函数每次只产生一个结果值,这个函数返回一个生成器(通过第一行输出可以看出来),用来产生连续的n值
  • 在创造生成器实例的时候,只需要像普通函数一样调用就可以,但是这个调用却不会执行这个函数,这个可以通过输出看出来
  • next()函数将生成器对象作为自己的参数,在第一次调用的时候,他执行了create_counter()函数到yield语句,返回产生的值2
  • 我们重复的调用next()函数,每次他都会从上次被挂起的地方开始执行,直到再次遇到了yield关键字

为了更加深刻的理解,我们再举一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding
def cube(n):
for i in range(n):
yield i ** 3

for i in cube(5):
print i

#output
0
1
8
27
64

所以从理解函数的角度出发我们可以将yield类比为return,但是功能确实完全不同,在for循环中,会自动遵循迭代规则,每次调用next()函数,所以上面的结果不难理解。

生成器表达式:

生成器表达式来自于迭代和列表解析的组合,关于列表解析的概念和用法不作赘述,生成器表达式和列表解析类似,但是他使用圆括号而不是方括号括起来的。如下代码:

1
2
3
4
5
6
7
8
9
10
>>> # 列表解析生成列表
>>> [ x ** 3 for x in range(5)]
[0, 1, 8, 27, 64]
>>>
>>> # 生成器表达式
>>> (x ** 3 for x in range(5))
<generator object <genexpr> at 0x000000000315F678>
>>> # 两者之间转换
>>> list(x ** 3 for x in range(5))
[0, 1, 8, 27, 64]

就操作而言,生成器表达式如果使用大量的next()函数会显得十分不方便,for循环会自动出发next函数,所以可以按下面方式使用:

1
2
3
4
5
6
7
>>> for n in (x ** 3 for x in range(5)):
print('%s, %s' % (n, n * n))
0, 0
1, 1
8, 64
27, 729
64, 4096

两者比较

一个迭代既可以被写成生成器函数,也可以被协程生成器表达式,均支持自动和手动迭代。而且这些生成器只支持一个active迭代,也就是说生成器的迭代器就是生成器本身。

补充

除了生成器表达式和生成器函数外,其实还可以用类实现,把生成器函数放到 `iter()` 方法中过去

问题

你想定义一个生成器函数,但是它会调用某个你想暴露给用户使用的外部状态值。

解决方案

如果你想让你的生成器暴露外部状态给用户, 别忘了你可以简单的将它实现为一个类,然后把生成器函数放到 `iter()` 方法中过去。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections import deque

class linehistory:
def __init__(self, lines, histlen=3):
self.lines = lines
self.history = deque(maxlen=histlen)

def __iter__(self):
for lineno, line in enumerate(self.lines, 1):
self.history.append((lineno, line))
yield line

def clear(self):
self.history.clear()

为了使用这个类,你可以将它当做是一个普通的生成器函数。 然而,由于可以创建一个实例对象,于是你可以访问内部属性值, 比如 history 属性或者是 clear() 方法。代码示例如下:

1
2
3
4
5
6
with open('somefile.txt') as f:
lines = linehistory(f)
for line in lines:
if 'python' in line:
for lineno, hline in lines.history:
print('{}:{}'.format(lineno, hline), end='')

讨论

关于生成器,很容易掉进函数无所不能的陷阱。 如果生成器函数需要跟你的程序其他部分打交道的话(比如暴露属性值,允许通过方法调用来控制等等), 可能会导致你的代码异常的复杂。 如果是这种情况的话,可以考虑使用上面介绍的定义类的方式。 在 `iter()` 方法中定义你的生成器不会改变你任何的算法逻辑。 由于它是类的一部分,所以允许你定义各种属性和方法来供用户使用。

一个需要注意的小地方是,如果你在迭代操作时不使用for循环语句,那么你得先调用 iter() 函数。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> f = open('somefile.txt')
>>> lines = linehistory(f)
>>> next(lines)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'linehistory' object is not an iterator

>>> # Call iter() first, then start iterating
>>> it = iter(lines)
>>> next(it)
'hello world\n'
>>> next(it)
'this is a test\n'
>>>
感谢您的阅读,本文由 雪释潮涯的博客~ 版权所有。如若转载,请注明出处: 雪释潮涯的博客~( https://www.xschaoya.com
python-dict查找两字典的相同点
OAuth2.0认证与客户端授权码模式

推荐阅读

留下足迹