在Python中,我如何表明我正在重写一个方法?

264
在Java中,例如,@Override注释不仅提供了覆盖的编译时检查,还可以生成极好的自描述代码。
我只是在寻找文档(虽然如果它能作为像pylint这样的检查器的指示符,那就更好了)。我可以在某个地方添加注释或文档字符串,但在Python中指示覆盖的惯用方式是什么?

13
换句话说,你不会明确表示你正在覆盖一个方法?这让读者自己去理解? - Bluu
3
是的,我知道从编译语言角度看这似乎是一个容易出错的情况,但你只需要接受它。实际上,在实践中,我并没有发现这成为什么大问题(在我的例子中是Ruby,而不是Python,但原理相同)。 - Ed S.
当然,完成了。Triptych的回答和mkorpela的回答都很简单,我喜欢这一点,但后者显式优于隐式的精神,以及可理解的防止错误的方法更胜一筹。 - Bluu
1
这不是直接相同的,但是抽象基类会检查所有抽象方法是否被子类覆盖。当然,如果你要覆盖具体方法,这是没有帮助的。 - letmaik
2
请参阅 PEP 698 https://peps.python.org/pep-0698/,Python 3.12 中已添加了 @override 装饰器,并且可以从 typing 导入,例如 from typing import override https://docs.python.org/3.12/library/typing.html#typing.override。 - Yogev Neumann
显示剩余3条评论
14个回答

269

基于此和fwc的答案,我创建了一个可以通过pip安装的软件包https://github.com/mkorpela/overrides

时不时地,我会来到这里看看这个问题。主要是在我们的代码库中再次看到相同的bug后:有人忘记在重命名“interface”中的方法时实现类……

嗯,Python不是Java,但Python很强大——显式比隐式更好——在现实世界中有真正具体的案例,在这些案例中这个东西会帮助我。

因此,这里是overrides装饰器的草图。这将检查作为参数给定的类是否与被装饰的方法具有相同的方法(或其他内容)名称。

如果你能想到更好的解决方案,请在这里发布!

def overrides(interface_class):
    def overrider(method):
        assert(method.__name__ in dir(interface_class))
        return method
    return overrider

它的工作原理如下:

class MySuperInterface(object):
    def my_method(self):
        print 'hello world!'


class ConcreteImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def my_method(self):
        print 'hello kitty!'

如果你编写了有错误的版本,那么在类加载期间它将会引发断言错误:

class ConcreteFaultyImplementer(MySuperInterface):
    @overrides(MySuperInterface)
    def your_method(self):
        print 'bye bye!'

>> AssertionError!!!!!!!

35
太棒了。这让我第一次尝试时发现了一个拼写错误。赞。 - Christopher Bruns
8
该方法不是每次执行时都会被调用 - 只有在方法创建时才会被调用。 - mkorpela
8
你的代码应该被包含在Python默认库系统中。为什么不将它放入pip系统中呢? :P - user2081554
8
我建议告知 Python 核心开发者有关这个包的信息,他们可能会考虑将 override 装饰器添加到核心 Python 系统中。 :) - user2081554
4
请注意:Python包比原始的草图更为复杂,不再需要重新指定基类。 - Xiong Chiamiov
显示剩余7条评论

33
从Python 3.12版本开始(发布日期为2023年秋季),可以实现这一点。我建议您查看此网站https://peps.python.org/pep-0698/。它非常详细地解释了如何在Python中像Java一样装饰方法。
以下是一个代码示例,更多细节请参阅上述网站。
from typing import override

class Parent:
    def foo(self) -> int:
        return 1

    def bar(self, x: str) -> str:
        return x

class Child(Parent):
    @override
    def foo(self) -> int:
        return 2

    @override
    def baz() -> int:  # Type check error: no matching signature in ancestor
        return 1

1
附加参考资料:https://docs.python.org/3.12/whatsnew/3.12.html#typing 和 https://github.com/python/cpython/pull/101564。 - Mr. Polywhirl

31
这是一个不需要指定interface_class名称的实现。
import inspect
import re

def overrides(method):
    # actually can't do this because a method is really just a function while inside a class def'n  
    #assert(inspect.ismethod(method))

    stack = inspect.stack()
    base_classes = re.search(r'class.+\((.+)\)\s*\:', stack[2][4][0]).group(1)

    # handle multiple inheritance
    base_classes = [s.strip() for s in base_classes.split(',')]
    if not base_classes:
        raise ValueError('overrides decorator: unable to determine base class') 

    # stack[0]=overrides, stack[1]=inside class def'n, stack[2]=outside class def'n
    derived_class_locals = stack[2][0].f_locals

    # replace each class name in base_classes with the actual class type
    for i, base_class in enumerate(base_classes):

        if '.' not in base_class:
            base_classes[i] = derived_class_locals[base_class]

        else:
            components = base_class.split('.')

            # obj is either a module or a class
            obj = derived_class_locals[components[0]]

            for c in components[1:]:
                assert(inspect.ismodule(obj) or inspect.isclass(obj))
                obj = getattr(obj, c)

            base_classes[i] = obj


    assert( any( hasattr(cls, method.__name__) for cls in base_classes ) )
    return method

2
有点神奇,但可以让典型的使用变得更加容易。你能提供一些使用示例吗? - Bluu
使用此装饰器的平均和最坏情况成本是多少,可以与内置装饰器(如 @classmethod 或 @property)进行比较? - larham1
5
@larham1 这个装饰器仅在类定义被分析时执行一次,而不是在每次调用时执行。因此,与程序运行时间相比,它的执行成本是无关紧要的。 - Abgan
由于PEP 487的存在,这在Python 3.6中会更加优美。 - Neil G
为了获得更好的错误信息: assert any(hasattr(cls, method.name) for cls in base_classes), '在基类中未找到被覆盖的方法“{}”.'.format(method.name) - Ivan Kovtun

24

如果你只是想用于文档目的,你可以定义自己的重写修饰器:

def override(f):
    return f


class MyClass (BaseClass):

    @override
    def method(self):
        pass

这真的只是一些华而不实的东西,除非你创建了一个检查覆盖的 override(f)。

但这是Python啊,为什么要像Java一样写呢?


2
可以通过检查来添加实际验证到 override 装饰器中。 - Erik Kaplun
111
这是Python,为什么要写得像Java一样?因为Java中的一些想法很好,值得扩展到其他语言中吗? - Piotr Dobrogost
15
当你在超类中重命名一个方法时,知道一些两级下的子类正在覆盖它会很好。当然,检查很容易,但语言解析器的一点帮助也不会有什么坏处。 - Abgan
12
因为这是个好主意。其他许多语言也有同样的特点并不能成为支持或反对的理由。 - sfkleach

7

在 @mkorpela 的优秀答案的基础上进行改进,这里提供了更精确的检查、命名和错误提示。

更精确的检查、命名和错误提示

def overrides(interface_class):
    """
    Function override annotation.
    Corollary to @abc.abstractmethod where the override is not of an
    abstractmethod.
    Modified from answer https://dev59.com/G3M_5IYBdhLWcg3w6X5e#8313042
    """
    def confirm_override(method):
        if method.__name__ not in dir(interface_class):
            raise NotImplementedError('function "%s" is an @override but that'
                                      ' function is not implemented in base'
                                      ' class %s'
                                      % (method.__name__,
                                         interface_class)
                                      )

        def func():
            pass

        attr = getattr(interface_class, method.__name__)
        if type(attr) is not type(func):
            raise NotImplementedError('function "%s" is an @override'
                                      ' but that is implemented as type %s'
                                      ' in base class %s, expected implemented'
                                      ' type %s'
                                      % (method.__name__,
                                         type(attr),
                                         interface_class,
                                         type(func))
                                      )
        return method
    return confirm_override


以下是实际使用效果:

NotImplementedError "未在基类中实现"

class A(object):
    # ERROR: `a` is not a implemented!
    pass

class B(A):
    @overrides(A)
    def a(self):
        pass

导致更具描述性的NotImplementedError错误

function "a" is an @override but that function is not implemented in base class <class '__main__.A'>

全栈

Traceback (most recent call last):
  …
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 110, in confirm_override
    interface_class)
NotImplementedError: function "a" is an @override but that function is not implemented in base class <class '__main__.A'>

NotImplementedError "预期实现的类型"

class A(object):
    # ERROR: `a` is not a function!
    a = ''

class B(A):
    @overrides(A)
    def a(self):
        pass

导致更详细的NotImplementedError错误

function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>

全栈
Traceback (most recent call last):
  …
  File "C:/Users/user1/project.py", line 135, in <module>
    class B(A):
  File "C:/Users/user1/project.py", line 136, in B
    @overrides(A)
  File "C:/Users/user1/project.py", line 125, in confirm_override
    type(func))
NotImplementedError: function "a" is an @override but that is implemented as type <class 'str'> in base class <class '__main__.A'>, expected implemented type <class 'function'>




@mkorpela的回答很棒的一点是检查是在某个初始化阶段进行的。这个检查不需要“运行”。参考之前的例子,class B从未被初始化(B()),但NotImplementedError仍会引发。这意味着overrides错误被更早地捕获。


嗨!这看起来很有趣。你能否考虑在我的ipromise项目上提交一个pull request?我已经添加了一个答案。 - Neil G
@NeilG 我fork了ipromise项目并进行了一些编码。看起来你已经在overrides.py中实现了这个功能。除了将异常类型从TypeError更改为NotImplementedError之外,我不确定我还能做出什么其他显著的改进。 - JamesThomasMoon
嘿!谢谢,我没有检查重写对象实际上是否具有类型 types.MethodType。你在答案中提出的建议很好。 - Neil G

4

Python不是Java。当然,没有编译时检查这样的东西。

我认为在文档字符串中加上注释就足够了。这样可以让任何使用你方法的用户键入help(obj.method)并查看该方法是一个重写。

你也可以使用class Foo(Interface)显式地扩展一个接口,这将允许用户键入help(Interface.method)来了解您的方法所提供的功能。


70
Java 中 @Override 的真正作用并不是为了文档记录,而是在你意图重写一个方法但实际上定义了一个新方法时(例如拼写错误),帮助你捕捉这个错误。在 Java 中,可能会发生因为使用了错误的签名而导致的问题,但在 Python 中不存在这个问题——不过拼写错误仍然是需要注意的。 - Pavel Minaev
2
@ Pavel Minaev: 确实,但是如果你在使用没有自动指示重写的IDE /文本编辑器(例如Eclipse的JDT会在行号旁边清晰显示),尤其是对于文档而言,这还是很方便的。 - Tuukka Mustonen
2
@PavelMinaev 错了。@Override 的主要作用之一是除了编译时检查外还有文档说明的作用。 - siamii
11
@siamii,我认为文档辅助工具非常好,但在所有官方的Java文档中,我只看到了编译时检查的重要性。请证明你的说法,即Pavel是“错误的”。 - Andrew Mellinger

2

像其他人所说的,与Java不同,Python中没有@Overide标记,但是您可以使用装饰器创建自己的标记。然而,我建议使用getattrib()全局方法,而不是使用内部字典,这样您将获得以下内容:

def Override(superClass):
    def method(func)
        getattr(superClass,method.__name__)
    return method

如果你愿意的话,可以在你自己的try catch中捕获getattr()并抛出你自己的错误,但我认为在这种情况下使用getattr方法更好。此外,这还可以捕获绑定到类的所有项,包括类方法和变量。

2
根据 @mkorpela 的完美回答,我编写了一个类似的程序包 (ipromise pypi github ),它可以进行更多的检查:
假设 A 继承自 BCB 继承自 C
模块 ipromise 进行以下检查:
  • 如果 A.f 覆盖了 B.f,那么 B.f 必须存在,并且 A 必须继承自 B。(这是 overrides 包中的检查)。

  • 您没有模式 A.f 声明它覆盖了 B.f,然后声明它覆盖了 C.fA 应该说它从 C.f 覆盖,因为 B 可能决定停止覆盖此方法,这不应导致下游更新。

  • 您没有模式 A.f 声明它覆盖了 C.f,但 B.f 没有声明其覆盖。

  • 您没有模式 A.f 声明它覆盖了 C.f,但 B.f 声明它从某个 D.f 覆盖。

此外,它还具有各种功能来标记和检查实现抽象方法。


1

从Python 3.6开始,@override提供的功能可以通过Python的描述符协议轻松实现,即使用set_name方法:

class override:
    def __init__(self, func):
       self._func = func
       update_wrapper(self, func)

    def __get__(self, obj, obj_type):
        if obj is None:
            return self
        return self._func

    def __set_name__(self, obj_type, name):
        self.validate_override(obj_type, name)

    def validate_override(self, obj_type, name):
        for parent in obj_type.__bases__:
            func = parent.__dict__.get(name, None)
            if callable(func):
                return
        else:
            raise NotImplementedError(f"{obj_type.__name__} does not override {name}")

请注意,这里一旦定义了包装类,就会调用set_name,我们可以通过调用其dunder方法bases来获取包装类的父类。
对于每个父类,我们想要检查该函数是否在类中实现,方法如下:
  1. 检查函数名是否在类字典中
  2. 它是可调用的

使用i将非常简单:
class AbstractShoppingCartService:
    def add_item(self, request: AddItemRequest) -> Cart:
        ...


class ShoppingCartService(AbstractShoppingCartService):
    @override
    def add_item(self, request: AddItemRequest) -> Cart:
        ...

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

1
你可以使用PEP 544中的协议。使用这种方法,仅在使用时声明接口实现关系。
假设你已经有了一个实现(我们称之为MyFoobar),你定义一个接口(一个协议),其中包含你的实现所有方法和字段的签名,我们称之为IFoobar
然后,在使用时,你声明实现实例绑定到接口类型,例如myFoobar: IFoobar = MyFoobar()。现在,如果你使用一个在接口中缺失的字段/方法,Mypy会在使用时抱怨(即使它在运行时可行!)。如果你未在实现中实现接口中的方法,Mypy也会抱怨。如果你实现了接口中不存在的内容,Mypy不会抱怨。但这种情况很少见,因为接口定义是紧凑且易于审查的。你将无法实际使用该代码,因为Mypy会抱怨。
现在,这并不涵盖超类和实现类中都有实现的情况,例如一些使用 ABC 的情况。但是,在 Java 中即使没有接口实现,也会使用 override。这个解决方案覆盖了这种情况。
from typing import Protocol

class A(Protocol):
    def b(self):
        ...
    def d(self):  # we forgot to implement this in C
        ...

class C:
    def b(self):
        return 0

bob: A = C()

类型检查结果为:

test.py:13: error: Incompatible types in assignment (expression has type "C", variable has type "A")
test.py:13: note: 'C' is missing following 'A' protocol member:
test.py:13: note:     d
Found 1 error in 1 file (checked 1 source file)

有示例吗?有什么可以通过/失败mypy的东西吗? - Bluu
@Bluu:是的,请查看定义协议部分。 - Janus Troelsen
你能否在回答中包含示例源代码和 mypy 结果?还需要指定版本号和工具(我认为 mypy 是这个问题线程中的新工具)吗?我认为这样会使你的回答更加自包含。 - Bluu
@Bluu 我已经添加了它。 - Janus Troelsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接