python 语法杂记--迭代器、生成器、上下文管理器

本文主要介绍 python 中几个重要的 "器"(迭代器、生成器、上下文管理器)的原理、实现与使用,还有一个装饰器在前面一篇文章已经进行了介绍,本文主要参考了 Python 之旅 中的相关章节。

迭代器

迭代器(iterator)就是用来遍历可迭代对象的(iterable),这两个概念要区分开。

iterable

像 list,tuple 等可以通过 for..in.. 进行遍历的对象就是可迭代对象,更严谨的定义则是:

含有 __iter__() 方法或 __getitem__() 方法的对象称之为可迭代对象.

可以使用 Python 内置的 hasattr() 函数来判断一个对象是不是可迭代的:

1
2
3
4
5
6
7
8
9
10
11
12
>>> hasattr((), '__iter__')
True
>>> hasattr([], '__iter__')
True
>>> hasattr({}, '__iter__')
True
>>> hasattr(123, '__iter__')
False
>>> hasattr('abc', '__iter__')
False
>>> hasattr('abc', '__getitem__')
True

另外,也可使用 isinstance() 进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
>>> from collections import Iterable

>>> isinstance((), Iterable) # 元组
True
>>> isinstance([], Iterable) # 列表
True
>>> isinstance({}, Iterable) # 字典
True
>>> isinstance('abc', Iterable) # 字符串
True
>>> isinstance(100, Iterable) # 数字
False

iterator

迭代器是一个对象,但比较特别,它需要遵循迭代器协议,具体协议如下

迭代器协议(iterator protocol)是指要实现对象的 __iter()__next() 方法(注意:Python3 要实现 __next__() 方法),其中,__iter()__ 方法返回迭代器对象本身,next() 方法返回容器的下一个元素,在没有后续元素时抛出 StopIteration 异常。

这里需要注意的是,虽然元组、列表和字典等对象是可迭代的,但它们却不是迭代器

首先,可以使用 hasattr() 进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> hasattr((1, 2, 3), '__iter__')
True
>>> hasattr((1, 2, 3), 'next') # 有 __iter__ 方法但是没有 next 方法,不是迭代器
False
>>>
>>> hasattr([1, 2, 3], '__iter__')
True
>>> hasattr([1, 2, 3], 'next')
False
>>>
>>> hasattr({'a': 1, 'b': 2}, '__iter__')
True
>>> hasattr({'a': 1, 'b': 2}, 'next')
False

同样也可以使用 isinstance() 进行判断:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections import Iterator
>>> isinstance((), Iterator)
False
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('', Iterator)
False
>>> isinstance(123, Iterator)
False

虽然这些可迭代对象不是迭代器,但是可以使用 Python 内置的 iter() 函数获得它们的迭代器对象,如下所示:

1
2
3
4
5
>>> from collections import Iterator
>>> isinstance(iter([1, 2, 3]), Iterator) # 使用 iter() 函数,获得迭代器对象
True
>>> isinstance(iter('abc'), Iterator)
True

事实上,Python 的 for 循环就是先通过内置函数 iter() 获得一个迭代器,然后再不断调用 next() 函数实现的,即:

1
2
for x in [1, 2, 3]:
print i

等价于

1
2
3
4
5
6
7
8
9
it = iter([1, 2, 3])

while True:
try:
x = next(it)
print x
except StopIteration:
# 没有后续元素,退出循环
break

下面是一个斐波那契数列迭代器,根据迭代器的定义,我们需要实现 __iter()__next() 方法(在 Python3 中是 __next__() 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1

# 返回迭代器对象本身
def __iter__(self):
return self

# 返回容器下一个元素
def next(self):
self.a, self.b = self.b, self.a + self.b
return self.a

def main():
fib = Fib()
for i in fib:
if i > 10:
break
print i

因此,关于迭代器和可迭代对象,需要注意下面三点

1. 元组、列表、字典和字符串对象是可迭代的,但不是迭代器,不过我们可以通过 iter() 函数获得一个迭代器对象 2. Python 的 for 循环实质上是先通过内置函数 iter() 获得一个迭代器,然后再不断调用 next() 函数实现的 3. 定义迭代器需要实现对象的 __iter()__next() 方法(Python3 要实现 __next__() 方法),其中,__iter()__ 方法返回迭代器对象本身,next() 方法返回容器的下一个元素,在没有后续元素时抛出 StopIteration 异常。

生成器

yield

生成器也是迭代器的一种,一个带有关键字 yield 的函数就是一个生成器函数,而当我们使用 yield 时,它帮我们自动创建了 __iter__()next() 方法,而且在没有数据时,也会抛出 StopIteration 异常,非常简洁和高效。如下是一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def generator_function():
... print 'hello 1'
... yield 1
... print 'hello 2'
... yield 2
... print 'hello 3'
>>>
>>> g = generator_function() # 函数没有立即执行,而是返回了一个生成器
>>> g.next() # 当使用 next()(或 next(g))的时候开始执行,遇到 yield 暂停并返回
hello 1
1
>>> g.next() # 从原来暂停的地方继续执行
hello 2
2
>>> g.next() # 从原来暂停的地方继续执行,没有 yield,抛出异常
hello 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

从上面的例子可知,带有 yield 的函数执行过程如下

1. 调用该函数的时候不会立即执行代码,而是返回了一个生成器对象; 2. 当使用 next() (在 for 循环中会自动调用 next()) 作用于返回的生成器对象时,函数开始执行,在遇到 yield 的时候会『暂停』并返回 yield 后的值 3. 当再次使用 next() 的时候,函数会从原来『暂停』的地方继续执行,直到遇到 yield 语句,如果没有 yield 语句,则抛出异常**

相比于迭代器,生成器这样的 lazy evaluation 能够节省更多的内存,同时让代码更加简洁,如前面定义的斐波那契数列迭代器,通过生成器实现的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def fib():
... a, b = 0, 1
... while True:
... a, b = b, a + b
... yield a
...
>>> f = fib()
>>> for item in f:
... if item > 10:
... break
... print item
...
1
1
2
3
5
8

send(), throw(), close()

除了上面提到的 yield,生成器还有一些其他的特殊方法:send(), throw()close(),分别用于给生成器发送消息、异常和关闭生成器。 具体用法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

In [2]: def generator_function():
...: # test send()
...: value1 = yield 0
...: print('value1 is ', value1)
...:
...: # test throw()
...: try:
...: yield 'Normal'
...: except ValueError:
...: yield 'Error'
...:
...: # test close()
...: yield 1
...: yield 2
...: yield 3
...:

In [3]: g = generator_function()

In [4]: next(g)
Out[4]: 0

In [5]: g.send(10)
value1 is 10
Out[5]: 'Normal'

In [6]: g.throw(ValueError)
Out[6]: 'Error'

In [7]: next(g)
Out[7]: 1

In [8]: g.close()

In [9]: next(g)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-9-5f315c5de15b> in <module>()
----> 1 next(g)

StopIteration:

在上面的代码中,先调用 next() 方法,使函数开始执行,代码执行到 yield 0 的时候暂停,返回了 0;接着,执行 send() 方法,它会恢复生成器的运行,并将发送的值赋给上次中断时 yield 表达式的执行结果,也就是 value1,这时控制台打印出 value1 的值,并继续执行,直到遇到 yield 后暂停,此时返回 'Normal', 因此,简单地说,send() 方法就是 next() 的功能,加上传值给 yield

接着, throw() 方法向生成器函数传递了 ValueError 异常,此时代码进入 except ValueError 语句,遇到 yield 'Error',暂停并返回 Error 字符串, 因此,简单的说,throw() 就是 next() 的功能,加上传异常给 yield。

最后使用了 close() 方法来关闭一个生成器。生成器被关闭后,再次调用 next() 方法,不管能否遇到 yield 关键字,都会抛出 StopIteration 异常

上下文管理器

__enter__() & __exit__()

上下文(context)在计算机中是个很常见的词汇,可以简单将其理解为运行时的环境,如进程上下文指的是进程在执行时 CPU 的所有寄存器中的值、进程的状态以及堆栈上的内容等,当系统需要切换到其他进程时,系统会保留当前进程的上下文,也就是运行时的环境,以便再次执行该进程。

而在 python 中上下文管理器最常见的场景便是 with 语句,with 一般用于对资源进行访问的场景,确保执行过程中出现异常情况时也可以对资源进行回收,比如自动关闭文件等。

类似迭代器协议(Iterator Protocol),上下文管理器(Context manager)也有上下文管理协议(Context Management Protocol)。

  • 上下文管理器协议,是指要实现对象的 __enter__()__exit__() 方法。
  • 上下文管理器也就是支持上下文管理器协议的对象,也就是实现了 __enter__()__exit__() 方法的对象。

如下是一个简单的上下文管理器的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from math import sqrt, pow

class Point(object):
def __init__(self, x, y):
print 'initialize x and y'
self.x, self.y = x, y

def __enter__(self):
print "Entering context"
return self

def __exit__(self, type, value, traceback):
print "Exiting context"

def get_distance(self):
distance = sqrt(pow(self.x, 2) + pow(self.y, 2))
return distance

使用 with 语句调用上下文管理器如下所示

1
2
3
4
5
6
7
8
with Point(3, 4) as pt:
print 'distance: ', pt.get_distance()

# output
initialize x and y # 调用了 __init__ 方法
Entering context # 调用了 __enter__ 方法
distance: 5.0 # 调用了 get_distance 方法
Exiting context # 调用了 __exit__ 方法

上面的 with 语句执行过程如下:

  1. Point(3, 4) 生成了一个上下文管理器;
  2. 调用上下文管理器的 __enter__() 方法,并__enter__() 方法的返回值赋给 as 字句中的变量 pt;
  3. 执行语句体(指 with 语句包裹起来的代码块)内容,输出 distance;
  4. 不管执行过程中是否发生异常,都执行上下文管理器的 __exit__() 方法

一般来说,__exit__() 方法负责执行清理工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None);如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback). 同时出现异常时,如果 __exit__(type, value, traceback) 返回 False 或 None,则会重新抛出异常,让 with 之外的语句逻辑来处理异常;如果返回 True,则忽略异常,不再对异常进行处理。

上面的 with 语句执行过程没有出现异常,下面是出现异常的情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
with Point(3, 4) as pt:
pt.get_length() # 访问了对象不存在的方法

# output
initialize x and y
Entering context
Exiting context
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-216-ab4a0e6b6b4a> in <module>()
1 with Point(3, 4) as pt:
----> 2 pt.get_length()

AttributeError: 'Point' object has no attribute 'get_length'

对前面的 __exit__() 方法修改如下

1
2
3
4
def __exit__(self, type, value, traceback):
print "Exception has been handled"
print "Exiting context"
return True

则执行相同过的代码的结果如下

1
2
3
4
5
6
7
8
with Point(3, 4) as pt:
pt.get_length() # 访问了对象不存在的方法

# output
initialize x and y
Entering context
Exception has been handled
Exiting context

contextlib

除了在类中定义 __enter____exit__ 方法来实现上下文管理器,我们还可以通过生成器函数和装饰器来实现上下文管理器,这个装饰器就在 python 提供的 contextlib 模块中。如下是个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from contextlib import contextmanager

@contextmanager
def point(x, y):
print 'before yield'
yield x * x + y * y
print 'after yield'

with point(3, 4) as value:
print 'value is: %s' % value

# output
before yield
value is: 25
after yield

可以看到,yield 产生的值赋给了 as 子句中的 value 变量

另外,需要强调的是,虽然通过使用 contextmanager 装饰器,可以不必再编写 __enter____exit__ 方法,但是获取和清理资源的操作仍需要我们自己编写:获取资源的操作定义在 yield 语句之前,释放资源的操作定义在 yield 语句之后。