Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheriting an alias with child class MRO

Tags:

python

class parent:
    def fun1(self):
        print("foo")

    fun2 = fun1

class child(parent):
    def fun1(self):
        print("bar")

child().fun2()

This code outputs "foo". I'm pretty sure I understand why this is happening, but I'm wondering if there is a straightforward way to implement what I want (inheritable aliases that behave according to MRO, so it outputs "bar") or a reason why this is bad programming practice.

like image 852
John K Avatar asked Jan 29 '23 10:01

John K


1 Answers

There is no way to directly do what you want.

To understand why, you need to understand the way method lookup works. It's explained in detail in the Descriptor HOWTO, and I tried to write a less expert-level explanation here, so I'll assume you read both of those and just show the effects:

>>> child.fun1
<function __main__.child.fun1>
>>> child.fun2
<function __main__.parent.fun1>
>>> child().fun2
<bound method parent.fun1 of <__main__.child object at 0x10b735ba8>>

Notice that child.fun2 is parent.fun1, not child.fun1. Which explains why child().fun2 ends up as a bound method around parent.fun1, even though the self ends up as a child instance. Why? Well, obviously fun2 is not in child.__dict__ (or we wouldn't need an MRO). And in parent.__dict__, it can't be anything but parent.fun1.

So, what are the workarounds?

You could make fun2 into a property that forwards to fun2, as Patrick Haugh suggested. Or you can just do this:

def fun2(self):
    return self.fun1()

This solution has the benefit of being dead simple—anyone who can Python will understand why it works.

But Patrick's solution has the advantage of making not just child().fun2(), but also child().fun2 (as an object you can pass around, compare for identity, etc.) work the way you want it to.


Meanwhile, there is an idiom closely related to what you're asking for, where a set of public methods that you don't expect to override call a "protected" implementation method that you do. For example, a simple 1D array-math class might do this:

def _math(lhs, rhs, op):
    # not actually a useful implementation...
    return [op(x, y) for x, y in zip(lhs, rhs)] 
def __add__(self, other):
    return self._math(other, add)
def __radd__(self, other):
    return self._math(other, add)
# etc.

And now there's no asymmetry between __add__ and __radd__, only between the "public" interface (__add__ and __radd__) and the "protected" one (_math).

like image 63
abarnert Avatar answered Jan 31 '23 00:01

abarnert