Доступ к переменным класса из понимания списка в определении класса

Как вы получаете доступ к другим переменным класса из понимания списка в определении класса? Следующие действия выполняются в 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.

Ответы

Ответ 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'),
       # ...
    ]]

Ответ 2

На мой взгляд, это недостаток в Python 3. Надеюсь, они его поменяют.

Старый путь (работает в 2.7, выбрасывает NameError: name 'x' is not defined в 3+):

class A:
    x = 4
    y = [x+i for i in range(1)]

ПРИМЕЧАНИЕ: простое определение с помощью Ax не решит проблему

Новый путь (работает в 3+):

class A:
    x = 4
    y = (lambda x=x: [x+i 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 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

Это ошибка в 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)]

Ограничение указано в конце этого раздела в справочном руководстве.

Ответ 5

Поскольку внешний итератор вычисляется в окружающей области, мы можем использовать 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'],
        # ...
    ])]