14

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' 

5 Answers 5

17
+100

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 the self 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 flag non_destrutible. This could be done by using a decorator to wrap the method with this descriptor functionality

class Muteable: def __init__(self, flag_attr): self.flag_attr = flag_attr def __call__(self, func): """Called when the decorator is applied""" self.func = func return self def __get__(self, instance, owner): if instance and getattr(instance, self.flag_attr, False): raise AttributeError('Objects of type {0} have no {1} method'.format(instance.__class__.__name__, self.func.__name__)) return self.func.__get__(instance, owner) class Planet: def __init__(self, name=""): pass @Muteable("undestroyable") def destroy(self): print("Destroyed") class BorgWorld(Planet): undestroyable = True 

And on the interactive prompt:

In [110]: Planet().destroy() Destroyed In [111]: BorgWorld().destroy() ... AttributeError: Objects of type BorgWorld have no destroy method In [112]: BorgWorld().destroy AttributeError: Objects of type BorgWorld have no destroy method 

Perceive that unlike simply overriding the method, this approach raises the error when the attribute is retrieved - and will even make hasattr work:

In [113]: hasattr(BorgWorld(), "destroy") Out[113]: False 

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 the owner class, where it was declared.

In [114]: BorgWorld.destroy Out[114]: <function __main__.Planet.destroy> 

Second approach: __delattr__ on the metaclass:

While writting the above, it occurred me that Pythn does have the __delattr__ special method. If the Planet class itself implements __delattr__ and we'd try to delete the destroy method on specifc derived classes, it wuld nt work: __delattr__ gards the attribute deletion of attributes in instances - and if you'd try to del 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":

class Deleted: def __init__(self, cls, name): self.cls = cls.__name__ self.name = name def __get__(self, instance, owner): raise AttributeError("Objects of type '{0}' have no '{1}' method".format(self.cls, self.name)) class Deletable(type): def __delattr__(cls, attr): print("deleting from", cls) setattr(cls, attr, Deleted(cls, attr)) class Planet(metaclass=Deletable): def __init__(self, name=""): pass def destroy(self): print("Destroyed") class BorgWorld(Planet): pass del BorgWorld.destroy 

And with this method, even trying to retrieve or check for the method existense on the class itself will work:

In [129]: BorgWorld.destroy ... AttributeError: Objects of type 'BorgWorld' have no 'destroy' method In [130]: hasattr(BorgWorld, "destroy") Out[130]: False 

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 a Deleted 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:

class Deleted: def __init__(self, name): self.name = name def __get__(self, instance, owner): raise AttributeError("No '{0}' method on class '{1}'".format(self.name, owner.__name__)) class Deletable(type): def __prepare__(mcls,arg): class D(dict): def __delitem__(self, attr): self[attr] = Deleted(attr) return D() class Planet(metaclass=Deletable): def destroy(self): print("destroyed") class BorgPlanet(Planet): del destroy 

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

class Deleted: def __init__(self, cls, name): self.cls = cls.__name__ self.name = name def __get__(self, instance, owner): raise AttributeError("Objects of type '{0}' have no '{1}' method".format(self.cls, self.name)) def mute(*methods): def decorator(cls): for method in methods: setattr(cls, method, Deleted(cls, method)) return cls return decorator class Planet: def destroy(self): print("destroyed") @mute('destroy') class BorgPlanet(Planet): pass 

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 the object 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 of type'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:

def blocker_getattribute(target, attr, attr_base): try: muted = attr_base.__getattribute__(target, '__muted__') except AttributeError: muted = [] if attr in muted: raise AttributeError("object {} has no attribute '{}'".format(target, attr)) return attr_base.__getattribute__(target, attr) def instance_getattribute(self, attr): return blocker_getattribute(self, attr, object) class M(type): def __init__(cls, name, bases, namespace): cls.__getattribute__ = instance_getattribute def __getattribute__(cls, attr): return blocker_getattribute(cls, attr, type) class Planet(metaclass=M): def destroy(self): print("destroyed") class BorgPlanet(Planet): __muted__=['destroy'] # or use a decorator to set this! :-) pass 
Sign up to request clarification or add additional context in comments.

4 Comments

I wondered whether anyone would bother with something like this (ridiculously over-engineered, but great fun) descriptor/metaclass approach. Kudos, sir! +50 bounty if you add a @mute('destroy') class decorator to avoid the del statement ;-)
@ZeroPiraeus: hmmm...class decorators - one does not even need a metaclass in this way, as the "deleted" descriptors can be set directly by it!
Awesome :-) Your metaclass with a custom __prepare__ isn't working (once the indentation error and incorrect BorgPlanet superclass are fixed, del destroy raises NameError), but the class decorator turned out very nicely :-) I'll apply the bounty once the 48 hour limit is up.
Sorry - the __prepare__ one was broken due to a bad signature in Deleted.__init__ - it is one of those cases were Python translates the exceptions (in this case, from a TypeError on the call to a NameError due to __delitem__ failing)
10

If Undestroyable is a unique (or at least unusual) case, it's probably easiest to just redefine destroy():

class Undestroyable(Planet): # ... def destroy(self): cls_name = self.__class__.__name__ raise AttributeError("%s has no attribute 'destroy'" % cls_name) 

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 with hasattr(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:

from abc import abstractmethod, ABCMeta class Destructible(metaclass=ABCMeta): @abstractmethod def destroy(self): pass class BasePlanet: # ... pass class Planet(BasePlanet, Destructible): def destroy(self): # ... pass class IndestructiblePlanet(BasePlanet): # ... pass 

This has the advantage that if you try to instantiate the abstract class Destructible, you'll get an error pointing you at the problem:

>>> Destructible() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class Destructible with abstract methods destroy 

… similarly if you inherit from Destructible but forget to define destroy():

class InscrutablePlanet(BasePlanet, Destructible): pass 

>>> InscrutablePlanet() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class InscrutablePlanet with abstract methods destroy 

Comments

5

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.

class Destructible(object): def destroy(self): pass class BasePlanet(object): ... class Planet(BasePlanet, Destructible): ... class IndestructiblePlanet(BasePlanet): # Does *not* inherit from Destructible ... 

You can provide suitable definitions for destroy in any of Destructible, Planet, or any class that inherits from Planet.

Comments

3

Metaclasses and descriptor protocols are fun, but perhaps overkill. Sometimes, for raw functionality, you can't beat good ole' __slots__.

class Planet(object): def __init__(self, name): self.name = name def destroy(self): print("Boom! %s is toast!\n" % self.name) class Undestroyable(Planet): __slots__ = ['destroy'] def __init__(self,name): super().__init__(name) print() x = Planet('Pluto') # Small, easy to destroy y = Undestroyable('Jupiter') # Too big to fail x.destroy() y.destroy() Boom! Pluto is toast! Traceback (most recent call last): File "planets.py", line 95, in <module> y.destroy() AttributeError: destroy 

4 Comments

Why is the __init__ method necessary in Undestroyable? I tried to implement Undestroyable without __init__ definition and the result was still the same. Any particular reason you included it?
It says here that while inheriting from a class without __slots__ the __dict__ of that class is always accessible so defining __slots__ is not useful? Can you please explain why/how it is different in this case?
@ShikharChauhan This was just to show that the __init__ method could be there (and used to initialize other stuff if necessary). You are right that for the minimal example it is not required.
@ShikharChauhan That statement about __slots__ is correct, in that the object instance will still have a __dict__. But the method attribute is in the class __dict__, not the instance __dict__.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.