出于不相关的原因,我以某种方式组合了一些数据结构,同时还用dict
替换了Python 2.7的默认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
此外,通过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'}]
问题:
yield
的版本有效?在进入详细信息之前,请注意以下几点:itertools.product
计算迭代器参数以计算乘积。从文档中等效的Python实现中可以看出(第一行是相关的):
itertools.product
您也可以使用自定义类和简短的测试脚本来尝试:
def product(*args, **kwds):
pools = map(tuple, args) * kwds.get('repeat', 1)
...
创建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
after评估最终的result
,上下文管理器list(result
已返回,因此所有对dict_as_ordereddict
的调用都将解析为普通的内置dict
。
[对于第二个示例,现在对dict
的内部调用仍然可以正常工作,现在返回一个生成器表达式,该表达式随后用作第二个combine
对combine
的调用的参数之一。正如我们在上面看到的,这些参数被立即求值,因此要求generator对象生成其值。为此,它需要解析itertools.product
。但是,现在我们仍在上下文管理器dict
中,因此dict_as_ordereddict
将被解析为dict
,它不接受关键字参数的非字符串键。
这里必须注意,使用OrderedDict
的第一个版本需要创建生成器对象才能返回它。这涉及创建return
对象。这意味着此版本与itertools.product
一样懒。
现在问为什么itertools.product
版本有效。通过使用yield
,调用该函数将返回一个生成器。现在这是一个真正的惰性版本,从某种意义上说,只有在请求项之前函数主体的执行才开始。这意味着对yield
的内部或外部调用都不会开始执行函数体,因此不会调用convert
,直到通过itertools.product
请求这些项目为止。您可以通过在该函数内并在上下文管理器的后面放置一个附加的print语句来检查它:
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
文字在Python 2中没有排序,关键字参数也不是,因此也违反了使用dict
的目的。另请注意,在Python 3中,已删除OrderedDict
的非字符串关键字参数行为,并且更新任何键的词典的简单方法是使用dict
。