使用少于基类的参数覆盖子类中的方法 - Python 3

时间:2017-06-09 11:45:38

标签: python inheritance method-signature

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

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

我为要放置在tkinter画布上的对象构建了一个基类,该基类与画布上已存在的其他对象相关。我们的想法是能够轻松地将它们贴在画布上已经没有绝对坐标的其他东西旁边。

我有一个方法move_rel_to_obj_y,其目的是将子类实例的y坐标置于画布上已存在的相对对象的y坐标的中心。

基本方法应该仅由子类使用。已经有2个子类,我想避免将这种方法复制粘贴到它们中。

现在在基类中,该方法将(self,obj,rel_obj)作为参数。 但是基类中的self不是我想要操作的子类的实例(在画布上移动图像)所以我的重写方法采用(self,rel_obj),因为我们现在假设obj变为self(实例)孩子班)我想搬家。

因此,我提出了以下解决方案。

我的问题是:

  1. 有没有其他方法可以将子实例传递给基本方法而不做我做的事情,那样更优雅?

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

  3. 感谢您帮助我!

    Tomasz Kluczkowski

    基类:

    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
    

    儿童班:

    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)
    

1 个答案:

答案 0 :(得分:2)

  

现在在基类中,该方法将(self,obj,rel_obj)作为参数。 但是基类中的self不是我想要操作的子类的实例

为什么???我想你不明白继承是如何工作的。 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,您的基类应该只使用selfrel_obj作为参数,并且不应该在子类中覆盖move_rel_to_obj_y()(除非您想要更改此方法的行为当然是给定的儿童班。)

作为一般规则,继承具有“is a”语义(“Child”是“Base”),并且子类应与基类100%兼容(具有完全相同的API)(并且彼此之间)当然) - 这被称为Liskov substitution principle。这至少适用于继承真正用于正确的子类型,而不仅仅是代码重用,这是继承的另一个用例 - 但如果只是代码重用,你可能想要使用组合/委托而不是继承:

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
  • 你可以传递你想要的任何“定位器”CanvasImage(只要它有正确的api),所以如果需要你可以有不同的定位策略(这被称为策略设计模式)。