Чтение/запись замыканий 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

person Benson    schedule 05.01.2010    source источник
comment
Ну, в данном случае itertools.count(1).next. По моему опыту, нелегко найти реальные случаи, когда идиоматический Python не такой чистый или элегантный, как Perl/JS/Scheme, использующий замыкания.   -  person Jason Orendorff    schedule 06.01.2010
comment
то, что вы на самом деле ищете, называется генераторами   -  person    schedule 06.01.2010
comment
Я выбрал этот пример кода, потому что он был простым, а не потому, что я действительно хотел использовать его в рабочей среде. Я постараюсь вспомнить хороший производственный пример и отредактировать его. Очевидно, мой выбор примера кода сбил людей с толку.   -  person Benson    schedule 06.01.2010
comment
@fuzzy-lollipop Это заведомо неверно. Я знаю, что такое генераторы, и часто ими пользуюсь. Генераторы отличные. Однако генераторы не являются замыканиями для чтения/записи.   -  person Benson    schedule 06.01.2010
comment
Корутины кажутся такими.   -  person Robert Rossney    schedule 06.01.2010
comment
Недавние правки Итана, хотя технически более точные, чем первоначальная формулировка, делают этот вопрос значительно менее доступным для среднего программиста. Я бы очень хотел откатить их назад, но я хотел бы услышать второе мнение.   -  person Benson    schedule 27.09.2011


Ответы (8)


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

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

person sdcvvc    schedule 05.01.2010
comment
Я понимаю, что это не лучший пример, который я мог бы использовать, просто мне показалось, что это самый простой пример для написания. Это именно то, что я искал (вместе с причиной, по которой они не добавили «нелокальный» много лет назад). Контрпример был просто фрагментом одноразового кода, чтобы донести суть. Спасибо, что сделали это явным. - person Benson; 06.01.2010
comment
@northtree Оператор nonlocal просто влияет на область видимости, но не влияет на потокобезопасность. Оператор count += 1 приведет к неправильным результатам, если его одновременно выполняют несколько потоков; его нужно выполнить в замке. Однако это ортогонально вопросу. Это зависит от варианта использования, должна ли блокировка присутствовать внутри c или это ответственность вызывающей стороны. - person sdcvvc; 28.03.2020

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

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

<rant on="Python scoping">

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

new = [x for x in old]

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

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

person Chris Lutz    schedule 05.01.2010
comment
Вся очень хорошая информация, и я ценю понимание. Я также согласен с вашей оценкой области действия Python. Тем не менее, вопрос был в том, почему я не могу?, а не как я могу?. Я хочу знать, почему язык был разработан именно так. - person Benson; 06.01.2010
comment
Отличный хак. Использовал его в python 2.7 и мне это нравится (хотя мне не нравится, что я должен это делать). - person Jason Webb; 13.12.2010

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

person Ignacio Vazquez-Abrams    schedule 05.01.2010
comment
Это фантастическая новость. Я буду смотреть в него. Означает ли это, что nonlocal сигнализирует интерпретатору создать преобразователь для этой функции? Вы знаете, как это работает? - person Benson; 06.01.2010
comment
Я не знаю всех подробностей, но nonlocal должен указать компилятору, что ему нужно будет пройтись по области видимости, чтобы найти имя. - person Ignacio Vazquez-Abrams; 06.01.2010
comment
Правильно. Насколько мне известно, единственный способ заставить это работать — по существу упаковать всю область действия охватывающей функции до того, как она будет GC'ирована в преобразователь. - person Benson; 06.01.2010

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

>>> 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 синтаксический вид вложенной области видимости без семантики и привело к несоответствиям, которые удивили некоторых программистов — например, рекурсивная функция, которая работала на верхнем уровне, переставала работать при перемещении внутрь другой функции, потому что рекурсивная функция собственное имя больше не будет отображаться в области его тела. Это нарушает интуитивное представление о том, что функция должна вести себя последовательно, когда ее помещают в разные контексты.

person jbochi    schedule 05.01.2010
comment
+1 Я был слишком сосредоточен на желаемом синтаксисе использования, что забыл о генераторах. Всегда хорошо. - person Chris Lutz; 06.01.2010
comment
Это хороший ответ, но это не ответ на вопросы, которые я задавал. Я хочу знать, почему Python разработан именно так, а не как его обойти. Я мог бы сделать это с помощью класса, с помощью генераторов (которые просто создают экземпляр класса, так что это одно и то же) или, возможно, другими способами. Но меня не волнует встречный экземпляр, я хочу знать, почему язык был разработан таким образом. - person Benson; 06.01.2010
comment
Python 2.x считает, что вы объявляете новую переменную, если она не находится в текущей области. Вот почему Python 3.0 представил ключевое слово nonlocal для обхода этой проблемы. - person jbochi; 06.01.2010
comment
Опять же, python думает, что вы объявляете новую переменную «что», а не «почему». Я понял это из сообщения об ошибке. :-) Тем не менее, я ценю понимание. - person Benson; 06.01.2010

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

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

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

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

person mipadi    schedule 05.01.2010
comment
+1 за способ сделать это, о котором я раньше не знал (и за согласие с бессмысленными правилами области видимости Python). - person Chris Lutz; 06.01.2010
comment
Это интересная идея, но фактически это просто объявление класса. На самом деле, это очень похоже на классы в стиле javascript. Но спасибо за понимание; Я не знал, что к функциям можно добавлять произвольные атрибуты. - person Benson; 06.01.2010
comment
Да вообще класс. И я обнаружил, что когда у вас возникает желание использовать атрибуты функций, вы должны либо (а) использовать генератор (как в этом примере), либо (б) использовать класс. Но я подумал, что стоит указать, что атрибуты функций действительно существуют и могут быть использованы в качестве решения вашей проблемы (хотя я согласен, что они довольно уродливы) . - person mipadi; 06.01.2010
comment
у вас есть семантическая ошибка в вашей программе: атрибут count должен применяться к внутренней функции c, а не к внешней функции (которая возвращает отдельные экземпляры внутренних функций) - person Jason S; 08.04.2011

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

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

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

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

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

person rob    schedule 05.01.2010
comment
Интересные идеи. С моей точки зрения, самое логичное, что нужно сделать, когда у вас есть lvalue, которому присваивается значение, — это сначала найти его в таблице символов. Если он существует, используйте его, если нет, создайте его в самой внутренней области видимости. Но может это только я...? - person Benson; 06.01.2010
comment
Я полностью с вами согласен, и меня несколько раз укусило подобное предположение в Python. По этой причине я нашел очень полезным рассуждать в терминах словарей: как только вы видите модель памяти таким образом, все становится намного яснее. - person rob; 06.01.2010
comment
Я знаю, что уже поздно, но я хотел добавить, что pep 3104 занимается этой идеей, и почему бы не использовать его. Кроме того, я попал в этот ТАК вопрос, потому что я читал PEP и хотел получить определение/объяснение для classic static nested scope каких-либо подсказок? - person Augusto Hack; 15.02.2013
comment
+1 :) Это был единственный ответ, который я нашел, пытаясь объяснить отсутствие полных замыканий первого класса в Python 2.x. - person James Mills; 14.06.2014

Дело не в том, что они доступны только для чтения, поскольку область действия более строгая, чем вы понимаете. Если вы не можете 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
person Caleb Hattingh    schedule 06.01.2010
comment
Оказывается, невозможность влиять на вещи в нелокальной неглобальной области видимости — это то же самое, что и доступ только для чтения. Явная область видимости на уровне модуля просто дает вам другой вид глобального, он не дает вам новый экземпляр для каждого выполнения функции counter(). - person Benson; 09.01.2010
comment
Уступил. Пожалуйста, смотрите мой новый дополнительный пример выше. Я пытаюсь выяснить, что вы сказали, что хотели бы достичь в своем первоначальном вопросе. - person Caleb Hattingh; 09.01.2010
comment
Теперь мне приходит в голову, что мой новый пример, использующий внутренний cinstance для хранения состояния count, почти идентичен вашему первому примеру в вопросе, за исключением того, что count теперь поддерживается внутри экземпляра. Для каждого вызова counter() существует отдельный экземпляр, что вы и хотели изначально. Что меня озадачивает, так это то, почему внутренняя переменная экземпляра cinstance обрабатывается иначе в отношении области видимости, чем исходная count в вашем первом примере; как вы можете проверить сами, приведенный выше код не создает UnboundLocalError на cinstance. - person Caleb Hattingh; 09.01.2010

Чтобы расширить ответ sdcvvc для передачи параметра в закрытие.

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

x = counter()
print([x(), x(100), x(-99)])

Поточно-безопасная версия:

import threading

def counter():
    count = 0
    _lock = threading.Lock()
    def c(delta=1):
        nonlocal count
        with _lock:
            count += delta
            return count
    return c
person northtree    schedule 20.03.2020