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

174

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

Марк Лодато
источник
Ваше сообщение об ошибке неверно. Я получаю NameError: global name 'x' is not definedна Python 3.2 и 3.3, что я и ожидал.
Мартин Питерс
Интересно ... Один очевидный обходной путь - назначить y после выхода из определения класса. Foo.y = [Foo.x для i в диапазоне (1)]
GPS
3
+ martijn-pieters ссылка на дубликат верна, там есть комментарий от + matt-b с объяснением: у списков в Python 2.7 нет собственного пространства имен (в отличие от выражений set или dict или выражений генератора ... замените ваш [ ] с {}, чтобы увидеть это в действии). У всех них есть свое собственное пространство имен в 3.
gps
@gps: или используйте вложенную область видимости, вставив (временную) функцию в набор определений классов.
Мартин Питерс
Я только что проверил на 2.7.11. Получил ошибку имени
Junchao Gu

Ответы:

244

Область видимости и список классов, набор или словарь, а также выражения генератора не смешиваются.

Почему; или официальное слово об этом

В 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>:строка), и выполняет это, как если бы это была функция:

>>> 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выполняется встроенный цикл. Таким образом, в 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'),
       # ...
    ]]
Мартейн Питерс
источник
21
Вы также можете использовать лямбду, чтобы исправить привязку:y = (lambda x=x: [x for i in range(1)])()
ecatmur
3
@ecatmur: Точно, в конце концов, lambdaэто просто анонимные функции.
Мартин Питерс
2
Для записи, обходной путь, который использует аргумент по умолчанию (для лямбды или функции) для передачи переменной класса, имеет ошибку. А именно, он передает текущее значение переменной. Таким образом, если переменная изменится позже, а затем будет вызвана лямбда или функция, лямбда или функция будут использовать старое значение. Это поведение отличается от поведения замыкания (которое будет захватывать ссылку на переменную, а не на ее значение), поэтому может быть неожиданным.
Нил Янг
9
Если для объяснения, почему что-то не работает интуитивно, требуется страница технической информации, я называю это ошибкой.
Джонатан
5
@JonathanLeaders: Не называйте это ошибкой , называйте это компромиссом . Если вам нужны A и B, но вы можете получить только один из них, то независимо от того, как вы решите, в некоторых ситуациях вам не понравится результат. Такова жизнь.
Лутц Пречелт
15

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

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

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

ПРИМЕЧАНИЕ: простое определение области действия A.xне решит проблему

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

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

Поскольку синтаксис настолько ужасен, я просто инициализирую все свои переменные класса в конструкторе.

Джонатан
источник
6
Проблема присутствует и в Python 2, при использовании выражений-генераторов, а также с пониманием множеств и словаря. Это не ошибка, это следствие того, как работают пространства имен классов. Это не изменится.
Мартин Питерс
4
И я отмечаю, что ваш обходной путь делает именно то, что уже сказано в моем ответе: создайте новую область (лямбда здесь ничем не отличается от использования defдля создания функции).
Мартин Питерс
1
Ага. Несмотря на то, что было бы неплохо получить ответ с кратким обзором, это неверно описывает поведение как ошибку, когда это является побочным эффектом работы языка (и, следовательно, не будет изменен)
jsbueno
Это другая проблема, которая на самом деле не является проблемой в Python 3. Это происходит только в IPython, когда вы вызываете его в режиме встраивания с помощью say python -c "import IPython;IPython.embed()". Запустите IPython напрямую, используя say, ipythonи проблема исчезнет.
Риаз Ризви
6

Принятый ответ дает отличную информацию, но здесь, по-видимому, есть еще несколько складок - различия между пониманием списка и выражениями генератора. Демо, с которым я играл:

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)
FMC
источник
2

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

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

bzip2
источник
1

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