Come evitare la condivisione dei dati di classe tra istanze?


146

Quello che voglio è questo comportamento:

class a:
    list = []

x = a()
y = a()

x.list.append(1)
y.list.append(2)
x.list.append(3)
y.list.append(4)

print(x.list) # prints [1, 3]
print(y.list) # prints [2, 4]

Ovviamente, ciò che accade realmente quando stampo è:

print(x.list) # prints [1, 2, 3, 4]
print(y.list) # prints [1, 2, 3, 4]

Chiaramente stanno condividendo i dati in classe a. Come posso ottenere istanze separate per raggiungere il comportamento che desidero?


14
Per favore, non usare listcome nome di attributo. listè una funzione integrata per costruire un nuovo elenco. Dovresti scrivere le classi di nomi con la lettera maiuscola.
Marc Tudurí,

Risposte:


147

Tu vuoi questo:

class a:
    def __init__(self):
        self.list = []

La dichiarazione delle variabili all'interno della dichiarazione di classe li rende membri "di classe" e non membri di istanza. Dichiarandoli all'interno del __init__metodo si assicura che venga creata una nuova istanza dei membri accanto a ogni nuova istanza dell'oggetto, che è il comportamento che si sta cercando.


5
Un ulteriore chiarimento: se si dovesse riassegnare la proprietà list in una delle istanze, ciò non influirebbe sulle altre. Quindi, se hai fatto qualcosa del genere x.list = [], puoi cambiarlo e non influenzare nessun altro. Il problema che affronti è quello x.liste y.listsono lo stesso elenco, quindi quando chiami append su uno, influisce sull'altro.
Matt Moriarity,

Ma perché questo accade solo per l'elenco? Quando ho dichiarato un numero intero o una stringa all'esterno dell'init , non è stato condiviso tra gli oggetti? Qualcuno può condividere qualsiasi link doc a questo concetto?
Amal Ts,

1
@AmalTs Sembra che tu non capisca come funziona l'assegnazione in Python. Guarda questo video o questo post SO . Il comportamento che vedi è causato dal fatto che stai modificando gli elenchi ma ribadendo i riferimenti a ints e stringhe.
Андрей Беньковский,

4
@AmalTs Nota: è considerato una cattiva pratica utilizzare gli attributi di classe come valori predefiniti "pigri" per gli attributi di istanza. Anche se gli attributi sono di tipo immutabile, è meglio assegnarli all'interno __init__.
Андрей Беньковский,

21

La risposta accettata funziona, ma un po 'più di spiegazione non fa male.

Gli attributi di classe non diventano attributi di istanza quando viene creata un'istanza. Diventano attributi di istanza quando viene assegnato loro un valore.

Nel codice originale nessun valore è assegnato listall'attributo dopo l'istanza; quindi rimane un attributo di classe. La definizione dell'elenco all'interno __init__funziona perché __init__viene chiamato dopo l'istanza. In alternativa, questo codice produrrebbe anche l'output desiderato:

>>> class a:
    list = []

>>> y = a()
>>> x = a()
>>> x.list = []
>>> y.list = []
>>> x.list.append(1)
>>> y.list.append(2)
>>> x.list.append(3)
>>> y.list.append(4)
>>> print(x.list)
[1, 3]
>>> print(y.list)
[2, 4]

Tuttavia, lo scenario confuso nella domanda non accadrà mai a oggetti immutabili come numeri e stringhe, perché il loro valore non può essere modificato senza assegnazione. Ad esempio un codice simile all'originale con tipo di attributo stringa funziona senza problemi:

>>> class a:
    string = ''


>>> x = a()
>>> y = a()
>>> x.string += 'x'
>>> y.string += 'y'
>>> x.string
'x'
>>> y.string
'y'

Quindi, per riassumere: gli attributi di classe diventano attributi di istanza se e solo se un valore viene loro assegnato dopo l'istanza, essendo nel __init__metodo o meno . Questa è una buona cosa perché in questo modo è possibile avere attributi statici se non si assegna mai un valore a un attributo dopo l'istanza.


11

Hai dichiarato "elenco" come "proprietà a livello di classe" e non "proprietà a livello di istanza". Per avere le proprietà dell'ambito a livello di istanza, è necessario inizializzarle facendo riferimento al parametro "self" nel __init__metodo (o altrove a seconda della situazione).

Non è necessario inizializzare rigorosamente le proprietà dell'istanza in __init__ metodo, ma facilita la comprensione.


11

Sebbene la risposta accettata sia corretta, vorrei aggiungere una breve descrizione.

Facciamo un piccolo esercizio

prima di tutto definire una classe come segue:

class A:
    temp = 'Skyharbor'

    def __init__(self, x):
        self.x = x

    def change(self, y):
        self.temp = y

Allora, cosa abbiamo qui?

  • Abbiamo una classe molto semplice che ha un attributo temp che è una stringa
  • Un __init__metodo che impostaself.x
  • Viene impostato un metodo di modifica self.temp

Abbastanza semplice finora, sì? Ora iniziamo a giocare con questa classe. Inizializziamo prima questa classe:

a = A('Tesseract')

Ora procedi come segue:

>>> print(a.temp)
Skyharbor
>>> print(A.temp)
Skyharbor

Bene, ha a.tempfunzionato come previsto, ma come diavolo ha A.tempfunzionato? Bene ha funzionato perché temp è un attributo di classe. Tutto in Python è un oggetto. Qui A è anche un oggetto di classe type. Pertanto, l'attributo temp è un attributo detenuto dalla Aclasse e se si modifica il valore di temp tramite A(e non attraverso un'istanza di a), il valore modificato si rifletterà in tutte le istanze della Aclasse. Andiamo avanti e facciamo questo:

>>> A.temp = 'Monuments'
>>> print(A.temp)
Monuments
>>> print(a.temp)
Monuments

Interessante vero? E notare che id(a.temp)e id(A.temp)sono ancora la stessa .

A qualsiasi oggetto Python viene assegnato automaticamente un __dict__attributo, che contiene il suo elenco di attributi. Analizziamo cosa contiene questo dizionario per i nostri oggetti di esempio:

>>> print(A.__dict__)
{
    'change': <function change at 0x7f5e26fee6e0>,
    '__module__': '__main__',
    '__init__': <function __init__ at 0x7f5e26fee668>,
    'temp': 'Monuments',
    '__doc__': None
}
>>> print(a.__dict__)
{x: 'Tesseract'}

Nota che l' tempattributo è elencato tra Agli attributi della classe mentre xè elencato per l'istanza.

Quindi, come mai otteniamo un valore definito a.tempse non è nemmeno elencato per l'istanza a. Bene, questa è la magia del __getattribute__()metodo. In Python la sintassi punteggiata invoca automaticamente questo metodo, quindi quando scriviamo a.temp, Python viene eseguito a.__getattribute__('temp'). Tale metodo esegue l'azione di ricerca dell'attributo, ovvero trova il valore dell'attributo guardando in luoghi diversi.

L'implementazione standard delle __getattribute__()ricerche prima il dizionario interno ( dict ) di un oggetto, quindi il tipo di oggetto stesso. In questo caso a.__getattribute__('temp')viene eseguito prima a.__dict__['temp']e poia.__class__.__dict__['temp']

Bene ora usiamo il nostro changemetodo:

>>> a.change('Intervals')
>>> print(a.temp)
Intervals
>>> print(A.temp)
Monuments

Bene, ora che abbiamo usato self, print(a.temp)ci dà un valore diverso daprint(A.temp) .

Ora se confrontiamo id(a.temp)e id(A.temp), saranno diversi.


3

Sì, è necessario dichiarare nel "costruttore" se si desidera che l'elenco diventi una proprietà dell'oggetto e non una proprietà di classe.


3

Quindi quasi ogni risposta qui sembra mancare un punto particolare. Le variabili di classe non diventano mai variabili di istanza, come dimostrato dal codice seguente. Utilizzando una metaclasse per intercettare l'assegnazione di variabili a livello di classe, possiamo vedere che quando a.myattr viene riassegnato, il metodo magico di assegnazione di campo sulla classe non viene chiamato. Questo perché l'assegnazione crea una nuova variabile di istanza . Questo comportamento non ha assolutamente nulla a che fare con la variabile di classe, come dimostrato dalla seconda classe che non ha variabili di classe e che tuttavia consente ancora l'assegnazione dei campi.

class mymeta(type):
    def __init__(cls, name, bases, d):
        pass

    def __setattr__(cls, attr, value):
        print("setting " + attr)
        super(mymeta, cls).__setattr__(attr, value)

class myclass(object):
    __metaclass__ = mymeta
    myattr = []

a = myclass()
a.myattr = []           #NOTHING IS PRINTED
myclass.myattr = [5]    #change is printed here
b = myclass()
print(b.myattr)         #pass through lookup on the base class

class expando(object):
    pass

a = expando()
a.random = 5            #no class variable required
print(a.random)         #but it still works

IN BREVE Le variabili di classe non hanno NIENTE a che fare con le variabili di istanza.

Più chiaramente capita solo che siano nell'ambito delle ricerche sulle istanze. Le variabili di classe sono in realtà variabili di istanza sull'oggetto classe stesso. Puoi anche avere variabili di metaclasse se vuoi, perché anche le metaclasse sono oggetti. Tutto è un oggetto, indipendentemente dal fatto che venga utilizzato per creare altri oggetti o meno, quindi non rimanere legato alla semantica dell'uso di altre lingue della classe di parole. In Python, una classe è in realtà solo un oggetto che viene utilizzato per determinare come creare altri oggetti e quali saranno i loro comportamenti. I metaclassi sono classi che creano classi, solo per illustrare ulteriormente questo punto.


0

Per proteggere la variabile condivisa da un'altra istanza, è necessario creare una nuova variabile di istanza ogni volta che si crea un'istanza. Quando dichiari una variabile all'interno di una classe, questa è una variabile di classe e condivisa da tutte le istanze. Se vuoi renderlo per esempio saggio devi usare il metodo init per reinizializzare la variabile come riferimento all'istanza

Da Python Objects and Class di Programiz.com :

__init__()funzione. Questa funzione speciale viene chiamata ogni volta che viene istanziato un nuovo oggetto di quella classe.

Questo tipo di funzione è anche chiamata costruttori nella programmazione orientata agli oggetti (OOP). Normalmente lo usiamo per inizializzare tutte le variabili.

Per esempio:

class example:
    list=[] #This is class variable shared by all instance
    def __init__(self):
        self.list = [] #This is instance variable referred to specific instance
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.