Qual è la differenza tra gli attributi di classe e di istanza?


132

Esiste una distinzione significativa tra:

class A(object):
    foo = 5   # some default value

vs.

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

Se stai creando molte istanze, c'è qualche differenza nelle prestazioni o nei requisiti di spazio per i due stili? Quando leggi il codice, ritieni che il significato dei due stili sia significativamente diverso?


1
Mi sono appena reso conto che una domanda simile è stata posta e ha risposto qui: stackoverflow.com/questions/206734/… Devo eliminare questa domanda?
Dan Homerick,

2
È la tua domanda, sentiti libero di eliminarlo. Dal momento che è tuo, perché chiedere l'opinione di qualcun altro?
S.Lott

Risposte:


146

Oltre alle considerazioni sulle prestazioni, esiste una differenza semantica significativa . Nel caso dell'attributo class, c'è solo un oggetto a cui si fa riferimento. Nell'istanza-attributo-impostato-all'istanziazione, possono essere indicati più oggetti. Per esempio

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]

4
Sono condivisi solo i tipi mutabili. Come per inte strsono ancora collegati a ciascuna istanza piuttosto che alla classe.
Babu,

11
@Babu: No, inte strsono anche condivisi, esattamente allo stesso modo. Puoi verificarlo facilmente con iso id. O semplicemente guarda in ogni istanza __dict__e classe __dict__. Di solito non importa molto se i tipi immutabili sono condivisi o meno.
abarnert,

17
Tuttavia, nota che se lo fai a.foo = 5, in entrambi i casi vedrai il b.fooritorno []. Questo perché nel primo caso, stai sovrascrivendo l'attributo class a.foocon un nuovo attributo di istanza con lo stesso nome.
Konstantin Schubert,

39

La differenza è che l'attributo sulla classe è condiviso da tutte le istanze. L'attributo su un'istanza è unico per quell'istanza.

Se provengono da C ++, gli attributi della classe sono più simili alle variabili membro statiche.


1
Non sono solo i tipi mutabili che sono condivisi? La risposta accettata mostra un elenco, che funziona, ma se è un int, sembra essere lo stesso di un attr attr: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe,

6
@Rafe: No, tutti i tipi sono condivisi. Il motivo per cui sei confuso è che ciò che a.foo.append(5)sta mutando il valore a cui si a.fooriferisce, mentre si a.foo = 5sta trasformando a.fooin un nuovo nome per il valore 5. Quindi, si finisce con un attributo di istanza che nasconde l'attributo di classe. Prova lo stesso a.foo = 5nella versione di Alex e vedrai che b.fooè invariato.
abarnert,

30

Ecco un ottimo post e riassumilo come di seguito.

class Bar(object):
    ## No need for dot syntax
    class_var = 1

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

## Need dot syntax as we've left scope of class namespace
Bar.class_var
## 1
foo = MyClass(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (Bar.__dict__)
foo.class_var
## 1

E in forma visiva

inserisci qui la descrizione dell'immagine

Assegnazione degli attributi di classe

  • Se un attributo di classe viene impostato accedendo alla classe, sovrascriverà il valore per tutte le istanze

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
    
  • Se una variabile di classe viene impostata accedendo a un'istanza, sostituirà il valore solo per quell'istanza . Questo essenzialmente sostituisce la variabile di classe e la trasforma in una variabile di istanza disponibile, intuitivamente, solo per quell'istanza .

    foo = Bar(2)
    foo.class_var
    ## 1
    foo.class_var = 2
    foo.class_var
    ## 2
    Bar.class_var
    ## 1
    

Quando useresti l'attributo class?

  • Memorizzazione di costanti . Poiché è possibile accedere agli attributi di classe come attributi della classe stessa, è spesso piacevole utilizzarli per archiviare costanti specifiche della classe a livello di classe

    class Circle(object):
         pi = 3.14159
    
         def __init__(self, radius):
              self.radius = radius   
        def area(self):
             return Circle.pi * self.radius * self.radius
    
    Circle.pi
    ## 3.14159
    c = Circle(10)
    c.pi
    ## 3.14159
    c.area()
    ## 314.159
    
  • Definizione dei valori predefiniti . Come esempio banale, potremmo creare un elenco limitato (ovvero un elenco che può contenere solo un determinato numero di elementi o meno) e scegliere di avere un limite predefinito di 10 elementi

    class MyClass(object):
        limit = 10
    
        def __init__(self):
            self.data = []
        def item(self, i):
            return self.data[i]
    
        def add(self, e):
            if len(self.data) >= self.limit:
                raise Exception("Too many elements")
            self.data.append(e)
    
     MyClass.limit
     ## 10
    

19

Dal momento che le persone nei commenti qui e in altre due domande contrassegnate come duplicate sembrano essere confuse su questo allo stesso modo, penso che valga la pena aggiungere una risposta aggiuntiva a quella di Alex Coventry .

Il fatto che Alex stia assegnando un valore di tipo mutabile, come un elenco, non ha nulla a che fare con il fatto che le cose siano condivise o meno. Possiamo vederlo con la idfunzione o l' isoperatore:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(Se ti stai chiedendo perché ho usato object()invece, diciamo, 5per evitare di imbattermi in altri due problemi che non voglio entrare qui; per due motivi diversi, i messaggi creati completamente separatamente 5possono finire per essere il stessa istanza del numero 5. Ma object()non è possibile crearli interamente creati separatamente .)


Quindi, perché a.foo.append(5)nell'esempio di Alex influisce b.foo, ma a.foo = 5nel mio esempio no? Bene, prova a.foo = 5nell'esempio di Alex e nota che non influisce neancheb.foo lì .

a.foo = 5sta solo diventando a.fooun nome per 5. Ciò non influisce b.foo, o qualsiasi altro nome per il vecchio valore a.fooa cui si riferiva. * È un po 'complicato creare un attributo di istanza che nasconda un attributo di classe, ** ma una volta ottenuto ciò, nulla di complicato è sta succedendo qui.


Spero che ora sia ovvio il motivo per cui Alex ha usato un elenco: il fatto che puoi mutare un elenco significa che è più facile mostrare che due variabili nominano lo stesso elenco, e anche che è più importante nel codice della vita reale sapere se hai due elenchi o due nomi per lo stesso elenco.


* La confusione per le persone che provengono da un linguaggio come C ++ è che in Python i valori non sono memorizzati in variabili. I valori vivono nella terra dei valori, da soli, le variabili sono solo nomi per valori e l'assegnazione crea semplicemente un nuovo nome per un valore. Se aiuta, pensa a ciascuna variabile Python come a shared_ptr<T>anziché a T.

** Alcune persone ne traggono vantaggio utilizzando un attributo di classe come "valore predefinito" per un attributo di istanza che le istanze possono o meno impostare. Questo può essere utile in alcuni casi, ma può anche essere fonte di confusione, quindi fai attenzione.


0

C'è un'altra situazione.

Gli attributi di classe e istanza sono Descrittore .

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

Sopra verrà generato:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

Lo stesso tipo di accesso all'istanza tramite classe o istanza restituisce risultati diversi!

E ho trovato nella definizione c.PyObject_GenericGetAttr, e un ottimo post .

Spiegare

Se l'attributo si trova nel dizionario delle classi che compongono. gli oggetti MRO, quindi controlla se l'attributo che sta cercando punta a un descrittore di dati (che non è altro che una classe che implementa sia __get__il __set__metodo che il metodo). In tal caso, risolvi la ricerca dell'attributo chiamando il __get__metodo del descrittore di dati (righe 28–33).

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.