ключевое слово nonlocal в 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 01

21
Спасибо за этот ответ. Я думаю, вы могли бы улучшить терминологию: вместо «может читать, не может изменять», возможно, «может ссылаться, не может назначать». Вы можете изменить содержимое объекта в нелокальной области, но вы не можете изменить, на какой объект идет ссылка.
metamatt

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)

Это не должно быть уродливым. Вместо inner.y используйте external.y. Вы можете определить external.y = 0 перед def inner.
jgomo3

1
Хорошо, я вижу, что мой комментарий неверен. Элиас Замария также реализовал решение external.y. Но, как отмечает Натаниэль [1], вам следует остерегаться. Я думаю, что этот ответ следует продвигать как решение, но обратите внимание на решение external.y и оговорки. [1] stackoverflow.com/questions/3190706/…
jgomo3

Это становится некрасивым, если вы хотите, чтобы более одной внутренней функции разделяли нелокальное изменяемое состояние. Скажите, что a inc()и a, dec()возвращенные из внешнего, увеличивают и уменьшают общий счетчик. Затем вам нужно решить, к какой функции присоединить текущее значение счетчика, и ссылаться на эту функцию из другой (-ых). Что выглядит несколько странно и асимметрично. Например, в 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

Каждый вызов external () создает новый и отличный класс, называемый контекстом (а не просто новый экземпляр). Таким образом, @Nathaniel избегает опасений по поводу общего контекста.

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

assert f() == 5

Ницца! Это действительно более элегантно и читабельно, чем словарь.
Винченцо

Я бы рекомендовал здесь вообще использовать слоты. Защищает от опечаток.
DerWeh 01

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

@BobStein Извините, я не совсем понимаю, о чем вы. Но, добавляя __slots__ = ()и создавая объект вместо использования класса, например context.z = 3, поднимет AttributeError. Это возможно для всех классов, если они не наследуются от класса, не определяющего слоты.
DerWeh 02

@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 это то, что должно охватывать ключевое слово nonlocal. Как упоминалось в 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

@Nathaniel: Дай мне посмотреть, правильно ли я это понимаю. Присвоение outer.yне включает ничего локального по отношению к вызову функции (экземпляру) outer(), но присваивается атрибуту объекта функции, который привязан к имени outerв его охватывающей области. И поэтому можно было бы одинаково хорошо использовали, в письменном виде outer.y, любое другое имя вместо outer, при условии , что , как известно, связаны в этой области. Это верно?
Marc van Leeuwen

1
Исправление, я должен был сказать, после «привязанного в этой области»: к объекту, тип которого позволяет устанавливать атрибуты (например, функция или любой экземпляр класса). Кроме того, поскольку эта область на самом деле находится дальше, чем та, которую мы хотим, разве это не предлагает следующее чрезвычайно уродливое решение: вместо outer.yиспользования имени inner.y(поскольку innerоно связано внутри вызова outer(), что является именно той областью, которую мы хотим), но поместив инициализация inner.y = 0 после определения внутреннего (поскольку объект должен существовать при создании его атрибута), но, конечно, до return inner?
Marc van Leeuwen

@MarcvanLeeuwen Похоже, ваш комментарий вдохновил на решение с inner.y Элиаса. Хорошая мысль с твоей стороны.
Ankur Agarwal

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

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 locals в то время, когда inner()определено, но изменение этого словаря не меняет vin outer(). Это больше не будет работать, когда у вас будет больше внутренних функций, которые хотят поделиться закрытой переменной. Замолвите inc()и dec()что увеличение и уменьшение общего счетчика.
BlackJack

@BlackJack nonlocal- это функция Python 3.
Marcin

Да, я знаю. Вопрос заключался в том, как добиться эффекта Python 3 nonlocalв Python 2 в целом . Ваши идеи не охватывают общий случай, а только те, у которых есть одна внутренняя функция. Взглянем на эту суть для примера. Обе внутренние функции имеют свой собственный контейнер. Вам нужен изменяемый объект в области внешней функции, как уже предлагали другие ответы.
BlackJack

@BlackJack Это не вопрос, но спасибо за ваш вклад.
Marcin

Да , что это вопрос. Взгляните на верхнюю часть страницы. У 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
на самом деле я предпочитаю решение с использованием словаря, но этот классный :)
Эзер Фернандес

0

Расширяя элегантное решение Мартино выше до практичного и несколько менее элегантного варианта использования, я получаю:

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.