Search code examples
pythoninheritancemethod-signature

Override method in child class using less arguments than base class - Python 3


I am building my first app (Weather report) and I am using tkinter and Python 3.6 for that. I am new to Python so I want to make sure I don't learn bad habits which later on will have to be unlearned :).

If there are any glaring issues with my code please comment on it - I need to know how to improve :) Thank you.

I have built a base class for objects to be placed on tkinter canvas in relation to other objects already existing on the canvas. The idea is to be able to easily stick them next to something else already on the canvas without absolute coordinates.

I have a method move_rel_to_obj_y which purpose is to centre y coordinate of an instance of the child class on centre of y coordinate of a relative object already present on the canvas.

The base method is supposed to be used only by the child class. There are already 2 child classes and I want to avoid copy-pasting this method into them.

Now in the base class the method takes (self, obj, rel_obj) as arguments. But the self in the base class is not the instance of the child class on which I want to operate (move the image on canvas) so my overridden method takes (self, rel_obj) as we assume now that obj becomes self (instance of the child class) which I want to move.

Therefore I came up with the solution that is below.

My question is:

  1. Is there any other way to pass child instance to the base method without doing what I did and that way is more elegant?

  2. In the override of the base method I use less arguments than in the base class and the signature of the method changes (as Pycharm warns :)). I added **kwargs to keep the amount of arguments same but it would work without doing that (I tested and it is not needed). What is the appropriate way of actually doing that kind of inheritance when amount of arguments changes? Should I avoid it at all costs? If we should avoid it then how to solve my problem of needing to pass child instance to the base method?

Thanks for helping me out !

Tomasz Kluczkowski

Base class:

class CanvasObject(object):
"""Base class to create objects on canvas.

Allows easier placement of objects on canvas in relation to other objects.

Args:
    object (object): Base Python object we inherit from.

"""

def __init__(self, canvas, coordinates=None, rel_obj=None,
             rel_pos=None, offset=None):
    """Initialise class - calculate x-y coordinates for our object.

    Allows positioning in relation to the rel_obj (CanvasText or CanvasImg object).
    We can give absolute position for the object or a relative one.
    In case of absolute position given we will ignore the relative parameter.
    The offset allows us to move the text away from the border of the relative object.

    Args:
        canvas (tk.Canvas): Canvas object to which the text will be attached to.
        image (str): String with a path to the image.
        coordinates (tuple): Absolute x, y coordinates where to place text in canvas. Overrides any parameters
            given in relative parameters section.
        rel_obj (CanvasText / CanvasImg): CanvasText / CanvasImg object which will be used
            as a relative one next to which text is meant to be written.
        rel_pos (str): String determining position of newly created text in relation to the relative object.
            Similar concept to anchor.
            TL - top-left, TM - top-middle, TR - top-right, CL - center-left, CC - center-center,
            CR - center-right, BL - bottom-left, BC - bottom-center, BR - bottom-right
        offset (tuple): Offset given as a pair of values to move the newly created object
            away from the relative object.

    :Attributes:
    :canvas (tk.Canvas): tkinter Canvas object.
    :pos_x (int): X coordinate for our object.
    :pos_y (int): Y coordinate for our object.

    """
    self.canvas = canvas
    pos_x = 0
    pos_y = 0

    if offset:
        offset_x = offset[0]
        offset_y = offset[1]
    else:
        offset_x = 0
        offset_y = 0
    if coordinates:
        pos_x = coordinates[0]
        pos_y = coordinates[1]
    elif rel_obj is not None and rel_pos is not None:
        # Get Top-Left and Bottom-Right bounding points of the relative object.
        r_x1, r_y1, r_x2, r_y2 = canvas.bbox(rel_obj.id_num)
        # TL - top - left, TM - top - middle, TR - top - right, CL - center - left, CC - center - center,
        # CR - center - right, BL - bottom - left, BC - bottom - center, BR - bottom - right

        # Determine position of CanvasObject on canvas in relation to the rel_obj.
        if rel_pos == "TL":
            pos_x = r_x1
            pos_y = r_y1
        elif rel_pos == "TM":
            pos_x = r_x2 - (r_x2 - r_x1) / 2
            pos_y = r_y1
        elif rel_pos == "TR":
            pos_x = r_x2
            pos_y = r_y1
        elif rel_pos == "CL":
            pos_x = r_x1
            pos_y = r_y2 - (r_y2 - r_y1) / 2
        elif rel_pos == "CC":
            pos_x = r_x2 - (r_x2 - r_x1) / 2
            pos_y = r_y2 - (r_y2 - r_y1) / 2
        elif rel_pos == "CR":
            pos_x = r_x2
            pos_y = r_y2 - (r_y2 - r_y1) / 2
        elif rel_pos == "BL":
            pos_x = r_x1
            pos_y = r_y2
        elif rel_pos == "BC":
            pos_x = r_x2 - (r_x2 - r_x1) / 2
            pos_y = r_y2
        elif rel_pos == "BR":
            pos_x = r_x2
            pos_y = r_y2
        else:
            raise ValueError("Please use the following strings for rel_pos: TL - top - left, "
                             "TM - top - middle, TR - top - right, CL - center - left,"
                             " CC - center - center, CR - center - right, BL - bottom - left, "
                             "BC - bottom - center, BR - bottom - right")
    self.pos_x = int(pos_x + offset_x)
    self.pos_y = int(pos_y + offset_y)

def move_rel_to_obj_y(self, obj, rel_obj):
    """Move obj relative to rel_obj in y direction. 
    Initially aligning centers of the vertical side of objects is supported.

    Args:
        obj (CanvasText | CanvasImg): Object which we want to move.
        rel_obj (CanvasText | CanvasImg): Object in relation to which we want to move obj. 

    Returns:
        None

    """
    # Find y coordinate of the center of rel_obj.
    r_x1, r_y1, r_x2, r_y2 = self.canvas.bbox(rel_obj.id_num)
    r_center_y = r_y2 - (r_y2 - r_y1) / 2

    # Find y coordinate of the center of our object.
    x1, y1, x2, y2 = self.canvas.bbox(obj.id_num)
    center_y = y2 - (y2 - y1) / 2

    # Find the delta.
    dy = int(r_center_y - center_y)

    # Move obj.
    self.canvas.move(obj.id_num, 0, dy)
    # Update obj pos_y attribute.
    obj.pos_y += dy

Child class:

class CanvasImg(CanvasObject):
"""Creates image object on canvas.

Allows easier placement of image objects on canvas in relation to other objects.

Args:
    CanvasObject (object): Base class we inherit from.

"""

def __init__(self, canvas, image, coordinates=None, rel_obj=None,
             rel_pos=None, offset=None, **args):
    """Initialise class.

    Allows positioning in relation to the rel_obj (CanvasText or CanvasImg object).
    We can give absolute position for the image or a relative one.
    In case of absolute position given we will ignore the relative parameter.
    The offset allows us to move the image away from the border of the relative object.
    In **args we place all the normal canvas.create_image method parameters.

    Args:
        canvas (tk.Canvas): Canvas object to which the text will be attached to.
        image (str): String with a path to the image.
        coordinates (tuple): Absolute x, y coordinates where to place text in canvas. Overrides any parameters
            given in relative parameters section.
        rel_obj (CanvasText / CanvasImg): CanvasText / CanvasImg object which will be used
            as a relative one next to which text is meant to be written.
        rel_pos (str): String determining position of newly created text in relation to the relative object.
            Similar concept to anchor.
            TL - top-left, TM - top-middle, TR - top-right, CL - center-left, CC - center-center, 
            CR - center-right, BL - bottom-left, BC - bottom-center, BR - bottom-right
        offset (tuple): Offset given as a pair of values to move the newly created text
            away from the relative object.
        **args: All the other arguments we need to pass to create_text method.

    :Attributes:
        :id_num (int): Unique Id number returned by create_image method which will help us identify objects
            and obtain their bounding boxes.

    """
    # Initialise base class. Get x-y coordinates for CanvasImg object.
    super().__init__(canvas, coordinates, rel_obj, rel_pos, offset)

    # Prepare image for insertion. Should work with most image file formats.
    img = Image.open(image)
    self.img = ImageTk.PhotoImage(img)
    id_num = canvas.create_image(self.pos_x, self.pos_y, image=self.img, **args)
    # Store unique Id number returned from using canvas.create_image method as an instance attribute.
    self.id_num = id_num

def move_rel_to_obj_y(self, rel_obj, **kwargs):
    """Move instance in relation to rel_obj. Align their y coordinate centers.
    Override base class method to pass child instance as obj argument automatically.


    Args:
        rel_obj (CanvasText | CanvasImg): Object in relation to which we want to move obj. 

        **kwargs (): Not used

    Returns:
        None
    """
    super().move_rel_to_obj_y(self, rel_obj)

Solution

  • Now in the base class the method takes (self, obj, rel_obj) as arguments. But the self in the base class is not the instance of the child class on which I want to operate

    Why ??? I think you don't understant how inheritance work actually. self is a reference to the instance on which a method has been called, the fact that the method is inherited doesn't change anything:

    # oop.py
    class Base(object):
        def do_something(self):
            print "in Base.do_something, self is a %s" % type(self)
    
    class Child(Base):
        pass
    
    class Child2(Base):
        def do_something(self):
            print "in Child2.do_something, self is a %s" % type(self)
            super(Child2, self).do_something()
    
    Base().do_something()
    Child().do_something()
    Child2.do_something()
    

    And the result:

    # python oop.py
    >>> in Base.do_something, self is a <class 'oop.Base'> 
    >>> in Base.do_something, self is a <class 'oop.Child'>
    >>> in Child2.do_something, self is a <class 'oop.Child2'>
    >>> in Base.do_something, self is a <class 'oop.Child2'>
    

    IOW, your base class should only take self and rel_obj as params and you shouldn't override move_rel_to_obj_y() in child classes (unless you want to change the behaviour of this method for a given child class of course).

    As a general rule, inheritance has a "is a" semantic (a "Child" is a "Base") and subclasses should be 100% compatible (have the exact same API) with the base class (and with each other of course) - this is known as the Liskov substitution principle. This at least holds when inheritance is really used for proper subtyping as opposed to mere code reuse which is another use case for inheritance - but if it's only for code reuse, you may want to use composition/delegation instead of inheritance:

    class CanvasObjectPositioner(object):
        def move_rel_to_obj_y(self, obj, rel_obj):
            # code here
    
    class CanvasImage(object):
        def __init__(self, positioner):
            self.positioner = positioner
    
        def move_rel_to_obj_y(self, rel_obj):
            self.positioner.move_rel_to_obj_y(self, rel_obj)
    
    positioner = CanvasObjectPositioner()
    img = CanvasImage(positioner)
    # etc
    

    This second approach may seem uselessly more complicated but it does have some advantages in the long run:

    • it's clear that CanvasImage is not a CanvasObjectPositioner
    • you can test CanvasObjectPositioner in isolation
    • you can pass a mock CanvasObjectPositioner to test CanvasImage in isolation
    • you can pass any "positioner" you want to CanvasImage (as long as it has the right api), so you can have different positioning strategies (this is known as the Strategy design pattern) if needed.