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

由于不相关的原因,我以某种方式组合了一些数据结构,同时还将 Python 2.7 的默认设置替换dict为OrderedDict. 数据结构使用元组作为字典中的键。请忽略那些细节(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,缺少dict使用元组作为关键字参数的特殊行为:


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

此外,实现 viayield而不是生成器表达式解决了这个问题:


def combine_yield(config_a, config_b):

    for first, second in itertools.product(config_a, config_b):

        yield dict(first, **second)


问题:

  1. 为什么生成器表达式中的某些项目(仅第一个?)在第二个示例中需要之前被评估,或者它需要什么?

  2. 为什么在第一个示例中没有对其进行评估?我实际上期望两者都有这种行为。

  3. 为什么yield基于 - 的版本有效?


慕神8447489
浏览 99回答 1
1回答

holdtom

在进入细节之前,请注意以下几点:itertools.product评估迭代器参数以计算产品。这可以从文档中等效的 Python 实现中看出(第一行是相关的):def product(*args, **kwds):&nbsp; &nbsp; pools = map(tuple, args) * kwds.get('repeat', 1)&nbsp; &nbsp; ...您也可以使用自定义类和简短的测试脚本来尝试:import itertoolsclass Test:&nbsp; &nbsp; def __init__(self):&nbsp; &nbsp; &nbsp; &nbsp; self.x = 0&nbsp; &nbsp; def __iter__(self):&nbsp; &nbsp; &nbsp; &nbsp; return self&nbsp; &nbsp; def next(self):&nbsp; &nbsp; &nbsp; &nbsp; print('next item requested')&nbsp; &nbsp; &nbsp; &nbsp; if self.x < 5:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.x += 1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return self.x&nbsp; &nbsp; &nbsp; &nbsp; raise StopIteration()t = Test()itertools.product(t, t)创建itertools.product对象将在输出中显示立即请求所有迭代器项。这意味着,只要您调用itertools.product迭代器参数就会被评估。这很重要,因为在第一种情况下,参数只是两个列表,所以没有问题。然后在上下文管理器返回后评估最终的resultvia ,因此所有调用都将被解析为正常的 builtin 。list(result dict_as_ordereddictdictdict现在对于第二个示例,内部调用combine仍然可以正常工作,现在返回一个生成器表达式,然后将其用作第二个combine调用的参数之一itertools.product。正如我们在上面看到的,这些参数被立即评估,因此生成器对象被要求生成它的值。为此,它需要解决dict. 但是现在我们仍然在上下文管理器dict_as_ordereddict中,因此dict将被解析为OrderedDict不接受关键字参数的非字符串键。需要注意的是,第一个使用的版本return需要创建生成器对象才能返回它。这涉及创建itertools.product对象。这意味着这个版本和itertools.product.现在来回答为什么该yield版本有效的问题。通过使用yield,调用该函数将返回一个生成器。现在这是一个真正的惰性版本,因为函数体的执行在请求项目之前不会开始。这意味着内部和外部调用都convert不会开始执行函数体并因此调用itertools.product,直到通过list(result). 您可以通过在该函数内并在上下文管理器后面放置一个额外的打印语句来检查:def combine(config_a, config_b):&nbsp; &nbsp; print 'start'&nbsp; &nbsp; # return (dict(first, **second) for first, second in itertools.product(config_a, config_b))&nbsp; &nbsp; for first, second in itertools.product(config_a, config_b):&nbsp; &nbsp; &nbsp; &nbsp; yield dict(first, **second)with dict_as_ordereddict():&nbsp; &nbsp; result = combine(combine(&nbsp; &nbsp; &nbsp; &nbsp; [{(0, 1): 'a', (2, 3): 'b'}],&nbsp; &nbsp; &nbsp; &nbsp; [{(4, 5): 'c', (6, 7): 'd'}]&nbsp; &nbsp; ),&nbsp; &nbsp; &nbsp; &nbsp; [{(8, 9): 'e', (10, 11): 'f'}]&nbsp; &nbsp; )print 'end of context manager'print list(result)print使用该yield版本,我们会注意到它会打印以下内容:end of context managerstartstart即,仅当通过 请求结果时才启动生成器list(result)。这与return版本不同(在上面的代码中取消注释)。现在你会看到startstart并且在到达上下文管理器的末尾之前,已经引发了错误。附带说明一下,为了使您的代码能够正常工作,替换dict需要无效(这是第一个版本),所以我完全不明白您为什么要使用该上下文管理器。其次,dict在 Python 2 中文字没有排序,关键字参数也没有排序,因此也违背了使用OrderedDict. 另请注意,在 Python 3 中,非字符串关键字参数的行为dict已被删除,更新任何键的字典的干净方法是使用dict.update.
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python