Каковы эти дополнительные символы в 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

Другие вопросы по тегам