2011-01-05 5 views
5

Date un'occhiata al seguente codice:Come accedere agli attributi di una classe di una superclasse in Python?

class A(object): 
    defaults = {'a': 1} 

    def __getattr__(self, name): 
     print('A.__getattr__') 
     return self.get_default(name) 

    @classmethod 
    def get_default(cls, name): 
     # some debug output 
     print('A.get_default({}) - {}'.format(name, cls)) 
     try: 
      print(super(cls, cls).defaults) # as expected 
     except AttributeError: #except for the base object class, of course 
      pass 

     # the actual function body 
     try: 
      return cls.defaults[name] 
     except KeyError: 
      return super(cls, cls).get_default(name) # infinite recursion 
      #return cls.__mro__[1].get_default(name) # this works, though 

class B(A): 
    defaults = {'b': 2} 

class C(B): 
    defaults = {'c': 3} 


c = C() 
print('c.a =', c.a) 

ho una gerarchia di classi ciascuna con un proprio dizionario che contiene alcuni valori di default. Se un'istanza di una classe non ha un particolare attributo, dovrebbe invece essere restituito un valore predefinito. Se nessun valore predefinito per l'attributo è contenuto nel dizionario defaults della classe corrente, deve essere cercato il dizionario defaults della superclasse.

Sto cercando di implementarlo utilizzando il metodo della classe ricorsivo get_default. Il programma si blocca in una ricorsione infinita, sfortunatamente. La mia comprensione di super() è ovviamente carente. Accedendo a __mro__, posso farlo funzionare correttamente, ma non sono sicuro che questa sia una soluzione adeguata.

Ho la sensazione che la risposta sia da qualche parte in this article, ma non sono ancora riuscita a trovarla. Forse ho bisogno di ricorrere all'utilizzo di un metaclass?

modifica: Nella mia domanda, __getattr__ primi assegni self.base. Se non è None, l'attributo deve essere recuperato da lì. Solo nell'altro caso, è necessario restituire un valore predefinito. Probabilmente potrei ignorare lo __getattribute__. Sarebbe la soluzione migliore?

modifica 2: Di seguito è riportato un esempio esteso della funzionalità che sto cercando. Attualmente è implementato usando __mro__ (suggerimento precedente di unutbu, in contrasto con il mio metodo ricorsivo originale). A meno che qualcuno non possa suggerire una soluzione più elegante, sono felice di usare questa implementazione. Spero che questo chiarisca le cose.

class A(object): 
    defaults = {'a': 1} 

    def __init__(self, name, base=None): 
     self.name = name 
     self.base = base 

    def __repr__(self): 
     return self.name 

    def __getattr__(self, name): 
     print(" '{}' attribute not present in '{}'".format(name, self)) 
     if self.base is not None: 
      print(" getting '{}' from base ({})".format(name, self.base)) 
      return getattr(self.base, name) 
     else: 
      print(" base = None; returning default value") 
      return self.get_default(name) 

    def get_default(self, name): 
     for cls in self.__class__.__mro__: 
      try: 
       return cls.defaults[name] 
      except KeyError: 
       pass 
     raise KeyError 

class B(A): 
    defaults = {'b': 2} 

class C(B): 
    defaults = {'c': 3} 


c1 = C('c1') 
c1.b = 55 

print('c1.a = ...'); print(' ...', c1.a) # 1 
print(); print('c1.b = ...'); print(' ...', c1.b) # 55 
print(); print('c1.c = ...'); print(' ...', c1.c) # 3 

c2 = C('c2', base=c1) 
c2.c = 99 

print(); print('c2.a = ...'); print(' ...', c2.a) # 1 
print(); print('c2.b = ...'); print(' ...', c2.b) # 55 
print(); print('c2.c = ...'); print(' ...', c2.c) # 99 

L'output:

c1.a = ... 
'a' attribute not present in 'c1' 
    base = None; returning default value 
    ... 1 

c1.b = ... 
    ... 55 

c1.c = ... 
'c' attribute not present in 'c1' 
    base = None; returning default value 
    ... 3 

c2.a = ... 
'a' attribute not present in 'c2' 
    getting 'a' from base (c1) 
'a' attribute not present in 'c1' 
    base = None; returning default value 
    ... 1 

c2.b = ... 
'b' attribute not present in 'c2' 
    getting 'b' from base (c1) 
    ... 55 

c2.c = ... 
    ... 99 
+4

Come mettere le impostazioni predefinite direttamente nello spazio dei nomi classe invece di utilizzare il dizionario "default"? Ciò renderebbe inutile qualsiasi magia - funzionerebbe semplicemente. –

+0

@Sven Sapevo che avrei dovuto dirlo :) Nella mia applicazione, '__getattr__' sta prima controllando un altro attributo (base). Solo se l'altro attributo è Nessuno, è necessario restituire il valore predefinito. Altrimenti, l'attributo deve essere cercato in base. –

+0

Sì, è così che funzionano gli attributi instance/class in Python. :) Controlla la risposta di Toni. Afaik il suo codice farà esattamente lo stesso tuo. –

risposta

0

La soluzione proposta nella seconda modifica della domanda è ancora l'unica che fornisce tutto ciò che richiede la mia applicazione. Mentre il codice di unutbu potrebbe essere più semplice da capire, la soluzione __mro__ offre alcuni vantaggi IMO (vedi commenti).

0

Si dovrebbe usare name mangling qui.

Rinomina defaults-__defaults

che dà ad ogni classe un attributo univoco in modo che non siano confusi con l'altro

8
Non

davvero una risposta, ma una constatazione:

Questo sembra overengineered a me, un trappola comune quando si cercano scuse per usare la magia di Python.

Se si può essere infastiditi nel definire un dettato defaults per una classe, perché non definire semplicemente gli attributi? L'effetto è lo stesso.

class A: 
    a = 1 

class B(A): 
    b = 2 

class C(B): 
    c = 3 


c = C() 
print('c.a =', c.a) 

EDIT:

Come per rispondere alla domanda, avrei probabilmente usare __getattribute__ in combinazione con il mio suggerimento in questo modo:

def __getattribute__(self, name): 
    try: 
     return object.__getattribute__(self.base, name) 
    except AttributeError: 
     return object.__getattribute__(self, name) 
+0

Vale la pena ricordare che è possibile impostare l'attributo anche su c. In pratica, ottieni un'impostazione di istanza, con un valore predefinito definito nella classe. –

+0

Questo non rispetterà il caso d'uso che ho aggiunto alla domanda. –

+0

@Brecht Machiels: Sì, lo farà. Provalo. –

1

ne dite:

class A(object): 
    def __init__(self,base=None): 
     self.a=1 
     if base is not None: 
      self.set_base(base) 
     super(A,self).__init__() 
    def set_base(self,base): 
     for key in ('a b c'.split()): 
      setattr(self,key,getattr(base,key)) 
class B(A): 
    def __init__(self,base=None): 
     self.b=2 
     super(B,self).__init__(base)   
class C(B): 
    def __init__(self,base=None): 
     self.c=3 
     super(C,self).__init__(base) 

c1=C() 
c1.b=55 
print(c1.a) 
print(c1.b) 
print(c1.c) 
# 1 
# 55 
# 3 

c2=C(c1) 
c2.c=99 
print(c2.a) 
print(c2.b) 
print(c2.c) 
# 1 
# 55 
# 99 

c1.set_base(c2) 
print(c1.a) 
print(c1.b) 
print(c1.c) 
# 1 
# 55 
# 99 
+0

Se la base è * non * 'Nessuna', è necessario restituire l'attributo corrispondente della base. Ma suppongo che non possiamo ottenere il nome dell'attributo nella funzione wrapper? –

+0

@Brecht Machiels: È infatti possibile passare parametri extra alla funzione 'check_base_first', accessibile da' wrapper'. – unutbu

+1

@Brecht Machiels: Sebbene possa esserci un modo per utilizzare le proprietà per fare ciò che si desidera, potrebbe non essere la soluzione giusta. Il problema che stai affrontando potrebbe essere un sintomo di una cattiva decisione progettuale in precedenza. Se rivedi la tua domanda per mostrare esattamente come vuoi che i tuoi oggetti si comportino (con 'base's), forse un design migliore può essere suggerito. – unutbu

2

Penso che il problema derivi da un errore dendardo allo scopo di super().

http://docs.python.org/library/functions.html#super

In sostanza, avvolgendo l'oggetto (o classe) a super() rende Python saltare la classe più di recente ereditato quando si fa ricerca attributo. Nel tuo codice, questo risulta nella classe C che viene saltata durante la ricerca di get_default, ma questo in realtà non fa nulla, poiché C non definisce comunque un get_default. Naturalmente, questo si traduce in un ciclo infinito.

La soluzione è quella di definire questa funzione in ogni classe che deriva da A. Questo può essere fatto utilizzando un metaclasse:

class DefaultsClass(type): 
    def __init__(cls, name, bases, dct): 

     def get_default(self, name): 
      # some debug output 
      print('A.get_default(%s) - %s' % (name, cls)) 
      try: 
       print(cls.defaults) # as expected 
      except AttributeError: #except for the base object class, of course 
       pass 

      # the actual function body 
      try: 
       return cls.defaults[name] 
      except KeyError: 
       return super(cls, self).get_default(name) # cooperative superclass 

     cls.get_default = get_default 
     return super(DefaultsClass, cls).__init__(name, bases, dct) 

class A(object): 
    defaults = {'a': 1} 
    __metaclass__ = DefaultsClass 

    def __getattr__(self, name): 
     return self.get_default(name) 



class B(A): 
    defaults = {'b': 2} 

class C(B): 
    defaults = {'c': 3} 


c = C() 
print('c.a =', c.a) 
print('c.b =', c.b) 
print('c.c =', c.c) 

risultati:

A.get_default(c) - <class '__main__.C'> 
{'c': 3} 
('c.c =', 3) 
A.get_default(b) - <class '__main__.C'> 
{'c': 3} 
A.get_default(b) - <class '__main__.B'> 
{'b': 2} 
('c.b =', 2) 
A.get_default(a) - <class '__main__.C'> 
{'c': 3} 
A.get_default(a) - <class '__main__.B'> 
{'b': 2} 
A.get_default(a) - <class '__main__.A'> 
{'a': 1} 
('c.a =', 1) 

Vorrei sottolineare che la maggior parte di Python la gente considererà questa una soluzione molto bizzarra, e dovresti usarla solo se hai davvero bisogno di , forse per supportare il codice legacy.

1

Per essere più chiari sul caso "base" e "predefinito".

>>> class A(object): 
...  a = 1 
... 
>>> class B(A): 
...  b = 2 
... 
>>> class C(B): 
...  c = 3 
... 
>>> a = A() 
>>> b = B() 
>>> c = C() 
>>> 
>>> b.b = 23 
>>> b.a 
1 
>>> b.b 
23 
>>> c.a 
1 
>>> c.b 
2 
>>> c.c 
3 
>>> c.c = 45 
>>> c.c 
45 

Questo copre il caso d'uso dichiarato. Non hai bisogno di magia. Se il tuo caso d'uso è un po 'diverso, spiega di cosa si tratta e ti diremo come farlo senza magia. ;)