解释在此生命游戏实现中yield的使用
在这场PyCon演讲中,Jack Diederich展示了一个“简单”的实现方式,来讲解康威的生命游戏。我对生命游戏和稍微复杂一点的Python不太熟悉,但这段代码看起来其实挺容易理解的,只是有两个地方让我有点困惑:
- 使用了
yield
。我之前见过用yield来创建生成器,但连续用八个我还是第一次见……这到底是返回八个生成器的列表,还是说它是怎么工作的呢? set(itertools.chain(*map(neighbors, board)))
。这里的星号是把应用了neighbors函数后的board结果列表展开,然后……我的脑袋都要炸了。
有没有人能试着解释一下这两个部分,给那些偶尔用map、filter和reduce拼凑Python代码,但并不是每天都在用Python的程序员呢?:-)
import itertools
def neighbors(point):
x, y = point
yield x + 1, y
yield x - 1, y
yield x, y + 1
yield x, y - 1
yield x + 1, y + 1
yield x + 1, y - 1
yield x - 1, y + 1
yield x - 1, y - 1
def advance(board):
newstate = set()
recalc = board | set(itertools.chain(*map(neighbors, board)))
for point in recalc:
count = sum((neigh in board) for neigh in neighbors(point))
if count == 3 or (count == 2 and point in board):
newstate.add(point)
return newstate
glider = set([(0,0), (1,0), (2, 0), (0,1), (1,2)])
for i in range(1000):
glider = advance(glider)
print glider
3 个回答
它只是返回一个包含所有单元格邻居的元组。如果你明白生成器的作用,就会发现使用生成器在处理大量数据时是个好习惯。这样你就不需要把所有数据都存储在内存里,而是只在需要的时候计算它。
哇,这个实现真不错,感谢分享!
关于 yield
,Martijn 的回答已经很全面了,没什么好补充的。
至于星号(star):map
会返回一个生成器或者一个列表(这取决于你用的是 Python 2 还是 3),而这个列表里的每一项都是一个生成器(来自 neighbors
),所以我们得到的是一个生成器的列表。
chain
可以接收多个可迭代对象作为参数,并把它们连接在一起,这意味着它会返回一个单一的可迭代对象,同时依次遍历所有这些对象。
因为我们有一个生成器的列表,而 chain
可以接收多个参数,所以我们用星号把这个生成器列表转换成参数。我们也可以用 chain.from_iterable
来实现同样的效果。
生成器有两个基本的工作原理:每当遇到一个 yield
语句时,它就会产生一个值,并且除非被迭代,否则它的代码会处于 暂停 状态。
无论生成器中有多少个 yield
语句,代码的执行顺序还是正常的 Python 顺序。在这种情况下,没有循环,只有一系列的 yield
语句,所以每次推进生成器时,Python 会执行下一行代码,也就是另一个 yield
语句。
关于 neighbors
生成器,事情是这样的:
生成器总是从暂停状态开始,所以调用
neighbors(position)
会返回一个还没有执行任何操作的生成器。当生成器被推进(调用
next()
)时,代码会执行直到第一个yield
语句。首先执行x, y = point
,然后计算x + 1, y
并返回。代码再次暂停。再次推进时,代码会执行直到下一个
yield
语句。它返回x - 1, y
。依此类推,直到函数完成。
set(itertools.chain(*map(neighbors, board)))
这一行做了以下事情:
map(neighbors, board)
为board
序列中的每一个位置生成一个迭代器。它简单地遍历board
,对每个值调用neighbors
,并返回一个新的结果序列。每个neighbors()
函数返回一个生成器。*parameter
语法将parameter
序列展开成一个参数列表,就好像函数是用parameter
中的每个元素作为单独的位置参数调用的。例如,param = [1, 2, 3]; foo(*param)
会变成foo(1, 2, 3)
。itertools.chain(*map(..))
将map
生成的每一个生成器作为一系列位置参数传递给itertools.chain()
。遍历chain
的输出意味着每一个生成器都会被依次遍历一次,顺序执行。所有生成的位置都会被添加到一个集合中,这样就基本上去掉了重复的值。
你可以将代码扩展为:
positions = set()
for board_position in board:
for neighbor in neighbors(board):
positions.add(neighbor)
在 Python 3 中,这一行可以用 itertools.chain.from_iterable()
更高效地表达,因为 Python 3 中的 map()
本身也是一个生成器;.from_iterable()
不会强制展开 map()
,而是会根据需要逐个遍历 map()
的结果。