nonlocal słowo kluczowe w Pythonie 2.x.


117

Próbuję zaimplementować zamknięcie w Pythonie 2.6 i muszę uzyskać dostęp do zmiennej nielokalnej, ale wygląda na to, że to słowo kluczowe nie jest dostępne w Pythonie 2.x. Jak uzyskać dostęp do zmiennych nielokalnych w domknięciach w tych wersjach Pythona?

Odpowiedzi:


125

Funkcje wewnętrzne mogą odczytywać zmienne nielokalne w wersji 2.x, ale nie wiążą ich ponownie. To denerwujące, ale możesz to obejść. Po prostu utwórz słownik i przechowuj w nim swoje dane jako elementy. Wewnętrzne funkcje nie mają zakazu mutowania obiektów, do których odnoszą się zmienne nielokalne.

Aby skorzystać z przykładu z Wikipedii:

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
dlaczego można zmodyfikować wartość ze słownika?
coelhudo

9
@coelhudo Ponieważ można modyfikować zmiennych nielokalnych. Ale nie możesz przypisać do zmiennych nielokalnych. Na przykład, będzie to podnieść UnboundLocalError: def inner(): print d; d = {'y': 1}. Tutaj print dodczytuje zewnętrzną, dtworząc w ten sposób zmienną nielokalną dw zakresie wewnętrznym.
suzanshakya

21
Dzięki za tę odpowiedź. Myślę jednak, że mógłbyś ulepszyć terminologię: zamiast „umiem czytać, nie można zmienić”, może „umiesz się odwoływać, nie możesz przypisać do”. Możesz zmienić zawartość obiektu w zakresie nielokalnym, ale nie możesz zmienić obiektu, do którego się odwołuje.
metamatt

3
Alternatywnie możesz użyć dowolnej klasy i utworzyć instancję obiektu zamiast słownika. Uważam, że jest znacznie bardziej elegancki i czytelny, ponieważ nie zalewa kodu literałami (kluczami słownika), a ponadto możesz skorzystać z metod.
Alois Mahdal

6
Myślę, że w przypadku Pythona najlepiej jest użyć czasownika „bind” lub „rebind” oraz użyć rzeczownika „name” zamiast „variable”. W Pythonie 2.x możesz zamknąć obiekt, ale nie możesz ponownie powiązać nazwy w oryginalnym zakresie. W języku takim jak C zadeklarowanie zmiennej powoduje zarezerwowanie pewnej ilości miejsca (statycznego lub tymczasowego na stosie). W Pythonie wyrażenie to X = 1po prostu wiąże nazwę Xz konkretnym obiektem ( intz wartością 1). X = 1; Y = Xwiąże dwie nazwy z tym samym dokładnym obiektem. W każdym razie niektóre obiekty są zmienne i możesz zmienić ich wartości.
steveha

37

Poniższe rozwiązanie jest inspirowane odpowiedzią Eliasa Zamarii , ale w przeciwieństwie do tej odpowiedzi poprawnie obsługuje wielokrotne wywołania funkcji zewnętrznej. „Zmienna” inner.yjest lokalna dla bieżącego wywołania outer. Tyle tylko, że nie jest to zmienna, ponieważ jest to zabronione, ale atrybut obiektu (obiekt jest innersamą funkcją ). Jest to bardzo brzydkie (zauważ, że atrybut można utworzyć dopiero po innerzdefiniowaniu funkcji), ale wydaje się skuteczne.

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)

To nie musi być brzydkie. Zamiast używać inner.y, użyj external.y. Możesz zdefiniować external.y = 0 przed def inner.
jgomo3

1
Ok, widzę, że mój komentarz jest błędny. Elias Zamaria również wdrożył rozwiązanie external.y. Ale jak komentuje Nathaniel [1], należy się wystrzegać. Myślę, że ta odpowiedź powinna być promowana jako rozwiązanie, ale zwróć uwagę na rozwiązanie zewnętrzne i wnęki. [1] stackoverflow.com/questions/3190706/ ...
jgomo3

To staje się brzydkie, jeśli chcesz, aby więcej niż jedna funkcja wewnętrzna współdzieliła nielokalny zmienny stan. Powiedz a inc()i dec()zwrócone z zewnątrz, które zwiększają i zmniejszają wspólny licznik. Następnie musisz zdecydować, do której funkcji dołączyć bieżącą wartość licznika i odwołać się do tej funkcji z innej funkcji. Co wygląda nieco dziwnie i asymetrycznie. Np. W dec()linii jak inc.value -= 1.
BlackJack,

33

Zamiast słownika jest mniej bałaganu w klasie nielokalnej . Modyfikowanie przykładu @ ChrisB :

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

Następnie

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

Każde wywołanie external () tworzy nową i odrębną klasę o nazwie context (a nie tylko nową instancję). Dzięki temu unika się ostrożności @ Nathaniel na temat współdzielonego kontekstu.

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

assert f() == 5

Miły! Jest to rzeczywiście bardziej eleganckie i czytelniejsze niż słownik.
Vincenzo

Generalnie polecam tutaj używanie automatów. Chroni Cię przed literówkami.
DerWeh

@DerCo ciekawe, nigdy o tym nie słyszałem . Czy możesz powiedzieć to samo o każdej klasie? Nienawidzę być zakłopotany literówkami.
Bob Stein

@BobStein Przepraszamy, naprawdę nie rozumiem, co masz na myśli. Ale dodając __slots__ = ()i tworząc obiekt zamiast używać klasy, na przykład context.z = 3podniósłby plik AttributeError. Jest to możliwe dla wszystkich klas, chyba że dziedziczą one po klasie nie definiującej slotów.
DerWeh

@DerWeh Nigdy nie słyszałem o używaniu automatów do wyłapywania literówek. Masz rację, to pomogłoby tylko w instancjach klas, a nie w zmiennych klasowych. Może chciałbyś stworzyć działający przykład na pyfiddle. Wymagałoby to co najmniej dwóch dodatkowych linii. Nie mogę sobie wyobrazić, jak byś to zrobił bez większego bałaganu.
Bob Stein

14

Myślę, że kluczem jest tutaj to, co rozumiesz przez „dostęp”. Nie powinno być problemu z odczytem zmiennej spoza zakresu zamknięcia, np.

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

powinien działać zgodnie z oczekiwaniami (drukowanie 3). Jednak przesłanianie wartości x nie działa, np.

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

będzie nadal drukować 3. Z mojego zrozumienia PEP-3104 właśnie to ma obejmować słowo kluczowe nonlocal. Jak wspomniano w PEP, możesz użyć klasy, aby osiągnąć to samo (trochę bałagan):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x

Zamiast tworzyć klasę i tworzyć jej instancję, można po prostu utworzyć funkcję: def ns(): passpo której następuje ns.x = 3. Nie jest ładny, ale dla mojego oka jest nieco mniej brzydki.
davidchambers

2
Czy jest jakiś szczególny powód, by głosować przeciw? Przyznaję, że moje rozwiązanie nie jest najbardziej eleganckie, ale działa ...
ig0774

2
O co chodzi class Namespace: x = 3?
Feuermurmel

3
Nie sądzę, aby w ten sposób symulowano nielokalne, ponieważ tworzy globalne odniesienie zamiast odniesienia w zamknięciu.
CarmeloS

1
Zgadzam się z @CarmeloS, twój ostatni blok kodu, który nie robi tego, co sugeruje PEP-3104 jako sposób na osiągnięcie czegoś podobnego w Pythonie 2. nsjest obiektem globalnym, dlatego możesz odwołać się ns.xna poziomie modułu w printinstrukcji na samym końcu .
martineau

13

Istnieje inny sposób implementacji zmiennych nielokalnych w Pythonie 2, na wypadek gdyby którakolwiek z odpowiedzi tutaj była niepożądana z jakiegokolwiek powodu:

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

Używanie nazwy funkcji w instrukcji przypisania zmiennej jest zbędne, ale wydaje mi się to prostsze i czystsze niż umieszczenie zmiennej w słowniku. Wartość jest zapamiętywana z jednego wezwania do drugiego, tak jak w odpowiedzi Chrisa B.


19
Uwaga: jeśli zaimplementujesz w ten sposób, f = outer()a później to zrobisz g = outer(), flicznik zostanie zresetowany. To dlatego, że oboje mają wspólny pojedyncza outer.y zmienna, a nie każdy posiada własną niezależną jeden. Chociaż ten kod wygląda bardziej estetycznie niż odpowiedź Chrisa B, jego sposób wydaje się być jedynym sposobem na emulację leksykalnego zakresu, jeśli chcesz wywołać outerwięcej niż jeden raz.
Nathaniel

@Nathaniel: Zobaczmy, czy dobrze to rozumiem. Przypisanie do outer.ynie obejmuje niczego lokalnego dla wywołania funkcji (instancji) outer(), ale przypisuje atrybut obiektu funkcji, który jest powiązany z nazwą outerw otaczającym ją zakresie. A zatem można by równie dobrze wykorzystane, na piśmie outer.y, żadnego innego imienia, a nie outer, pod warunkiem, że wiadomo, że jest związany w tym zakresie. Czy to jest poprawne?
Marc van Leeuwen

1
Powinienem był powiedzieć, że po „zawiązaniu w tym zakresie”: do obiektu, którego typ umożliwia ustawienie atrybutów (jak funkcja lub dowolna instancja klasy). Ponadto, ponieważ ten zakres jest w rzeczywistości dalej niż ten, którego chcemy, nie sugerowałoby to następującego wyjątkowo brzydkiego rozwiązania: zamiast outer.yużyć nazwy inner.y(ponieważ innerjest ona związana wewnątrz wywołania outer(), co jest dokładnie takim zakresem, jaki chcemy), ale umieszczając inicjalizacja inner.y = 0 po zdefiniowaniu inner (bo obiekt musi istnieć, gdy tworzony jest atrybut), ale oczywiście przed return inner?
Marc van Leeuwen

@MarcvanLeeuwen Wygląda na to, że Twój komentarz zainspirował rozwiązanie z inner.y autorstwa Eliasa. Dobra myśl.
Ankur Agarwal,

Uzyskiwanie dostępu do zmiennych globalnych i ich aktualizowanie prawdopodobnie nie jest tym, co większość ludzi próbuje zrobić, modyfikując przechwycenia zamknięcia.
binki

12

Oto coś zainspirowanego sugestią, którą Alois Mahdal poczynił w komentarzu dotyczącym innej odpowiedzi :

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)

Aktualizacja

Kiedy ostatnio spojrzałem na to wstecz, uderzyło mnie to, jak wygląda dekorator - kiedy dotarło do mnie, że wdrożenie go w taki sposób, aby było bardziej ogólne i użyteczne (chociaż prawdopodobnie w pewnym stopniu obniża to jego czytelność).

# 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)

Zauważ, że obie wersje działają zarówno w Pythonie 2, jak i 3.


Ten wydaje się być najlepszy pod względem czytelności i zachowania zakresu leksykalnego.
Aaron S. Kurland

3

W regułach określania zakresu Pythona jest brodawka - przypisanie powoduje, że zmienna jest lokalna dla jej bezpośrednio obejmującego zakres funkcji. W przypadku zmiennej globalnej można to rozwiązać za pomocą globalsłowa kluczowego.

Rozwiązaniem jest wprowadzenie obiektu, który jest współdzielony między dwoma zakresami, który zawiera zmienne modyfikowalne, ale do niego odwołuje się zmienna, która nie jest przypisana.

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

Alternatywą jest hakowanie niektórych zakresów:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

Być może będziesz w stanie wymyślić jakąś sztuczkę, aby uzyskać nazwę parametru outer, a następnie przekazać ją jako nazwę zmiennej, ale bez polegania na nazwie, outerktórej chciałbyś użyć kombinatora Y.


Obie wersje działają w tym konkretnym przypadku, ale nie zastępują nonlocal. locals()tworzy słownik outer()s locals w tym czasie inner()jest zdefiniowany, ale zmiana tego słownika nie zmienia vin outer(). To już nie zadziała, gdy masz więcej funkcji wewnętrznych, które chcą udostępniać zmienną zamkniętą. Powiedz a, inc()a dec()zwiększaj i zmniejszaj wspólny licznik.
BlackJack

@BlackJack nonlocalto funkcja Pythona 3.
Marcin

Tak, wiem. Pytanie brzmiało, jak ogólnie osiągnąć efekt Pythona 3 nonlocalw Pythonie 2 . Twoje pomysły nie obejmują przypadku ogólnego, ale tylko ten z jedną funkcją wewnętrzną. Spójrz na tę istotę jako przykład. Obie funkcje wewnętrzne mają swój własny kontener. Potrzebujesz zmiennego obiektu w zakresie funkcji zewnętrznej, jak sugerowały już inne odpowiedzi.
BlackJack

@BlackJack To nie jest pytanie, ale dziękuję za Twój wkład.
Marcin

Tak, oto jest pytanie. Spójrz na górę strony. adinsa ma Pythona 2.6 i potrzebuje dostępu do zmiennych nielokalnych w zamknięciu bez nonlocalsłowa kluczowego wprowadzonego w Pythonie 3.
BlackJack

3

Inny sposób, aby to zrobić (chociaż jest zbyt szczegółowy):

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
właściwie wolę rozwiązanie z użyciem słownika, ale ten jest fajny :)
Ezer Fernandes

0

Rozszerzając eleganckie rozwiązanie Martineau powyżej o praktyczny i nieco mniej elegancki przypadek użycia otrzymuję:

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

Użyj zmiennej globalnej

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

Osobiście nie lubię zmiennych globalnych. Ale moja propozycja jest oparta na odpowiedzi https://stackoverflow.com/a/19877437/1083704

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

gdzie użytkownik musi zadeklarować zmienną globalną ranks, za każdym razem, gdy musisz wywołać metodę report. Moje ulepszenie eliminuje potrzebę inicjowania zmiennych funkcji od użytkownika.


O wiele lepiej jest używać słownika. Możesz odwoływać się do instancji w programie inner, ale nie możesz jej przypisywać, ale możesz modyfikować jej klucze i wartości. Pozwala to uniknąć używania zmiennych globalnych.
johannestaas
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.