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


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)

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)]

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


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'],
        # ...
    ])]
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.