【问题标题】:Best practices for multiple inheritance in this Python code此 Python 代码中多重继承的最佳实践
【发布时间】:2026-01-10 09:20:05
【问题描述】:

我对某些 Python 类中的多继承设计有一些疑问。

问题是我想扩展 ttk 按钮。这是我最初的建议(我省略了缩短方法中的所有源代码,除了 init 方法):

import tkinter as tk
import tkinter.ttk as ttk

class ImgButton(ttk.Button):
    """
    This has all the behaviour for a button which has an image
    """
    def __init__(self, master=None, **kw):
        super().__init__(master, **kw)
        self._img = kw.get('image')

    def change_color(self, __=None):
        """
        Changes the color of this widget randomly
        :param __: the event, which is no needed
        """
        pass

    def get_style_name(self):
        """
        Returns the specific style name applied for this widget
        :return: the style name as a string
        """
        pass

    def set_background_color(self, color):
        """
        Sets this widget's background color to that received as parameter
        :param color: the color to be set
        """
        pass

    def get_background_color(self):
        """
        Returns a string representing the background color of the widget
        :return: the color of the widget
        """
        pass

    def change_highlight_style(self, __=None):
        """
        Applies the highlight style for a color
        :param __: the event, which is no needed
        """
        pass

但后来我意识到我还想要这个 ImgButton 的子类,如下所示:

import tkinter as tk
import tkinter.ttk as ttk

class MyButton(ImgButton):
    """
    ImgButton with specifical purpose
    """

    IMG_NAME = 'filename{}.jpg'
    IMAGES_DIR = os.path.sep + os.path.sep.join(['home', 'user', 'myProjects', 'myProject', 'resources', 'images'])
    UNKNOWN_IMG = os.path.sep.join([IMAGES_DIR, IMG_NAME.format(0)])
    IMAGES = (lambda IMAGES_DIR=IMAGES_DIR, IMG_NAME=IMG_NAME: [os.path.sep.join([IMAGES_DIR, IMG_NAME.format(face)]) for face in [1,2,3,4,5] ])()

    def change_image(self, __=None):
        """
        Changes randomly the image in this MyButton
        :param __: the event, which is no needed
        """
        pass

    def __init__(self, master=None, value=None, **kw):
        # Default image when hidden or without value

        current_img = PhotoImage(file=MyButton.UNKNOWN_IMG)
        super().__init__(master, image=current_img, **kw)
        if not value:
            pass
        elif not isinstance(value, (int, Die)):
            pass
        elif isinstance(value, MyValue):
            self.myValue = value
        elif isinstance(value, int):
            self.myValue = MyValue(value)
        else:
            raise ValueError()
        self.set_background_color('green')
        self.bind('<Button-1>', self.change_image, add=True)


    def select(self):
        """
        Highlights this button as selected and changes its internal state
        """
        pass

    def toggleImage(self):
        """
        Changes the image in this specific button for the next allowed for MyButton
        """
        pass

继承权在他看来很自然。当我注意到 ImgButton 中的大多数方法都可以用于我将来可能创建的任何 Widget 时,问题就来了。

所以我正在考虑制作一个:

class MyWidget(ttk.Widget):

将所有有助于小部件颜色的方法放入其中,然后我需要 ImgButton 从 MyWidget 和 ttk.Button 继承:

class ImgButton(ttk.Button, MyWidget):  ???

or

class ImgButton(MyWidget, ttk.Button):  ???

已编辑:我还希望我的对象是可记录的,所以我做了这个类:

class Loggable(object):
    def __init__(self) -> None:
        super().__init__()
        self.__logger = None
        self.__logger = self.get_logger()

        self.debug = self.get_logger().debug
        self.error = self.get_logger().error
        self.critical = self.get_logger().critical
        self.info = self.get_logger().info
        self.warn = self.get_logger().warning


    def get_logger(self):
        if not self.__logger:
            self.__logger = logging.getLogger(self.get_class())
        return self.__logger

    def get_class(self):
        return self.__class__.__name__

那么现在:

class ImgButton(Loggable, ttk.Button, MyWidget):  ???

or

class ImgButton(Loggable, MyWidget, ttk.Button):  ???

or

class ImgButton(MyWidget, Loggable, ttk.Button):  ???

# ... this could go on ...

我来自 Java,我不知道多重继承的最佳实践。我不知道我应该如何以最佳顺序对父母进行排序,或者对设计这种多重继承有用的任何其他事情。

我搜索了该主题并找到了很多解释 MRO 的资源,但没有关于如何正确设计多重继承的内容。我不知道我的设计是不是做错了,但我觉得这感觉很自然。

如果您提供一些建议,以及有关此主题的一些链接或资源,我将不胜感激。

非常感谢。

【问题讨论】:

    标签: python-3.x tkinter multiple-inheritance ttk method-resolution-order


    【解决方案1】:

    这些天我一直在阅读有关多重继承的内容,并且学到了很多东西。我在最后链接了我的来源、资源和参考资料。

    我的主要和最详细的资料来源是《Fluent python》一书,我发现它可以在线免费阅读。

    这里描述了多继承的方法解析顺序和设计场景以及操作步骤:

    1. 识别和分离接口代码。定义方法但不一定具有实现的类(这些应该被覆盖)。这些通常是 ABC(抽象基类)。他们为创建“IS-A”关系的子类定义了一个类型

    2. 识别并分离 mixins 的代码。 mixin 是一个类,它应该带来一系列相关的新方法实现以在子级中使用,但没有定义正确的类型。根据这个定义,ABC 可以是 mixin,但不能反过来。 mixin 既没有定义也没有接口,也没有类型

    3. 当开始使用 ABC 或类以及 mixins 继承时,您应该只从一个具体的超类和多个 ABC 或 mixin 继承:

    例子:

    class MyClass(MySuperClass, MyABC, MyMixin1, MyMixin2):
    

    就我而言:

    class ImgButton(ttk.Button, MyWidget):
    
    1. 如果某些类的组合特别有用或经常使用,您应该将它们加入到具有描述性名称的类定义下

    例子:

     class Widget(BaseWidget, Pack, Grid, Place):
         pass
    

    我认为 Loggable 将是一个 Mixin,因为它为一个功能收集了方便的实现,但没有定义一个真正的类型。所以:

    class MyWidget(ttk.Widget, Loggable): # May be renamed to LoggableMixin
    
    1. 优先考虑对象组合而不是继承:如果您可以考虑通过将类保存在属性中而不是扩展或继承它来使用类的任何方式,那么您应该避免继承。

      "Fluent python" - (Chapter 12) in Google books

      Super is super

      Super is harmful

      Other problems with super

      Weird super behaviour

    【讨论】:

      【解决方案2】:

      原则上,使用多重继承会增加复杂性,所以除非我确定它需要它,否则我会避免它。从您的帖子中,您已经了解 super() 和 MRO 的使用。

      一个常见的建议是尽可能使用组合而不是多重继承。

      另一种方法是仅从一个可实例化的父类继承子类,使用抽象类作为其他父类。也就是说,他们向这个子类添加方法,但自己永远不会被实例化。就像在 Java 中使用接口一样。这些抽象类也称为 mixin,但它们的使用(或滥用)也值得商榷。见Mixins considered harmful

      至于您的 tkinter 代码,除了记录器代码缩进之外,我看不出有什么问题。也许小部件可以有一个记录器而不是从它继承。我认为 tkinter 的危险在于数百种可用方法之一的错误覆盖。

      【讨论】:

      • 我已经想到了与 tkinter 相同的危险,但是为了制作新的视觉控件,扩展现有的视觉控件感觉很自然。我知道 mixins,我没有理由反对使用它们。心里,我想现在我会使用继承,让 MyWidget 与父母 Loggable 和 ttk.Widget 一起成为孩子。 ImgButton 将是父母 MyWidget 和 ttk.Button 的孩子。每个级别有两个父级,但再往下则不再需要多重继承。