如何在 PyQt 中向现有 API 添加重载

How to add overloads to an existing API in PyQt

有一个现有的 class,其 __init__() 的参数已经采用不同的数字和类型。我希望 subclass 添加一个新的参数。不知道我打算怎么写subclass的new __init__() definition.

我没有现有基础 class 的源代码(它可能是用 C++ 编写的)。 help(QListWidgetItem) 给我:

class QListWidgetItem(sip.wrapper)
 |  QListWidgetItem(parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(str, parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(QIcon, str, parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(QListWidgetItem)

我的编辑器 (PyCharm) 识别这些并提供上下文相关的完成。它的行为就好像它们是通过 @overload 指令声明的,我希望保留它。

请注意,不仅参数的数量是可变的,而且类型也是。例如,查看所有重载参数 #1 可能是 QListWidgetstrQIconQListWidgetItem,或者甚至没有提供,这取决于影响第二个参数可以是什么等

我想再添加一个:

MyListWidgetItem(text: str, value: QVariant, parent: QListWidget = None, type: int = QListWidgetItem.Type)

请注意,我的新 QVariant 参数排在第二位,我希望它是位置性的而不是关键字命名的。

所以我需要在调用时识别这个新的;我需要拉出我的新 value: QVariant 来设置我的新成员变量,我还需要在调用基础 class 构造函数之前将其删除。

我知道对于声明我将添加一个重载,例如:

class MyListWidgetItem(QListWidgetItem)
    @overload
    def __init__(self, text: str, value: QVariant, parent: QListWidget=None, type: int=QListWidgetItem):
        pass

(我假设这将使现有的 QListWidgetItem @overload 仍然可以通过我派生的 MyListWidgetItem 获得?)

实际的 定义 呢?它有什么作用以及应该如何 declared/written?

我需要在它被调用时认出这个新的;我需要拉出我的新 value: QVariant 来设置我的变量,我还需要在调用基础 class 构造函数之前将其删除。

我只能猜测:我的工作是为了识别我的情况,这样写:

if len(arguments) >= 2:
    if isinstance(arguments[0], str) and isinstance(arguments[1], QVariant):
        self.value = arguments[1]
        del arguments[1]

然后:我是否应该为我的新子 class 编写单个 __init__() 定义( 而不是 @overload 声明)行:

def __init__(self, *__args)
    ...
    super().__init__(*__args)

或使用不同的、显式的、类型化的参数,如:

def __init__(self, arg1: typing.Union[QListWidget, str, icon, QListWidgetItem, None], arg2: typing..., arg3: typing..., arg4)
    ...
    super().__init__(arg1, arg2, arg3, arg4)

后者看起来很复杂?前一种方法是直接在 *__args 上声明和工作是最好的方法吗?

[编辑:如果它对生成某种解决方案有任何影响,我愿意通过 value: QVariant = ... 使我的新参数可选。或者,如果答案是,比方说,"You won't be able to do it quite your way because ..., but the better way to do this is to make it a named-keyword-only argument because then you can ...",或者其他什么,我会考虑这个问题。]

没有 one-size-fits-all 答案,而且 pyQt(实际上是 Qt 之上的一个薄层,并公开了相当多的 C++ 习语)不一定代表最常见的用例。

作为一般规则,显式复制(和扩展)parent class 的初始化程序原型被认为是更好的做法,因此您(通用 "you" -> 任何拥有维护代码)不必阅读 parent class 文档等即可了解预期内容。

现在在某些情况下

  • parent class 需要很多参数
  • 你的子class不会混淆那些论点
  • 你的子class只想添加参数
  • 您/您的团队可以将这些参数添加为 keyword-only 参数或强制它们出现在任何 parent 之前一个
  • 您/您的团队可以放弃 autodoc/类型提示等

然后使用*args**kwargs确实是一个解决方案:

def __init__(self, my_positional_arg, *args, **kwargs)
    # use .pop() to avoid passing it to the parent
    my_own_kw_arg = kw.pop("my_own_kw_arg", "nothing")
    super().__init__(*args, **kwargs)

请注意,在这种情况下,您确实想要支持*args**kwargs,因为即使所需的位置参数可以作为命名参数传递,当 class(或函数 FWIW)需要很多参数(记住名称比记住位置更容易......)时,它们通常是如何实际传递的。

这里有两个不同的问题。

第一个特定于 PyCharm,它使用 typing module. It generates pyi files with stubs defining the APIs of various third-party libraries (such as PyQt) so that it can provide auto-completion and such like. In addition, it supports user-defined type-hints and pyi files, which are documented here: Type Hinting in PyCharm。但是,因为我不是 PyCharm 用户,所以我无法就您应该如何定义自己的重载存根以便它们扩充现有的 PyQt 存根提供任何实用的建议。不过,我认为这一定是可能的。

第二个问题确切地涉及如何为现有的 PyQt API 实现函数重载。对此的简短回答是您不能:Python 根本不支持 C++ 支持的重载。这是因为 Python 是动态类型的,所以这种类型的重载在那里没有意义。但是,可以通过多种方式解决此问题以提供等效行为。

对于您的具体情况,最简单的解决方案需要做出小的妥协。您的问题是:"Note that my new QVariant argument is in second place, and I wish it to be positional not keyword-named"。如果你愿意放弃这个要求,它会让事情 变得容易很多 ,因为你可以这样定义你的 sub-class:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, *args, value=None, **kwargs):
        super().__init__(*args, **kwargs)

或者像这样:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, *args, **kwargs):
        value = kwargs.pop('value', None)
        super().__init__(*args, **kwargs)

这些子classes 将支持所有现有的 PyQt 重载,而无需确切地知道它们是如何定义的,因为您只是将参数传递给基本实现。提供正确的参数完全取决于用户,但是由于基数 class 是由 PyQt 提供的,如果给出错误的参数,它会自动引发 TypeError。这一切都有助于保持实现非常简单,但它确实非常重视正确记录您的 API,因为函数签名本身很少或根本没有提供关于正确参数应该是什么的提示。但是,如果您还可以找到一种方法来利用 PyCharm 的类型提示支持,如上文所建议的那样,那应该会让您非常接近于一个非常简单、可行的解决方案。

但如果您不愿意妥协怎么办?如果你考虑这个签名,就可以看到这引发的直接问题:

QListWidgetItem(parent: QListWidget = None, type: int = QListWidgetItem.Type)

这允许创建一个没有参数的项目。但这会立即破坏定义 required 参数的任何新重载,因为如果在运行时调用构造函数时缺少它们​​,Python 将引发 TypeError。解决此问题的唯一方法是使用 *args, **kwargs 签名,然后明确检查 __init__ 正文中所有参数的数量和类型。实际上,这就是 functools.singledispatch and third-party packages like multipledispatch 所做的,只是通过装饰器。不过,这并没有真正绕过上述问题——它只是将它移到别处,让您不必维护一大堆复杂的样板代码。

我不打算在这里给出任何调度风格的例子:首先,因为我不知道它们将如何在 PyCharm(甚至 PyQt,就此而言)中发挥作用,其次,因为它们已经包含在更通用的 SO 问题中,例如:Python function overloading。我的建议是从上面给出的更简单的实现开始,然后如果您发现 确实 需要添加带有非关键字参数的重载,请考虑尝试其他方法。

最后一个要考虑的方法是所谓的标准厨房水槽过载。在这种方法中,您只需忘记现有 API 的签名并定义您的 sub-class 如下:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, text='', value=None, parent=None, icon=None,
                       item=None, type=QListWidgetItem.Type):
        if item is not None:
            super().__init__(item)
        elif icon is not None:
            super().__init__(icon, text, parent, type)
        else:
            super().__init__(text, parent, type)

或者如果您不关心 type 和复制构造函数:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, text='', value=None, parent=None, icon=None):
        if icon is not None:
            super().__init__(icon, text, parent)
        else:
            super().__init__(text, parent)

绝大多数 Python/PyQt 代码可能使用了这种方法的一些变体。因此,practicality beats purity,我猜...