Подтвердить что ты не робот

Чтение/запись закрытий на Python

Закрытие - невероятно полезная языковая функция. Они позволяют нам делать умные вещи, которые в противном случае занимали бы много кода, и часто позволяют нам писать более элегантный и понятный код. В 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
4b9b3361

Ответ 1

Чтобы развернуть ответ Ignacio:

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, более идиоматичны.

Ответ 2

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

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)

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

EDIT: Это то, что я получаю от того, что не читаю большой блог с текстом.

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

<rant on="Python scoping">

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

new = [x for x in old]

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

Во всяком случае </rant>

Ответ 3

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

Ответ 4

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

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

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

EDIT. Я считаю, что окончательный ответ на ваш вопрос PEP-3104:

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

Это не то, что они доступны только для чтения, так как объем более строгий, чем вы понимаете. Если вы не можете 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