parola chiave non locale in Python 2.x


117

Sto cercando di implementare una chiusura in Python 2.6 e ho bisogno di accedere a una variabile non locale ma sembra che questa parola chiave non sia disponibile in python 2.x. Come si accede alle variabili non locali nelle chiusure in queste versioni di python?

Risposte:


125

Le funzioni interne possono leggere variabili non locali in 2.x, ma non riassociarle . Questo è fastidioso, ma puoi aggirarlo. Basta creare un dizionario e memorizzare i dati come elementi al suo interno. Funzioni interne non sono vietate dalla mutando gli oggetti che le variabili non locali riferiscono.

Per utilizzare l'esempio di Wikipedia:

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
perché è possibile modificare il valore dal dizionario?
coelhudo

9
@coelhudo Perché puoi modificare le variabili non locali. Ma non puoi eseguire assegnazioni a variabili non locali. Ad esempio, tale da far salire UnboundLocalError: def inner(): print d; d = {'y': 1}. Qui, print dlegge esterno dcreando così una variabile non locale dnell'ambito interno.
suzanshakya

21
Grazie per questa risposta. Penso che potresti migliorare la terminologia: invece di "può leggere, non può cambiare", forse "può fare riferimento, non può assegnare a". È possibile modificare il contenuto di un oggetto in un ambito non locale, ma non è possibile modificare l'oggetto a cui si fa riferimento.
metamatt

3
In alternativa, è possibile utilizzare una classe arbitraria e istanziare un oggetto invece del dizionario. Lo trovo molto più elegante e leggibile poiché non inonda il codice di letterali (chiavi del dizionario), inoltre puoi utilizzare metodi.
Alois Mahdal

6
Per Python penso che sia meglio usare il verbo "bind" o "rebind" e usare il sostantivo "nome" piuttosto che "variabile". In Python 2.x, puoi chiudere su un oggetto ma non puoi riassociare il nome nello scope originale. In un linguaggio come il C, la dichiarazione di una variabile riserva dello spazio di archiviazione (sia statico che temporaneo nello stack). In Python, l'espressione X = 1lega semplicemente il nome Xa un oggetto particolare ( inte al valore 1). X = 1; Y = Xlega due nomi allo stesso identico oggetto. Ad ogni modo, alcuni oggetti sono mutabili e puoi cambiare i loro valori.
steveha

37

La seguente soluzione si ispira alla risposta di Elias Zamaria , ma contrariamente a quella risposta gestisce correttamente più chiamate della funzione esterna. La "variabile"inner.y è locale alla chiamata corrente di outer. Solo che non è una variabile, dato che è proibito, ma un attributo di oggetto (l'oggetto è la funzione innerstessa). Questo è molto brutto (nota che l'attributo può essere creato solo dopo che la innerfunzione è stata definita) ma sembra efficace.

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)

Non deve essere brutto. Invece di usare inner.y, usa outer.y. È possibile definire outer.y = 0 prima di def inner.
jgomo3

1
Ok, vedo che il mio commento è sbagliato. Elias Zamaria ha implementato anche la soluzione outer.y. Ma come commentato da Nathaniel [1], dovresti stare attento. Penso che questa risposta dovrebbe essere promossa come la soluzione, ma nota sulla soluzione outer.y e cavetti. [1] stackoverflow.com/questions/3190706/...
jgomo3

Questo diventa brutto se si desidera che più di una funzione interna condivida uno stato mutabile non locale. Pronuncia un inc()e un dec()restituito dall'esterno che incrementa e decrementa un contatore condiviso. Quindi devi decidere a quale funzione allegare il valore del contatore corrente e fare riferimento a quella funzione dall'altra (e). Che sembra un po 'strano e asimmetrico. Ad esempio in dec()una riga come inc.value -= 1.
BlackJack

33

Piuttosto che un dizionario, c'è meno confusione in una classe non locale . La modifica di @ ChrisB esempio :

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

Poi

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

Ogni chiamata outer () crea una nuova e distinta classe chiamata context (non semplicemente una nuova istanza). Quindi evita che @ Nathaniel faccia attenzione al contesto condiviso.

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

assert f() == 5

Bello! Questo è davvero più elegante e leggibile del dizionario.
Vincenzo

Consiglierei di usare le slot in generale qui. Ti protegge dagli errori di battitura.
DerWeh

@DerWeh interessante, non ne ho mai sentito parlare . Potresti dire lo stesso di qualsiasi lezione? Odio essere sconcertato dagli errori di battitura.
Bob Stein

@BobStein Scusa, non capisco davvero cosa intendi. Ma aggiungendo __slots__ = ()e creando un oggetto invece di usare la classe, ad esempio, context.z = 3si solleva un file AttributeError. È possibile per tutte le classi, a meno che non ereditino da una classe che non definisce gli slot.
DerWeh

@DerWeh Non ho mai sentito parlare di usare gli slot per rilevare errori di battitura. Hai ragione, aiuterebbe solo nelle istanze di classe, non nelle variabili di classe. Forse vorresti creare un esempio funzionante su pyfiddle. Richiederebbero almeno due linee aggiuntive. Non riesco a immaginare come lo faresti senza più confusione.
Bob Stein

14

Penso che la chiave qui sia cosa intendi per "accesso". Non dovrebbero esserci problemi con la lettura di una variabile al di fuori dell'ambito di chiusura, ad es.

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

dovrebbe funzionare come previsto (stampa 3). Tuttavia, l'override del valore di x non funziona, ad es.

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

stamperà ancora 3. Dalla mia comprensione di PEP-3104 questo è ciò che la parola chiave non locale intende coprire. Come accennato nel PEP, puoi usare una classe per ottenere la stessa cosa (un po 'disordinato):

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

Invece di creare una classe e istanziarla, si può semplicemente creare una funzione: def ns(): passseguita da ns.x = 3. Non è carino, ma è leggermente meno brutto ai miei occhi.
davidchambers

2
Qualche motivo particolare per il voto negativo? Ammetto che la mia non sia la soluzione più elegante, ma funziona ...
ig0774

2
Di cosa class Namespace: x = 3?
Feuermurmel

3
Non penso che in questo modo simuli non locale, poiché crea un riferimento globale invece di un riferimento in una chiusura.
CarmeloS

1
Sono d'accordo con @CarmeloS, il tuo ultimo blocco di codice nel non fare ciò che PEP-3104 suggerisce come modo per realizzare qualcosa di simile in Python 2.è nsun oggetto globale ed è per questo che puoi fare riferimento ns.xa livello di modulo printnell'istruzione alla fine .
martineau

13

C'è un altro modo per implementare variabili non locali in Python 2, nel caso in cui una qualsiasi delle risposte qui non sia desiderabile per qualsiasi motivo:

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

È ridondante usare il nome della funzione nell'istruzione di assegnazione della variabile, ma mi sembra più semplice e pulito che mettere la variabile in un dizionario. Il valore viene ricordato da una chiamata all'altra, proprio come nella risposta di Chris B.


19
Attenzione: se implementato in questo modo, se lo fai f = outer()e successivamente lo fai g = outer(), fil contatore di sarà azzerato. Questo perché entrambi condividono una singola outer.y variabile, invece di avere ciascuno la propria variabile indipendente. Sebbene questo codice sembri esteticamente più gradevole della risposta di Chris B, il suo modo sembra essere l'unico modo per emulare lo scoping lessicale se si desidera chiamare outerpiù di una volta.
Nathaniel

@ Nathaniel: Fammi vedere se ho capito bene. L'assegnazione a outer.ynon coinvolge nulla di locale alla chiamata di funzione (istanza) outer(), ma assegna a un attributo dell'oggetto funzione che è associato al nome outernel suo ambito di inclusione . E quindi si potrebbe altrettanto bene usare, per iscritto outer.y, qualsiasi altro nome invece di outer, purché si sappia che è vincolato a tale scopo. È corretto?
Marc van Leeuwen

1
Correzione, avrei dovuto dire, dopo "vincolato in tale ambito": a un oggetto il cui tipo consente di impostare attributi (come una funzione o qualsiasi istanza di classe). Inoltre, poiché questo ambito è effettivamente più lontano di quello che vogliamo, questo non suggerirebbe la seguente soluzione estremamente brutta: invece di outer.yusare il nome inner.y(poiché innerè vincolato all'interno della chiamataouter() , che è esattamente l'ambito che vogliamo), ma mettere l'inizializzazione inner.y = 0 dopo la definizione di interno (poiché l'oggetto deve esistere quando viene creato il suo attributo), ma ovviamente prima return inner?
Marc van Leeuwen

@MarcvanLeeuwen Sembra che il tuo commento abbia ispirato la soluzione con inner.y di Elias. Bel pensiero da parte tua.
Ankur Agarwal

L'accesso e l'aggiornamento delle variabili globali probabilmente non è ciò che la maggior parte delle persone cerca di fare quando si modificano le acquisizioni di una chiusura.
binki

12

Ecco qualcosa ispirato da un suggerimento di Alois Mahdal in un commento riguardante un'altra risposta :

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)

Aggiornare

Dopo aver ripensato a questo di recente, sono rimasto colpito da quanto fosse simile a un decoratore - quando mi sono reso conto che implementarlo come se fosse uno lo avrebbe reso più generico e utile (anche se così facendo degrada in una certa misura la sua leggibilità).

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

Nota che entrambe le versioni funzionano sia in Python 2 che in 3.


Questo sembra essere il migliore in termini di leggibilità e conservazione dell'ambito lessicale.
Aaron S. Kurland

3

C'è una verruca nelle regole di scoping di Python: l'assegnazione rende una variabile locale al suo ambito di funzione che lo racchiude immediatamente. Per una variabile globale, risolveresti questo problema con la globalparola chiave.

La soluzione è introdurre un oggetto condiviso tra i due ambiti, che contenga variabili mutabili, ma a cui si faccia riferimento tramite una variabile non assegnata.

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

Un'alternativa è rappresentata dall'hackery di alcuni ambiti:

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

Potresti essere in grado di escogitare qualche trucco per ottenere il nome del parametro outere quindi passarlo come varname, ma senza fare affidamento sul nome outervorresti che fosse necessario utilizzare un combinatore Y.


Entrambe le versioni funzionano in questo caso particolare ma non sostituiscono nonlocal. locals()crea un dizionario di outer()s locals al momento in cui inner()è definito ma la modifica di quel dizionario non cambia vin outer(). Questo non funzionerà più quando hai più funzioni interne che vogliono condividere una variabile chiusa. Pronuncia un inc()e dec()quell'incrementa e decrementa un contatore condiviso.
BlackJack

@BlackJack nonlocalè una funzionalità di Python 3.
Marcin

Si, lo so. La domanda era come ottenere l'effetto di Python 3 nonlocalin Python 2 in generale . Le tue idee non coprono il caso generale ma solo quello con una funzione interna. Dai un'occhiata a questa sintesi per un esempio. Entrambe le funzioni interne hanno il proprio contenitore. Hai bisogno di un oggetto mutabile nell'ambito della funzione esterna, come già suggerito da altre risposte.
BlackJack

@ BlackJack Non è questa la domanda, ma grazie per il tuo contributo.
Marcin

Sì, questa è la domanda. Dai uno sguardo all'inizio della pagina. adinsa ha Python 2.6 e necessita dell'accesso a variabili non locali in una chiusura senza la nonlocalparola chiave introdotta in Python 3.
BlackJack

3

Un altro modo per farlo (anche se è troppo prolisso):

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
in realtà preferisco la soluzione usando un dizionario, ma questo è fantastico :)
Ezer Fernandes

0

Estendendo l'elegante soluzione di Martineau sopra a un caso d'uso pratico e un po 'meno elegante ottengo:

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

Usa una variabile globale

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

Personalmente non mi piacciono le variabili globali. Ma la mia proposta si basa sulla risposta https://stackoverflow.com/a/19877437/1083704

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

dove l'utente deve dichiarare una variabile globale ranks, ogni volta che è necessario chiamare il file report. Il mio miglioramento elimina la necessità di inizializzare le variabili di funzione dall'utente.


È molto meglio usare un dizionario. Puoi fare riferimento all'istanza in inner, ma non puoi assegnarle, ma puoi modificarne le chiavi e i valori. Ciò evita l'uso di variabili globali.
johannestaas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.