6 votes

Comment accéder aux attributs de classe d'une superclasse en Python?

Jetez un œil au code suivant :

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) # récursion infinie
            #return cls.__mro__[1].get_default(name) # cela fonctionne, cependant

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

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

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

J'ai une hiérarchie de classes, chacune avec son propre dictionnaire contenant des valeurs par défaut. Si une instance d'une classe n'a pas un attribut particulier, une valeur par défaut pour celui-ci devrait être renvoyée à la place. S'il n'y a pas de valeur par défaut pour l'attribut dans le dictionnaire defaults de la classe actuelle, le dictionnaire defaults de la superclasse devrait être recherché.

Je tente de mettre en œuvre cela en utilisant la méthode de classe récursive get_default. Malheureusement, le programme reste coincé dans une récursion infinie. Ma compréhension de super() est manifestement insuffisante. En accédant à __mro__, je peux arriver à le faire fonctionner correctement, mais je ne suis pas sûr que ce soit une solution appropriée.

J'ai l'impression que la réponse se trouve quelque part dans cet article, mais je n'ai pas encore réussi à la trouver. Peut-être devrais-je recourir à l'utilisation d'une métaclass ?

edit: Dans mon application, __getattr__ vérifie d'abord self.base. S'il n'est pas égal à None, l'attribut doit être récupéré à partir de là. Seulement dans l'autre cas, une valeur par défaut doit être renvoyée. Je pourrais probablement remplacer __getattribute__. Est-ce que ce serait la meilleure solution ?

edit 2: Ci-dessous se trouve un exemple étendu de la fonctionnalité que je recherche. Elle est actuellement implémentée en utilisant __mro__ (la suggestion précédente de unutbu, par opposition à ma méthode récursive d'origine). À moins que quelqu'un ne puisse suggérer une solution plus élégante, je suis satisfait d'utiliser cette implémentation. J'espère que cela éclaircit les choses.

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

La sortie :

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

8voto

Toni Ruža Points 5538

Pas vraiment une réponse mais une observation :

Cela me semble surconçu, un piège commun lorsqu'on cherche des excuses pour utiliser la magie du python.

Si vous prenez la peine de définir un dictionnaire defaults pour une classe, pourquoi ne pas simplement définir les attributs à la place ? l'effet est le même.

class A:
    a = 1

class B(A):
    b = 2

class C(B):
    c = 3

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

EDIT :

Quant à répondre à la question, je utiliserais probablement __getattribute__ en combinaison avec ma suggestion comme ceci :

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

2voto

bukzor Points 11085

Je pense que le problème résulte d'un malentendu sur le but de super().

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

Essentiellement, le fait d'envelopper votre objet (ou classe) dans super() fait que Python saute la classe la plus récemment héritée lors de la recherche d'attributs. Dans votre code, cela entraîne le saut de la classe C lors de la recherche de get_default, mais cela n'a vraiment aucun effet, car C ne définit de toute façon pas de get_default. Naturellement, cela entraîne une boucle infinie.

La solution est de définir cette fonction dans chaque classe qui dérive de A. Cela peut être fait en utilisant une métaclass :

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)

résultats :

A.get_default(c) - 
{'c': 3}
('c.c =', 3)
A.get_default(b) - 
{'c': 3}
A.get_default(b) - 
{'b': 2}
('c.b =', 2)
A.get_default(a) - 
{'c': 3}
A.get_default(a) - 
{'b': 2}
A.get_default(a) - 
{'a': 1}
('c.a =', 1)

Je tiens à noter que la plupart des personnes qui utilisent Python considéreront cette solution comme très bizarre, et vous ne devriez l'utiliser que si vous en avez vraiment besoin, peut-être pour prendre en charge du code hérité.

1voto

unutbu Points 222216

Que diriez-vous de :

classe A(objet):
    def __init__(self,base=None):
        self.a=1
        if base n'est pas None:
            self.set_base(base)
        super(A,self).__init__() 
    def set_base(self,base):
        pour clé dans ('a b c'.split()):
            setattr(self,key,getattr(base,key))
classe B(A): 
    def __init__(self,base=None):
        self.b=2
        super(B,self).__init__(base)        
classe 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

1voto

Lennart Regebro Points 52510

Pour être plus clair à propos de votre cas de "base" par rapport à de "défaut".

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

Cela couvre votre cas d'utilisation déclaré. Vous n'avez besoin d'aucune magie. Si votre cas d'utilisation est quelque peu différent, expliquez ce que c'est, et nous vous dirons comment le faire sans magie. ;)

0voto

gnibbler Points 103484

Vous devriez utiliser le mélange de noms ici.

Renommez defaults en __defaults

Cela donne à chaque classe un attribut non ambigu pour ne pas les confondre entre eux

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X