解释在此生命游戏实现中yield的使用

3 投票
3 回答
1022 浏览
提问于 2025-04-17 17:18

这场PyCon演讲中,Jack Diederich展示了一个“简单”的实现方式,来讲解康威的生命游戏。我对生命游戏和稍微复杂一点的Python不太熟悉,但这段代码看起来其实挺容易理解的,只是有两个地方让我有点困惑:

  1. 使用了yield。我之前见过用yield来创建生成器,但连续用八个我还是第一次见……这到底是返回八个生成器的列表,还是说它是怎么工作的呢?
  2. 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 个回答

0

它只是返回一个包含所有单元格邻居的元组。如果你明白生成器的作用,就会发现使用生成器在处理大量数据时是个好习惯。这样你就不需要把所有数据都存储在内存里,而是只在需要的时候计算它。

1

哇,这个实现真不错,感谢分享!

关于 yield,Martijn 的回答已经很全面了,没什么好补充的。

至于星号(star):map 会返回一个生成器或者一个列表(这取决于你用的是 Python 2 还是 3),而这个列表里的每一项都是一个生成器(来自 neighbors),所以我们得到的是一个生成器的列表。

chain 可以接收多个可迭代对象作为参数,并把它们连接在一起,这意味着它会返回一个单一的可迭代对象,同时依次遍历所有这些对象。

因为我们有一个生成器的列表,而 chain 可以接收多个参数,所以我们用星号把这个生成器列表转换成参数。我们也可以用 chain.from_iterable 来实现同样的效果。

11

生成器有两个基本的工作原理:每当遇到一个 yield 语句时,它就会产生一个值,并且除非被迭代,否则它的代码会处于 暂停 状态。

无论生成器中有多少个 yield 语句,代码的执行顺序还是正常的 Python 顺序。在这种情况下,没有循环,只有一系列的 yield 语句,所以每次推进生成器时,Python 会执行下一行代码,也就是另一个 yield 语句。

关于 neighbors 生成器,事情是这样的:

  1. 生成器总是从暂停状态开始,所以调用 neighbors(position) 会返回一个还没有执行任何操作的生成器。

  2. 当生成器被推进(调用 next())时,代码会执行直到第一个 yield 语句。首先执行 x, y = point,然后计算 x + 1, y 并返回。代码再次暂停。

  3. 再次推进时,代码会执行直到下一个 yield 语句。它返回 x - 1, y

  4. 依此类推,直到函数完成。

set(itertools.chain(*map(neighbors, board))) 这一行做了以下事情:

  1. map(neighbors, board)board 序列中的每一个位置生成一个迭代器。它简单地遍历 board,对每个值调用 neighbors,并返回一个新的结果序列。每个 neighbors() 函数返回一个生成器。

  2. *parameter 语法将 parameter 序列展开成一个参数列表,就好像函数是用 parameter 中的每个元素作为单独的位置参数调用的。例如,param = [1, 2, 3]; foo(*param) 会变成 foo(1, 2, 3)

    itertools.chain(*map(..))map 生成的每一个生成器作为一系列位置参数传递给 itertools.chain()。遍历 chain 的输出意味着每一个生成器都会被依次遍历一次,顺序执行。

  3. 所有生成的位置都会被添加到一个集合中,这样就基本上去掉了重复的值。

你可以将代码扩展为:

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() 的结果。

撰写回答