tra - visibilità variabili python




Come posso passare una variabile per riferimento? (16)

Non ci sono variabili in Python

La chiave per comprendere il passaggio dei parametri è smettere di pensare alle "variabili". Ci sono nomi e oggetti in Python e insieme appaiono come variabili, ma è utile distinguere sempre i tre.

  1. Python ha nomi e oggetti.
  2. L'assegnazione lega un nome a un oggetto.
  3. Passare un argomento in una funzione associa anche un nome (il nome del parametro della funzione) a un oggetto.

Questo è tutto ciò che c'è da fare. La mutabilità è irrilevante per questa domanda.

Esempio:

a = 1

Ciò lega il nome a a un oggetto di tipo intero che contiene il valore 1.

b = x

Ciò associa il nome b allo stesso oggetto al quale è attualmente associato il nome x . In seguito, il nome b non ha più nulla a che fare con il nome x .

Vedere le sezioni 3.1 e 4.2 nel riferimento al linguaggio Python 3.

Quindi nel codice mostrato nella domanda, la dichiarazione self.Change(self.variable) lega il nome var (nell'ambito della funzione Change ) all'oggetto che contiene il valore 'Original' e l'assegnazione var = 'Changed' ( nel corpo della funzione Change ) assegna di nuovo lo stesso nome: a qualche altro oggetto (che capita di contenere anche una stringa ma potrebbe essere qualcos'altro interamente).

La documentazione di Python non è chiara se i parametri vengono passati per riferimento o valore e il codice seguente produce il valore invariato 'Originale'

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change(self.variable)
        print(self.variable)

    def change(self, var):
        var = 'Changed'

C'è qualcosa che posso fare per passare la variabile per riferimento reale?


(modifica - Blair ha aggiornato la sua risposta estremamente popolare in modo che sia ora precisa)

Penso che sia importante notare che il post corrente con il maggior numero di voti (di Blair Conrad), pur essendo corretto rispetto al suo risultato, è fuorviante ed è borderline errato in base alle sue definizioni. Mentre ci sono molti linguaggi (come C) che permettono all'utente di passare per riferimento o passare per valore, Python non è uno di questi.

La risposta di David Cournapeau indica la vera risposta e spiega perché il comportamento nel post di Blair Conrad sembra essere corretto mentre le definizioni non lo sono.

Nella misura in cui Python passa per valore, tutte le lingue passano per valore poiché è necessario inviare una parte di dati (che si tratti di un "valore" o di un "riferimento"). Tuttavia, ciò non significa che Python passi per valore nel senso che un programmatore C potrebbe pensarci.

Se vuoi il comportamento, la risposta di Blair Conrad va bene. Ma se vuoi sapere quali sono i motivi per cui Python non passa né per valore né per riferimento, leggi la risposta di David Cournapeau.


Effbot (alias Fredrik Lundh) ha descritto lo stile di passaggio delle variabili di Python come call-by-object: http://effbot.org/zone/call-by-object.htm

Gli oggetti sono allocati nell'heap e i puntatori a loro possono essere passati ovunque.

  • Quando si esegue un'assegnazione come x = 1000 , viene creata una voce di dizionario che associa la stringa "x" nello spazio dei nomi corrente a un puntatore all'oggetto intero contenente mille.

  • Quando aggiorni "x" con x = 2000 , viene creato un nuovo oggetto intero e il dizionario viene aggiornato per puntare al nuovo oggetto. Il vecchio oggetto mille è invariato (e può o non può essere vivo a seconda che qualcun altro faccia riferimento all'oggetto).

  • Quando si esegue un nuovo assegnamento come y = x , viene creata una nuova voce di dizionario "y" che punta allo stesso oggetto della voce per "x".

  • Oggetti come stringhe e interi sono immutabili . Ciò significa semplicemente che non ci sono metodi che possono modificare l'oggetto dopo che è stato creato. Ad esempio, una volta creato l'intero numero mille, non cambierà mai. La matematica viene eseguita creando nuovi oggetti interi.

  • Gli oggetti come gli elenchi sono mutabili . Ciò significa che il contenuto dell'oggetto può essere modificato da qualsiasi cosa che punta all'oggetto. Ad esempio, x = []; y = x; x.append(10); print y x = []; y = x; x.append(10); print y x = []; y = x; x.append(10); print y verrà stampato [10] . La lista vuota è stata creata. Sia "x" che "y" puntano alla stessa lista. Il metodo append muta (aggiorna) l'oggetto list (come aggiungere un record a un database) e il risultato è visibile sia a "x" che a "y" (proprio come un aggiornamento del database sarebbe visibile a ogni connessione a quel database).

Spero che chiarisca il problema per te.


Gli argomenti sono passati per incarico . La logica alla base di questo è duplice:

  1. il parametro passato è in realtà un riferimento a un oggetto (ma il riferimento viene passato per valore)
  2. alcuni tipi di dati sono mutabili, ma altri no

Così:

  • Se si passa un oggetto mutabile in un metodo, il metodo ottiene un riferimento a quello stesso oggetto e si può mutarlo per il piacere del proprio cuore, ma se si rebind il riferimento nel metodo, l'ambito esterno non ne saprà nulla e dopo hai finito, il riferimento esterno punterà ancora all'oggetto originale.

  • Se si passa un oggetto immutabile a un metodo, non è ancora possibile riassociare il riferimento esterno e non è possibile nemmeno mutare l'oggetto.

Per renderlo ancora più chiaro, facciamo alcuni esempi.

Elenco: un tipo mutabile

Proviamo a modificare l'elenco passato a un metodo:

def try_to_change_list_contents(the_list):
    print('got', the_list)
    the_list.append('four')
    print('changed to', the_list)

outer_list = ['one', 'two', 'three']

print('before, outer_list =', outer_list)
try_to_change_list_contents(outer_list)
print('after, outer_list =', outer_list)

Produzione:

before, outer_list = ['one', 'two', 'three']
got ['one', 'two', 'three']
changed to ['one', 'two', 'three', 'four']
after, outer_list = ['one', 'two', 'three', 'four']

Poiché il parametro passato è un riferimento a outer_list , non una sua copia, possiamo usare i metodi dell'elenco di mutazioni per cambiarlo e fare in modo che le modifiche si riflettano nello scope esterno.

Ora vediamo cosa succede quando proviamo a cambiare il riferimento passato come parametro:

def try_to_change_list_reference(the_list):
    print('got', the_list)
    the_list = ['and', 'we', 'can', 'not', 'lie']
    print('set to', the_list)

outer_list = ['we', 'like', 'proper', 'English']

print('before, outer_list =', outer_list)
try_to_change_list_reference(outer_list)
print('after, outer_list =', outer_list)

Produzione:

before, outer_list = ['we', 'like', 'proper', 'English']
got ['we', 'like', 'proper', 'English']
set to ['and', 'we', 'can', 'not', 'lie']
after, outer_list = ['we', 'like', 'proper', 'English']

Poiché il parametro the_list stato passato per valore, l'assegnazione di una nuova lista non ha avuto alcun effetto che il codice esterno al metodo potesse vedere. the_list era una copia del riferimento outer_list e avevamo il the_list in una nuova lista, ma non c'era modo di cambiare dove outer_list .

Stringa - un tipo immutabile

È immutabile, quindi non c'è nulla che possiamo fare per cambiare il contenuto della stringa

Ora proviamo a cambiare il riferimento

def try_to_change_string_reference(the_string):
    print('got', the_string)
    the_string = 'In a kingdom by the sea'
    print('set to', the_string)

outer_string = 'It was many and many a year ago'

print('before, outer_string =', outer_string)
try_to_change_string_reference(outer_string)
print('after, outer_string =', outer_string)

Produzione:

before, outer_string = It was many and many a year ago
got It was many and many a year ago
set to In a kingdom by the sea
after, outer_string = It was many and many a year ago

Di nuovo, poiché il parametro the_string stato passato per valore, l'assegnazione di una nuova stringa non ha avuto alcun effetto che il codice esterno al metodo potesse vedere. the_string era una copia del riferimento outer_string , e avevamo il the_string a una nuova stringa, ma non c'era modo di cambiare dove puntato outer_string .

Spero che questo chiarisca un po 'le cose.

EDIT: È stato notato che questo non risponde alla domanda che @David ha originariamente chiesto, "C'è qualcosa che posso fare per passare la variabile per riferimento reale?". Lavoriamo su questo.

Come possiamo aggirare questo?

Come mostra la risposta di @ Andrea, potresti restituire il nuovo valore. Questo non cambia il modo in cui le cose vengono passate, ma ti permette di ottenere le informazioni che vuoi escludere:

def return_a_whole_new_string(the_string):
    new_string = something_to_do_with_the_old_string(the_string)
    return new_string

# then you could call it like
my_string = return_a_whole_new_string(my_string)

Se volessi davvero evitare di usare un valore di ritorno, potresti creare una classe per conservare il tuo valore e passarlo nella funzione o usare una classe esistente, come una lista:

def use_a_wrapper_to_simulate_pass_by_reference(stuff_to_change):
    new_string = something_to_do_with_the_old_string(stuff_to_change[0])
    stuff_to_change[0] = new_string

# then you could call it like
wrapper = [my_string]
use_a_wrapper_to_simulate_pass_by_reference(wrapper)

do_something_with(wrapper[0])

Anche se questo sembra un po 'macchinoso.


Ho trovato le altre risposte piuttosto lunghe e complicate, quindi ho creato questo semplice diagramma per spiegare il modo in cui Python tratta variabili e parametri.


Il problema deriva da un fraintendimento di quali variabili sono in Python. Se sei abituato alle lingue più tradizionali, hai un modello mentale di ciò che accade nella seguente sequenza:

a = 1
a = 2

Si ritiene che a sia un percorso di memoria che memorizza il valore 1 , quindi viene aggiornato per memorizzare il valore 2 . Non è così che funzionano le cose in Python. Piuttosto, a inizio come riferimento a un oggetto con il valore 1 , quindi viene riassegnato come riferimento a un oggetto con il valore 2 . Questi due oggetti possono continuare a coesistere anche se non si fa più riferimento al primo; in effetti possono essere condivisi da un numero qualsiasi di altri riferimenti all'interno del programma.

Quando si chiama una funzione con un parametro, viene creato un nuovo riferimento che si riferisce all'oggetto passato. Questo è separato dal riferimento utilizzato nella chiamata di funzione, quindi non c'è modo di aggiornare quel riferimento e farlo riferirsi ad un nuovo oggetto. Nel tuo esempio:

def __init__(self):
    self.variable = 'Original'
    self.Change(self.variable)

def Change(self, var):
    var = 'Changed'

self.variable è un riferimento all'oggetto stringa 'Original' . Quando chiami Change , crei una seconda var riferimento all'oggetto. All'interno della funzione, la self.variable riferimento viene riassegnata a un oggetto stringa diverso 'Changed' , ma la self.variable riferimento è separata e non cambia.

L'unico modo per aggirare questo è passare un oggetto mutevole. Poiché entrambi i riferimenti si riferiscono allo stesso oggetto, eventuali modifiche all'oggetto si riflettono in entrambe le posizioni.

def __init__(self):         
    self.variable = ['Original']
    self.Change(self.variable)

def Change(self, var):
    var[0] = 'Changed'

Lo schema di assegnazione pass-by di Python non è esattamente come l'opzione dei parametri di riferimento di C ++, ma risulta in pratica molto simile al modello di passaggio degli argomenti del linguaggio C (e di altri) nella pratica:

  • Gli argomenti immutabili vengono effettivamente passati "in base al valore ". Gli oggetti come gli interi e le stringhe vengono passati dal riferimento all'oggetto anziché dalla copia, ma poiché non è possibile modificare gli oggetti immutabili in alcun modo, l'effetto è simile alla creazione di una copia.
  • Gli argomenti mutabili vengono effettivamente passati " con il puntatore ". Anche oggetti come elenchi e dizionari vengono passati dal riferimento all'oggetto, che è simile al modo in cui C passa gli array come puntatori: gli oggetti mutabili possono essere modificati nella funzione, proprio come i C array .

Non è né pass-by-value né pass-by-reference: è call-by-object. Vedi questo, di Fredrik Lundh:

http://effbot.org/zone/call-by-object.htm

Ecco una citazione significativa:

"... le variabili [nomi] non sono oggetti, non possono essere indicati da altre variabili o indicati da oggetti."

Nel tuo esempio, quando viene chiamato il metodo Change , per esso viene creato uno namespace ; e var diventa un nome, all'interno di tale spazio dei nomi, per l'oggetto stringa 'Original' . L'oggetto ha quindi un nome in due spazi dei nomi. Quindi var = 'Changed' lega var a un nuovo oggetto stringa, e quindi lo spazio dei nomi del metodo si dimentica di 'Original' . Infine, quel namespace è dimenticato e la stringa 'Changed' insieme ad essa.


Tecnicamente, Python utilizza sempre il passaggio per valori di riferimento . Ho intenzione di ripetere la mia altra risposta per sostenere la mia affermazione.

Python utilizza sempre valori di riferimento pass-by. Non c'è eccezione. Qualsiasi assegnazione variabile significa copiare il valore di riferimento. Nessuna eccezione. Qualsiasi variabile è il nome associato al valore di riferimento. Sempre.

Puoi pensare a un valore di riferimento come indirizzo dell'oggetto target. L'indirizzo viene automaticamente dereferenziato quando utilizzato. In questo modo, lavorando con il valore di riferimento, sembra che tu lavori direttamente con l'oggetto di destinazione. Ma c'è sempre un riferimento nel mezzo, un passo in più per saltare al bersaglio.

Ecco l'esempio che dimostra che Python utilizza il passaggio per riferimento:

Se l'argomento è stato passato per valore, non è possibile modificare il lst esterno. Il verde sono gli oggetti di destinazione (il nero è il valore memorizzato all'interno, il rosso è il tipo di oggetto), il giallo è la memoria con il valore di riferimento all'interno - disegnato come la freccia. La freccia blu continua è il valore di riferimento passato alla funzione (tramite il percorso della freccia blu tratteggiata). Il brutto giallo scuro è il dizionario interno. (In realtà potrebbe essere disegnato anche come un'ellisse verde. Il colore e la forma dicono solo che è interno.)

È possibile utilizzare la funzione built-in id() per apprendere quale sia il valore di riferimento (ovvero, l'indirizzo dell'oggetto target).

Nei linguaggi compilati, una variabile è uno spazio di memoria in grado di acquisire il valore del tipo. In Python, una variabile è un nome (catturato internamente come stringa) associato alla variabile di riferimento che contiene il valore di riferimento dell'oggetto di destinazione. Il nome della variabile è la chiave nel dizionario interno, la parte valore di quella voce del dizionario memorizza il valore di riferimento sulla destinazione.

I valori di riferimento sono nascosti in Python. Non esiste alcun tipo di utente esplicito per la memorizzazione del valore di riferimento. Tuttavia, è possibile utilizzare un elemento di lista (o elemento in qualsiasi altro tipo di contenitore adatto) come variabile di riferimento, poiché tutti i contenitori memorizzano gli elementi anche come riferimenti agli oggetti di destinazione. In altre parole, gli elementi non sono in realtà contenuti all'interno del contenitore, ma solo i riferimenti agli elementi.


Un semplice trucco che uso normalmente è quello di racchiuderlo in una lista:

def Change(self, var):
    var[0] = 'Changed'

variable = ['Original']
self.Change(variable)      
print variable[0]

(Sì, so che questo può essere sconveniente, ma a volte è abbastanza semplice farlo).


Sebbene il passaggio per riferimento non sia qualcosa che si adatta bene a Python e dovrebbe essere usato raramente, ci sono alcune soluzioni alternative che possono effettivamente funzionare per ottenere l'oggetto attualmente assegnato a una variabile locale o addirittura riassegnare una variabile locale all'interno di una funzione chiamata.

L'idea di base è di avere una funzione che può fare quell'accesso e può essere passata come oggetto in altre funzioni o memorizzata in una classe.

Un modo è quello di utilizzare global(per le variabili globali) o nonlocal(per le variabili locali in una funzione) in una funzione wrapper.

def change(wrapper):
    wrapper(7)

x = 5
def setter(val):
    global x
    x = val
print(x)

La stessa idea funziona per leggere e delmisurare una variabile.

Per la sola lettura c'è anche un modo più breve di usare semplicemente lambda: xquale restituisce un callable che quando chiamato restituisce il valore corrente di x. Questo è un po 'come "chiamare per nome" usato nelle lingue del lontano passato.

Passare 3 wrapper per accedere a una variabile è un po 'ingombrante, quindi è possibile includerli in una classe che ha un attributo proxy:

class ByRef:
    def __init__(self, r, w, d):
        self._read = r
        self._write = w
        self._delete = d
    def set(self, val):
        self._write(val)
    def get(self):
        return self._read()
    def remove(self):
        self._delete()
    wrapped = property(get, set, remove)

# left as an exercise for the reader: define set, get, remove as local functions using global / nonlocal
r = ByRef(get, set, remove)
r.wrapped = 15

Il supporto per la "riflessione" di Python consente di ottenere un oggetto che sia in grado di riassegnare un nome / una variabile in un determinato ambito senza definire esplicitamente funzioni in tale ambito:

class ByRef:
    def __init__(self, locs, name):
        self._locs = locs
        self._name = name
    def set(self, val):
        self._locs[self._name] = val
    def get(self):
        return self._locs[self._name]
    def remove(self):
        del self._locs[self._name]
    wrapped = property(get, set, remove)

def change(x):
    x.wrapped = 7

def test_me():
    x = 6
    print(x)
    change(ByRef(locals(), "x"))
    print(x)

Qui la ByRefclasse include un accesso al dizionario. Quindi l'accesso agli attributi wrappedè tradotto in un accesso agli elementi nel dizionario passato. Passando il risultato del builtin localse il nome di una variabile locale questo finisce per accedere a una variabile locale. La documentazione di Python a partire da 3.5 suggerisce che la modifica del dizionario potrebbe non funzionare ma sembra funzionare per me.


dato il modo in cui Python gestisce valori e riferimenti ad essi, l'unico modo per fare riferimento a un attributo di istanza arbitrario è il nome:

class PassByReferenceIsh:
    def __init__(self):
        self.variable = 'Original'
        self.change('variable')
        print self.variable

    def change(self, var):
        self.__dict__[var] = 'Changed'

nel codice reale si aggiungerebbe, naturalmente, un controllo degli errori nella ricerca dict.


A parte tutte le grandi spiegazioni su come funziona questa roba in Python, non vedo un semplice suggerimento per il problema. Come sembra che tu crei oggetti e istanze, il modo pitone di gestire le variabili di istanza e modificarle è il seguente:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.Change()
        print self.variable

    def Change(self):
        self.variable = 'Changed'

Nei metodi di istanza, normalmente si fa riferimento selfad accedere agli attributi di istanza. È normale impostare gli attributi di istanza __init__e leggerli o modificarli nei metodi di istanza. Questo è anche il motivo per cui si passa selfil primo argomento a def Change.

Un'altra soluzione potrebbe essere quella di creare un metodo statico come questo:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.variable = PassByReference.Change(self.variable)
        print self.variable

    @staticmethod
    def Change(var):
        var = 'Changed'
        return var

C'è un piccolo trucco per passare un oggetto per riferimento, anche se la lingua non lo rende possibile. Funziona anche in Java, è la lista con un oggetto. ;-)

class PassByReference:
    def __init__(self, name):
        self.name = name

def changeRef(ref):
    ref[0] = PassByReference('Michael')

obj = PassByReference('Peter')
print obj.name

p = [obj] # A pointer to obj! ;-)
changeRef(p)

print p[0].name # p->name

È un brutto trucco, ma funziona. ;-P


Ho usato il seguente metodo per convertire rapidamente un paio di codici Fortran in Python. È vero, non è passato per riferimento come la domanda originale è stata posta, ma in alcuni casi è un semplice aggirare.

a=0
b=0
c=0
def myfunc(a,b,c):
    a=1
    b=2
    c=3
    return a,b,c

a,b,c = myfunc(a,b,c)
print a,b,c

Un sacco di informazioni sulle risposte qui, ma penso che un punto aggiuntivo non sia chiaramente menzionato qui esplicitamente. Citando dalla documentazione di Python https://docs.python.org/2/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

"In Python, le variabili che sono solo referenziate all'interno di una funzione sono implicitamente globali.Se una variabile viene assegnata a un nuovo valore in qualsiasi parte all'interno del corpo della funzione, si presume che sia un locale.Se una variabile viene mai assegnata a un nuovo valore all'interno della funzione, la variabile è implicitamente locale e devi dichiararla esplicitamente come "globale", anche se un po 'sorprendente all'inizio, una considerazione del momento spiega questo: da un lato, richiedere global per le variabili assegnate fornisce una barra contro effetti collaterali indesiderati. D'altra parte, se per tutti i riferimenti globali fosse necessario il global, si utilizzerebbe sempre tutto il mondo. Dovresti dichiarare come globale ogni riferimento a una funzione built-in oa un componente di un modulo importato.Questo disordine avrebbe vanificato l'utilità della dichiarazione globale per l'identificazione degli effetti collaterali. "

Anche quando si passa un oggetto mutabile a una funzione, questo si applica ancora. E a me spiega chiaramente la ragione della differenza di comportamento tra l'assegnazione all'oggetto e il funzionamento sull'oggetto nella funzione.

def test(l):
    print "Received", l , id(l)
    l = [0, 0, 0]
    print "Changed to", l, id(l)  # New local object created, breaking link to global l

l= [1,2,3]
print "Original", l, id(l)
test(l)
print "After", l, id(l)

dà:

Original [1, 2, 3] 4454645632
Received [1, 2, 3] 4454645632
Changed to [0, 0, 0] 4474591928
After [1, 2, 3] 4454645632

L'assegnazione a una variabile globale che non è dichiarata globale crea quindi un nuovo oggetto locale e interrompe il collegamento all'oggetto originale.







pass-by-reference