【问题标题】:How to detect if `__init_subclass__` has been overridden in a subclass?如何检测 `__init_subclass__` 是否已在子类中被覆盖?
【发布时间】:2021-12-15 14:50:01
【问题描述】:

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

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

如果Foo 中的方法没有Bar 中被覆盖,则表达式Bar.mymethod is Foo.mymethod 将评估为True,但如果方法 将评估为False已在Bar 中覆盖。此技术适用于从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__ 在超类中未定义后是否已在子类中定义?

【问题讨论】:

  • 一种检查覆盖的方法可能不能保证适用于所有实现是Bar.__init_subclass__.__func__ is Foo.__init_subclass__.__func__
  • @MichaelButscher:不幸的是,这甚至不适用于纯 CPython 和问题中简单的 FooBar 示例类。用 C 编写的类方法(如 object.__init_subclass__)使用与用 Python 编写的类方法不同的类型,并且 C 方法的类型不支持 __func__

标签: python oop inheritance


【解决方案1】:

__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

这是因为绑定方法在 Python 中的工作方式:在 Python 中,方法是 descriptors

观察以下代码行关于实例方法的等价性。在实例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__每次都会产生一个新对象;只有通过访问 class 上的实例方法,我们才能测试身份,就像您在问题中所做的那样。然而,对于classmethods,即使从中访问方法也会在方法上调用__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__。这很奇怪——我们刚刚确定此方法不适用于classmethods,而且__new__ 肯定看起来classmethod__new__的第一个参数命名为cls,毕竟!但是,Python documentation 明确表明情况并非如此:

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__ 是一个特殊的类方法,不管你是否用classmethod 装饰它。就像 Foo().mymethod 每次通过类实例访问属性时都会返回一个新的 method 实例一样,Foo.__init_subclass__ 每次通过类本身访问属性时都会产生一个新的 instance 方法。

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

    【讨论】:

      【解决方案3】:

      __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的十六进制是0x000002D70AAC5340Foo的十六进制是0x000002D70AACAF70


      正如您在__init_subclass__ 的文档中看到的那样:

      类方法 对象.__init_subclass__(cls)

      上面写着“类方法”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-05
        • 1970-01-01
        • 2013-07-13
        • 1970-01-01
        • 1970-01-01
        • 2012-09-18
        相关资源
        最近更新 更多