нелокальне ключове слово в Python 2.x


117

Я намагаюся реалізувати закриття в Python 2.6, і мені потрібно отримати доступ до нелокальної змінної, але здається, що це ключове слово недоступне в python 2.x. Як слід отримувати доступ до нелокальних змінних у закриттях у цих версіях python?

Відповіді:


125

Внутрішні функції можуть читати нелокальні змінні в 2.x, лише не відновлювати їх. Це дратує, але можна обійтись. Просто створіть словник і зберігайте свої дані як елементи в ньому. Внутрішні функції не забороняють мутувати об'єкти, на які посилаються нелокальні змінні.

Щоб використовувати приклад з Вікіпедії:

def outer():
    d = {'y' : 0}
    def inner():
        d['y'] += 1
        return d['y']
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

4
чому можна змінити значення зі словника?
— coelhudo

9
@coelhudo Тому що ви можете змінювати нелокальні змінні. Але ви не можете виконувати призначення нелокальних змінних. Наприклад, це підніме UnboundLocalError: def inner(): print d; d = {'y': 1}. Тут print dчитається зовнішня, dстворюючи таким чином нелокальну змінну dу внутрішній області.
— suzanshakya

21
Дякую за цю відповідь. Я думаю, ви могли б вдосконалити термінологію: замість "може читати, не можу змінити", можливо "може посилатися, не можу призначити". Ви можете змінити вміст об'єкта в нелокальній області, але ви не можете змінити, на який об'єкт йдеться.
— метамат

3
Крім того, ви можете використовувати довільний клас та об'єкт інстанції замість словника. Я вважаю це набагато більш елегантним і читабельним, оскільки він не заливає код буквами (словниковими ключами), плюс ви можете використовувати методи.
— Алоїз Магдал

6
Для Python я вважаю, що найкраще використовувати дієслово "прив'язувати" чи "відновлювати" та використовувати іменник "ім'я", а не "змінна". У Python 2.x ви можете закрити об'єкт, але не зможете відновити ім'я в початковій області. У такій мові, як C, оголошення змінної залишає деяке сховище (або статичне сховище, або тимчасове на стеку). У Python вираз X = 1просто пов'язує ім'я Xз певним об'єктом (а intзі значенням 1). X = 1; Y = Xприв’язує два імені до одного і того ж точного об’єкта. У всякому разі, деякі об'єкти є змінними і ви можете змінити їх значення.
— steveha

37

Наступне рішення натхнене відповіддю Еліаса Замарії , але всупереч цій відповіді правильно обробляти кілька викликів зовнішньої функції. "Змінна" inner.yє локальною для поточного виклику outer. Тільки це не змінна, оскільки це заборонено, а атрибут об'єкта (об'єктом є сама функція inner). Це дуже некрасиво (зауважте, що атрибут можна створити лише після визначення innerфункції), але здається ефективним.

def outer():
    def inner():
        inner.y += 1
        return inner.y
    inner.y = 0
    return inner

f = outer()
g = outer()
print(f(), f(), g(), f(), g()) #prints (1, 2, 1, 3, 2)

Це не повинно бути негарним. Замість використання Internal.y використовуйте external.y. Ви можете визначити external.y = 0 перед початком def.
— jgomo3

1
Гаразд, я бачу, що мій коментар невірний. Еліас Замарія також здійснив рішення external.y. Але як коментує Натаніел [1], ви повинні бути обачними. Я думаю, що цю відповідь слід пропагувати як рішення, але зауважте про рішення. [1] stackoverflow.com/questions/3190706 / ...
— jgomo3

Це стає некрасивим, якщо потрібно більше однієї внутрішньої функції, яка ділиться нелокальним станом, що змінюється. Скажіть "a" inc()та " dec()return", що повернувся із зовнішнього збільшення та зменшення загального лічильника. Тоді ви повинні вирішити, до якої функції слід приєднати поточне значення лічильника та посилатись на цю функцію з інших (-ів). Що виглядає дещо дивно і несиметрично. Наприклад у dec()такій лінії, як inc.value -= 1.
— BlackJack

33

Замість словника, до нелокального класу є менше скупчення . Зміна @ ChrisB в прикладі :

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

Тоді

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

Кожен зовнішній () виклик створює новий і чіткий клас, який називається контекстом (а не лише новим екземпляром). Таким чином, це уникає побоювання @ Nathaniel про спільний контекст.

g = outer()
assert g() == 1
assert g() == 2

assert f() == 5

Приємно! Це дійсно більш елегантно і читабельно, ніж словник.
— Вінченцо

Я б рекомендував тут використовувати слоти взагалі. Це захищає вас від помилок.
— DerWeh

@DerWeh цікаво, я ніколи про це не чув . Не могли б ви сказати те саме для будь-якого класу? Я ненавиджу, коли мене спалахують друкарські помилки.
— Боб Штейн

@BobStein Вибачте, я не дуже розумію, що ви маєте на увазі. Але додаючи __slots__ = ()та створюючи об’єкт замість використання класу, наприклад context.z = 3, піднімемо AttributeError. Це можливо для всіх класів, якщо вони не успадковують клас, який не визначає слоти.
— DerWeh

@DerWeh Я ніколи не чув про використання слотів для лову помилок. Ви маєте рацію, це допоможе лише в екземплярах класу, а не в змінних класу. Можливо, ви хочете створити робочий приклад на pyfiddle. Було б потрібно щонайменше дві додаткові лінії. Не можу уявити, як би ви це зробили, без більшої безладу.
— Боб Штейн

14

Я думаю, тут ключовим є те, що ви маєте на увазі під "доступом". Не повинно виникнути проблем з читанням змінної за межами області закриття, наприклад,

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

має працювати як очікувалося (друк 3). Однак перевизначення значення x не працює, наприклад,

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

все одно буде надруковано 3. З мого розуміння PEP-3104, саме це має на увазі нелокальне ключове слово. Як згадувалося в PEP, ви можете використовувати клас, щоб виконати те ж саме (безладно):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x

Замість того, щоб створити клас та інстанціювати його, можна просто створити функцію: def ns(): passдалі ns.x = 3. Це не дуже, але це трохи менш потворно для мого ока.
— davidchambers

2
Якась конкретна причина для голосування? Зізнаюся, моє не найвишуканіше рішення, але воно працює ...
— ig0774

2
Про що class Namespace: x = 3?
— Feuermurmel

3
Я не думаю, що цей спосіб імітує нелокальне, оскільки він створює глобальну посилання замість посилання в закритті.
— CarmeloS

1
Я погоджуюся з @CarmeloS, ваш останній блок коду не виконувати те, що PEP-3104 пропонує як спосіб здійснити щось подібне в Python 2. ns- це глобальний об'єкт, саме тому ви можете посилатися ns.xна рівні модуля в printзаяві в самому кінці .
— мартіно

13

Є ще один спосіб реалізації нелокальних змінних в Python 2, якщо будь-який із відповідей тут небажаний з будь-якої причини:

def outer():
    outer.y = 0
    def inner():
        outer.y += 1
        return outer.y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Зайвим є використання назви функції в операторі присвоєння змінної, але для мене це виглядає більш простим і чистішим, ніж розміщення змінної у словнику. Значення запам'ятовується від одного дзвінка до іншого, як і у відповіді Кріса Б.


19
Будь ласка, будьте уважні: якщо реалізовано таким чином, якщо ви зробите це, f = outer()а потім пізніше g = outer(), тоді fлічильник буде скинутий. Це тому, що вони обидва ділять одну outer.y змінну, а не кожну свою незалежну. Хоча цей код виглядає більш естетично, ніж відповідь Кріса Б., здається, його спосіб є єдиним способом наслідувати лексичне визначення, якщо ви хочете зателефонувати outerне один раз.
— Натаніел

@Nathaniel: Дозвольте мені подивитися, чи правильно я це розумію. Призначення outer.yне передбачає нічого локального для виклику функції (екземпляра) outer(), але призначає атрибут об'єкта функції, який пов'язаний з ім'ям outerв його обкладинці . І тому можна було б однаково добре використовували, в письмовому вигляді outer.y, будь-яке інше ім'я замість outer, при умови , що , як відомо, пов'язані в цій області. Це правильно?
— Марк ван Левен

1
Виправлення, я мав би сказати, після "пов'язаного в цій області": до об'єкта, тип якого дозволяє встановити атрибути (наприклад, функції або будь-якого екземпляра класу). Крім того, оскільки ця сфера дійсно виходить за рамки тієї, яку ми хочемо, чи не запропонувало б таке надзвичайно потворне рішення: замість того, щоб outer.yвикористовувати ім’я inner.y(оскільки innerприв’язане всередині виклику outer(), саме такий обсяг, який ми хочемо), але ставити ініціалізація inner.y = 0 після визначення внутрішнього (як об'єкт повинен існувати, коли створюється його атрибут), але звичайно раніше return inner?
— Марк ван Левен

@MarcvanLeeuwen Схоже, ваш коментар надихнув рішення на I. Гарна думка.
— Анкур Агарвал

Доступ та оновлення глобальних змінних - це, мабуть, не те, що намагаються зробити більшість людей під час вимкнення записів про закриття.
— бінкі

12

Ось щось, натхнене пропозицією Алоїза Магдала, зробленої в коментарі щодо іншої відповіді :

class Nonlocal(object):
    """ Helper to implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


def outer():
    nl = Nonlocal(y=0)
    def inner():
        nl.y += 1
        return nl.y
    return inner

f = outer()
print(f(), f(), f()) # -> (1 2 3)

Оновлення

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

# Implemented as a decorator.

class Nonlocal(object):
    """ Decorator class to help implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self._vars = kwargs

    def __call__(self, func):
        for k, v in self._vars.items():
            setattr(func, k, v)
        return func


@Nonlocal(y=0)
def outer():
    def inner():
        outer.y += 1
        return outer.y
    return inner


f = outer()
print(f(), f(), f()) # -> (1 2 3)

Зауважте, що обидві версії працюють в обох Python 2 та 3.


Цей, здається, найкращий з точки зору читабельності та збереження лексичного обстеження.
— Аарон С. Курланд

3

Існує бородавка в правилах розміщення python - присвоєння робить змінну локальною для її прямо огороджувальної області функцій. Для глобальної змінної ви вирішите це за допомогоюglobal ключового слова.

Рішення полягає в тому, щоб ввести об'єкт, який розділяється між двома областями, який містить змінні змінні, але сам посилається через змінну, яка не призначена.

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

Альтернативою є кілька областей хакерства:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

Можливо, вам вдасться розібратися в хитрість, щоб отримати ім'я параметра outer, а потім передати його як varname, але не покладаючись на ім'я, яке outerвам потрібно використовувати Y-комбінатор.


Обидві версії працюють у цьому конкретному випадку, але не є заміною nonlocal. locals()створює словник outer()s місцевих жителів на час inner()визначено, але зміна цього словника не змінює vв outer(). Це більше не працюватиме, якщо у вас більше внутрішніх функцій, які хочуть ділитися закритою над змінною. Замовте inc()і dec()що збільшення і зменшення загального лічильника.
— BlackJack

@BlackJack nonlocal- функція python 3.
— Марцін

Так, я знаю. Питання полягало в тому, як досягти ефекту Python 3nonlocal у Python 2 загалом . Ваші ідеї не стосуються загальної справи, а лише тієї, що має одну внутрішню функцію. Погляньте на приклад цю суть . Обидві внутрішні функції мають свій контейнер. Вам потрібен змінний об'єкт в області зовнішньої функції, як це вже запропоновано в інших відповідях.
— BlackJack

@BlackJack Це не питання, але дякую за ваш внесок.
— Марцін

Так, це питання. Погляньте вгорі сторінки. adinsa має Python 2.6 і потребує доступу до нелокальних змінних у закритому режимі без nonlocalключового слова, введеного в Python 3.
— BlackJack

3

Ще один спосіб зробити це (хоча це занадто багатослівно):

import ctypes

def outer():
    y = 0
    def inner():
        ctypes.pythonapi.PyCell_Set(id(inner.func_closure[0]), id(y + 1))
        return y
    return inner

x = outer()
x()
>> 1
x()
>> 2
y = outer()
y()
>> 1
x()
>> 3

1
насправді я віддаю перевагу рішенню за допомогою словника, але цей класний :)
— Ezer Fernandes

0

Розширюючи вишукане рішення Martineau вище до практичного та дещо менш елегантного випадку використання, я отримую:

class nonlocals(object):
""" Helper to implement nonlocal names in Python 2.x.
Usage example:
def outer():
     nl = nonlocals( n=0, m=1 )
     def inner():
         nl.n += 1
     inner() # will increment nl.n

or...
    sums = nonlocals( { k:v for k,v in locals().iteritems() if k.startswith('tot_') } )
"""
def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

def __init__(self, a_dict):
    self.__dict__.update(a_dict)

-3

Використовуйте глобальну змінну

def outer():
    global y # import1
    y = 0
    def inner():
        global y # import2 - requires import1
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Особисто мені не подобаються глобальні змінні. Але моя пропозиція заснована на https://stackoverflow.com/a/19877437/1083704 відповіді

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

де користувачеві потрібно оголосити глобальну змінну ranks, кожен раз, коли вам потрібно викликати report. Моє вдосконалення усуває необхідність ініціалізувати змінні функції у користувача.


Набагато краще використовувати словник. Ви можете посилатися на екземпляр inner, але не можете призначити його, але ви можете змінити його ключі та значення. Це дозволяє уникнути використання глобальних змінних.
— johannestaas
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.