Прежде чем углубляться в детали, обратите внимание на следующее: itertools.product
оценивает аргументы итератора для вычисления продукта. Это видно из эквивалентной реализации Python в документации (первая строка актуальна):
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
, аргументы итератора будут оценены. Это важно, потому что в первом случае аргументы - это просто два списка, и поэтому проблем нет. Затем вы оцениваете окончательный result
через list(result
после , когда менеджер контекста dict_as_ordereddict
вернулся, и поэтому все вызовы dict
будут разрешены как обычные встроенные dict
.
Теперь для второго примера внутренний вызов 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):
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
.