Каковы эти дополнительные символы в symtable понимания?
Я использую symtable
чтобы получить таблицы символов фрагмента кода. Любопытно, что при использовании понимания (listcomp, setcomp и т. Д.), Есть некоторые дополнительные символы, которые я не определил.
Воспроизведение (с использованием CPython 3.6):
import symtable
root = symtable.symtable('[x for x in y]', '?', 'exec')
# Print symtable of the listcomp
print(root.get_children()[0].get_symbols())
Выход:
[<symbol '.0'>, <symbol '_[1]'>, <symbol 'x'>]
Условное обозначение x
ожидается. Но каковы .0
а также _[1]
?
Обратите внимание, что с любой другой непонятной конструкцией я получаю именно те идентификаторы, которые использовали в коде. Например, lambda x: y
приводит только к символам [<symbol 'x'>, <symbol 'y'>]
,
Кроме того, документы говорят, что symtable.Symbol
является...
Запись в
SymbolTable
соответствует идентификатору в источнике.
... хотя эти идентификаторы явно не указаны в источнике.
2 ответа
Эти два имени используются для реализации списочных представлений как отдельной области, и они имеют следующее значение:
.0
неявный аргумент, используемый для итерируемогоy
в твоем случае)._[1]
является временным именем в таблице символов, используемой для целевого списка. Этот список в конечном итоге попадает в стек. *
Понимание списка (а также сложного и сложного выражения и выражения генератора) выполняется в новой области видимости. Для этого Python эффективно создает новую анонимную функцию.
Поскольку это действительно функция, вам нужно передать в качестве аргумента итерируемое, которое вы просматриваете в цикле. Это то, что .0
для, это первый неявный аргумент (так в индексе 0
). Таблица символов, которую вы создали, явно перечисляет .0
в качестве аргумента:
>>> root = symtable.symtable('[x for x in y]', '?', 'exec')
>>> type(root.get_children()[0])
<class 'symtable.Function'>
>>> root.get_children()[0].get_parameters()
('.0',)
Первый дочерний элемент вашей таблицы - это функция с одним аргументом .0
,
Понимание списка также должно создавать выходной список, и этот список можно рассматривать как локальный. Это _[1]
временная переменная На самом деле он никогда не становится именованной локальной переменной в создаваемом объекте кода; вместо этого эта временная переменная хранится в стеке.
Вы можете увидеть объект кода, созданный при использовании compile()
:
>>> code_object = compile('[x for x in y]', '?', 'exec')
>>> code_object
<code object <module> at 0x11a4f3ed0, file "?", line 1>
>>> code_object.co_consts[0]
<code object <listcomp> at 0x11a4ea8a0, file "?", line 1>
Таким образом, существует внешний объект кода, а в константах - другой объект вложенного кода. Этот последний фактический объект кода для цикла. Оно использует .0
а также x
в качестве локальных переменных. Также требуется 1 аргумент; имена для аргументов являются первыми co_argcount
значения в co_varnames
кортеж:
>>> code_object.co_consts[0].co_varnames
('.0', 'x')
>>> code_object.co_consts[0].co_argcount
1
Так .0
здесь имя аргумента
_[1]
временная переменная обрабатывается в стеке, см. разборка:
>>> import dis
>>> dis.dis(code_object.co_consts[0])
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 8 (to 14)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 4
>> 14 RETURN_VALUE
Здесь мы видим .0
ссылка снова. _[1]
это BUILD_LIST
код операции, помещающий объект списка в стек, затем .0
ставится в стек для FOR_ITER
код операции для итерации (код операции удаляет итерацию из .0
снова из стека).
Каждый результат итерации помещается в стек FOR_ITER
, выскочил снова и сохранен в x
с STORE_FAST
затем снова загружается в стек LOAD_FAST
, в заключение LIST_APPEND
берет верхний элемент из стека и добавляет его в список, на который ссылается следующий элемент в стеке, чтобы _[1]
,
JUMP_ABSOLUTE
затем возвращает нас к вершине цикла, где мы продолжаем итерацию, пока итерация не будет завершена. В заключение, RETURN_VALUE
снова возвращает вершину стека _[1]
для звонящего.
Внешний объект кода выполняет загрузку вложенного объекта кода и вызывает его как функцию:
>>> dis.dis(code_object)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x11a4ea8a0, file "?", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (y)
8 GET_ITER
10 CALL_FUNCTION 1
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Так что это делает объект функции, с именем функции <listcomp>
(полезно для отслеживания), нагрузки y
, производит итератор для него (моральный эквивалент iter(y)
и вызывает функцию с этим итератором в качестве аргумента.
Если вы хотите перевести это в Psuedo-код, это будет выглядеть так:
def <listcomp>(.0):
_[1] = []
for x in .0:
_[1].append(x)
return _[1]
<listcomp>(iter(y))
_[1]
временная переменная, конечно, не нужна для выражений генератора:
>>> symtable.symtable('(x for x in y)', '?', 'exec').get_children()[0].get_symbols()
[<symbol '.0'>, <symbol 'x'>]
Вместо добавления в список объект функции выражения генератора выдает значения:
>>> dis.dis(compile('(x for x in y)', '?', 'exec').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 10 (to 14)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 YIELD_VALUE
10 POP_TOP
12 JUMP_ABSOLUTE 2
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
Вместе с внешним байт-кодом выражение генератора эквивалентно:
def <genexpr>(.0):
for x in .0:
yield x
<genexpr>(iter(y))
* Временная переменная фактически больше не нужна; они использовались в первоначальной реализации пониманий, но этот коммит с апреля 2007 года переместил компилятор только на использование стека, и это стало нормой для всех выпусков 3.x, а также для Python 2.7. Сгенерированное имя все еще проще воспринимать как ссылку на стек. Поскольку переменная больше не нужна, я подал вопрос 32836 об ее удалении, а Python 3.8 и далее больше не будет включать его в таблицу символов.
В Python 2.6 вы все еще можете увидеть фактическое временное имя в разборке:
>>> import dis
>>> dis.dis(compile('[x for x in y]', '?', 'exec'))
1 0 BUILD_LIST 0
3 DUP_TOP
4 STORE_NAME 0 (_[1])
7 LOAD_NAME 1 (y)
10 GET_ITER
>> 11 FOR_ITER 13 (to 27)
14 STORE_NAME 2 (x)
17 LOAD_NAME 0 (_[1])
20 LOAD_NAME 2 (x)
23 LIST_APPEND
24 JUMP_ABSOLUTE 11
>> 27 DELETE_NAME 0 (_[1])
30 POP_TOP
31 LOAD_CONST 0 (None)
34 RETURN_VALUE
Обратите внимание, как на самом деле имя должно быть снова удалено!
Таким образом, способ реализации списочного понимания на самом деле заключается в создании объекта-кода, что-то вроде создания одноразовой анонимной функции для целей видимости:
>>> import dis
>>> def f(y): [x for x in y]
...
>>> dis.dis(f)
1 0 LOAD_CONST 1 (<code object <listcomp> at 0x101df9db0, file "<stdin>", line 1>)
3 LOAD_CONST 2 ('f.<locals>.<listcomp>')
6 MAKE_FUNCTION 0
9 LOAD_FAST 0 (y)
12 GET_ITER
13 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
16 POP_TOP
17 LOAD_CONST 0 (None)
20 RETURN_VALUE
>>>
Осматривая объект кода, я могу найти .0
условное обозначение:
>>> dis.dis(f.__code__.co_consts[1])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (x)
12 LOAD_FAST 1 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
Обратите внимание LOAD_FAST
в коде объекта list-comp, кажется, загружается безымянный аргумент, который будет соответствовать GET_ITER