python yield使用浅析

摘自:https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/
初学python的人经常会发现python函数中使用了很多yield关键字,然而,带有yield关键字的函数的执行流程和普通函数的执行流程不同,yield带来了什么作用,为什么要设计yield?本文将由浅入深地讲解yield的概念和用法,帮助读者体会yield在python中的简单而强大的功能
您可能听说过,带有yield的函数在python中称为generator(生成器),何谓生成器?先抛开generator,以一个常见的编程题目来展示yield的概念。

如何生成斐波那契数列

版本1(直接输出斐波那契数列)

斐波那契额数列的概念搭建应该比较清晰,相信大家能共很轻易的写出如下的算法来计算斐波那契数列:

1
2
3
4
5
6
def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1

执行fab(5),我们会的到如下的结果:

1
2
3
4
5
6
>>> fab(5)
1
1
2
3
5

结果没有问题,但是有经验的开发者会指出,直接在fab函数中 print打印出结果可复用性较差,因为fab返回的结果是None,其他函数无法获取该函数生成的斐波那契数列。所以要提高该函数的可复用性,最好不要直接打印出数列,而是返回一个list

版本二(返回list)

1
2
3
4
5
6
7
8
9
def fab(max):
n, a, b = 0, 0, 1
L = []
while n < max:
L.append(b)
a, b = b, a + b
n = n + 1
return L

使用如下方式打印出斐波那契数列:

1
2
3
4
5
6
7
>>> for n in fab(5):
... print =n
1
1
2
3
5

上述版本获取了可复用性的要求,但是该函数在运行的过程中占用的内存会随着参数max的增大而增大,如果要控制内存占用,最好不要用list来保存中介按结果,而是通过iterable对象来迭代。例如在python2.x中

1
2
3
for i in range(0,100)
for i in xrange(0,100)

前者会生成一个长度为100的list,而后者则不会生成一个100的list,而是在每次迭代中返回下一个数值,内存占用空间很小。因为xrange不返回list,而返回一个iterable的意向,利用iterable我们可以吧fab函数写成一个支持iterable的class

版本三(实现支持iterable的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class fab(object):
def __init__(self, max):
self.max = maxself.n, self.a. self.b = 0, 0, 1
def __iter__(self):
return self
def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration

Fab函数通过next不断返回数列的下一个数,内存占用始终为常数

1
2
3
4
5
6
7
>>>for n in fab(5):
... print n
1
1
2
3
5

上述代码虽然实现了我们版本二的要求,但是代码远远没有第一个版本简洁。如果想要保持第一版的简洁,这个时候就要用上yield

版本四

1
2
3
4
5
6
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

第四个版本和第一个版本相比仅仅把print b该成了yield b,就在保持简洁性的同时获得了iterable的效果。调用第四个版本和第二个版本的fab完全一致:

1
2
3
4
5
6
7
>>>for n in fab(5):
... print n
1
1
2
3
5

简单的将,yield的作用就是把一个函数变成了一个generator,带有yield的函数不在是一个普通函数,python解释器会将其视为一个generator,调用fab(5)不会执行fab函数,而是返回一个iterable对象。在for循环执行的时候,每次循环都会执行fab内部的代码,执行到yield b的时候,fab就返回一个迭代之,下次迭代时,代码从yieldb 的下一条语句执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到yield
也可以调用fab(5)的next()方法进行回去每次计算的值。

yield函数的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
File "<stdin>". line 1, in <module>
StopIteration

当函数执行结束的时候,generator自动自动抛出StopIteration的异常,表示迭代的结束,而在for循环中,我们不需要手动的进行处理异常,循环会自动的正常结束。

一个带有yield的函数就是一盒generator,它和普通的函数不同,声称一个generator看起来想函数调用,但是部执行任何函数代码,直到对其调用next()(注意在for循环中会自动调用next)才开始执行。虽然执行流程和普通函数一样,但是每执行到一个yield语句,就会中断,并返回一个迭代值,下次执行的时候从yield的下一个语句开始执行。看起来像是一个函数在正常执行的过程中被yield中断了数次,每次中断都会通过yield返回当前迭代器的值。
yield的好处显而易见,把一个函数该写成generator就获得了迭代能力,比起在类的实例中保存状态计算下一个next的值,更加使代码清洁,而且执行流程非常清晰

判断是否为generator

方法是使用isgeneratorfunction来进行判断

1
2
from inspect import isgeneratorfunction
isgeneratorfunction(fab)

注意fab不可迭代,而fab(5)可迭代

return的作用

在一个generator function中,若函数中没有return语句,默认为函数执行到函数结尾,而如果中间遇到return语句,则直接判处StopIteration异常,结束迭代。
使用yield进行读取文件的例子

1
2
3
4
5
6
7
8
9
def read_file(fpath):
block_size = 1024
with open(fpath, "rb") as f:
while True:
block = f.read(block_size)
if block:
yield block
else
return

上述当文件读取完毕的时候奖直接返回StopIteration异常,结束迭代。

Comment and share

python装饰器

预备知识

一级对象

python将一切(包括函数)视为object的子类,即一切皆为对象,因此函数可以像变量一样被指向和传递,下面我们来看一个例子。

1
2
3
4
def foo():
pass
#注意issubclass是python的一个内置函数,用于判断两个类是不是子类关系
print issubclass(foo.__class__, object)

输出结果:

1
True

上述代码说明了python中的函数都是object的子类,下面我们看一下函数被当做参数传递的效果

1
2
3
4
5
6
7
def foo(func):
func()
def bar():
print "bar"
foo(bar)

运行结果如下:

1
bar

python中的作用域 namespace

python 提供namespace来重新实现函数/方法,变量等信息的区别,七一共有三种作用域:

  • local namespace:作用范围为当前函数或者类方法
  • global namespace:作用范围为当前模块
  • build-in namespace:作用范围为所有模块

当变量或者方法出现重名的情况时,python会按照local->global->build-in的顺序去搜索,并以第一个找到的元素为当前的namespace,此种做法与C/C++中的相似

*args和**kwargs

在python中我们使用*args和kwargs传递可变长参数,*args用作传递非命名键值可变长参数列表(位置参数);**kwargs用作传递键值可变长参数

  • *args:把所有的参数按照出现顺序打包成一个list
  • **kwargs:把所有的key-value形式的参数打包成一个dict

例子:

1
2
3
4
5
6
7
def add(x, y):
print x + y
params_list = (1, 2)
dict_list = {"x":1, "y":2}
add(*params_list)
add(**dict_list)

打印结果:

1
2
3
3

python装饰器入门

python允许你,作为程序员,使用函数完成一些很酷的事情。在python中,函数是一级对象(first-class),这就意味着你可以像使用字符串,整数,或者其他对象一样使用函数。例如,你可以将函数赋值给变量:

1
2
3
4
5
def square(n):
return n * n
print square(4) #16
alias = square
print alias(5) #25

然而一等函数的真正威力在于你可以把函数传给其他函数,或者从其他函数中返回函数。函数的内值函数map利用了这种能力:给map传一个函数以及一个列表,他会依次以列表中的每个元素作为参数调用你传给它的函数,从而生成一个新的列表。如下所示的例子中应用了上面的square函数:

1
2
number = [1,2,3,4]
print map(square, number) #[1,4,9,16]

如果一个函数接受一个函数作为参数或者返回一个函数,则这个函数被称为高阶函数虽然map简单使用了我们传给它的函数,而没有改变这个函数,但我们也可以使用高阶函数去改变其他函数的行为。
例如:
假设有这样一个函数,会被调用很多次,以致运行待解非常昂贵:

1
2
def fib(n):
return n if n in [0,1] else fib(n - 2) + fib(n - 1)

为了提高这个函数的效率,我们一般会保存计算过程中得出的中间结果,这样对于函数调用树中经常出现某个n,当需要计算n对应的结果时,就不需要重复计算了。有很多种方式可以实现这一点。例如我们可以将这些结果存在一个字典中,当某个值为参数调用fib函数的时候,首先去字典中查一下结果是否已经计算出来,如果计算出来直接返回反之计算。
但是这样的话,每次我们想调用fib函数,都需要重复那段相同的字典检查样板式代码。相反,如果让fib函数自己在内部负责存储结果,那么在其他代码中调用fib,就非常方便,只要简单的调用它就好了。这种技术被称为memoization
当然我们可以把这种memozition代码直接放入fib函数中,但是python给我们提供了一种更加优雅的选择因为可以编写修改其他函数的函数,那么我们就可以便携一个通用的memozation函数,以一个函数作为参考,并返回这个函数的memozation版本。

1
2
3
4
5
6
7
8
9
def memoze(fn):
store_results = {}
def memoized(args):
try:
return store_results[args]
except:
result = store_result[args] = fn(args)
return result
return memoized

如上,momoize函数以另一个函数作为参数,函数体中创建了一个字典对象来存储函数调用结果:键为被memoized包含后的函数的参数,值为以键为参数调用函数的返回值。memoize函数返回一个新的函数,这个函数会首先检查store_results中是否存在与当前参数对应的条目,如果有则直接返回,如果没有,则使用原始函数进行计算。memoize返回的这种新的函数常被称为包装器函数。因为它只是另外一个真正起作用的函数外面的一个薄层。
很好,我们现在有了memoize函数,现在将fib函数传给它,从而得到了一个经过包装的fib,这个版本的fib函数不需要重复以前的那样繁重的工作:

1
2
3
4
def fib(n):
return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
fib = memoize(fib)

通过高阶函数memoize,我们获得了memoization带来的好处,并不需要对fib函数自己做出任何的改变,以免夹杂着memoization的代码而模糊了函数的实质工作。但是,你也许注意到上面的代码看着还是有点别扭,因为我们必须写三遍fib。由于这种模式(传递一个函数给另一个函数,然后将结果返回给与原来那个函数同名的函数变量,在使用包装器函数的代码中极为常见),python提供了一种特殊的愈发:装饰器

1
2
3
@memoize
def fib(n):
return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

这里我们说memoize函数装饰了fib函数。需要注意的是这仅是语法上的简便写法(被称为“语法糖”)。这段代码与前面的代码片段做的是同样的事情:定义一个名为fib的函数,把它传给memoize函数,将返回结果存为名为fib的函数变量。特殊的(看起来有点奇怪的)@语法只是减少了冗余。

你可以使用多个装饰器,它会自底向上逐个起作用,例如假如有另外一个装饰器函数decorate

1
2
3
4
@memoize
@decorate
def fib(n):
return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

等价于:

1
2
3
def fib(n):
return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
fib = memoize(decorator(fib))

当装饰器函数含有参数的时候,方法是

1
2
3
@mimoize("172.168.1.1")
fib(n):
return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China