生成器表达式与生成器函数以及令人惊讶的急切求值

Generator expressions vs generator functions and surprisingly eager evaluation

出于不相关的原因,我以某种方式组合了一些数据结构,同时还用 OrderedDict 替换了 Python 2.7 的默认 dict。数据结构使用元组作为字典中的键。请忽略那些细节(dict 类型的替换在下面没有用,但它在真实代码中)。

import __builtin__
import collections
import contextlib
import itertools


def combine(config_a, config_b):
    return (dict(first, **second) for first, second in itertools.product(config_a, config_b))


@contextlib.contextmanager
def dict_as_ordereddict():
    dict_orig = __builtin__.dict
    try:
        __builtin__.dict = collections.OrderedDict
        yield
    finally:
        __builtin__.dict = dict_orig

这最初按预期工作(dict 可以将非字符串关键字参数作为特例):

print 'one level nesting'
with dict_as_ordereddict():
    result = combine(
        [{(0, 1): 'a', (2, 3): 'b'}],
        [{(4, 5): 'c', (6, 7): 'd'}]
    )
print list(result)
print

输出:

one level nesting
[{(0, 1): 'a', (4, 5): 'c', (2, 3): 'b', (6, 7): 'd'}]

但是,当嵌套调用combine生成器表达式时,可以看出dict引用被视为OrderedDict,缺少[=17=的特殊行为] 使用元组作为关键字参数:

print 'two level nesting'
with dict_as_ordereddict():
    result = combine(combine(
        [{(0, 1): 'a', (2, 3): 'b'}],
        [{(4, 5): 'c', (6, 7): 'd'}]
    ),
        [{(8, 9): 'e', (10, 11): 'f'}]
    )
print list(result)
print

输出:

two level nesting
Traceback (most recent call last):
  File "test.py", line 36, in <module>
    [{(8, 9): 'e', (10, 11): 'f'}]
  File "test.py", line 8, in combine
    return (dict(first, **second) for first, second in itertools.product(config_a, config_b))
  File "test.py", line 8, in <genexpr>
    return (dict(first, **second) for first, second in itertools.product(config_a, config_b))
TypeError: __init__() keywords must be strings

此外,通过 yield 而不是生成器表达式实现可以解决问题:

def combine_yield(config_a, config_b):
    for first, second in itertools.product(config_a, config_b):
        yield dict(first, **second)


print 'two level nesting, yield'
with dict_as_ordereddict():
    result = combine_yield(combine_yield(
        [{(0, 1): 'a', (2, 3): 'b'}],
        [{(4, 5): 'c', (6, 7): 'd'}]
    ),
        [{(8, 9): 'e', (10, 11): 'f'}]
    )
print list(result)
print

输出:

two level nesting, yield
[{(0, 1): 'a', (8, 9): 'e', (2, 3): 'b', (4, 5): 'c', (6, 7): 'd', (10, 11): 'f'}]

问题:

  1. 为什么生成器表达式中的某些项目(仅第一个?)在第二个示例中被要求先于要求进行评估,或者要求它做什么?
  2. 为什么第一个例子没有求值?我实际上期望两者都有这种行为。
  3. 为什么基于 yield 的版本有效?

在进入细节之前请注意以下几点:itertools.product 计算迭代器参数以计算乘积。这可以从文档中等效的 Python 实现中看出(第一行是相关的):

def product(*args, **kwds):
    pools = map(tuple, args) * kwds.get('repeat', 1)
    ...

您也可以尝试使用自定义 class 和简短的测试脚本:

import itertools


class Test:
    def __init__(self):
        self.x = 0

    def __iter__(self):
        return self

    def next(self):
        print('next item requested')
        if self.x < 5:
            self.x += 1
            return self.x
        raise StopIteration()


t = Test()
itertools.product(t, t)

创建 itertools.product 对象将在输出中显示立即请求所有迭代器项。

这意味着,一旦您调用 itertools.product,就会计算迭代器参数。这很重要,因为在第一种情况下,参数只是两个列表,所以没有问题。然后你通过 list(result 评估最终的 result 上下文管理器 dict_as_ordereddict 已经 returned 所以所有调用 dict 将被解析为正常的内置 dict.

现在,对于第二个示例,对 combine 的内部调用仍然可以正常工作,现在 return 生成一个生成器表达式,然后将其用作第二个 combine 的参数之一对 itertools.product 的呼叫。正如我们在上面看到的,这些参数会立即被求值,因此生成器对象被要求生成它的值。为此,它需要解析 dict。但是现在我们仍然在上下文管理器 dict_as_ordereddict 中,因此 dict 将被解析为 OrderedDict,它不接受关键字参数的非字符串键。

这里需要注意的是,使用 return 的第一个版本需要创建生成器对象才能 return 它。这涉及创建 itertools.product 对象。这意味着这个版本和 itertools.product.

一样懒惰

现在回答为什么 yield 版本有效的问题。通过使用 yield,调用该函数将 return 一个生成器。现在这是一个真正的惰性版本,因为函数体的执行直到请求项目才开始。这意味着对 convert 的内部调用和外部调用都不会开始执行函数体并因此调用 itertools.product,直到通过 list(result) 请求项目为止。您可以通过在该函数内并在上下文管理器后面放置一个额外的打印语句来检查:

def combine(config_a, config_b):
    print 'start'
    # return (dict(first, **second) for first, second in itertools.product(config_a, config_b))
    for first, second in itertools.product(config_a, config_b):
        yield dict(first, **second)

with dict_as_ordereddict():
    result = combine(combine(
        [{(0, 1): 'a', (2, 3): 'b'}],
        [{(4, 5): 'c', (6, 7): 'd'}]
    ),
        [{(8, 9): 'e', (10, 11): 'f'}]
    )
print 'end of context manager'
print list(result)
print

对于 yield 版本,我们会注意到它打印了以下内容:

end of context manager
start
start

即仅当通过 list(result) 请求结果时,生成器才会启动。这与 return 版本不同(取消上面代码中的注释)。现在你会看到

start
start

并且在到达上下文管理器的末尾之前,错误已经出现。

附带说明一下,为了让您的代码正常工作,dict 的替换需要无效(而且是针对第一个版本),所以我不明白您为什么要使用那个上下文管理器。其次,dict 字面值在 Python 2 中没有排序,关键字参数也没有排序,因此也违背了使用 OrderedDict 的目的。另请注意,在 Python 3 中,dict 的非字符串关键字参数行为已被删除,更新任何键的字典的干净方法是使用 dict.update.