web-dev-qa-db-fra.com

Python: suppression d'un attribut de classe dans une sous-classe

J'ai une sous-classe et je veux qu'elle pas inclue un attribut de classe présent sur la classe de base.

J'ai essayé, mais cela ne fonctionne pas:

>>> class A(object):
...     x = 5
>>> class B(A):
...     del x
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    class B(A):
  File "<pyshell#1>", line 2, in B
    del x
NameError: name 'x' is not defined

Comment puis-je faire ceci?

43
Ram Rachum

Réfléchissez bien à la raison pour laquelle vous voulez faire cela; vous ne le faites probablement pas. Pensez à ne pas faire hériter B de A.

L'idée du sous-classement est de spécialiser un objet. En particulier, les enfants d'une classe doivent être des instances valides de la classe parente:

>>> class foo(dict): pass
>>> isinstance(foo(), dict)
... True

Si vous implémentez ce comportement (avec par exemple x = property(lambda: AttributeError)), vous cassez le concept de sous-classement, et c'est mauvais.

12
Katriel

Vous pouvez utiliser delattr(class, field_name) pour le supprimer de la définition de classe.

Exemple complet:

# python 3
class A:  
    def __init__(self):
        self.field_to_keep = "keep this in B"
        self.field_to_delete = "don't want this in B"

class B(A):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        delattr(self, 'field_to_delete')
51
Bhushan

Vous n'avez pas besoin de le supprimer. Remplacez-le.

class B(A):
   x = None

ou tout simplement ne pas y faire référence.

Ou envisagez une conception différente (attribut d'instance?).

17
Keith

Aucune des réponses n'avait fonctionné pour moi.

Par exemple, delattr(SubClass, "attrname") (ou son équivalent exact, del SubClass.attrname) Ne "masquera" pas une méthode parent, car ce n'est pas ainsi que fonctionne la résolution de méthode. Il échouerait avec AttributeError('attrname',) à la place, car la sous-classe n'a pas attrname. Et, bien sûr, remplacer l'attribut par None ne le supprime pas réellement.

Considérons cette classe de base:

class Spam(object):
    # Also try with `expect = True` and with a `@property` decorator
    def expect(self):
        return "This is pretty much expected"

Je ne connais que deux façons de le sous-classer, en masquant l'attribut expect:

  1. Utilisation d'une classe de descripteur qui soulève AttributeError de __get__ . Lors de la recherche d'attributs, il y aura une exception, généralement indiscernable d'un échec de recherche.

    La manière la plus simple consiste simplement à déclarer une propriété qui déclenche AttributeError. C'est essentiellement ce que @JBernardo avait suggéré.

    class SpanishInquisition(Spam):
        @property
        def expect(self):
            raise AttributeError("Nobody expects the Spanish Inquisition!")
    
    assert hasattr(Spam, "expect") == True
    # assert hasattr(SpanishInquisition, "expect") == False  # Fails!
    assert hasattr(SpanishInquisition(), "expect") == False
    

    Cependant, cela ne fonctionne que pour les instances et non pour les classes (l'assertion hasattr(SpanishInquisition, "expect") == True serait rompue).

    Si vous souhaitez que toutes les affirmations ci-dessus restent vraies, utilisez ceci:

    class AttributeHider(object):
        def __get__(self, instance, owner):
            raise AttributeError("This is not the attribute you're looking for")
    
    class SpanishInquisition(Spam):
        expect = AttributeHider()
    
    assert hasattr(Spam, "expect") == True
    assert hasattr(SpanishInquisition, "expect") == False  # Works!
    assert hasattr(SpanishInquisition(), "expect") == False
    

    Je pense que c'est la méthode la plus élégante, car le code est clair, générique et compact. Bien sûr, il faut vraiment réfléchir à deux fois si la suppression de l'attribut est ce qu'ils veulent vraiment.

  2. Remplacement de la recherche d'attribut avec la méthode magique __getattribute__ . Vous pouvez le faire soit dans une sous-classe (ou dans un mixin, comme dans l'exemple ci-dessous, car je ne voulais l'écrire qu'une seule fois), et cela masquerait l'attribut de la sous-classe instances. Si vous souhaitez également masquer la méthode de la sous-classe, vous devez utiliser des métaclasses.

    class ExpectMethodHider(object):
        def __getattribute__(self, name):
            if name == "expect":
                raise AttributeError("Nobody expects the Spanish Inquisition!")
            return super().__getattribute__(name)
    
    class ExpectMethodHidingMetaclass(ExpectMethodHider, type):
        pass
    
    # I've used Python 3.x here, thus the syntax.
    # For Python 2.x use __metaclass__ = ExpectMethodHidingMetaclass
    class SpanishInquisition(ExpectMethodHider, Spam,
                             metaclass=ExpectMethodHidingMetaclass):
        pass
    
    assert hasattr(Spam, "expect") == True
    assert hasattr(SpanishInquisition, "expect") == False
    assert hasattr(SpanishInquisition(), "expect") == False
    

    Cela semble pire (plus verbeux et moins générique) que la méthode ci-dessus, mais on peut également considérer cette approche.

    Notez que cela ne fonctionne pas sur des méthodes spéciales ("magiques") (par exemple __len__), Car celles-ci contournent __getproperty__. Consultez la section Special Method Lookup de la documentation Python pour plus de détails. Si c'est ce dont vous avez besoin pour annuler, remplacez-le simplement et appelez object implémentation, en sautant le parent.

Inutile de dire que cela ne s'applique qu'aux "classes de nouveau style" (celles qui héritent de object), car les méthodes magiques et les protocoles de descripteur n'y sont pas pris en charge. Espérons que ce soit une chose du passé.

7
drdaeman

Vous pouvez peut-être définir x comme property et déclencher AttributeError chaque fois que quelqu'un essaie d'y accéder.

>>> class C:
        x = 5

>>> class D(C):
        def foo(self):
             raise AttributeError
        x = property(foo)

>>> d = D()
>>> print(d.x)
File "<pyshell#17>", line 3, in foo
raise AttributeError
AttributeError
7
JBernardo

J'ai également eu le même problème et je pensais avoir une raison valable de supprimer l'attribut de classe dans la sous-classe: ma superclasse (appelez-la A) avait une propriété en lecture seule qui fournissait la valeur de l'attribut, mais dans ma sous-classe (appelez-le B), l'attribut était une variable d'instance en lecture/écriture. J'ai trouvé que Python appelait la fonction de propriété même si je pensais que la variable d'instance aurait dû la remplacer. J'aurais pu créer une fonction getter distincte à utiliser pour accéder à la propriété sous-jacente, mais cela semblait être un encombrement inutile et inélégant de l'espace de noms de l'interface (comme si cela importait vraiment).

En fait, la réponse a été de créer une nouvelle superclasse abstraite (appelez-la S) avec les attributs communs d'origine de A, et de faire dériver A et B de S. Puisque Python a du typage de canard , peu importe que B n'étende pas A, je peux toujours les utiliser aux mêmes endroits, car ils implémentent implicitement la même interface.

5
acjay

Essayer de le faire est probablement une mauvaise idée, mais ...

Il ne semble pas être possible de le faire via un héritage "approprié" en raison de la façon dont la recherche B.x fonctionne par défaut. Lorsque vous obtenez B.x le x est d'abord recherché dans B et s'il n'y est pas trouvé, il est recherché dans A, mais d'un autre côté lors de la définition ou de la suppression de B.x uniquement B sera recherché. Donc par exemple

>>> class A:
>>>     x = 5

>>> class B(A):
>>>    pass

>>> B.x
5

>>> del B.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>  
AttributeError: class B has no attribute 'x'

>>> B.x = 6
>>> B.x
6

>>> del B.x
>>> B.x
5

Ici, nous voyons que nous ne semblons pas pouvoir supprimer B.x car il n'existe pas (A.x existe et est ce qui est servi lorsque vous évaluez B.x). Cependant, en définissant B.x à 6 le B.x existera, il peut être récupéré par B.x et supprimé par del B.x par lequel il cesse d'exister, donc après cela à nouveau A.x sera servi en réponse à B.x.

Ce que vous pourriez faire d'autre part est d'utiliser des métaclasses pour faire B.x augmenter AttributeError:

class NoX(type):
    @property
    def x(self):
        raise AttributeError("We don't like X")

class A(object):
    x = [42]

class B(A, metaclass=NoX):
    pass

print(A.x)
print(B.x)

Maintenant, bien sûr, les puristes peuvent crier que cela brise le LSP, mais ce n'est pas si simple. Tout se résume à si vous considérez que vous avez créé un sous-type en faisant cela. Les méthodes issubclass et isinstance disent oui, mais LSP dit non (et de nombreux programmeurs supposent "oui" puisque vous héritez de A).

Le LSP signifie que si B est un sous-type de A alors nous pourrions utiliser B chaque fois que nous pourrions utiliser A, mais puisque nous ne pouvons pas le faire en faisant cette construction, nous pourrions conclure que B n'est en fait pas un sous-type de A et donc LSP n'est pas violé.

1
skyking