Python – Inherited methods break when overriding __init__

Question:

I have a geometric base class ExtrudedSurface and a child class Cylinder, which is a ‘kind of’ extruded surface.
The base class has a method to translate itself (not in-place) by constructing a modified version of itself.

I would like to re-use this method by the child class Cylinder, and have it return a new, translated Cylinder. However, as implemented now this does not work because Cylinder has a different __init__ signature which is called in translate.

What is the best way to achieve this? How can Cylinder use the inherited method translate and return a new Cylinder object?

EDIT 1: I think it has to do with LSP violation but I’m not sure.

class ExtrudedSurface:
    def __init__(self, profile: Curve, direction: Vector, length: float):
        """
        An extruded surface geometry created by sweeping a 3D 'profile' curve along a 
        'direction' for a given 'length'.
        """

        self.profile = profile
        self.direction = direction.normalize()
        self.length = length

    def translate(self, translation: Vector) -> ExtrudedSurface:
        """Return a translated version of itself."""

        return self.__class__(
            self.profile.translate(translation),
            self.length,
            self.direction
        )



class Cylinder(ExtrudedSurface):
    def __init__(self, point: Point, direction: Vector, radius: float, length: float):
        """Cylinder surface.

        Args:
            point: Center point of extruded circle.
            direction: Direction vector of the cylinder (axis).
            radius: Cylinder radius.
            length: Extrusion length in 'direction'.
        """

        direction = direction.normalize()

        profile = Circle(point, direction, radius)

        super().__init__(profile, direction, length)
Asked By: CyrielN

||

Answers:

Short story short: the by-the-book approach there is to override the translate() method, as well, and call the updated constructor from there.

Now, you can refactor your class initialization and separate attribute setting from other needed actions, and then create a new class-method to clone an instance with all the attributes from a first instance, and just call this initialization, if needed.

If no initialization is needed, just a "clone" method is needed. If you happen to call this method __copy__, then you can use the copy.copy call for that, which is almost as if it was an operator in Python, and it can, by itself, have value for your end-users.

Moreover — if your class requires no initialization besides setting plain attributes, and no calculations at all, copy.copy will just work out of the box with your instances – no extra __copy__ method needed:

from copy import copy

class ExtrudedSurface:
    def __init__(self, profile: Curve, direction: Vector, length: float):
        """
        An extruded surface geometry created by sweeping a 3D 'profile' curve along a 
        'direction' for a given 'length'.
        """

        self.profile = profile
        self.direction = direction.normalize()
        self.length = length

    def translate(self, translation: Vector) -> ExtrudedSurface:
        """Return a translated version of itself."""

        new_profile = self.profile.translate(translation)
        new_instance = copy(self)
        new_instance.profile = new_profile
        return new_instance
     


class Cylinder(ExtrudedSurface):

    def __init__(self, point: Point, direction: Vector, radius: float, length: float):
        ...
        

Just attempt to the fact that copy will not recursively copy the attributes, so, if the self.direction vector is a mutable object in the framework you are using, this will keep both clones bound to the same vector, and if it changes in the original, that change will be reflected in the clone. By the nature of your code I am assuming everything is immutable there and all transforms create new instances: then this will work. Otherwise, you should also copy the original .direction attribute into the new instance.

In time: yes, that is an LSP violation – but I always think that LSP is given more importance than it should when it come to practical matters.

The more generic code I described, should your initialization be more complex would be:


class Base:
    def __init__(self, base_args):
        #code to set initial attributes
        ...
        # call inner init with possible sideeffects:
        self.inner_init()
    
    def inner_init(self):
        # code with side effects (like, attaching instance to a container)
        # should be written in order to be repeat-proof - calls on the same instance have no effect
        ...

   def __copy__(self):
      new_instance = self.__class__.new()  
      new_instance.__dict__.update(self.__dict__)
      self.inner_init()
Answered By: jsbueno
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.