Чтение / запись закрытий Python - PullRequest
33 голосов
/ 06 января 2010

Закрытия - невероятно полезная языковая функция. Они позволяют нам делать умные вещи, которые в противном случае потребовали бы много кода, и часто позволяют нам писать код, который был бы более элегантным и понятным. В Python 2.x имена переменных замыканий не могут быть восстановлены; то есть функция, определенная внутри другой лексической области видимости, не может делать что-то вроде some_var = 'changed!' для переменных вне своей локальной области видимости. Может кто-нибудь объяснить, почему это так? Были ситуации, в которых я хотел бы создать замыкание, которое связывает переменные во внешней области видимости, но это было невозможно. Я понимаю, что почти во всех случаях (если не во всех) такое поведение может быть достигнуто с помощью классов, но часто оно не такое чистое или элегантное. Почему я не могу сделать это с закрытием?

Вот пример закрытия переплета:

def counter():
    count = 0
    def c():
        count += 1
        return count
    return c

Это текущее поведение при вызове:

>>> c()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in c
UnboundLocalError: local variable 'count' referenced before assignment

Вместо этого я бы хотел, чтобы это было:

>>> c()
1
>>> c()
2
>>> c()
3

Ответы [ 7 ]

27 голосов
/ 06 января 2010

Чтобы расширить ответ Игнасио:

def counter():
    count = 0
    def c():
        nonlocal count
        count += 1
        return count
    return c

x = counter()
print([x(),x(),x()])

дает [1,2,3] в Python 3; вызовы counter() дают независимые счетчики. Другие решения, особенно использующие itertools / yield, более идиоматичны.

22 голосов
/ 06 января 2010

Вы можете сделать это, и это будет работать примерно так же:

class counter(object):
    def __init__(self, count=0):
        self.count = count
    def __call__(self):
        self.count += 1
        return self.count    

Или немного взломать:

def counter():
    count = [0]
    def incr(n):
        n[0] += 1
        return n[0]
    return lambda: incr(count)

Я бы пошел с первым решением.

РЕДАКТИРОВАТЬ: Это то, что я получаю за то, что я не читаю большой текстовый блог.

В любом случае, причина закрытия Python довольно ограничена: «потому что Гвидо чувствовал, что это так». Python был разработан в начале 90-х, во времена расцвета OO. Закрытия были довольно низкими в списке языковых особенностей, которые люди хотели. По мере того, как функциональные идеи, такие как функции первого класса, замыкания и другие вещи, становятся популярными, такие языки, как Python, вынуждены были их использовать, поэтому их использование может быть немного неловким, потому что это не то, для чего язык был разработан. *

<rant on="Python scoping">

Кроме того, Python (2.x) имеет довольно странные (на мой взгляд) представления о области видимости, которые, помимо прочего, мешают разумной реализации замыканий. Меня всегда беспокоит то, что это:

new = [x for x in old]

Оставляет нам имя x, определенное в области, в которой мы его использовали, так как это (на мой взгляд) концептуально меньшая область. (Хотя Python получает баллы за согласованность, поскольку то же самое с циклом for ведет себя так же. Единственный способ избежать этого - использовать map.)

В любом случае, </rant>

17 голосов
/ 06 января 2010

nonlocal в 3.x должно исправить это.

13 голосов
/ 06 января 2010

Я бы использовал генератор:

>>> def counter():
    count = 0
    while True:
        count += 1
        yield(count)

>>> c = counter()
>>> c.next()
1
>>> c.next()
2
>>> c.next()
3

РЕДАКТИРОВАТЬ : я считаю, что окончательный ответ на ваш вопрос PEP-3104 :

На большинстве языков, которые поддерживают вложенные Области, код может ссылаться или перепривязывать (назначить) любое имя в ближайшем объем ограждения. В настоящее время Python Код может ссылаться на имя в любом объем, но он может только перепривязать имена в двух областях: локальная область (путем простого назначения) или глобальная область модуля (с использованием глобальной декларация).

Это ограничение было поднято многими раз в списке рассылки Python-Dev и в других местах, и привело к расширению обсуждение и множество предложений о путях снять это ограничение. Это ПКП обобщает различные альтернативы которые были предложены вместе с преимуществами и недостатками, которые были упомянуты для каждого.

До версии 2.1, обработка Python областей действия напоминали стандартные C: в файле было только два уровни охвата, глобальные и локальные. В С, это естественное следствие тот факт, что определения функций не может быть вложенным. Но в Python, хотя функции обычно определяются на верхнем уровне, функция определение может быть выполнено где угодно. Это дало Python синтаксический появление вложенных областей без семантика, и уступил несоответствия, которые были удивительными некоторым программистам - например, рекурсивная функция, которая работала на верхний уровень перестал бы работать, когда перешел в другую функцию, потому что собственное имя рекурсивной функции больше не будет виден в его объем тела. Это нарушает Интуиция, что функция должна вести себя последовательно, когда помещены в разные контексты.

7 голосов
/ 06 января 2010

Функции также могут иметь атрибуты, поэтому это тоже будет работать:

def counter():
    def c():
        while True:
            yield c.count
            c.count += 1
    c.count = 0
    return c

Однако в этом конкретном примере я бы использовал генератор, предложенный jbochi.

Что касается , почему , я не могу сказать наверняка, но я предполагаю, что это не явный выбор дизайна, а скорее остаток иногда странных правил видимости Python (и особенно несколько странная эволюция своих правил определения объема).

6 голосов
/ 06 января 2010

Это поведение довольно подробно объясняется в официальном руководстве по Python , а также в модели исполнения Python . В частности, из учебника:

Особой особенностью Python является то, что - если никакое глобальное утверждение не действует - присвоения имен всегда идут в самая внутренняя сфера.

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

Некоторая дополнительная информация поступает от PEP 3104 , который пытается разрешить эту ситуацию для Python 3.0.
Там вы можете увидеть, что это так, потому что в определенный момент времени оно рассматривалось как лучшее решение вместо введения классических статических вложенных областей (см. Re: Scoping (было ли решено связывание Re: Lambda?) ).

Тем не менее, у меня также есть своя собственная интерпретация.
Python реализует пространства имен как словари; когда поиск для переменной терпит неудачу во внутренней части, тогда она пытается во внешней и так далее, пока не достигнет встроенных функций.
Тем не менее, привязка к переменной - это совершенно другой материал, потому что вам нужно указать конкретное пространство имен - оно всегда будет самым внутренним (если вы не установите флаг «global», это означает, что он всегда глобален Пространство имен).
В конце концов, различные алгоритмы, используемые для поиска и привязки переменных, являются причиной того, что замыкания будут доступны только для чтения в Python.
Но, опять же, это всего лишь мои предположения: -)

1 голос
/ 06 января 2010

Дело не в том, что они предназначены только для чтения, поскольку объем более строг, что вы понимаете. Если вы не можете nonlocal в Python 3+, то вы можете по крайней мере использовать явную область видимости. Python 2.6.1, с явной областью видимости на уровне модуля:

>>> def counter():
...     sys.modules[__name__].count = 0
...     def c():
...         sys.modules[__name__].count += 1
...         return sys.modules[__name__].count
...     sys.modules[__name__].c = c
...     
>>> counter()
>>> c()
1
>>> c()
2
>>> c()
3

Требуется немного больше работы, чтобы иметь более ограниченную область видимости для переменной count вместо использования псевдоглобальной переменной модуля (все еще Python 2.6.1):

>>> def counter():
...     class c():
...         def __init__(self):
...             self.count = 0
...     cinstance = c()
...     def iter():
...         cinstance.count += 1
...         return cinstance.count
...     return iter
... 
>>> c = counter()
>>> c()
1
>>> c()
2
>>> c()
3
>>> d = counter()
>>> d()
1
>>> c()
4
>>> d()
2
...