Доступ к переменным класса из понимания списка в определении класса
Как получить доступ к другим переменным класса из понимания списка в определении класса? Следующее работает в Python 2, но не работает в Python 3:
class Foo:
x = 5
y = [x for i in range(1)]
Python 3.11 выдает ошибку:
NameError: name 'x' is not defined
Попытка Foo.x
тоже не работает. Есть идеи, как это сделать в Python 3?
Немного более сложный мотивирующий пример:
from collections import namedtuple
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for args in [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
]]
В этом примере apply()
было бы достойным обходным решением, но, к сожалению, оно удалено из Python 3.
Переведено автоматически
Ответ 1
Понимание области видимости класса и списка, набора или словаря, а также выражений генератора, не сочетаются.
Почему; или официальное слово по этому поводу
Вы не можете получить доступ к области класса из функций, понимания списка или выражений генератора, заключенных в этой области; они действуют так, как будто этой области не существует. В Python 2 понимание списка было реализовано с использованием ярлыка, поэтому на самом деле можно получить доступ к области видимости класса, но в Python 3 они получили свою собственную область видимости (как и должны были иметь с самого начала), и, таким образом, ваш пример прерывается. Другие типы понимания имеют свою собственную область видимости независимо от версии Python, поэтому аналогичный пример с пониманием set или dict не сработал бы в Python 2.
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}
Подробнее
В Python 3 пониманиям списка была предоставлена собственная область видимости (локальное пространство имен), чтобы предотвратить перетекание их локальных переменных в окружающую область видимости (см. Понимание списка повторно связывает имена даже после понимания области видимости. Это правильно?). Это здорово, когда такое понимание списка используется в модуле или функции, но в классах определение области видимости немного, хм, странно.
Это задокументировано в pep 227:
Имена в области видимости класса недоступны. Имена разрешаются в самой внутренней области видимости функции. Если определение класса встречается в цепочке вложенных областей, процесс разрешения пропускает определения классов.
и в class
документации по составному оператору:
Затем набор классов выполняется в новом фрейме выполнения (см. Раздел Именование и привязка), используя вновь созданное локальное пространство имен и исходное глобальное пространство имен. (Обычно набор содержит только определения функций.) Когда набор класса завершает выполнение, его фрейм выполнения отбрасывается, но сохраняется его локальное пространство имен. [4] Затем создается объект класса с использованием списка наследования для базовых классов и сохраненного локального пространства имен для словаря атрибутов.
Выделено мной; фрейм выполнения - это временная область.
Поскольку область действия переопределяется как атрибуты объекта класса, что позволяет использовать ее также и как нелокальную область действия, это приводит к неопределенному поведению; что произойдет, если метод класса, на который ссылаются x
как на вложенную переменную области действия, затем также манипулирует Foo.x
, например? Что еще более важно, что бы это значило для подклассов Foo
? Python должен по-разному относиться к области видимости класса, поскольку она сильно отличается от области видимости функции.
И последнее, но определенно не менее важное: в связанном разделе Именование и привязка в документации по модели выполнения явно упоминаются области действия класса:
Область действия имен, определенных в блоке класса, ограничена блоком класса; она не распространяется на блоки кода методов – сюда входят понимания и выражения генератора, поскольку они реализованы с использованием области действия функции. Это означает, что следующее завершится ошибкой:
class A:
a = 42
b = list(a + i for i in range(10))
(Небольшое) исключение; или почему одна часть может все еще работать
Есть одна часть выражения понимания или генератора, которая выполняется во внешней области видимости, независимо от версии Python. Это будет выражение для самой внешней итерации. В вашем примере это range(1)
:
y = [x for i in range(1)]
# ^^^^^^^^
Таким образом, использование x
в этом выражении не выдаст ошибку:
# Runs fine
y = [i for i in range(x)]
Это относится только к самой внешней итерации; если понимание содержит несколько for
предложений, итерации для внутренних for
предложений оцениваются в области понимания:
# NameError
y = [i for i in range(1) for j in range(x)]
# ^^^^^^^^^^^^^^^^^ -----------------
# outer loop inner, nested loop
Это проектное решение было принято для того, чтобы выдавать ошибку во время создания genexp, а не во время итерации, когда создание самой внешней итерации выражения генератора выдает ошибку, или когда самая внешняя итерация оказывается не итерабельной. Понимание разделяет это поведение для обеспечения согласованности.
Заглядываем под капот; или, намного подробнее, чем вы когда-либо хотели
Вы можете увидеть все это в действии, используя dis
модуль. Я использую Python 3.3 в следующих примерах, потому что он добавляет полные имена, которые четко идентифицируют объекты кода, которые мы хотим проверить. Созданный байт-код в остальном функционально идентичен Python 3.2.
Для создания класса Python, по сути, берет весь набор, составляющий тело класса (поэтому все с отступом на один уровень глубже, чем class <name>:
строка), и выполняет это так, как если бы это была функция:
>>> import dis
>>> def foo():
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo)
2 0 LOAD_BUILD_CLASS
1 LOAD_CONST 1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>)
4 LOAD_CONST 2 ('Foo')
7 MAKE_FUNCTION 0
10 LOAD_CONST 2 ('Foo')
13 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
16 STORE_FAST 0 (Foo)
5 19 LOAD_FAST 0 (Foo)
22 RETURN_VALUE
Сначала LOAD_CONST
загружается объект кода для Foo
тела класса, затем преобразуется в функцию и вызывается. Результат этого вызова затем используется для создания пространства имен класса, its __dict__
. Пока все хорошо.
Здесь следует отметить, что байт-код содержит вложенный объект кода; в Python определения классов, функции, понимания и генераторы представлены как объекты кода, которые содержат не только байт-код, но также структуры, представляющие локальные переменные, константы, переменные, взятые из глобальных значений, и переменные, взятые из вложенной области видимости. Скомпилированный байт-код ссылается на эти структуры, и интерпретатор python знает, как получить к ним доступ с учетом представленных байт-кодов.
Здесь важно помнить, что Python создает эти структуры во время компиляции; class
suite - это объект кода (<code object Foo at 0x10a436030, file "<stdin>", line 2>
), который уже скомпилирован.
Давайте проверим тот объект code, который создает само тело класса; объекты code имеют co_consts
структуру:
>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
2 0 LOAD_FAST 0 (__locals__)
3 STORE_LOCALS
4 LOAD_NAME 0 (__name__)
7 STORE_NAME 1 (__module__)
10 LOAD_CONST 0 ('foo.<locals>.Foo')
13 STORE_NAME 2 (__qualname__)
3 16 LOAD_CONST 1 (5)
19 STORE_NAME 3 (x)
4 22 LOAD_CONST 2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>)
25 LOAD_CONST 3 ('foo.<locals>.Foo.<listcomp>')
28 MAKE_FUNCTION 0
31 LOAD_NAME 4 (range)
34 LOAD_CONST 4 (1)
37 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
40 GET_ITER
41 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
44 STORE_NAME 5 (y)
47 LOAD_CONST 5 (None)
50 RETURN_VALUE
Приведенный выше байт-код создает тело класса. Функция выполняется, и результирующее locals()
пространство имен, содержащее x
и y
, используется для создания класса (за исключением того, что оно не работает, потому что x
не определено как глобальное). Обратите внимание, что после сохранения 5
в x
загружается другой объект кода; это понимание списка; он заключен в объект функции точно так же, как было тело класса; созданная функция принимает позиционный аргумент, range(1)
итерируемый для использования в своем циклическом коде, приведенном к итератору. Как показано в байт-коде, range(1)
вычисляется в области видимости класса.
Из этого вы можете видеть, что единственная разница между объектом кода для функции или генератора и объектом кода для понимания заключается в том, что последний выполняется немедленно при выполнении родительского объекта кода; байт-код просто создает функцию "на лету" и выполняет ее за несколько небольших шагов.
Python 2.x использует вместо этого встроенный байт-код, вот вывод из Python 2.7:
2 0 LOAD_NAME 0 (__name__)
3 STORE_NAME 1 (__module__)
3 6 LOAD_CONST 0 (5)
9 STORE_NAME 2 (x)
4 12 BUILD_LIST 0
15 LOAD_NAME 3 (range)
18 LOAD_CONST 1 (1)
21 CALL_FUNCTION 1
24 GET_ITER
>> 25 FOR_ITER 12 (to 40)
28 STORE_NAME 4 (i)
31 LOAD_NAME 2 (x)
34 LIST_APPEND 2
37 JUMP_ABSOLUTE 25
>> 40 STORE_NAME 5 (y)
43 LOAD_LOCALS
44 RETURN_VALUE
Объект кода не загружается, вместо этого FOR_ITER
выполняется встроенный цикл. Итак, в Python 3.x генератору списков был предоставлен собственный объект кода, что означает, что у него есть своя область видимости.
Однако понимание было скомпилировано вместе с остальным исходным кодом python, когда модуль или скрипт был впервые загружен интерпретатором, и компилятор не считает набор классов допустимой областью видимости. Любые переменные, на которые ссылаются в понимании списка, должны рекурсивно просматриваться в области, окружающей определение класса. Если компилятор не нашел переменную, он помечает ее как глобальную. Дизассемблирование объекта кода понимания списка показывает, что x
действительно загружен как глобальный:
>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
4 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_GLOBAL 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
Этот фрагмент байт-кода загружает первый переданный аргумент (range(1)
итератор) и точно так же, как версия Python 2.x, использует FOR_ITER
для перебора по нему и создания выходных данных.
Если бы мы определили x
в foo
функции вместо этого, x
была бы переменная cell (ячейки относятся к вложенным областям):
>>> def foo():
... x = 2
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
5 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_DEREF 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
LOAD_DEREF
Будет косвенно загружать x
объекты cell объекта кода:
>>> foo.__code__.co_cellvars # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars # Refers to `x` in foo
('x',)
>>> foo().y
[2]
Фактическая ссылка ищет значение из текущих структур данных фрейма, которые были инициализированы из .__closure__
атрибута функционального объекта. Поскольку функция, созданная для объекта кода понимания, снова отбрасывается, мы не можем проверить закрытие этой функции. Чтобы увидеть закрытие в действии, нам пришлось бы вместо этого проверить вложенную функцию:
>>> def spam(x):
... def eggs():
... return x
... return eggs
...
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5
Итак, подведем итог:
- Понимания списка получают свои собственные объекты кода в Python 3 (вплоть до Python 3.11), и нет никакой разницы между объектами кода для функций, генераторов или понимания; объекты кода понимания заключаются во временный объект функции и вызываются немедленно.
- Объекты кода создаются во время компиляции, и любые нелокальные переменные помечаются либо как глобальные, либо как свободные переменные, в зависимости от вложенных областей кода. Тело класса не считается областью для поиска этих переменных.
- При выполнении кода Python должен смотреть только на глобальные переменные или закрытие выполняемого в данный момент объекта. Поскольку компилятор не включил тело класса в качестве области видимости, пространство имен временной функции не учитывается.
Обходной путь; или что с этим делать
Если вы хотите создать явную область видимости для x
переменной, как в функции, вы можете использовать переменные области видимости класса для понимания списка:
>>> class Foo:
... x = 5
... def y(x):
... return [x for i in range(1)]
... y = y(x)
...
>>> Foo.y
[5]
"Временная" y
функция может быть вызвана напрямую; при выполнении мы заменяем ее возвращаемым значением. Ее область действия учитывается при разрешении x
:
>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)
Конечно, люди, читающие ваш код, будут немного ломать голову над этим; возможно, вы захотите вставить туда большой жирный комментарий, объясняющий, почему вы это делаете.
Лучший способ обойти это - просто использовать __init__
для создания переменной экземпляра вместо:
def __init__(self):
self.y = [self.x for i in range(1)]
и избегайте ломания головы и вопросов, требующих объяснения. Для вашего собственного конкретного примера я бы даже не сохранял namedtuple
в классе; либо используйте выходные данные напрямую (вообще не сохраняйте сгенерированный класс), либо используйте глобальный:
from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])
class StateDatabase:
db = [State(*args) for args in [
('Alabama', 'Montgomery'),
('Alaska', 'Juneau'),
# ...
]]
PEP 709, часть Python 3.12, снова вносит некоторые изменения
В Python 3.12 понимание стало намного более эффективным за счет удаления вложенной функции и встраивания цикла, сохраняя при этом отдельную область видимости. Подробности о том, как это было сделано, изложены в PEP 709 - Встроенные понимания, но суть в том, что вместо создания нового функционального объекта и последующего его вызова с помощью LOAD_CONST
, MAKE_FUNCTION
и CALL
байт-кодов, любые конфликтующие имена, используемые в цикле, сначала перемещаются в стек перед выполнением встроенного байт-кода понимания.
Важно отметить, что это изменение влияет только на производительность и взаимодействие с областью видимости класса не изменилось. Вы по-прежнему не можете получить доступ к именам, созданным в области видимости класса, по причинам, изложенным выше.
При использовании Python 3.12.0b4 байт-код для Foo
класса теперь выглядит следующим образом:
# creating `def foo()` and its bytecode elided
Disassembly of <code object Foo at 0x104e97000, file "<stdin>", line 2>:
2 0 RESUME 0
2 LOAD_NAME 0 (__name__)
4 STORE_NAME 1 (__module__)
6 LOAD_CONST 0 ('foo.<locals>.Foo')
8 STORE_NAME 2 (__qualname__)
3 10 LOAD_CONST 1 (5)
12 STORE_NAME 3 (x)
4 14 PUSH_NULL
16 LOAD_NAME 4 (range)
18 LOAD_CONST 2 (1)
20 CALL 1
28 GET_ITER
30 LOAD_FAST_AND_CLEAR 0 (.0)
32 LOAD_FAST_AND_CLEAR 1 (i)
34 LOAD_FAST_AND_CLEAR 2 (x)
36 SWAP 4
38 BUILD_LIST 0
40 SWAP 2
>> 42 FOR_ITER 8 (to 62)
46 STORE_FAST 1 (i)
48 LOAD_GLOBAL 6 (x)
58 LIST_APPEND 2
60 JUMP_BACKWARD 10 (to 42)
>> 62 END_FOR
64 SWAP 4
66 STORE_FAST 2 (x)
68 STORE_FAST 1 (i)
70 STORE_FAST 0 (.0)
72 STORE_NAME 5 (y)
74 RETURN_CONST 3 (None)
Здесь наиболее важным байт-кодом является байт-код со смещением 34:
34 LOAD_FAST_AND_CLEAR 2 (x)
Это принимает значение переменной x
в локальной области видимости и помещает его в стек, а затем очищает имя. Если нет переменной x
в текущей области видимости, эти магазины кесарево NULL
значение в стеке. Теперь имя исчезает из локальной области видимости, пока не будет достигнут байт-код со смещением 66:
66 STORE_FAST 2 (x)
Это восстанавливает x
то, что было до понимания списка; если a NULL
было сохранено в стеке, чтобы указать, что переменной с именем не было x
, то переменной все равно не будет x
после выполнения этого байт-кода.
Остальной байт-код между вызовами LOAD_FAST_AND_CLEAR
и STORE_FAST
более или менее такой же, как и раньше, с SWAP
байт-кодами, используемыми для доступа к итератору для range(1)
объекта вместо LOAD_FAST (.0)
байт-кода функции в более ранних версиях Python 3.x.
Ответ 2
На мой взгляд, это недостаток Python 3. Я надеюсь, что они это изменят.
Новый способ:
class Foo:
x = 5
y = (lambda x=x: [x for i in range(1)])()
Поскольку синтаксис настолько уродлив, я просто инициализирую все свои переменные класса в конструкторе, как правило
Ответ 3
Принятый ответ предоставляет отличную информацию, но, похоже, здесь есть несколько других недостатков - различия между пониманием списка и выражениями генератора. Демо, с которым я поиграл.:
class Foo:
# A class-level variable.
X = 10
# I can use that variable to define another class-level variable.
Y = sum((X, X))
# Works in Python 2, but not 3.
# In Python 3, list comprehensions were given their own scope.
try:
Z1 = sum([X for _ in range(3)])
except NameError:
Z1 = None
# Fails in both.
# Apparently, generator expressions (that's what the entire argument
# to sum() is) did have their own scope even in Python 2.
try:
Z2 = sum(X for _ in range(3))
except NameError:
Z2 = None
# Workaround: put the computation in lambda or def.
compute_z3 = lambda val: sum(val for _ in range(3))
# Then use that function.
Z3 = compute_z3(X)
# Also worth noting: here I can refer to XS in the for-part of the
# generator expression (Z4 works), but I cannot refer to XS in the
# inner-part of the generator expression (Z5 fails).
XS = [15, 15, 15, 15]
Z4 = sum(val for val in XS)
try:
Z5 = sum(XS[i] for i in range(len(XS)))
except NameError:
Z5 = None
print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)
Ответ 4
Простое добавление for x in [x]
в качестве первого for
предложения, чтобы сделать x
доступным в области понимания:
class Foo:
x = 5
y = [x for x in [x] for i in range(1)]
И с for State in [State]
в другом вашем случае:
from collections import namedtuple
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for State in [State] for args in [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
]]
Или с несколькими переменными:
class Line:
a = 19
b = 4
y = [a*x + b
for a, b in [(a, b)]
for x in range(10)]