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