SICP5

生成器迭代器的关系

一、python中的迭代器

首先要区分开迭代器和可迭代对象,前者是指实现了__next__StopIteration迭代器协议的python对象,后者是指可以使用iter得到其迭代器的对象。比如,iter(range(n))可以获得range对象的迭代器,实际上使用iter可以从任何可迭代对象中获得迭代器,迭代器唯一能做的就是next直到StopIteration。所有的迭代器都是可迭代对象,意思是你可以从一个迭代器中得到一个迭代器,因此你可以遍历一个迭代器。通常对迭代器使用iter方法返回的是该迭代器自身。

1
2
3
4
a = [1,2,3]

it1 = iter(a)
it2 = iter(it1)# 其实it1和it2是同一个对象的两个引用

enumerate、zip、reversed和其他一些内置函数会返回迭代器。生成器(无论来自生成器函数还是生成器表达式)是一种创建迭代器的简单方法,python对根据yield关键字自动实现迭代器协议。注意下面的代码中,a不是一个元组,a是一个生成器对象。生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。看到这里可能有点乱,这些对象的具体关系可以看文章的封面图,自己想一下range对象在图的什么位置?(图片来源于:Iterables vs. Iterators vs. Generators

1
a = (i for i in range(10))

range(当然了,迭代器也是惰性的),却不是迭代器,因为迭代器中的元素是消耗的,然后range对象却不是,而且range对象还实现了长度和下标的方法,因此更准确的来看,range更像是一个列表,只不过是惰性求值的。因为range本身不是迭代器,因为无法对于range调用range函数。与迭代器不同的是,我们可以遍历一个 range 对象而不「消耗」它。range 对象在某种意义上是「惰性的」,因为它不会生成创建时包含的每个数字,相反,当我们在循环中需要的时候,它才将这些数字返回给我们。如果你想要一个 range 对象的描述,可以称它们为懒序列,range 是序列(如列表,元组和字符串),但并不包含任何内存中的内容,而是通过实时计算来返回需要的值。迭代器的特点是,当遍历它时,这些元素将从迭代器中被消耗掉,有时候这个特性可以派上用场(以特殊的方式处理迭代器)。

1
2
3
4
5
6
7
8
9
10
11
12
13
a = range(10)

# 这一段代码可以多次执行
for i in a:
print(a)

print(5 in a)
print(5 in a)

b = iter([1,2,3])

print(2 in b)
print(2 in b)

迭代器不需要事先准备好整个迭代过程中的所有元素,仅仅在迭代到某个元素时才计算该元素,而在这之前或之后,元素可以不存在或销毁,因此迭代器是一种重要的节省内存的方式,这一点在无限流数据中体现的更为明显。迭代器另外的好处是,提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示,这种好处本身来源于python中所谓的“迭代器协议”实际上是实现了设计模式中的迭代器模式,这种设计模式用于以不同的方式来遍历整个整合对象,把在元素之间游走的责任交给迭代器,而不是聚合对象。使用迭代器模式,可以帮助我们:(1)访问一个聚合对象的内容而无须暴露它的内部表示。 (2)需要为聚合对象提供多种遍历方式。(3)为遍历不同的聚合结构提供一个统一的接口。迭代器模式的类图如下:

迭代器模式类图

二、设计一个惰性序列

这里我们设计一个惰性求值的懒序列,主要思想与代码参考《SICP-python描述》一书。如果让我们设计一个range对象,最简单的设计思想就是维护上下界与__iter__接口,求指定下标的值可以直接用start+index*step得到,长度也可以使用(end-start)/step求得。但是如果是更复杂的懒序列呢?比如我想实现一个输出素数的懒序列该如何实现?下面的设计中使用递归的思路完成对懒序列的实现,可以在其中发现函数式编程思想的影子。下面的Stream类使用延时计算的方法模拟了一个流对象,然而并没有显式的直接维护其局部状态,并不断地对其进行更新或者赋值,我们要做的只维护当前值与计算下一个值的方法,这种结构无法随机访问,但是非常适合顺序访问,因此也很容易实现它的迭代器接口。

在python中,有很多接受可迭代对象并返回迭代器的函数,例如mapfilterreduce等,这些函数都是基于一个考虑:对于序列,可以应用某种操作,但又不会带来将序列作为表的操作带来的某种代价。利用流结构可以兼顾两者:序列操作的优雅性与增量计算的高效性。这里用到了一个思想,即偏序对是我们构建一切的基石。我们使用偏序对构建序列,即把序列看作两部分:第一个元素与剩余元素构成的序列,这样的话递归的形式就自然而然的出现了。然而递归的部分我们不会真的存储起来,而是存储递归部分的计算方法,等到需要的时候才真正的计算出来。但是这个区别对于使用者是完全透明的,也就是说,作为一种数据抽象,流和表完全一致,两者不同的地方在于元素的求值时间,一个是在构造时,一个是在使用时。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from memory_profiler import profile
import sys

class Stream(object):
"""A lazily computed recursive list."""
def __init__(self, first, compute_rest, empty=False):
self.first = first
self._compute_rest = compute_rest
self.empty = empty
self._rest = None
self._computed = False

@property
def rest(self):
"""Return the rest of the stream, computing it if necessary."""
assert not self.empty, 'Empty streams have no rest.'
if not self._computed:
self._rest = self._compute_rest(self.first)
self._computed = True
return self._rest

def __repr__(self):
if self.empty:
return '<empty stream>'
if self._rest:
return 'Stream({0}, \n{1})'.format(repr(self.first), repr(self._rest))
else:
return 'Stream({0}, \n{1})'.format(repr(self.first), repr(self._compute_rest))

Stream.empty = Stream(None, None, True)


def make_next_integer(first):
print("{} -> lazy computing -> {}".format(first, first+1))
return Stream(first+1, make_next_integer)


def map_stream(fn, s):
if s.empty:
return s
def compute_rest(first):
return map_stream(fn, s._compute_rest(first))
new_val = fn(s.first)
return Stream(new_val, compute_rest)



def filter_stream(fn, s):
if s.empty:
return s
def compute_rest(first):
debugframe() # 1
return filter_stream(fn, s._compute_rest(first))
if fn(s.first):
return Stream(s.first, compute_rest)
else:
return compute_rest(s.first)


def primes(pos_stream):
def not_divible(x):
print("try divide", pos_stream.first)
if x % pos_stream.first != 0:
return True
else:
print(x, "%", pos_stream.first, "!=0")
return False
def compute_rest(first):
return primes(filter_stream(not_divible, pos_stream.rest))
return Stream(pos_stream.first, compute_rest)


def truncate_stream(s, k):
if s.empty or k == 0:
return Stream.empty
def compute_rest():
return truncate_stream(s.rest, k-1)
return Stream(s.first, compute_rest)

def stream_to_list(s):
r = []
while not s.empty:
r.append(s.first)
s = s.rest
return r


def debugframe():
frame = sys._getframe()
f_count = 1
while frame.f_back:
name = frame.f_code.co_name
if name == "<module>":
break
print("[{}]".format(name))
frame = frame.f_back

执行show1show2函数可以看出来,map_streamfilter_stream这两个函数其实是为返回的Stream绑定了新的“延时函数”,这个新的函数中通过闭包绑定了旧的延时递推函数以及新的过滤或者映射函数。

重点看filter_stream函数,这个函数其实是干了这么一个事,在函数内部弄了一个新的compute_rest函数,用这个函数替换原来的Stream中的延时函数。特殊的是,这个新的compute_rest是一个闭包,并绑定了filter_streamStream原始的延时求值函数,而filter_stream`也是一个闭包。用图来表示是这样的。

新的Stream每次访问属性rest的时候,就是执行这个新的闭包版本的compute_rest,然后这个函数会通过原func和当前的first计算出新的Stream,然后执行绑定其上的filter_stream,判断当前的Stream符不符合要求,不符合要求的话会递归的执行filter_stream,直到遇到符合要求的。可以通过在注释为1的执行debugframe函数输出函数调用栈观察这种现象。

了解了这一点之后再看primes函数,该函数实现了一个惰性的基于埃拉托斯特尼筛法的素数序列。每次要执行rest函数时才会计算。不过需要注意的是,该函数在哪里执行了筛法?可以执行以下代码段:

1
2
3
4
5
pos = Stream(2, make_next_integer)
p = primes(pos)
for i in range(3):
p = p.rest
print("="*20)

可以清楚地看到,每次执行是,函数栈的深度会越来越大,是因为filter函数每次都会进行一次新的绑定。如下图所示。

三、构建数据处理管道

我们使用协程,引入了一种不同的方式来解构复杂的计算。它是一种针对有序数据的任务处理方式。就像子过程那样,协程会计算复杂计算的一小步。但是,在使用协程时,没有主函数来协调结果。反之,协程会自发链接到一起来组成流水线。可能有一些协程消耗输入数据,并把它发送到其它协程。也可能有一些协程,每个都对发送给它的数据执行简单的处理步骤。最后可能有另外一些协程输出最终结果。

协程和子过程的差异是概念上的:子过程在主函数中位于下级,但是协程都是平等的,它们协作组成流水线,不带有任何上级函数来负责以特定顺序调用它们。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def gen_str(s):
"""a string generator, using s as a seed
"""
salt = "usbdhcgsta"
for i in range(10):
s = s[1:] + salt[i]
yield s

def producer_str(start, next_str, next_coroutine):
"""
start是一个种子
next_str是父生成器
next_coroutine是子生成器
"""
for s in next_str(start):
print("{} -> {} : {}".format(next_str.__name__, next_coroutine.__name__, s))
next_coroutine.send(s)
next_coroutine.close()

def filter_start(start, next_coroutine):
print("start [filter_start]")
try:
while True:
s = (yield)
print("[filter receive] : ", s)
if s.startswith(start):
print("[filter -> {}]:{}".format(next_coroutine.__name__, s))
next_coroutine.send(s)
except GeneratorExit:
next_coroutine.close()
print("[filter_start] done")

def consumer_print():
print("start [consumer_print]")
try:
while True:
s = (yield)
print("->[consumer_print] : {}".format(s))
except GeneratorExit:
print("[consumer_print] done!")

myprinter = consumer_print()
next(myprinter) # start it

myfilter = filter_start("p", myprinter)
next(myfilter) # start it

producer_str("apple", gen_str, myfilter)
"""输出为:
start [consumer_print]
start [filter_start]
gen_str -> filter_start : ppleu
[filter receive] : ppleu
[filter -> consumer_print]:ppleu
->[consumer_print] : ppleu
gen_str -> filter_start : pleus
[filter receive] : pleus
[filter -> consumer_print]:pleus
->[consumer_print] : pleus
gen_str -> filter_start : leusb
[filter receive] : leusb
gen_str -> filter_start : eusbd
[filter receive] : eusbd
gen_str -> filter_start : usbdh
[filter receive] : usbdh
gen_str -> filter_start : sbdhc
[filter receive] : sbdhc
gen_str -> filter_start : bdhcg
[filter receive] : bdhcg
gen_str -> filter_start : dhcgs
[filter receive] : dhcgs
gen_str -> filter_start : hcgst
[filter receive] : hcgst
gen_str -> filter_start : cgsta
[filter receive] : cgsta
"""

注意后续有其他处理协程的函数内部,需要负责将后续的携程关闭,否则后续的协程会一直等待。

参考文章

廖雪峰:异步IO
详见Python:range 对象并不是迭代器

本站总访问量