Доступ к переменным класса из списка понимания в определении класса
Как вы получаете доступ к другим переменным класса из понимания списка в определении класса? Следующее работает в Python 2, но не работает в Python 3:
class Foo:
x = 5
y = [x for i in range(1)]
Python 3.2 выдает ошибку:
NameError: global 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.
10 ответов
Область видимости и список классов, набор или словарь, а также выражения генератора не смешиваются.
Почему; или официальное слово об этом
В Python 3 для списочных представлений была назначена собственная область видимости (локальное пространство имен), чтобы их локальные переменные не могли перетекать в окружающую область (см. Перечень пониманий списков в Python, связывающий имена даже после области видимости. Это правильно?). Это замечательно, когда используется такое понимание списка в модуле или в функции, но в классах определение области видимости немного странно.
Это задокументировано в ОП 227:
Имена в области видимости не доступны. Имена разрешаются в самой внутренней области действия функции. Если определение класса встречается в цепочке вложенных областей, процесс разрешения пропускает определения класса.
и в class
составная ведомость документации:
Затем набор класса выполняется в новом фрейме выполнения (see section Именование и связывание), используя недавно созданное локальное пространство имен и исходное глобальное пространство имен. (Обычно набор содержит только определения функций.) Когда набор класса завершает выполнение, его кадр выполнения отбрасывается, но его локальное пространство имен сохраняется. [4] Объект класса затем создается с использованием списка наследования для базовых классов и сохраненного локального пространства имен для словаря атрибутов.
Акцент мой; кадр выполнения - это временная область.
Поскольку область видимости повторно используется в качестве атрибутов объекта класса, что позволяет использовать ее как нелокальную область действия, что ведет к неопределенному поведению; что произойдет, если метод класса ссылается на x
в качестве вложенной области видимости, а затем манипулирует Foo.x
ну как например? Что еще более важно, что бы это значило для подклассов Foo
? Python должен относиться к области видимости класса по-разному, поскольку она сильно отличается от области видимости функции.
Наконец, но не в последнюю очередь, в связанном разделе " Именование и привязка " в документации по модели выполнения явно упоминаются области действия классов:
Область имен, определенных в блоке класса, ограничена блоком класса; он не распространяется на блоки кода методов - это включает в себя понимания и выражения генератора, поскольку они реализованы с использованием области действия функции. Это означает, что следующее не удастся:
class A: a = 42 b = list(a + i for i in range(10))
Итак, подведем итог: вы не можете получить доступ к области видимости класса из функций, списков или выражений генератора, заключенных в эту область; они действуют так, как будто эта область не существует. В 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. Это было бы выражением для самой внешней итерации. В вашем примере это 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)]
Это проектное решение было принято для того, чтобы выдать ошибку во время создания genexp вместо времени итерации, когда создание самой внешней итерируемой выражения-генератора генерирует ошибку, или когда самая внешняя итерация оказывается не повторяемой. Понимания разделяют это поведение для согласованности.
Заглядывая под капот; или, более подробно, чем вы когда-либо хотели
Вы можете увидеть все это в действии, используя dis
модуль. Я использую Python 3.3 в следующих примерах, потому что он добавляет квалифицированные имена, которые аккуратно идентифицируют объекты кода, которые мы хотим проверить. Полученный байт-код функционально идентичен Python 3.2.
Чтобы создать класс, Python по существу берет весь набор, который составляет тело класса (так что все отступы на один уровень глубже, чем class <name>:
line) и выполняет это, как если бы это была функция:
>>> 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
тело класса, затем превращает это в функцию и вызывает ее. Результат этого вызова затем используется для создания пространства имен класса, его __dict__
, Все идет нормально.
Здесь следует отметить, что байт-код содержит объект вложенного кода; в Python определения классов, функции, понимания и генераторы все представлены как объекты кода, которые содержат не только байт-код, но также и структуры, которые представляют локальные переменные, константы, переменные, взятые из глобальных переменных, и переменные, взятые из вложенной области видимости. Скомпилированный байт-код ссылается на эти структуры, и интерпретатор python знает, как получить доступ к тем, которые представлены представленными байт-кодами.
Важно помнить, что Python создает эти структуры во время компиляции; class
suite - это объект кода (<code object Foo at 0x10a436030, file "<stdin>", line 2>
) это уже скомпилировано.
Давайте проверим тот объект кода, который создает само тело класса; объекты кода имеют 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
будет переменной ячейки (ячейки относятся к вложенным областям):
>>> 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
из объектов кода объекта ячейки:
>>> 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 должен только смотреть на глобальные переменные или замыкание текущего выполняемого объекта. Поскольку компилятор не включал тело класса в качестве области видимости, пространство имен временной функции не рассматривается.
Обходной путь; или что с этим делать
Если бы вы должны были создать явную область для 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'),
# ...
]]
По сути, это проблема в Python 3. Надеюсь, они это изменят.
Ошибка (работает в 2.7):
x = 4
y = [x+i for i in range(1)]
Чтобы обойти это (работает в 3+):
x = 4
y = (lambda x=x: [x+i for i in range(1)])()
Принятый ответ дает отличную информацию, но здесь, по-видимому, есть еще несколько недостатков - различия между пониманием списка и выражениями генератора. Демо-версия, с которой я играл:
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)
Просто добавив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)]
Поскольку внешний итератор оценивается в окружающей области, мы можем использовать zip
вместе с itertools.repeat
перенести зависимости в область понимания:
import itertools as it
class Foo:
x = 5
y = [j for i, j in zip(range(3), it.repeat(x))]
Можно также использовать вложенные for
циклы в понимании и включают зависимости в самой внешней итерации:
class Foo:
x = 5
y = [j for j in (x,) for i in range(3)]
Для конкретного примера OP:
from collections import namedtuple
import itertools as it
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for State, args in zip(it.repeat(State), [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
])]
Это ошибка в Python. Понимания рекламируются как эквивалентные циклам for, но в классах это не так. По крайней мере, до Python 3.6.6, в понимании, используемом в классе, внутри понимания доступна только одна переменная снаружи понимания, и она должна использоваться как самый внешний итератор. В функции это ограничение области применения не применяется.
Чтобы проиллюстрировать, почему это ошибка, давайте вернемся к исходному примеру. Это не удается:
class Foo:
x = 5
y = [x for i in range(1)]
Но это работает:
def Foo():
x = 5
y = [x for i in range(1)]
Ограничение указано в конце этого раздела в справочном руководстве.
Можно использоватьfor
петля:
class A:
x=5
##Won't work:
## y=[i for i in range(101) if i%x==0]
y=[]
for i in range(101):
if i%x==0:
y.append(i)
Поправьте меня, я не ошибаюсь...
Это может быть дизайн, но ИМХО, это плохой дизайн. Я знаю, что я не эксперт здесь, и я пытался прочитать обоснование этого, но это просто выше моего понимания, как я думаю, что это было бы для любого среднего программиста Python.
Мне кажется, что понимание не сильно отличается от обычного математического выражения. Например, если 'foo' является переменной локальной функции, я могу легко сделать что-то вроде:
(foo + 5) + 7
Но я не могу:
[foo + x for x in [1,2,3]]
Для меня тот факт, что одно выражение существует в текущей области видимости, а другое создает собственную область видимости, очень удивителен и, без шуток, «непонятен».
Просто забавный пример.
Если вы хотите сохранить его как генератор списка, это также работает с генераторами вложенных списков. Перенесите значение в глобальное пространство имен, но сохраните имя его класса.
class Foo:
global __x
__x = 5
y = [_Foo__x for i in range(1)]
С уважением
Я потратил некоторое время, чтобы понять, почему это функция, а не ошибка.
Рассмотрим простой код:
a = 5
def myfunc():
print(a)
Поскольку в myfunc() не определено «a», область действия расширится, и код будет выполнен.
Теперь рассмотрим тот же код в классе. Это не может работать, потому что это полностью испортит доступ к данным в экземплярах класса. Вы никогда не узнаете, обращаетесь ли вы к переменной в базовом классе или в экземпляре.
Понимание списка - это лишь частичный случай того же эффекта.