I have a master class for a planet:
class Planet:
def __init__(self,name):
self.name = name
(...)
def destroy(self):
(...)
I also have a few classes that inherit from Planet
and I want to make one of them unable to be destroyed (not to inherit the destroy
function)
Example:
class Undestroyable(Planet):
def __init__(self,name):
super().__init__(name)
(...)
#Now it shouldn't have the destroy(self) function
So when this is run,
Undestroyable('This Planet').destroy()
it should produce an error like:
AttributeError: Undestroyable has no attribute 'destroy'
Metaclasses and descriptor protocols are fun, but perhaps overkill. Sometimes, for raw functionality, you can't beat good ole'
__slots__
.If
Undestroyable
is a unique (or at least unusual) case, it's probably easiest to just redefinedestroy()
:From the point of view of the user of the class, this will behave as though
Undestroyable.destroy()
doesn't exist … unless they go poking around withhasattr(Undestroyable, 'destroy')
, which is always a possibility.If it happens more often that you want subclasses to inherit some properties and not others, the mixin approach in chepner's answer is likely to be more maintainable. You can improve it further by making
Destructible
an abstract base class:This has the advantage that if you try to instantiate the abstract class
Destructible
, you'll get an error pointing you at the problem:… similarly if you inherit from
Destructible
but forget to definedestroy()
:You cannot inherit only a portion of a class. Its all or nothing.
What you can do is to put the destroy function in a second level of the class, such you have the Planet-class without the destry-function, and then you make a DestroyablePlanet-Class where you add the destroy-function, which all the destroyable planets use.
Or you can put a flag in the construct of the Planet-Class which determines if the destroy function will be able to succeed or not, which is then checked in the destroy-function.
The mixin approach in other answers is nice, and probably better for most cases. But nevertheless, it spoils part of the fun - maybe obliging you to have separate planet-hierarchies - like having to live with two abstract classes each ancestor of "destroyable" and "non-destroyable".
First approach: descriptor decorator
But Python has a powerful mechanism, called the "descriptor protocol", which is used to retrieve any attribute from a class or instance - it is even used to ordinarily retrieve methods from instances - so, it is possible to customize the method retrieval in a way it checks if it "should belong" to that class, and raise attribute error otherwise.
The descriptor protocol mandates that whenever you try to get any attribute from an instance object in Python, Python will check if the attribute exists in that object's class, and if so, if the attribute itself has a method named
__get__
. If it has,__get__
is called (with the instance and class where it is defined as parameters) - and whatever it returns is the attribute. Python uses this to implement methods: functions in Python 3 have a__get__
method that when called, will return another callable object that, in turn, when called will insert theself
parameter in a call to the original function.So, it is possible to create a class whose
__get__
method will decide whether to return a function as a bound method or not depending on the outer class been marked as so - for example, it could check an specific flagnon_destrutible
. This could be done by using a decorator to wrap the method with this descriptor functionalityAnd on the interactive prompt:
Perceive that unlike simply overriding the method, this approach raises the error when the attribute is retrieved - and will even make
hasattr
work:Although, it won't work if one tries to retrieve the method directly from the class, instead of from an instance - in that case the
instance
parameter to__get__
is set to None, and we can't say from which class it was retrieved - just theowner
class, where it was declared.Second approach:
__delattr__
on the metaclass:While writting the above, it occurred me that Pythn does have the
__delattr__
special method. If thePlanet
class itself implements__delattr__
and we'd try to delete thedestroy
method on specifc derived classes, it wuld nt work:__delattr__
gards the attribute deletion of attributes in instances - and if you'd try todel
the "destroy" method in an instance, it would fail anyway, since the method is in the class.However, in Python, the class itself is an instance - of its "metaclass". That is usually
type
. A proper__delattr__
on the metaclass of "Planet" could make possible the "disinheitance" of the "destroy" method by issuing a `del UndestructiblePlanet.destroy" after class creation.Again, we use the descriptor protocol to have a proper "deleted method on the subclass":
And with this method, even trying to retrieve or check for the method existense on the class itself will work:
metaclass with a custom
__prepare__
method.Since metaclasses allow one to customize the object that contains the class namespace, it is possible to have an object that responds to a
del
statement within the class body, adding aDeleted
descriptor.For the user (programmer) using this metaclass, it is almost the samething, but for the
del
statement been allowed into the class body itself:(The 'deleted' descriptor is the correct form to mark a method as 'deleted' - in this method, though, it can't know the class name at class creation time)
As a class decorator:
And given the "deleted" descriptor, one could simply inform the methods to be removed as a class decorator - there is no need for a metaclass in this case:
Modifying the
__getattribute__
mechanism:For sake of completeness - what really makes Python reach methods and attributes on the super-class is what happens inside the
__getattribute__
call. n theobject
version of__getattribute__
is where the algorithm with the priorities for "data-descriptor, instance, class, chain of base-classes, ..." for attribute retrieval is encoded.So, changing that for the class is an easy an unique point to get a "legitimate" attribute error, without need for the "non-existent" descritor used on the previous methods.
The problem is that
object
's__getattribute__
does not make use oftype
's one to search the attribute in the class - if it did so, just implementing the__getattribute__
on the metaclass would suffice. One have to do that on the instance to avoid instance lookp of an method, and on the metaclass to avoid metaclass look-up. A metaclass can, of course, inject the needed code:Rather than remove an attribute that is inherited, only inherit
destroy
in the subclasses where it is applicable, via a mix-in class. This preserves the correct "is-a" semantics of inheritance.You can provide suitable definitions for
destroy
in any ofDestructible
,Planet
, or any class that inherits fromPlanet
.