2011-01-05 12 views
5

Werfen Sie einen Blick auf den folgenden Code:Wie auf die Klassenattribute einer Superklasse in Python zugreifen?

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) 

Ich habe eine Hierarchie von Klassen jeweils mit einem eigenen Wörterbuch einige Standardwerte enthält. Wenn eine Instanz einer Klasse kein bestimmtes Attribut aufweist, sollte stattdessen ein Standardwert für diese Klasse zurückgegeben werden. Wenn in dem defaults Wörterbuch der aktuellen Klasse kein Standardwert für das Attribut enthalten ist, sollte das defaults Wörterbuch der Oberklasse durchsucht werden.

Ich versuche, dies mit der rekursiven Klassenmethode get_default zu implementieren. Das Programm bleibt leider in einer unendlichen Rekursion stecken. Mein Verständnis von super() fehlt offensichtlich. Wenn ich auf __mro__ zugreife, kann ich zwar richtig funktionieren, aber ich bin mir nicht sicher, ob das eine richtige Lösung ist.

Ich habe das Gefühl, die Antwort ist irgendwo in this article, aber ich konnte es noch nicht finden. Vielleicht muss ich auf eine Metaklasse zurückgreifen?

bearbeiten: In meiner Anwendung überprüft __getattr__ zuerst self.base. Wenn es nicht None ist, muss das Attribut von dort abgerufen werden. Nur im anderen Fall muss ein Standardwert zurückgegeben werden. Ich könnte wahrscheinlich __getattribute__ überschreiben. Wäre das die bessere Lösung?

bearbeiten 2: Unten ist ein erweitertes Beispiel für die Funktionalität, die ich suche. Es wird derzeit unter Verwendung von __mro__ implementiert (unutbus früherer Vorschlag, im Gegensatz zu meiner ursprünglichen rekursiven Methode). Wenn nicht jemand eine elegantere Lösung vorschlagen kann, bin ich glücklich, diese Implementierung zu verwenden. Ich hoffe, das klärt die Dinge auf.

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 

Der Ausgang:

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

Wie wäre es, die Standardwerte direkt in den Klassennamespace zu platzieren, anstatt das Wörterbuch 'defaults' zu verwenden? Das würde jegliche Magie unnötig machen - es würde einfach funktionieren. –

+0

@Sven Ich wusste, dass ich das hätte erwähnen sollen :) In meiner Anwendung prüft der '__getattr__' zuerst ein anderes Attribut (Basis). Nur wenn das andere Attribut None ist, sollte der Standardwert zurückgegeben werden. Andernfalls muss das Attribut in der Basis gesucht werden. –

+0

Ja, so funktionieren Instanz-/Klassenattribute in Python. :) Überprüfen Sie Tonis Antwort. Afaik wird seinen Code genauso machen wie deiner. –

Antwort

0

Die Lösung in der zweiten Bearbeitung der Frage vorgeschlagen ist immer noch die einzige, die alles bietet, was meine Anwendung erfordert. Während der Code von unutbu einfacher zu verstehen ist, bietet die __mro__ Lösung einige Vorteile IMO (siehe Kommentare).

0

sollten Sie name mangling hier verwenden.

Rename defaults-__defaults

, dass jede Klasse eine eindeutige Attribut gibt, so dass sie miteinander

8

nicht wirklich eine Antwort nicht verwirrt, sondern eine Beobachtung:

Das sieht für mich overengineered ein allgemeine Falle, wenn man Ausreden sucht, um Python-Magie zu benutzen.

Wenn Sie sich bemühen, ein defaults dict für eine Klasse zu definieren, warum nicht nur die Attribute definieren? Der Effekt ist der gleiche.

class A: 
    a = 1 

class B(A): 
    b = 2 

class C(B): 
    c = 3 


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

EDIT:

Was die Frage zu beantworten, würde ich wahrscheinlich __getattribute__ in Kombination mit meinem Vorschlag wie folgt verwenden:

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

Es ist erwähnenswert, dass Sie das Attribut auch auf c setzen können. Damit erhalten Sie in der Praxis eine Instanzeinstellung mit einem in der Klasse definierten Standard. –

+0

Dies entspricht nicht dem Anwendungsfall, den ich der Frage hinzugefügt habe. –

+0

@Brecht Machiels: Ja wird es. Versuch es. –

1

Wie wäre:

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

Wenn base * not * 'None' ist, muss das entsprechende Attribut der Basis zurückgegeben werden. Aber ich denke, wir können den Namen des Attributs in der Wrapper-Funktion nicht bekommen? –

+0

@Brecht Machiels: Es ist tatsächlich möglich, zusätzliche Parameter an die 'check_base_first'-Funktion zu übergeben, auf die von' wrapper' zugegriffen werden kann. – unutbu

+1

@Brecht Machiels: Obwohl es eine Möglichkeit gibt, Eigenschaften zu verwenden, um das zu tun, was Sie wollen, ist es möglicherweise nicht die richtige Lösung. Das Problem, mit dem Sie konfrontiert sind, könnte früher ein Symptom für eine schlechte Designentscheidung sein. Wenn Sie Ihre Frage überarbeiten, um genau zu zeigen, wie sich Ihre Objekte verhalten sollen (mit "Basis"), kann vielleicht ein besseres Design vorgeschlagen werden. – unutbu

2

Ich denke, das Problem ergibt sich aus Misun den Zweck von super() zu verstehen.

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

Im Wesentlichen Ihr Objekt Einwickeln (oder Klasse) in super() macht Python überspringen Die zuletzt vererbten Klasse, wenn Attribut-Lookup. In Ihrem Code führt dies dazu, dass die Klasse C bei der Suche nach get_default übersprungen wird, was aber nichts bewirkt, da C sowieso kein get_default definiert. Dies führt natürlich zu einer Endlosschleife.

Die Lösung ist diese Funktion in jeder Klasse zu definieren, die von A. leitet Dieses mit einem metaclass getan werden kann:

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) 

Ergebnisse:

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) 

ich, dass die meisten Python beachten sollte Leute werden dies als eine sehr bizarre Lösung betrachten, und Sie sollten nur verwenden, wenn Sie wirklich benötigen, vielleicht um Legacy-Code zu unterstützen.

1

Um klarer über Ihre "Basis" vs "Standard" -Fall zu sein.

>>> 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 

Dies deckt Ihren angegebenen Anwendungsfall ab. Du brauchst keine Magie. Wenn Ihr Anwendungsfall etwas anders ist, erklären Sie, was es ist und wir sagen Ihnen, wie Sie das ohne Magie machen können. ;)