Почему при определении класса можно присвоить атрибуту объект с таким же глобальным именем?
Если мы определим глобальное имя и попытаемся присвоить его внутри функции такому же локальному имени, то получим ошибку UnboundLocalError:
>>> x = 42
>>> def f():
... x = x
... return f'{x = }'
...
>>> f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in f
UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
Но если мы сделаем так с классом, то ошибки не будет:
>>> x = 42
>>> class A:
... x = x
...
>>> a = A()
>>> print(a.x)
42
При этом внутри класса мы можем разместить выполняемый код, как если бы это была процедура:
>>> x = 42
>>> class B:
... x += x
... for y in range(3):
... x += y
... print(f'{x = }')
...
x = 87
>>> b = B()
>>> print(f'{x = }, {b.x = }')
x = 42, b.x = 87
Почему возможно такое обращение с глобальными переменными внутри класса?
Ответы (1 шт):
python 3.13
Чтобы понять механику обращения с переменными, рассмотрим байткод создания класса:
>>> dis('class A: x = x')
0 RESUME 0
1 LOAD_BUILD_CLASS
PUSH_NULL
LOAD_CONST 0 (<code object A at 0x7f32779ffad0, file "<dis>", line 1>)
MAKE_FUNCTION
LOAD_CONST 1 ('A')
CALL 2
STORE_NAME 0 (A)
RETURN_CONST 2 (None)
Disassembly of <code object A at 0x7f32779ffad0, file "<dis>", line 1>:
1 RESUME 0
LOAD_NAME 0 (__name__)
STORE_NAME 1 (__module__)
LOAD_CONST 0 ('A')
STORE_NAME 2 (__qualname__)
LOAD_CONST 1 (1)
STORE_NAME 3 (__firstlineno__)
LOAD_NAME 4 (x)
STORE_NAME 4 (x)
LOAD_CONST 2 (())
STORE_NAME 5 (__static_attributes__)
RETURN_CONST 3 (None)
Мы видим, что процесс начинается с LOAD_BUILD_CLASS, инициирующего выполнение функции __build_class__. Она в свою очередь принимает первым параметром функцию, скомпилированную из тела класса как обычный скрипт. Последняя выполняется отдельным фреймом, пространство локальных имен которого подменяется внешним словарем. После выполнения кода этот словарь используется для создания атрибутов класса.*
Поскольку код класса компилируется как если бы это был самостоятельный модуль, обращение к переменным происходит через LOAD_NAME и STORE_NAME. В реализации первого имя переменной ищется поочередно сначала в словаре локальных имен фрейма, затем глобальных имен этого же фрейма, и наконец в builtins. А вот STORE_NAME сохраняет объекты только в словаре локальных имен. Другими словами, пространство имен условно можно представить как ChainMap(local_namespace, globals(), builtins.__dict__)
Итого, код вида x = x, выполняемый как тело класса, сначала обнаружит x в глобальном словаре созданного по нему фрейма, но сохранит извлеченный объект в локальном словаре по тому же имени, после чего последующие обращения к x в пределах тела класса будут находить его среди локальных имен и останавливать поиск.
Действия интерпретатора можно смоделировать в языке Python таким образом:
from types import FunctionType
class_body = '''
x = x
'''
co = compile(class_body, '<string>', 'exec')
func = FunctionType(co, globals())
A = __build_class__(func, 'A')
Или через type и создание словаря локальных имен:
local_namespace = {}
exec(class_body, globals(), local_namespace)
A = type('A', (), local_namespace)
* Подробнее см. builtin___build_class__, которая передаёт словарь атрибутов в _PyEval_Vector, где он используется в _PyEvalFramePushAndInit как пространство локальных имен при создания фрейма. В других версиях Python детали могут отличаються, но сохраняется общая логика.