使用比基础 class - Python 更少的参数覆盖 child class 中的方法 3

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

我正在构建我的第一个应用程序(天气报告),我正在为此使用 tkinter 和 Python 3.6。我是 Python 的新手,所以我想确保我不会养成以后必须改掉的坏习惯 :)。

如果我的代码有任何明显的问题,请发表评论 - 我需要知道如何改进 :) 谢谢。

我已经为 object 建立了一个基础 class 以放置在 tkinter canvas 上,与 [=56= 上已经存在的其他 object 相关].这个想法是能够轻松地将它们粘贴到 canvas 上已有的其他东西旁边,而无需绝对坐标。

我有一个方法move_rel_to_obj_y,其目的是将 child class 的一个实例的 y 坐标居中在相对 object 的 y 坐标的中心出现在 canvas.

基本方法应该仅供 child class 使用。已经有 2 child classes,我想避免 copy-pasting 这种方法进入他们。

现在在基础 class 中,该方法将 (self, obj, rel_obj) 作为参数。 但是 base class 中的 self 不是我要操作的 child class 的实例(将图像移动到 canvas)所以我重写的方法采用( self, rel_obj) 因为我们现在假设 obj 变成了我要移动的 self(child class 的实例)。

因此我想出了下面的解决方案。

我的问题是:

  1. 有没有其他方法可以将 child 实例传递给基本方法,而不用像我那样做,而且这种方法更优雅?

  2. 在基本方法的覆盖中,我使用的参数比在基本方法 class 中少,并且方法的签名发生变化(如 Pycharm 警告 :))。我添加了 **kwargs 以保持参数的数量不变,但不这样做它也可以工作(我测试过并且不需要)。当参数数量发生变化时,实际进行这种继承的合适方法是什么?我应该不惜一切代价避免它吗? 如果我们应该避免它那么如何解决我需要将 child 实例传递给基本方法的问题?

谢谢你帮我!

Tomasz Kluczkowski

基础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)

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

为什么???我认为您不了解继承实际上是如何工作的。 self 是对调用方法的实例的引用,继承该方法的事实不会改变任何东西:

# 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()

结果:

# 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,你的基础 class 应该只接受 selfrel_obj 作为参数,你不应该覆盖 child [=48] 中的 move_rel_to_obj_y() =]es(当然,除非您想针对给定的 child class 更改此方法的行为)。

作为一般规则,继承具有 "is a" 语义("Child" 是 "Base")并且子classes 应该是 100% 兼容的(具有API) 与基础 class 完全相同(当然还有彼此)——这被称为 Liskov substitution principle。这至少在继承真正用于适当的子类型而不是单纯的代码重用时成立,这是继承的另一个用例 - 但如果它仅用于代码重用,您可能想要使用 composition/delegation 而不是继承:

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

第二种方法可能看起来更复杂但毫无用处,但它确实有一些优点 运行:

  • 很明显 CanvasImage 不是 CanvasObjectPositioner
  • 您可以单独测试 CanvasObjectPositioner
  • 您可以传递一个模拟 CanvasObjectPositioner 来单独测试 CanvasImage
  • 你可以把任何你想要的"positioner"传递给CanvasImage(只要它有合适的api),这样你就可以有不同的定位策略(这就是所谓的Strategy设计模式)如果需要的话。