Вложен генераторен израз - неочакван резултат

Ето кода на теста:

units = [1, 2]
tens = [10, 20]
nums = (a + b for a in units for b in tens)
units = [3, 4]
tens = [30, 40]
[x for x in nums]

При предположението, че изразът на генератора на ред 3 (nums = ...) формира итератор, бих очаквал крайният резултат да отразява крайните присвоени стойности за units и tens. OTOH, ако този генераторен израз трябваше да бъде оценен на ред 3, създавайки резултатния кортеж, тогава бих очаквал да се използват първите дефиниции на units и tens.

Това, което виждам, е МИКС; т.е. резултатът е [31, 41, 32, 42]!?

Може ли някой да обясни това поведение?


person Bill Cohagan    schedule 27.03.2014    source източник
comment
Отговорът е същият; units е аргумент на генераторния израз 'функция', докато tens се разглежда като глобален. Така че units е обвързано на ред 3, tens не е.   -  person Martijn Pieters    schedule 27.03.2014
comment
Имайте предвид, че това не е специфично за Python 3.   -  person Steven Rumbalski    schedule 27.03.2014
comment
@StevenRumbalski: не, важи за всички версии на Python от 2.4 нататък, където бяха въведени генераторни изрази.   -  person Martijn Pieters    schedule 27.03.2014
comment
Току-що открих (от приятеля, който ми изпрати този пъзел), че идва от http://web.archive.org/web/20111003161227/http://web.mit.edu/rwbarton/www/python.html (и споменато в ballingt.com/2014 /03/23/). Все още не съм наясно с приложимите правила за обхват, но ще продължа да си блъскам главата срещу предоставените тук обяснения, докато не го разбера. (Мисля, че предпочитам правилата за обхват в Scheme!)   -  person Bill Cohagan    schedule 28.03.2014


Отговори (1)


Генераторен израз създава нещо като функция; един само с един аргумент, най-външният итерируем.

Тук това е units и това е обвързано като аргумент към генераторния израз, когато генераторният израз е създаден.

Всички други имена са или локални (като a и b), глобални или затваряния. tens се търси като глобален, така че се търси всеки път, когато напредвате в генератора.

В резултат на това units е свързано с генератора на ред 3, tens се търси, когато повторите израза на генератора на последния ред.

Можете да видите това, когато компилирате генератора в байткод и проверявате този байткод:

>>> import dis
>>> genexp_bytecode = compile('(a + b for a in units for b in tens)', '<file>', 'single')
>>> dis.dis(genexp_bytecode)
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f013ae0, file "<file>", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (units)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 PRINT_EXPR
             17 LOAD_CONST               2 (None)
             20 RETURN_VALUE

Байткодът MAKE_FUNCTION превърна кодовия обект на израза на генератора във функция и тя се извиква незабавно, предавайки iter(units) като аргумент. Името tens изобщо не се споменава тук.

Това е документирано в оригиналните PEP генератори:

Само най-външният for-израз се оценява незабавно, останалите изрази се отлагат, докато генераторът не бъде стартиран:

g = (tgtexp  for var1 in exp1 if exp2 for var2 in exp3 if exp4)

е еквивалентно на:

def __gen(bound_exp):
    for var1 in bound_exp:
        if exp2:
            for var2 in exp3:
                if exp4:
                    yield tgtexp
g = __gen(iter(exp1))
del __gen

и в референция за генераторни изрази:

Променливите, използвани в израза на генератора, се оценяват лениво, когато методът __next__() се извиква за обект на генератор (по същия начин като нормалните генератори). Най-лявата клауза for обаче се оценява незабавно, така че генерираната от нея грешка може да се види преди всяка друга възможна грешка в кода, който обработва генераторния израз. Следващите for клаузи не могат да бъдат оценени веднага, тъй като може да зависят от предишния for цикъл. Например: (x*y for x in range(10) for y in bar(x)).

PEP има отличен раздел, мотивиращ защо имената (различни от най-външните итерируеми) се обвързват късно, вижте Ранно свързване срещу късно свързване.

person Martijn Pieters    schedule 27.03.2014
comment
Можете ли да посочите къде е документирано това? Това е нещо като неочаквано поведение. Това е ясно от разглобения код, но бих искал да прочета каква е логиката зад него. - person Paulo Bu; 27.03.2014
comment
@PauloBu: моделът за изпълнение и документация за изрази намек за това; разбира се, че генераторните изрази и разбиранията на списък, набор и диктовка използват отделен обхват. - person Martijn Pieters; 27.03.2014
comment
Не е неочаквано поведение. Генераторът препраща към списъка [10,20], защото е обвързан с името, което използвате в израза на генератора. След това свързвате друг списък [30,40] с име, което няма нищо общо с генератора. - person Lorenzo Gatti; 27.03.2014
comment
интересно Бях изненадан от това. Знаете ли причината защо tens също не е затворено? Има усещане за произволност, с което не съм свикнал, когато използвам Python. - person Steven Rumbalski; 27.03.2014
comment
@PauloBu: изразът на генератора PEP го изписва. - person Martijn Pieters; 27.03.2014
comment
@StevenRumbalski: Дори като затваряне няма да бъде препратен, докато цикълът не бъде повторен. - person Martijn Pieters; 27.03.2014
comment
@LorenzoGatti Мога да го разбера, но по-скоро ще се отнасям еднакво към двамата. Ето защо помолих документите да видят каква е причината зад това поведение. - person Paulo Bu; 27.03.2014
comment
@MartijnPieters Благодаря за информацията :) - person Paulo Bu; 27.03.2014
comment
Хм... Играех си с това и си помислих, че може би мога да поставя tens в друг генераторен израз и по този начин да го уловя: nums = (a + b for a in units for b in (c for c in tens)). Това обаче не промени резултатите от изпълнението на оперативната програма. Изглежда, че ще трябва наистина да мисля за това, а не интуитивно. - person Steven Rumbalski; 27.03.2014
comment
@StevenRumbalski: Тъй като вложеният генератор се създава за всеки цикъл над units. Итерируемият външен цикъл е единственият обект, който е обвързан. - person Martijn Pieters; 27.03.2014
comment
@MartijnPieters: Да, виждам го сега. В противен случай би било проблематично, тъй като можех да повторя втория генератор само веднъж. - person Steven Rumbalski; 27.03.2014
comment
Намерих за полезно четенето на PEP, особено раздела Ранно свързване срещу късно свързване. Сега виждам това дизайнерско решение чрез леща с по-голяма практичност и чистота. - person Steven Rumbalski; 27.03.2014