如何检测 __init_subclass__` 是否已在子类中被重写?

Ale*_*ood 5 python oop inheritance

通常在Python中,可以使用以下技术来检测子类中的方法是否已被重写:

>>> class Foo:
...     def mymethod(self): pass
...
>>> class Bar(Foo): pass
...
>>> Bar.mymethod is Foo.mymethod 
True
Run Code Online (Sandbox Code Playgroud)

如果来自的方法尚未在 中被重写,则该表达式Bar.mymethod is Foo.mymethod将计算为,但如果该方法在 中被重写,该表达式将计算为。此技术适用于从 继承的 dunder 方法以及非 dunder 方法:TrueFooBarFalseBarobject

>>> Bar.__new__ is Foo.__new__
True
>>> Bar.__eq__ is Foo.__eq__
True
Run Code Online (Sandbox Code Playgroud)

我们可以在函数中形式化这个逻辑,如下所示:

def method_has_been_overridden(superclass, subclass, method_name):
    """
    Return `True` if the method with the name `method_name`
    has been overridden in the subclass
    or an intermediate class in the method resolution order
    """
    if not issubclass(subclass, superclass):
        raise ValueError(
            "This function only makes sense if `subclass` is a subclass of `superclass`"
        )
    subclass_method = getattr(subclass, method_name)
    if not callable(method):
        raise ValueError(f"'{subclass.__name__}.{method_name}' is not a method")
    return subclass_method is not getattr(superclass, method_name, object())
Run Code Online (Sandbox Code Playgroud)

然而,当涉及到以下两种方法时,该技术就会失败:__init_subclass____subclasshook__

>>> class Foo: pass
...
>>> class Bar(Foo): pass
...
>>> Bar.__init_subclass__ is Foo.__init_subclass__
False
>>> Bar.__subclasshook__ is Foo.__subclasshook__
False
Run Code Online (Sandbox Code Playgroud)

还有一个更令人困惑的例子:

>>> type.__init_subclass__ is type.__init_subclass__
False
Run Code Online (Sandbox Code Playgroud)

我有两个问题:

  1. 为什么这项技术在使用这些方法时会失败,并且仅使用这些方法?(我还没有找到这种技术失败的任何其他例子——但如果有的话,我很想知道它们!)
  2. 是否有一种替代技术可用于检测在超类中__init_subclass__未定义__subclasshook__后是否已在子类中定义?

Ale*_*ood 3

__init_subclass__是一个特殊的方法,因为classmethod即使您@classmethod在定义它时不使用它来装饰它,它也是隐式的。然而,这里的问题并不是因为这__init_subclass__是一种特殊方法。相反,您用来检测某个方法是否已在子类中被重写的技术存在一个根本错误:它根本不适用于任何classmethods:

>>> class Foo:
...     def mymethod(self): pass
...     @classmethod
...     def my_classmethod(cls): pass
...
>>> class Bar(Foo): pass
...
>>> Bar.mymethod is Foo.mymethod
True
>>> Bar.my_classmethod is Foo.my_classmethod
False
Run Code Online (Sandbox Code Playgroud)

这是因为Python 中绑定方法的工作方式:Python 中的方法是描述符

观察以下代码行与实例方法的等效性。第一种(更正常的)调用mymethod实例的方法f只是第二种调用实例方法的方法的语法糖f

>>> class Foo:
...     def mymethod(self):
...         print('Instance method')
... 
>>> f = Foo()
>>> f.mymethod()
Instance method
>>> Foo.__dict__['mymethod'].__get__(f, Foo)()
Instance method
Run Code Online (Sandbox Code Playgroud)

每次调用__get__in 中的未绑定方法都会生成一个新对象;Foo.__dict__只有通过访问上的实例方法,我们才能测试身份,就像您在问题中所做的那样。然而,对于s,即使从classmethod访问该方法也会调用该方法:__get__

>>> class Foo:
...     @classmethod
...     def my_classmethod(cls):
...         print('Class method')
...
>>> Foo.my_classmethod is Foo.my_classmethod
False
>>> Foo.my_classmethod()
Class method
>>> Foo.__dict__['my_classmethod'].__get__(Foo, Foo)()
Class method
Run Code Online (Sandbox Code Playgroud)

关于什么__new__

您的问题指出您现有的方法适用于__new__. 这很奇怪——我们刚刚确定这个方法不适用于classmethods,而且看起来__new__确实像. 毕竟,第一个参数名为!然而,Python 文档明确表明事实并非如此:classmethod__new__cls

object.__new__(cls[, ...])

调用以创建 class 的新实例cls__new__()是一个静态方法(特殊情况,因此不需要这样声明),它将请求实例的类作为其第一个参数。

是一个staticmethod,不是一个classmethod!谜团已揭开。

检测子类是否覆盖超类方法的更好方法

确定某个方法是否已在子类中被重写的唯一可靠方法是按__dict__方法解析顺序遍历每个类:

def method_has_been_overridden(superclass, subclass, method_name):
    """
    Return `True` if the method with the name `method_name`
    has been overridden in the subclass
    or an intermediate class in the method resolution order
    """
    if not issubclass(subclass, superclass):
        raise ValueError(
            "This function only makes sense if `subclass` is a subclass of `superclass`"
        )
    subclass_method = getattr(subclass, method_name)
    if not callable(subclass_method):
        raise ValueError(f"'{subclass.__name__}.{method_name}' is not a method")
    for cls in subclass.__mro__:
        if cls is superclass:
            return False
        if method_name in cls.__dict__:
            return True
Run Code Online (Sandbox Code Playgroud)

此函数可以正确确定 或__init_subclass__任何其他classmethod, 是否已在子类中被重写:

>>> class Foo: pass
...
>>> class Bar(Foo): pass
...
>>> class Baz(Foo):
...     def __init_subclass__(cls, *args, **kwargs):
...         return super().__init_subclass__(*args, **kwargs)
>>> method_has_been_overridden(Foo, Bar, '__init_subclass__')
False
>>> method_has_been_overridden(Foo, Baz, '__init_subclass__')
True
Run Code Online (Sandbox Code Playgroud)

非常感谢@chepnerU12-Forward,他们的出色回答帮助我解决了这个问题。