I am manipulating the creation of classes via Python's metaclasses. However, although a class has a attribute thanks to its parent, I can not delete it.
class Meta(type):
def __init__(cls, name, bases, dct):
super().__init__(name, bases, dct)
if hasattr(cls, "x"):
print(cls.__name__, "has x, deleting")
delattr(cls, "x")
else:
print(cls.__name__, "has no x, creating")
cls.x = 13
class A(metaclass=Meta):
pass
class B(A):
pass
The execution of the above code yield an AttributeError
when class B
is created:
A has no x, creating
B has x, deleting
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-3-49e93612dcb8> in <module>()
10 class A(metaclass=Meta):
11 pass
---> 12 class B(A):
13 pass
14 class C(B):
<ipython-input-3-49e93612dcb8> in __init__(cls, name, bases, dct)
4 if hasattr(cls, "x"):
5 print(cls.__name__, "has x, deleting")
----> 6 delattr(cls, "x")
7 else:
8 print(cls.__name__, "has no x, creating")
AttributeError: x
Why can't I delete the existing attribute?
EDIT: I think my question is different to delattr on class instance produces unexpected AttributeError which tries to delete a class variable via the instance. In contrast, I try to delete a class variable (alias instance) via the class (alias instance). Thus, the given fix does NOT work in this case.
EDIT2: olinox14 is right, it's an issue of "delete attribute of parent class". The problem can be reduced to:
class A:
x = 13
class B(A):
pass
del B.x
As you concluded in your simplified version, what goes on is simple: the attribute "x" is not in the class, it is in the superclasses, and normal Python attribute lookup will fetch it from there for reading - and on writting, that is, setting a new cls.x
will create a local x in he subclass:
In [310]: class B(A):
...: pass
...:
In [311]: B.x
Out[311]: 1
In [312]: del B.x
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-312-13d95ac593bf> in <module>
----> 1 del B.x
AttributeError: x
In [313]: B.x = 2
In [314]: B.__dict__["x"]
Out[314]: 2
In [315]: B.x
Out[315]: 2
In [316]: del B.x
In [317]: B.x
Out[317]: 1
If you need to suppress atributes in inherited classes, it is possible, though, through a custom __getattribute__
method (not __getattr__
) in the metaclass. There is even no need for other methods on the metaclass (though you can use them, for, for example, editing a list of attributes to suppress)
class MBase(type):
_suppress = set()
def __getattribute__(cls, attr_name):
val = super().__getattribute__(attr_name)
# Avoid some patologic re-entrancies
if attr_name.startswith("_"):
return val
if attr_name in cls._suppress:
raise AttributeError()
return val
class A(metaclass=MBase):
x = 1
class B(A):
_suppress = {"x",}
If one tries to get B.x
it will raise.
With this startegy, adding __delattr__
and __setattr__
methods to the metaclass enables one to del attributes that are defined in the superclasses just on the subclass:
class MBase(type):
_suppress = set()
def __getattribute__(cls, attr_name):
val = super().__getattribute__(attr_name)
# Avoid some patologic re-entrancies
if attr_name.startswith("_"):
return val
if attr_name in cls._suppress:
raise AttributeError()
return val
def __delattr__(cls, attr_name):
# copy metaclass _suppress list to class:
cls._suppress = set(cls._suppress)
cls._suppress.add(attr_name)
try:
super().__delattr__(attr_name)
except AttributeError:
pass
def __setattr__(cls, attr_name, value):
super().__setattr__(attr_name, value)
if not attr_name.startswith("_"):
cls._suppress -= {attr_name,}
class A(metaclass=MBase):
x = 1
class B(A):
pass
And on the console:
In [400]: B.x
Out[400]: 1
In [401]: del B.x
In [402]: B.x
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
...
In [403]: A.x
Out[403]: 1
It seems like python register the x
variable as a paramameter of the A class:
Then, when you try to delete it from the B
class, there is some conflict with the delattr
method, like mentionned in the link that @David Herring provided...
A workaround could be deleting the parameter from the A
class explicitly:
delattr(A, "x")