Python方法覆盖,签名重要吗?

Posted

技术标签:

【中文标题】Python方法覆盖,签名重要吗?【英文标题】:Python Method overriding, does signature matter? 【发布时间】:2011-08-27 10:33:04 【问题描述】:

假设我有

class Super():
  def method1():
    pass

class Sub(Super):
  def method1(param1, param2, param3):
      stuff

这是正确的吗?对method1的调用是否总是转到子类?我的计划是有 2 个子类,每个子类都使用不同的参数覆盖 method1

【问题讨论】:

【参考方案1】:

在 Python 中,方法只是附加到类的字典中的键值对。当您从基类派生一个类时,您实际上是在说方法名称将在第一个派生类字典中查找,然后在基类字典中查找。为了“覆盖”一个方法,您只需在派生类中重新声明该方法即可。

那么,如果在派生类中更改被覆盖方法的签名会怎样?如果在派生实例上调用一切正常,但如果在基实例上进行调用,则会收到错误消息,因为基类对同一方法名称使用不同的签名。

然而,在某些情况下,您希望派生类方法具有 附加 参数,并且您希望方法调用在基础上也能正常工作。这被称为“Liskov 替换原则”(或LSP),它保证如果人们从基础实例切换到派生实例,反之亦然,他们不必修改他们的代码。要在 Python 中执行此操作,您需要使用以下技术设计基类:

class Base:
    # simply allow additional args in base class
    def hello(self, name, *args, **kwargs):
        print("Hello", name)

class Derived(Base):
      # derived class also has unused optional args so people can
      # derive new class from this class as well while maintaining LSP
      def hello(self, name, age=None, *args, **kwargs):
          super(Derived, self).hello(name, age, *args, **kwargs) 
          print('Your age is ', age)

b = Base()
d = Derived()

b.hello('Alice')        # works on base, without additional params
b.hello('Bob', age=24)  # works on base, with additional params
d.hello('Rick')         # works on derived, without additional params
d.hello('John', age=30) # works on derived, with additional params

上面将打印:

你好爱丽丝 你好鲍勃 你好瑞克 你的年龄没有 你好约翰 你的年龄是 30 。 Play with this code

【讨论】:

感谢几年后的这次更新,提供了更清晰、更可行的讨论,以及一个工作示例和游戏笔! 我们应该在*args之后的hello的签名中加入年龄吗?如果不是这样,d.hello("John", "blue", age=30) 之类的代码将不起作用。我的意思是,一般来说,位置参数应该总是在 kwargs 之前定义 我认为您的问题的答案取决于我们是否要允许使用默认选项的参数(age 这里)也可以按位置设置或仅设置为 kwarg。请注意,您的建议仅适用于 Python 3,您可以在其中使用 * 指定仅关键字参数。参见例如this. 很好的答案,但在“LSP 保证如果人从基础实例切换到派生实例或反之亦然”中,“反之亦然”部分不正确。 如果你在派生类方法的签名中省略了*args, **kwargs,那么它仍然有效吗?【参考方案2】:

Python 将允许这样做,但如果打算从外部代码执行 method1(),那么您可能需要重新考虑这一点,因为它违反了 LSP,因此不会总是正常工作。

【讨论】:

Sub.method1 接受 3 个参数,而 Super.method1 不接受,从而使它们实际上是不同的接口,这是否违反了 LSP? @Unode:正确。这可以通过让子类方法的参数都具有默认值来解决,但随后您会了解哪些默认值是合适的。 我明白了。但只是为了澄清。如果父方法 1 被定义为 Super.method1(param1=None, param2=None, param3=None) 它仍然会违反 LSP 如果在子类上它被定义为 Sub.method1(param1, param2, param3) 对吗?因为属性在一种情况下是强制性的,但在另一种情况下不是。因此,据我了解,在不更改子类接口的情况下,不违反 LSP 的唯一方法是在父类上使用没有默认值的参数。我对此是否正确还是过度解释了 LSP? @Unode:也正确。让合约在子类中变得更少限制违反了 LSP。 method1__init__ 时除外,此时 LSP 不适用【参考方案3】:

在 python 中,所有类方法都是“虚拟的”(就 C++ 而言)。因此,对于您的代码,如果您想在超类中调用 method1(),它必须是:

class Super():
    def method1(self):
        pass

class Sub(Super):
    def method1(self, param1, param2, param3):
       super(Sub, self).method1() # a proxy object, see http://docs.python.org/library/functions.html#super
       pass

而且方法签名确实很重要。你不能调用这样的方法:

sub = Sub()
sub.method1() 

【讨论】:

【参考方案4】:

如果可以使用默认参数,您可以这样做:

>>> class Super():
...   def method1(self):
...     print("Super")
...
>>> class Sub(Super):
...   def method1(self, param1="X"):
...     super(Sub, self).method1()
...     print("Sub" + param1)
...
>>> sup = Super()
>>> sub = Sub()
>>> sup.method1()
Super
>>> sub.method1()
Super
SubX

【讨论】:

【参考方案5】:

它会起作用的:

>>> class Foo(object):
...   def Bar(self):
...     print 'Foo'
...   def Baz(self):
...     self.Bar()
... 
>>> class Foo2(Foo):
...   def Bar(self):
...     print 'Foo2'
... 
>>> foo = Foo()
>>> foo.Baz()
Foo
>>> 
>>> foo2 = Foo2()
>>> foo2.Baz()
Foo2

但是,通常不建议这样做。看看S.Lott的回答:Methods with the same name and different arguments are a code smell。

【讨论】:

以上是关于Python方法覆盖,签名重要吗?的主要内容,如果未能解决你的问题,请参考以下文章

您可以使用不同但“兼容”的签名覆盖接口方法吗?

Python中的sorted()有啥神奇的方法吗?

Python中的sorted()有啥神奇的方法吗?

将要覆盖的实现方法分开是pythonic吗?

Python需要disconnect信号吗

重写和重载的三点区别