1

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?

yixuan
  • 375
  • 2
  • 17
  • Ok, but when do you want it to be legal to call `superclass_only()`? – quamrana Sep 03 '21 at 09:33
  • There's no such thing. Any child class *is a* superclass, meaning you should be able to use an instance of a child class anywhere you could use an instance of the superclass. This behaviour would break that basic assumption. You could do this with `__` name mangling if it's an *internal* call in the super class, but in your example the method is a public method expected to be part of the public interface of any `Base` and all its sub classes. – deceze Sep 03 '21 at 09:33
  • This feels the same as [Prevent function overriding in Python](https://stackoverflow.com/questions/3948873/prevent-function-overriding-in-python) – Sayse Sep 03 '21 at 09:36
  • @quamrana I just want superclass to call the method but subclasses – yixuan Sep 03 '21 at 09:41
  • @deceze Maybe it's not appropriate from perspective of inheritance, but is there any approach to implement it? – yixuan Sep 03 '21 at 09:43
  • What's the use case? Perhaps we can tell you a proper OOP way that covers what you're trying to achieve here. – deceze Sep 03 '21 at 09:46
  • 1
    You are breaking the [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). Of course there are times when you must break it, but do so carefully. – timgeb Sep 03 '21 at 09:48
  • Of course, you could explicitly check the type of `self` in `superclass_only` and raise an error if it's not `Base`. – timgeb Sep 03 '21 at 09:50
  • @deceze The design is most likely wrong for the case I met before. For that, I maybe will ask another question in SO. when I review it, I get a question: I wonder then how can I do it in way asked above – yixuan Sep 03 '21 at 10:00

2 Answers2

2

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)

jsbueno
  • 99,910
  • 10
  • 151
  • 209
1

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

blhsing
  • 91,368
  • 6
  • 71
  • 106