È possibile modificare la variabile in Python che è in ambito esterno, ma non globale?


108

Dato il seguente codice:

def A() :
    b = 1

    def B() :
        # I can access 'b' from here.
        print( b )
        # But can i modify 'b' here? 'global' and assignment will not work.

    B()
A()

Per il codice nella B()variabile funzione bè in ambito esterno, ma non in ambito globale. È possibile modificare la bvariabile dall'interno della B()funzione? Sicuramente posso leggerlo da qui e print(), ma come modificarlo?


Scusa, ovviamente 2.7 :). Per le regole di scoping di python 3 sono cambiate.
grigoryvp

Puoi finché bè mutevole. Un incarico a bmaschererà l'ambito esterno.
JimB

4
È uno degli imbarazzi di Python che nonlocalnon è stato trasferito a 2.x. È una parte intrinseca del supporto di chiusura.
Glenn Maynard

Risposte:


98

Python 3.x ha la nonlocalparola chiave . Penso che questo faccia quello che vuoi, ma non sono sicuro che tu stia eseguendo python 2 o 3.

L'istruzione nonlocal fa in modo che gli identificatori elencati facciano riferimento a variabili associate in precedenza nell'ambito più vicino. Questo è importante perché il comportamento predefinito per l'associazione consiste nella ricerca prima nello spazio dei nomi locale. L'istruzione consente al codice incapsulato di riassociare le variabili al di fuori dell'ambito locale oltre all'ambito globale (modulo).

Per python 2, di solito uso solo un oggetto modificabile (come un elenco o dict) e cambio il valore invece di riassegnare.

esempio:

def foo():
    a = []
    def bar():
        a.append(1)
    bar()
    bar()
    print a

foo()

Uscite:

[1, 1]

16
Un bel modo per farlo è class nonlocal: passnello scopo esterno. Quindi nonlocal.xpuò essere assegnato nell'ambito interno.
kindall

1
Fino ad ora, ho già due suggerimenti di Python semplici, ma molto utili: il tuo è il secondo :) Grazie @kindall!
swdev

@kindall è un ottimo trucco: è leggermente diverso dalla sintassi di Python 3 e molto più leggibile rispetto al passaggio di un oggetto mutabile.
dimo414

2
@kindall molto pulito grazie cumuli :) probabilmente ha bisogno di un nome diverso però perché interrompe la compatibilità. In python 3 è un conflitto di parole chiave e causerà un file SyntaxError. Forse NonLocal?
Adam Terrey

o, dal momento che è tecnicamente una classe Nonlocal,? :-)
kindall

19

È possibile utilizzare una classe vuota per contenere un ambito temporaneo. È come il mutevole ma un po 'più carino.

def outer_fn():
   class FnScope:
     b = 5
     c = 6
   def inner_fn():
      FnScope.b += 1
      FnScope.c += FnScope.b
   inner_fn()
   inner_fn()
   inner_fn()

Questo produce il seguente output interattivo:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined

È strano che la classe con i suoi campi sia "visibile" in una funzione interna ma le variabili no, a meno che tu non definisca la variabile esterna con la parola chiave "nonlocal".
Celdor

12

Sono un po 'nuovo in Python, ma ho letto un po' di questo. Credo che il meglio che otterrai sia simile alla soluzione Java, che è racchiudere la tua variabile esterna in un elenco.

def A():
   b = [1]
   def B():
      b[0] = 2
   B()
   print(b[0])

# The output is '2'

Modifica: immagino che questo fosse probabilmente vero prima di Python 3. Sembra che nonlocalsia la tua risposta.


4

No non puoi, almeno in questo modo.

Perché "l'operazione set" creerà un nuovo nome nell'ambito corrente, che copre quello esterno.


"che coprono quello esterno" Cosa intendi? La definizione di un oggetto con il nome b in una funzione annidata non ha alcuna influenza su un oggetto con lo stesso nome nello spazio esterno di questa funzione
eyquem

1
@eyquem cioè, ovunque sia l'istruzione di assegnazione, introdurrà il nome nell'intero ambito corrente. Come il codice di esempio della domanda, se è: def C (): print (b) b = 2 "b = 2" introdurrà il nome b nell'intero ambito della funzione C, quindi quando print (b), lo farà prova a ottenere b nello scope della funzione C locale ma non in quello esterno, il locale b non è ancora stato inizializzato, quindi ci sarà un errore.
zchenah

1

Per chiunque guardi a questo molto più tardi una soluzione alternativa più sicura ma più pesante è. Senza la necessità di passare variabili come parametri.

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

1

La risposta breve che funzionerà automaticamente

Ho creato una libreria python per risolvere questo problema specifico. Viene rilasciato per mancanza di licenza, quindi usalo come preferisci. Puoi installarlo con pip install seapieo controllare la home page qui https://github.com/hirsimaki-markus/SEAPIE

user@pc:home$ pip install seapie

from seapie import Seapie as seapie
def A():
    b = 1

    def B():
        seapie(1, "b=2")
        print(b)

    B()
A()

uscite

2

gli argomenti hanno il seguente significato:

  • Il primo argomento è l'ambito di esecuzione. 0 significherebbe locale B(), 1 significa genitore A()e 2 significherebbe nonno <module>aka globale
  • Il secondo argomento è una stringa o un oggetto codice che si desidera eseguire nell'ambito specificato
  • Puoi anche chiamarlo senza argomenti per la shell interattiva all'interno del tuo programma

La lunga risposta

Questo è più complicato. Seapie funziona modificando i frame nello stack di chiamate utilizzando l'API CPython. CPython è lo standard de facto, quindi la maggior parte delle persone non deve preoccuparsene.

Le parole magiche che probabilmente ti interessano se stai leggendo questo sono le seguenti:

frame = sys._getframe(1)          # 1 stands for previous frame
parent_locals = frame.f_locals    # true dictionary of parent locals
parent_globals = frame.f_globals  # true dictionary of parent globals

exec(codeblock, parent_globals, parent_locals)

ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),ctypes.c_int(1))
# the magic value 1 stands for ability to introduce new variables. 0 for update-only

Quest'ultimo costringerà gli aggiornamenti a passare all'ambito locale. gli ambiti locali sono tuttavia ottimizzati in modo diverso dall'ambito globale, quindi l'introduzione di nuovi oggetti presenta alcuni problemi quando si tenta di chiamarli direttamente se non vengono inizializzati in alcun modo. Copierò alcuni modi per aggirare questi problemi dalla pagina GitHub

  • Assegna, importa e definisci i tuoi oggetti in anticipo
  • Assegnare in anticipo un segnaposto ai tuoi oggetti
  • Riassegna l'oggetto a se stesso nel programma principale per aggiornare la tabella dei simboli: x = locals () ["x"]
  • Usa exec () nel programma principale invece di chiamare direttamente per evitare l'ottimizzazione. Invece di chiamare x do: exec ("x")

Se ritieni che l'utilizzo exec()non sia qualcosa con cui vuoi andare, puoi emulare il comportamento aggiornando il vero dizionario locale (non quello restituito da locals ()). Copierò un esempio da https://faster-cpython.readthedocs.io/mutable.html

import sys
import ctypes

def hack():
    # Get the frame object of the caller
    frame = sys._getframe(1)
    frame.f_locals['x'] = "hack!"
    # Force an update of locals array from locals dict
    ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),
                                          ctypes.c_int(0))

def func():
    x = 1
    hack()
    print(x)

func()

Produzione:

hack!

0

Non penso che dovresti volerlo fare. Le funzioni che possono alterare le cose nel loro contesto circostante sono pericolose, poiché quel contesto può essere scritto senza la conoscenza della funzione.

Puoi renderlo esplicito, rendendo B un metodo pubblico e C un metodo privato in una classe (probabilmente il modo migliore); o utilizzando un tipo modificabile come un elenco e passandolo esplicitamente a C:

def A():
    x = [0]
    def B(var): 
        var[0] = 1
    B(x)
    print x

A()

2
Come puoi scrivere una funzione senza conoscere le funzioni annidate al suo interno? Le funzioni e le chiusure annidate sono una parte intrinseca della funzione in cui sono racchiuse.
Glenn Maynard,

Hai bisogno di conoscere l'interfaccia delle funzioni racchiuse nel tuo, ma non dovresti sapere cosa succede al loro interno. Inoltre, non ci si può aspettare che tu sappia cosa succede nelle funzioni che chiamano, ecc.! Se una funzione modifica un membro non globale o non di classe, di solito dovrebbe renderlo esplicito attraverso la sua interfaccia, cioè prenderlo come parametro.
Sideshow Bob,

Python non ti obbliga ad essere così bravo, ovviamente, da qui la nonlocalparola chiave, ma sta a te usarla con grande cautela.
Sideshow Bob,

5
@ Bob: non ho mai scoperto che usare chiusure come questa sia pericoloso, a parte le stranezze linguistiche. Pensa ai locali come a una classe temporanea e alle funzioni locali come ai metodi della classe, e non è più complicato di così. YMMV, immagino.
Glenn Maynard

0

Puoi, ma dovrai usare la dichiarazione globale (non è una buona soluzione come sempre quando usi le variabili globali, ma funziona):

def A():
    global b
    b = 1

    def B():
      global b
      print( b )
      b = 2

    B()
A()

Vedi la mia risposta che spiega il potenziale svantaggio di questa soluzione
eyquem

4
L'uso di una variabile globale è completamente diverso.
Glenn Maynard

0

Non so se esiste un attributo di una funzione che fornisce lo __dict__spazio esterno della funzione quando questo spazio esterno non è lo spazio globale == il modulo, che è il caso in cui la funzione è una funzione annidata, in Python 3.

Ma in Python 2, per quanto ne so, non esiste un tale attributo.

Quindi le uniche possibilità per fare quello che vuoi è:

1) utilizzando un oggetto mutevole, come detto da altri

2)

def A() :
    b = 1
    print 'b before B() ==', b

    def B() :
        b = 10
        print 'b ==', b
        return b

    b = B()
    print 'b after B() ==', b

A()

risultato

b before B() == 1
b == 10
b after B() == 10

.

Nota

La soluzione di Cédric Julien ha un inconveniente:

def A() :
    global b # N1
    b = 1
    print '   b in function B before executing C() :', b

    def B() :
        global b # N2
        print '     b in function B before assigning b = 2 :', b
        b = 2
        print '     b in function B after  assigning b = 2 :', b

    B()
    print '   b in function A , after execution of B()', b

b = 450
print 'global b , before execution of A() :', b
A()
print 'global b , after execution of A() :', b

risultato

global b , before execution of A() : 450
   b in function B before executing B() : 1
     b in function B before assigning b = 2 : 1
     b in function B after  assigning b = 2 : 2
   b in function A , after execution of B() 2
global b , after execution of A() : 2

La b globale dopo l'esecuzione di A()è stata modificata e potrebbe non essere desiderata

Questo è il caso solo se c'è un oggetto con identificatore b nello spazio dei nomi globale

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.