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


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, що я б очікував.
Martijn Pieters

Цікаво ... Одним очевидним вирішенням є призначення y після виходу з визначення класу. Foo.y = [Foo.x для i в діапазоні (1)]
gps

3
+ посилання martijn-pieters на дублікат є правильним, там є коментар від + matt-b з поясненням: розуміння списку Python 2.7 не мають власного простору імен (на відміну від розуміння набору чи виводу або виразів генератора ... замініть [ ] з {}, щоб побачити це в дії). Вони все мають свій власний простір імен в 3.
GPS

@gps: Або використовувати вкладений діапазон, вставивши (тимчасову) функцію в набір визначення класу.
Martijn Pieters

Я щойно тестував 2.7.11. Є помилка ім'я
Junchao Gu

Відповіді:


244

Обсяг та список класів, розуміння набору чи словника, а також вирази генератора не змішуються.

Чому; або, офіційне слово з цього приводу

У Python 3 розуміння списків отримали належну область власного простору (локальний простір імен), щоб не допустити, щоб їх локальні змінні перетікали в навколишнє поле (див. Див. Пересвідчення імен списку Python навіть після сфери розуміння. Це правильно? ). Це чудово, коли використовується таке розуміння списку в модулі або у функції, але в класах, визначення обсягу - це трохи, ем, дивно .

Це задокументовано в pep 227 :

Назви в межах класу недоступні. Імена розв’язуються в найпотужнішій області застосування функції. Якщо визначення класу відбувається в ланцюжку вкладених областей, процес роздільної здатності пропускає визначення класів.

та у classскладеній документації на виписку :

Потім набір класів виконується в новому кадрі виконання (див. Розділ Іменування та прив'язка ), використовуючи щойно створений локальний простір імен та оригінальний глобальний простір імен. (Зазвичай пакет містить лише визначення функцій.) Коли набір класу закінчує виконання, його рамка виконання відкидається, але локальне місце імен зберігається . [4] Потім об’єкт класу створюється за допомогою списку спадкування для базових класів та збереженого локального простору імен для словника атрибутів.

Наголос мій; кадр виконання - це тимчасова область.

Оскільки сфера області перестановляється як атрибути на об'єкт класу, що дозволяє використовувати його як нелокальну область, а також призводить до невизначеної поведінки; що трапиться, якщо метод класу, який називається xвкладеною змінною області, потім маніпулює Foo.xтакож, наприклад? Що ще важливіше, що це буде означати для підкласів Foo? Python має для лікування рамки класу по- різному , як це дуже відрізняється від області видимості функції.

І останнє, але, безумовно, не менш важливе, в зв'язаному розділі Іменування та прив'язки в документації про модель виконання чітко згадуються сфери застосування класу:

Область імен, визначених у блоці класів, обмежена блоком класів; він не поширюється на кодові блоки методів - це включає розуміння та вирази генератора, оскільки вони реалізовані за допомогою функції функції. Це означає, що не вдасться:

class A:
     a = 42
     b = list(a + i for i in range(10))

Отже, підсумовуючи: ви не можете отримати доступ до області класу з функцій, розуміння списків або виразів генератора, що додаються до цієї області; вони діють так, ніби цього сфери не існує. У Python 2 розуміння списків було реалізовано за допомогою ярлика, але в Python 3 вони отримали власну область функцій (як це повинно було бути в усьому світі), і таким чином ваш приклад порушується. Інші типи розуміння мають власну сферу застосування незалежно від версії Python, тому подібний приклад із розумінням набору чи диктату порушиться у 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це лише анонімні функції.
Martijn Pieters

2
Для запису обхід, який використовує аргумент за замовчуванням (лямбда або функція) для передачі в змінну класу, має gotcha. А саме, воно передає поточне значення змінної. Отже, якщо змінна зміниться пізніше, а потім викликається лямбда або функція, лямбда або функція буде використовувати старе значення. Така поведінка відрізняється від поведінки закриття (яке б охоплювало посилання на змінну, а не її значення), тому може бути несподіваною.
Ніл Янг

9
Якщо для цього потрібна сторінка технічної інформації, яка пояснює, чому щось не працює інтуїтивно, я називаю це помилкою.
Джонатан

5
@JonathanLeaders: Не називайте це помилкою , не називайте це компромісом . Якщо ви хочете A і B, але можете отримати лише одну з них, то як би ви не вирішили, у деяких ситуаціях результат вам не сподобається. Це життя.
Lutz Prechelt

15

На мою думку, це недолік у Python 3. Я сподіваюся, що вони це змінять.

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

Оскільки синтаксис такий некрасивий, я просто ініціалізую всі конструктивні змінні в конструкторі, як правило


6
Проблема присутня і в Python 2, коли використовується генераторні вирази, а також з розуміннями набору та словника. Це не помилка, це наслідок того, як працюють простори імен класів. Це не зміниться.
Martijn Pieters

4
І зауважу, що ваше вирішення виконує саме те, що вже вказано у моїй відповіді: створити новий діапазон (лямбда тут не відрізняється від використання defдля створення функції).
Martijn Pieters

1
так. Незважаючи на те, що приємно отримати відповідь з обходом з першого погляду, він неправильно констатує поведінку як помилку, коли це є побічним ефектом того, як мова працює (і, отже, не буде змінено)
jsbueno

Це інша проблема, яка насправді не є проблемою в Python 3. Вона виникає лише в IPython, коли ви викликаєте її у вбудованому режимі за допомогою say python -c "import IPython;IPython.embed()". Запустіть IPython безпосередньо, використовуючи слово say, ipythonі проблема зникне.
Riaz Rizvi

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. Поняття рекламуються як рівнозначні для циклів, але це не вірно в класах. Щонайменше, до 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)]

Для конкретного прикладу ОП:

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.