Четене/запис на затваряния на 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
Последните редакции на Ethan, макар и технически по-точни от оригиналната формулировка, правят този въпрос значително по-малко достъпен за средния програмист. Наистина бих искал да ги върна, но бих искал второ мнение.   -  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
Осъзнавам, че това не е най-добрият пример, който можех да използвам, просто изглеждаше като най-простият пример за писане. Това е точно това, което търсих (заедно с причината, поради която не са добавили „nonlocal“ преди години). Примерът за противопоставяне беше само част от код за изхвърляне, за да се разбере идеята. Благодаря ви, че казахте това изрично. - 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-те, в разцвета на OO. Затварянията бяха доста ниско в списъка с езикови функции, които хората искаха. Тъй като функционални идеи като първокласни функции, затваряния и други неща си проправят път към масовата популярност, езици като 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 сигнализира на интерпретатора да създаде thunk за тази функция? Знаете ли как става това? - person Benson; 06.01.2010
comment
Не знам всички подробности, но nonlocal трябва да посочи на компилатора, че ще трябва да обходи обхватите, за да намери името. - person Ignacio Vazquez-Abrams; 06.01.2010
comment
вярно AFAIK, единственият начин това да работи е по същество да се опакова целият обхват на обхващащата функция, преди да бъде GC'd в thunk. - 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

РЕДАКТИРАНЕ: Вярвам, че крайният отговор на вашия въпрос е 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: Обхват (решено ли е Re: Lambda обвързване?)).

Въпреки това, аз също имам своя собствена интерпретация.
Python имплементира пространствата от имена като речници; когато търсене за променлива е неуспешно във вътрешния, след това се опитва във външния и така нататък, докато стигне до вградените.
Въпреки това, свързването на променлива е напълно различни неща, защото трябва да посочите определено пространство от имена - че то винаги е най-вътрешното (освен ако не зададете флага "global", това означава, че винаги е глобалното пространство от имена).
В крайна сметка различните използвани алгоритми за търсене и обвързване на променливи са причината затварянията да бъдат само за четене в Python.
Но, отново, това е само моя спекулация :-)

person rob    schedule 05.01.2010
comment
Интересни идеи. От моя гледна точка най-логичното нещо, което трябва да направите, когато имате lvalue, на която се присвоява, е първо да го потърсите в таблицата със символи. Ако съществува, използвайте го, ако не, създайте го в най-вътрешния обхват. Но може би това съм само аз...? - person Benson; 06.01.2010
comment
Напълно съм съгласен с вас и бях ухапан няколко пъти от подобно предположение в Python. Поради тази причина намерих много полезно да разсъждавам по отношение на речниците: веднага щом видите модела на паметта по този начин, нещата стават много по-ясни. - person rob; 06.01.2010
comment
Знам, че е късно, но исках да добавя, че pep 3104 се занимава с тази идея и защо не го използвате. Освен това попаднах в този SO въпрос, защото четях 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

Необходима е малко повече работа, за да имате по-ограничен обхват за променливата за броене, вместо да използвате променлива на псевдоглобален модул (все още 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 за предаване на param на затваряне.

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