I have a few class hierarchies with a common method, where I want the subclasses to call the parent method before executing the subclassed method. But when writing the root classes, obviously there's no parent method to call.
It's not a big deal to add super().f(...)
to the subclass methods, but it's annoying that I sometimes forget that a class is not a root class and miss that until I trace the calls to find where the call chain stops. Is there a decorator that will do that automatically?
I tried this:
def extend_super(orig_method):
def decorated(*args, **kwargs):
if len(args) >= 1:
selfarg = args[0]
selfarg_qualname = selfarg.__class__.__qualname__
expect_qualname = f"{selfarg_qualname}.{orig_method.__name__}"
if expect_qualname == orig_method.__qualname__:
try:
super_method = getattr(super(type(selfarg), selfarg), orig_method.__name__)
super_method.__func__(*args, **kwargs)
except AttributeError:
pass
return orig_method(*args, **kwargs)
return decorated
It works - it will call the parent method, stop at the root, and will even tolerate decorating functions if you do that accidentally. But it seems awkward since it uses string operations to work. Is there an existing decorator that will do what I want, or is there a better way to make this one?
Edit:
Second edit to add that this doesn't work: explanation below.
I wanted to move the overhead of finding any parent class method out of the function call itself. I looked at gc
to get a list of all references to the function passed to the decorator, but there are no classes in that list. This is because the decorator will always be called before the class has been created, so there is no class to find.
I found an example of a Python decorator which counts the number of times it's called. It does this by adding a field to the decorated function itself, and storing the count there (https://python-course.eu/advanced-python/decorators-decoration.php).
So I figured the decorator could find the superclass method the first time it's called, and save it. That part would have to be done once whatever method was used, but all subsequent calls would just used the saved method, so it can now be used efficiently. This is what it looks like:
def extend_super(orig_method):
def decorated(*args, **kwargs):
if not decorated.checked_super_method:
decorated.checked_super_method = True
if len(args) >= 1:
selfarg = args[0]
selfarg_qualname = selfarg.__class__.__qualname__
expect_qualname = f"{selfarg_qualname}.{orig_method.__name__}"
if expect_qualname == orig_method.__qualname__:
super_class = super(type(selfarg), selfarg)
try:
decorated.super_method = getattr(super_class, orig_method.__name__)
except AttributeError:
pass
if decorated.super_method is not None:
decorated.super_method.__func__(*args, **kwargs)
return orig_method(*args, **kwargs)
decorated.checked_super_method = False
decorated.super_method = None
return decorated
I wasn't sure about adding attributes to a function, but it looks like that ability was added intentionally. Caching the result of an operation is an example use for it, which is what I'm doing here, so I'm satisfied with this.
Edit:
This doesn't work above the very lowest level, because the self
arg will always be the class of the first call. super()
will find the level above, and the code will call that, but that call will still have the original self
arg, and super()
will find the same level as before. This will repeat until a recursion level is reached.
A real solution might be able to manually take the original selfarg.__class__.__bases__
list and search it until the class of the current function call is found, or there are no more parent classes (s.__class__.__bases__[0] == type
), but that is getting to be a lot more work than I want to do, to simplify something pretty minor that I thought would be easy.