Home > Software design >  How to define a superclass-only method?
How to define a superclass-only method?

Time:09-17

The snippet:

class Base:                                                                     
    def superclass_only(self):
        return 'yes'


class Foo(Base):
    pass


foo = Foo()
>>> foo.superclass_only()
yes

# Expection is to raise error
>>> foo.superclass_only()
traceback 
...
AttributeError: 'Foo' object has no attribute 'superclass_only'

How can I do if I just want to define a superclass-only method?

CodePudding user response:

TL;DR: prefix the method name with __ to trigger Python's name mangling mechanism.

answer:

You normally can't do that: it is not how inheritance is supposed to work. If you need to "hide away" methods in the "subclasses", you should rething your approach.

One first thing is to use the naming convention to indicate the method is private, which in Python we do by adding a "_" prefix to the method name: that should be an indicator to users of your Foo class that the reserved method should be used only by whoever writes the code in Base and be let alone.

Another thing is to think if you would not be better with composition than with inheritance in this case: if your Base class knows to do things that Foo can't do on itself, can you really say that "Foo objects are also Base objects"? (which is what inheritance is about).

Maybe, the better design is:

class Base:
   ...

class Bar:
   def method_foo_cant_do(...):
       ...

class Foo(Base):
   def __init__(self, ...):
       self.bar = Bar()
       ...

And finally, although not designed for that, and rather meant to avoid method-name clashes in complex hierarchies, Python has a "name mangling" mechanism, which will transparently change a method name to one including the class name as prefix. This will avoid casual use of the method in subclasses, and be an even stronger indicator that it should be used in "Base" along - but won't "prevent at all costs" that it be called.

The way to go is simply prefix the method with two underscores. At compilation time, Python translates the method to be f"_{class_name}__{method_name}", at the method declaration and in all references to it inside the class where it is declared. So Foo.__superclass_only will not reach Base.__superclass_only since the later has had its name mangled to Base._Base__superclass_only:

class Base:                                                                     
    def __superclass_only(self):
        return 'yes'

class Foo(Base):
    pass

And on the interactive interpreter:

In [3]: f= Foo()                                                                                  

In [4]: f.__superclass_only()                                                                     
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-19c8185aa9ad> in <module>
----> 1 f.__superclass_only()

But it is still reachable by using the transformed name: f._Base__superclass_only() would work.

Another thing that Python allows is to customize the way attributes are retrieved for a given class: the somewhat search for attributes and methods in a class is performed by the __getattribute__ method in each class (do not mistake it with __getattr__ which is simpler and designed to be hit only when an attribute is not found).

Reimplementing __getattribute__ is error prone and would likely leave you worse than the way you started with, and given a foo object, one would stil be able to call the superclass_only by doing Base.superclass_only(foo, ...) (i.e.:retrieving the method as an unbound method (function) from the Base class itself and passing in the foo instance manually to become the "self" argument), and mitigating this would require you to implement a correct __get_attribute__ on the metaclass - (and that would still be ultimately bypassable by one who could read the source code)

CodePudding user response:

You can wrap the superclass-only method with a decorator function that validates the current instance's class name against the method's class name, which can be derived from the method's __qualname__ attribute:

def superclass_only(method):
    def wrapper(self, *args, **kwargs):
        if self.__class__.__name__ != method.__qualname__.split('.')[-2]:
            raise NotImplementedError
        return method(self, *args, **kwargs)
    return wrapper

so that with:

class Base:
    @superclass_only
    def method(self):
        return 'yes'

class Foo(Base):
    pass

Calling Base().method() returns 'yes', while calling Foo().method() raises NotImplementedError.

Demo: https://replit.com/@blhsing/SpringgreenHonorableCharacters

  • Related