【问题标题】:Signatures of factory methods of subclasses子类工厂方法的签名
【发布时间】:2021-04-20 17:13:46
【问题描述】:

子类的方法与基类的相应方法具有相同的签名是一种很好的做法。如果违反这一原则,PyCharm 会给出警告:

Signature of method does not match signature of base method in class

这个原则有(至少)一个例外:Python 初始化方法__init__。子类通常具有与其父类不同的初始化参数。它们可能有额外的参数,也可能有更少的参数,通常是通过对父类的参数使用一个常量值来获得。

由于 Python 不支持具有不同签名的多个初始化方法,因此具有不同构造函数的 Pythonic 方式称为工厂方法(例如:https://stackoverflow.com/a/682545/10816965)。

PyCharm 认为这些工厂方法也不例外,子类的方法应该与对应的父类具有相同的签名。当然,我可以忽略这些警告——因为这些工厂方法类似于__init____new__,我认为可以认为这些警告是错误的。

但是,我想知道我是否在这里遗漏了什么,并且我的编码风格不是最佳实践。

所以我的问题是:这是 PyCharm 的意外行为,还是这种模式确实有更 Pythonic 的方式?

class A:
    @classmethod
    def from_something(cls, something):
        self = cls()
        # f(self, something)
        return self

    def __init__(self):
        pass


class B(A):
    @classmethod
    def from_something(cls, something, param):  # PyCharm warning:
        # Signature of method 'B.from_something()' does not match
        # signature of base method in class 'A'

        self = cls(param)
        # g(self, something, param)
        return self

    def __init__(self, param):
        super().__init__()
        self.param = param


class C:
    @classmethod
    def from_something(cls, something, param):
        self = cls(param)
        # f(self, something, param)
        return self

    def __init__(self, param):
        self.param = param


class D(C):
    @classmethod
    def from_something(cls, something):  # PyCharm warning: Signature of
        # method 'D.from_something()' does not match signature of base
        # method in class 'C'

        self = cls()
        # g(self, something)
        return self

    def __init__(self):
        super().__init__(None)

【问题讨论】:

  • 我认为这是因为使用不同的签名你不能再替换,比如用B 实例替换A 实例,用OOP 术语说B 仍然是A。请注意,如果该方法是从基类使用的相同something 类型创建子类的工厂,为什么它需要更多参数。如果BA,则应该可以以相同的方式创建它。如果你不在乎,也许这不是问题。但是,您仍然可以使用 *args, **kwargs 参数来保持相同的明显签名。
  • 是的,例如方法,这就是它们应该具有相同签名的原因。但是,对于诸如工厂方法之类的类方法,情况并非如此。使用*args**kwargs 不是我的选择,我宁愿忽略警告。
  • 你能详细说明为什么类方法不是这样吗?
  • 另见答案here
  • @progmatico 我会说类方法通常不打算从实例中调用。尽管 Python 允许这样做,但我认为,如果您将 bB 的实例称为 b.from_something(something) 而不是 B.from_something(something),那么这也是不好的编码风格。在某些情况下,从实例调用类方法可能是合适的,但至少对于工厂方法,我会避免这种情况。

标签: python inheritance pycharm subclassing factory-method


【解决方案1】:

要考虑的主要问题是:

  • 基类和派生类之间重写方法(未重载)签名的兼容性。

要完全解决和理解这一点,让我们首先考虑:

在 Python 中,方法仅按名称查找,类似于属性。您可以通过查看__dict__(例如Class.__dict__instance.__dict__)来检查它们在实例或类上的存在情况

自定义类,3.2. The standard type hierarchy

一个类有一个由字典对象实现的命名空间。类属性引用被翻译成这个字典中的查找,例如,C.x 被翻译成 C.__dict__["x"](尽管有许多钩子允许其他定位属性的方法)。 如果在那里找不到属性名称,则在基类中继续进行属性搜索。

如果我们在没有 def right(self, one): 方法的情况下定义 Bottom,我们将从 __dict__ 得到

>>> Bottom.__dict__

{'__module__': '__main__',
'__init__': <function Bottom.__init__ at 0x0000024BD43058B0>,
'__doc__': None}

如果我们在 Bottom 类中重写 def right(self, one): 方法,__dict__ 现在将拥有

>>> Bottom.__dict__

{'__module__': '__main__',
'__init__': <function Bottom.__init__ at 0x0000024BD43058B0>,
'right': <function Bottom.right at 0x0000024BD43483A0>,
'__doc__': None}

这与其他 OO 语言(如 Java)不同,Java 的 method overloading 具有基于参数数量和类型的解析/查找,而不仅仅是名称。在这方面,Python 是overriding 方法,因为仅对名称/类/实例进行查找。 Python 确实支持类型提示“重载”(严格意义上的),参见Function/method overloading, PEP 484 -- Type Hints

9.5. Inheritance,Python 教程。

派生类可以覆盖其基类的方法。因为方法在调用同一对象的其他方法时没有特权,一个基类的方法调用同一基类中定义的另一个方法,最终可能会调用一个派生类的方法,而该派生类的方法会覆盖它。强>

让我们验证一下上面的操作,一个最小的例子:

class Top:
    def left(self, one):
        self.right(one, 2)

    def right(self, one, two):
        pass

    def __init__(self):
        pass

class Bottom(Top):
    def right(self, one):
        pass

    def __init__(self, one):
        super().__init__()

运行示例:

>>> t = Top()
>>> t.left(1)

>>> b = Bottom()
>>> b.left(1)

Traceback (most recent call last):
  File "<input>", line 18, in <module>
  File "<input>", line 3, in left
TypeError: right() takes 2 positional arguments but 3 were given

在这里您可以看到,更改派生类中方法的签名会破坏基类内部的方法调用。

这是一个严重的副作用,因为派生类只是约束了基类。您创建了一个反模式的向上依赖项。通常,您希望约束/依赖项在继承中向下移动,而不是向上移动。您刚刚从单向依赖变为双向依赖。 (在实践中,这会给现在必须考虑和解决额外依赖的程序员增加更多的工作量 - 它与Principle of least astonishment 相违背,下一个查看您的代码的程序员可能会不高兴。)


或者这种模式确实有更 Pythonic 的方式吗?

Pythonic 在这里意味着你可以双向进行,你有选择:

  1. 使用不同的签名覆盖方法,需要:
  • 了解其中的含义并增加了双向依赖。
  • 如果您选择让 linter 警告静音,它可能会让您之后的程序员更加不高兴(他们现在被剥夺了公平的警告)。
  1. 使用4.7.4. Arbitrary Argument Lists
  • 这可能是更Pythonic 的选择,因为它更简单。您可以做的是在文档字符串中记录工厂方法返回类的实例,并且可变参数签名中传递的参数应遵循构造函数的参数,如下所示:
class Top:

    def __init__(self):
        pass

    @classmethod
    def from_something(cls, *args, **kwargs) -> "Top":
        """Factory method initializes and returns an instance of the class.
        The arguments should follow the signature of the constructor.
        """

class Bottom(Top):

    def __init__(self, one):
        super().__init__()

    @classmethod
    def from_something(cls, *args, **kwargs) -> "Bottom":
        """Factory method initializes and returns an instance of the class.
        The arguments should follow the signature of the constructor.
        """
  • 附注对于提示返回类型的最后一个 "gotcha" 类型,请参阅this excellent post,在此示例中,为了简单起见,使用了“前向声明”。它不会更改签名,只会更改方法的 __annotations__ 属性。但是,“作为 Pythonic” 我们可以汇款给 a BDFL post 我不完全确定提示返回的类型是否违反了 Liskov Substitution Principle 但 Mypy 和 PyCharm linter 似乎让我侥幸逃脱它...

【讨论】:

  • 你的证明让我确信 mypy 的行为是不同的,并且会关闭警告。谢谢你的详细解答!另外,感谢检查名称,我以为是 PyArgumentList 并且已经想知道为什么这不适用于 @classmethod 装饰器。
  • @SebastianThomas 你好,由于几个原因,我的回答并不完全正确。当我有时间收集我的想法(本周)时,我会尝试重写它。对于像 Java 这样的语言,这种事情有一个简单直接的答案,但对于 Python,它需要很多事后的想法。对此我很抱歉,我已经多次遇到 PyCharm linter 的问题before,这就是我未能正确考虑这个问题的原因。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-23
  • 2021-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多