Ответ 1
Область видимости и список классов, набор или словарь, а также выражения генератора не смешиваются.
Почему; или официальное слово об этом
В Python 3 для списочных представлений была назначена собственная область видимости (локальное пространство имен), чтобы их локальные переменные не могли перетекать в окружающую область видимости (см. Перечень пониманий списков Python связывает имена даже после области понимания. Это правильно?). Это замечательно при использовании такого понимания списков в модуле или в функции, но в классах, определение области видимости немного странно.
Это задокументировано в ОП 227:
Имена в области видимости не доступны. Имена разрешаются в самой внутренней области действия функции. Если определение класса встречается в цепочке вложенных областей, процесс разрешения пропускает определения класса.
и в документации составной инструкции class
:
Затем набор classs выполняется в новом фрейме выполнения (see section Именование и связывание), используя недавно созданное локальное пространство имен и исходное глобальное пространство имен. (Обычно набор содержит только определения функций.) Когда набор classs заканчивает выполнение, его кадр выполнения отбрасывается, но его локальное пространство имен сохраняется. [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
- это объект кода (<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
объект не загружен, вместо этого FOR_ITER
цикл 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__
- просто использовать __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'),
# ...
]]