Home > Back-end >  Inheritance: emulating non-virtual functions in Python
Inheritance: emulating non-virtual functions in Python

Time:03-18

I am new to Python, I am coming from C so I suspect my way of thinking is "tainted" by my preconceived notions. I will explain what I am trying to do and the issue I am facing, but please be aware that the code below is an "artificial" little example that reproduces my issue.

Say that at some point I have this scenario, where B only overrides A.plot_and_clear() as that is all I need from B:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def clear(self):
        print("clear A start")
        self.x = 0
        self.y = 0
        print("clear A end")

    def plot(self):
        print("plot A start")
        print(str(self.x))
        print(str(self.y))
        print("plot A end")

    def plot_and_clear(self):
        print("plot & clear A start")
        self.plot()
        self.clear()
        print("plot & clear A end")


class B(A):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def plot_and_clear(self):
        print("plot & clear B BAD start")
        super().plot_and_clear()
        print(str(self.z))
        self.z = 0
        print("plot & clear B BAD end")

def main():
    myObject = B(1, 2, 3)
    myObject.plot_and_clea()

main()

In this case the output is exactly what I would expect and goes as follows:

plot & clear B start
plot & clear A start
plot A start
1
2
plot A end
clear A start
clear A end
plot & clear A end
3
plot & clear B end

Later on I realize that I also need to override A.plot() with B.plot(), with all the rest remaining the same like so:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def clear(self):
        print("clear A start")
        self.x = 0
        self.y = 0
        print("clear A end")

    def plot(self):
        print("plot A start")
        print(str(self.x))
        print(str(self.y))
        print("plot A end")

    def plot_and_clear(self):
        print("plot & clear A start")
        self.plot()
        self.clear()
        print("plot & clear A end")


class B(A):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def plot(self):
        print("plot B start")
        super().plot()
        print(str(self.z))
        print("plot B end")

    def plot_and_clear(self):
        print("plot & clear B BAD start")
        super().plot_and_clear()
        print(str(self.z))
        self.z = 0
        print("plot & clear B BAD end")

def main():
    myObject = B(1, 2, 3)
    myObject.plot_and_clea()

main()

Now if we run the same main, the output is changed and erroneous and goes like this:

plot & clear B start
plot & clear A start
plot B start
plot A start
1
2
plot A end
3
plot B end
clear A start
clear A end
plot & clear A end
3
plot & clear B end

This is because now, the super().plot_and_clear() from B.plot_and_clear() calls B.plot() instead of A.plot(). The result of this is that by simply adding a function I have broken the previously good behavior of B.plot_and_clear(), which is puzzling to say the least.

I understand this is due to how the MRO works in python, which as it seems, is completely flipped compared to c . Now wheter or not we know the exact reason why this happens I would still argue that the behavior is not desirable and there should be a way to prevent it, either by choosing some "safe" code structure or with some other language constructs.

Any idea of how I can either work "around" or "along" this aspect of the language?

Thank you very much.

CodePudding user response:

__init__ should only be used to initialize an existing object. (Though the creation of the object and the call to __init__ usually both happen inside the call to the type itself.)

Use dedicated class methods as alternative constructors (such as copy constructors or constructing an object from another object). For example,

class Object:
    def __init__(self, *, mass=0, **kwargs):
        super().__init__(**kwargs)
        self.mass_ = mass

    @classmethod
    def from_object(cls, obj, **kwargs):
        return cls(mass=obj.mass_, **kwargs)


class Vehicle(Object):
    def __init__(self, *, wheels=4, **kwargs):
        super().__init__(**kwargs)
        self.wheels_ = wheels
 
    @classmethod
    def from_vehicle(cls, vehicle, **kwargs):
        return cls(mass=vehicle.mass_, wheels=vehicle.wheels_, **kwargs)


o = Object(mass=100)
v1 = Vehicle.from_object(o)
v2 = Vehicle.from_vehicle(v2)

v3 = Vehicle.from_object(o, wheels=6)
v4 = Vehicle.from_vehicle(v3)

See https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ for why we use keyword arguments.

Even though from_object itself does not expect any additional keyword arguments, we accept keywords aruments that cls (which could be Object or any subclass of Object) might expect.

Note, too, that Vehicle itself doesn't have to define from_object; Vehicle.from_object will instead use Object.from_object to create a vehicle. That might sound strange, but the job of Object.from_object is not necessarily to create an Object, but to know how to "unpack" an instance of Object in order to create an instance of cls.

CodePudding user response:

Thanks to the lengthy discussion in the comments above I think I have grasped the concept that in python all functions are virtual, and thus the described behavior is exactly what one would expect in C as well, when all base-class methods were virtual.

It would be however rather nice to have the option to declare base-class functions as non-virtual, so that one can control exactly how a base class method behaves when calling it from within another base-class method, regardless of what child classes do with their overrides.

So far I found two methods to somewhat reproduce that functionality or at least to partially control the behavior of the base-class:

Method 1): Call the base-class methods explicitly within other base-class methods when non-virtual behavior is needed:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def clear(self):
        print("clear A start")
        self.x = 0
        self.y = 0
        print("clear A end")

    def plot(self):
        print("plot A start")
        print(str(self.x))
        print(str(self.y))
        print("plot A end")

    def plot_and_clear(self):
        print("plot & clear A start")
        A.plot(self)
        A.clear(self)
        print("plot & clear A end")


class B(A):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def plot(self):
        print("plot B start")
        super().plot()
        print(str(self.z))
        print("plot B end")

    def plot_and_clear(self):
        print("plot & clear B BAD start")
        super().plot_and_clear()
        print(str(self.z))
        self.z = 0
        print("plot & clear B BAD end")

def main():
    myObject = B(1, 2, 3)
    myObject.plot_and_clea()

main()

# Output as desired despite the presence of overrides:

# plot & clear B start
# plot & clear A start
# plot A start
# 1
# 2
# plot A end
# clear A start
# clear A end
# plot & clear A end
# 3
# plot & clear B end

Method 2) Name-Mangle those functions that need to be non-virtual, and only use the name-mangled version internally:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def clear(self):
        print("clear A start")
        self.x = 0
        self.y = 0
        print("clear A end")
    __clear = clear

    def plot(self):
        print("plot A start")
        print(str(self.x))
        print(str(self.y))
        print("plot A end")
    __plot = plot


    def plot_and_clear(self):
        print("plot & clear A start")
        self.__plot()
        self.__clear()
        print("plot & clear A end")


class B(A):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

    def plot(self):
        print("plot B start")
        super().plot()
        print(str(self.z))
        print("plot B end")

    def plot_and_clear(self):
        print("plot & clear B BAD start")
        super().plot_and_clear()
        print(str(self.z))
        self.z = 0
        print("plot & clear B BAD end")

def main():
    myObject = B(1, 2, 3)
    myObject.plot_and_clea()

main()

# Output as desired despite the presence of overrides:

# plot & clear B start
# plot & clear A start
# plot A start
# 1
# 2
# plot A end
# clear A start
# clear A end
# plot & clear A end
# 3
# plot & clear B end

I hope this helps anyone that, like me, is having a tough time switching from a static to a dynamic language, even though probably at some point we should better embrace the language's core philosophy without trying to forcefully fit it to our old ways :)

  • Related