如何检测子类是否覆盖了 `__init_subclass__` 方法?

5

通常在Python中,可以使用以下技术检测子类是否覆盖了方法:

>>> class Foo:
...     def mymethod(self): pass
...
>>> class Bar(Foo): pass
...
>>> Bar.mymethod is Foo.mymethod 
True

如果在Bar中没有覆盖Foo的方法,则表达式Bar.mymethod is Foo.mymethod将计算为True,但是如果在Bar中覆盖了该方法,则计算结果为False。此技术适用于从object继承的dunder方法以及非dunder方法:

>>> Bar.__new__ is Foo.__new__
True
>>> Bar.__eq__ is Foo.__eq__
True

我们可以编写一个函数来形式化这个逻辑,如下所示:
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())

然而,当涉及到__init_subclass____subclasshook__这两种方法时,该技术将失败:

>>> class Foo: pass
...
>>> class Bar(Foo): pass
...
>>> Bar.__init_subclass__ is Foo.__init_subclass__
False
>>> Bar.__subclasshook__ is Foo.__subclasshook__
False

而,更让人迷惑的例子如下:

>>> type.__init_subclass__ is type.__init_subclass__
False

我有两个问题:

  1. 为什么这种技术只会与这些方法失败?(我没有找到任何其他这种技术失败的例子--但如果有,请告诉我!)
  2. 是否有一种替代技术,可以用来检测在超类中未定义的情况下,子类是否已定义了__init_subclass____subclasshook__

1
一个检查覆盖的方法,可能无法保证适用于所有实现,是 Bar.__init_subclass__.__func__ is Foo.__init_subclass__.__func__ - Michael Butscher
2
@MichaelButscher:不幸的是,这甚至对于简单的CPython和问题中的简单“Foo”和“Bar”示例类也不起作用。在C中编写的classmethods(例如object.__init_subclass__)使用与在Python中编写的classmethods不同的类型,并且C方法的类型不支持__func__ - user2357112
3个回答

4

__init_subclass__是一种特殊的类方法,无论您是否使用classmethod进行修饰,都会被视为类方法。就像Foo().mymethod每次通过类实例访问属性时都会返回一个新的method实例一样,Foo.__init_subclass__每次通过类本身访问属性时都会产生一个新的instance方法。

另一方面,__subclasshook__必须声明为类方法才能正常工作,如果您将其定义为简单函数/实例方法,则不会被视为类方法。


3

__init_subclass__ 是一种特殊的方法,即使在定义时没有使用 @classmethod 修饰符,它也隐式地成为一个 classmethod。然而,这里的问题并不是因为 __init_subclass__ 是一个特殊方法。相反,您使用的技术来检测一个子类是否覆盖了一个方法存在基本错误:它根本不能用于任何 classmethod

>>> 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

这是由于Python中的绑定方法的工作方式:在Python中,方法是描述符

观察以下代码行在实例方法方面的等效性。在实例f上调用mymethod的第一种(更常见)方式仅仅是语法糖,用来调用实例f上的方法的第二种方式:
>>> class Foo:
...     def mymethod(self):
...         print('Instance method')
... 
>>> f = Foo()
>>> f.mymethod()
Instance method
>>> Foo.__dict__['mymethod'].__get__(f, Foo)()
Instance method

Foo.__dict__中调用未绑定方法的__get__每次都会产生一个新对象;只有通过在上访问实例方法才能测试标识,就像您在问题中所做的那样。然而,对于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

那么__new__呢?

你的问题指出你现有的方法可以使用__new__。这很奇怪——我们刚刚证明了这个方法不能用于classmethod,而__new__看起来确实像一个classmethod。毕竟,__new__的第一个参数被命名为cls!然而,Python文档清楚地表明这根本不是这种情况:

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

用于创建类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

这个函数能正确地确定__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

感谢@chepnerU12-Forward的精彩答案,帮助我解决了这个问题。


2

__init_subclass____subclasshook__是类方法。你可以在这里看到:

>>> Bar.__sizeof__
<method '__sizeof__' of 'object' objects>
>>> Bar.__eq__
<slot wrapper '__eq__' of 'object' objects>
>>> Bar.__subclasshook__
<built-in method __subclasshook__ of type object at 0x000002D70AAC5340>
>>> Bar.__init_subclass__
<built-in method __init_subclass__ of type object at 0x000002D70AAC5340>
>>> Foo.__init_subclass__
<built-in method __init_subclass__ of type object at 0x000002D70AACAF70>
>>> 

__init_subclass____subclasshook__是针对不同实例的类引用,Bar的十六进制是0x000002D70AAC5340,而Foo的十六进制是0x000002D70AACAF70


如您在__init_subclass__文档中所见,它写道:

classmethod object.__init_subclass__(cls)

它说“classmethod”。


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