Вопрос по list-comprehension, python-3.x, python-internals, scope, python – Доступ к переменным класса из списка понимания в определении класса

135

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

Я только что проверил на 2.7.11. Получил ошибку имени Junchao Gu
@gps: или используйте вложенную область видимости, вставив (временную) функцию в набор определений классов. Martijn Pieters
Зачем вам это нужно, просто из любопытства? Ry-
Интересно ... Один очевидный обходной путь - назначить y после выхода из определения класса. Foo.y = [Foo.x для i в диапазоне (1)] gps

Ваш Ответ

5   ответов
179

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

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

В Python 3 для списочных представлений была назначена собственная область видимости (локальное пространство имен), чтобы предотвратить попадание их локальных переменных в окружающую область видимости (см.Понимание списка Python связывает имена даже после объема понимания. Это правильно?). Тот'Замечательно, когда используется такое понимание списка в модуле или в функции, но в классах область видимости немного, хм,странный.

Это задокументировано вчеловек 227:

Имена в области видимости не доступны. Имена разрешаются в самой внутренней области действия функции. Если определение класса встречается в цепочке вложенных областей, процесс разрешения пропускает определения класса.

и вclass Сводная документация:

Класс'Затем набор s выполняется в новом фрейме выполнения (см. разделНаименование и привязка), используя только что созданное локальное пространство имен и исходное глобальное пространство имен. (Обычно пакет содержит только определения функций.) Когда классS Suite заканчивает выполнение,его кадр выполнения отбрасывается, но его локальное пространство имен сохраняется.[4] Затем создается объект класса с использованием списка наследования для базовых классов и сохраненного локального пространства имен для словаря атрибутов.

Акцент мой; кадр выполнения - это временная область.

Поскольку область видимости повторно используется в качестве атрибутов объекта класса, что позволяет использовать ее как нелокальную область видимости, что ведет к неопределенному поведению; что произойдет, если метод класса ссылается наx в качестве вложенной области видимости, а затем манипулируетFoo.x ну как например? Что еще более важно, что бы это значило для подклассовFoo? питонимеет трактовать область видимости по-другому, так как она сильно отличается от области видимости функции.

И последнее, но не менее важное:Наименование и привязка В разделе документации по модели выполнения в явном виде упоминаются области действия классов:

Область имен, определенных в блоке класса, ограничена блоком класса; это не распространяется на блоки кода методов - это включает в себя понимания и выражения генератора, поскольку они реализованы с использованием области действия функции. Это означает, что следующее не удастся:

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. Это было бы выражением для самой внешней итерации. В вашем примере этоs: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 : 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         
</code>

Первый<code>LOAD_CONST</code> загружает объект кода для<code>Foo</code> тело класса, затем превращает это в функцию и вызывает ее.результат этого вызова затем используется для создания пространства имен класса, его<code>__dict__</code>, Все идет нормально.

Здесь следует отметить, что байт-код содержит объект вложенного кода; в Python определения классов, функции, понимания и генераторы все представлены как объекты кода, которые содержат не только байт-код, но также и структуры, которые представляют локальные переменные, константы, переменные, взятые из глобальных переменных, и переменные, взятые из вложенной области видимости. Скомпилированный байт-код ссылается на эти структуры, и интерпретатор python знает, как получить доступ к тем, которые представлены представленными байт-кодами.

Важно помнить, что Python создает эти структуры во время компиляции;<code>class</code> suite - это объект кода (<code></code>

@ecatmur: Точно,lambda В конце концов, это просто анонимные функции. Martijn Pieters
Вы также можете использовать лямбду, чтобы исправить привязку:y = (lambda x=x: [x for i in range(1)])() ecatmur
Если требуется страница технической информации, чтобы объяснить, почему что-то не такне работает интуитивно, я называю это ошибкой. Jonathan
@eryksun: верно; Я держал это просто намеренно там; Я'Мы перефразировали его, чтобы было ясно, что Python косвенно смотрит на замыкания. Martijn Pieters
2

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

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

мы можем использовать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'],
        # ...
    ])]
1

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

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

12

Old Way (работает в 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)])()

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

Это другая проблема, которая на самом деле неt проблема в Python 3. Это происходит только в IPython, когда вы вызываете его в режиме встраивания с помощью скажемpython -c "import IPython;IPython.embed()", Запустите IPython напрямую, используя скажемipython и проблема исчезнет. Riaz Rizvi
Эта проблема присутствует и в Python 2, при использовании выражений генератора, а также с пониманием множеств и словаря. Это не ошибка, это следствие того, как работают пространства имен классов. Это'не изменится. Martijn Pieters
Ага. Несмотря на то, что было бы неплохо получить ответ с кратким описанием обходного пути, в этом случае ошибочно говорится о поведении как об ошибке, когда это является побочным эффектом работы языка (и, следовательно, выиграл 'не может быть изменено) jsbueno
И я отмечаю, что ваш обходной путь делает именно то, что уже сказано в моем ответе: создайте новую область видимости (лямбда не отличается здесь от использованияdef создать функцию). Martijn Pieters

Похожие вопросы