Оценка атрибутов класса и генераторов

Как именно Python оценивает атрибуты класса? Я наткнулся на интересную причуду (в Python 2.5.2), которую я хотел бы объяснить.

У меня есть класс с некоторыми атрибутами, которые определены в терминах других ранее определенных атрибутов. Когда я пытаюсь использовать объект-генератор, Python выдает ошибку, но если я использую простое обычное понимание списка, нет проблем.

Здесь приведен пример. Обратите внимание, что единственное отличие состоит в том, что Brie использует выражение генератора, а Cheddar использует понимание списка.

# Using a generator expression as the argument to list() fails
>>> class Brie :
...     base = 2
...     powers = list(base**i for i in xrange(5))
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in Brie
  File "<stdin>", line 3, in <genexpr>
NameError: global name 'base' is not defined

# Using a list comprehension works
>>> class Cheddar :
...     base = 2
...     powers = [base**i for i in xrange(5)]
... 
>>> Cheddar.powers
[1, 2, 4, 8, 16]

# Using a list comprehension as the argument to list() works
>>> class Edam :
...     base = 2
...     powers = list([base**i for i in xrange(5)])
...
>>> Edam.powers
[1, 2, 4, 8, 16]

(Мой фактический случай был более сложным, и я создавал dict, но это минимальный пример, который я мог найти.)

Мое единственное предположение состоит в том, что методы подсчета списков вычисляются в этой строке, но выражения генератора вычисляются после окончания класса, после чего область изменений изменилась. Но я не уверен, почему выражение генератора не действует как замыкание и сохраняет ссылку на базу в области на линии.

Есть ли причина для этого, и если да, то как я должен думать о механизме оценки атрибутов класса?

Ответы

Ответ 1

Да, это немного изворотливо. Класс действительно не вводит новую область, это просто выглядит немного похоже на это; конструкции, подобные этому, раскрывают разницу.

Идея состоит в том, что, когда вы используете выражение генератора, это эквивалентно его использованию с помощью лямбда:

class Brie(object):
    base= 2
    powers= map(lambda i: base**i, xrange(5))

или явно как оператор функции:

class Brie(object):
    base= 2

    def __generatePowers():
        for i in xrange(5):
            yield base**i

    powers= list(__generatePowers())

В этом случае ясно, что base не имеет возможности для __generatePowers; результаты исключения для обоих (если вам не повезло, чтобы иметь также base global, и в этом случае вы получите ошибку).

Это не происходит для понимания списков из-за некоторых внутренних деталей о том, как они оцениваются, однако это поведение исчезает в Python 3, что в обоих случаях не будет одинаковым. Некоторые обсуждения здесь.

Обходной путь можно использовать с помощью лямбда с той же техникой, на которую мы полагались назад в плохие старые дни перед nested_scopes:

class Brie(object):
    base= 2
    powers= map(lambda i, base= base: base**i, xrange(5))

Ответ 2

От PEP 289:

После изучения многих возможностей консенсус показал, что обязательные вопросы было трудно понять и что пользователи следует настоятельно рекомендовать использовать генераторные выражения внутри функций которые потребляют свои аргументы немедленно. Для более сложных приложения, полный генератор определения всегда превосходят условия очевидности сферы, времени жизни и привязки [6].

[6] (1, 2) Обсуждение патчей и альтернативные патчи на Source Forge http://www.python.org/sf/872326

Как показано выражения генератора, насколько я могу это понять.