[英]How to detect if `__init_subclass__` has been overridden in a subclass?
通常在 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
,但如果该方法已在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
我有两个问题:
__init_subclass__
或__subclasshook__
在超类中未定义后是否已在子类中定义? __init_subclass__
是一个特殊的类方法,无论你是否用classmethod
装饰它。 就像Foo().mymethod
每次通过类实例访问属性时都会返回一个新的method
实例一样,每次通过类本身访问属性时, Foo.__init_subclass__
生成一个新的instance
方法。
另一方面, __subclasshook__
必须声明为类方法才能正常工作。 如果将其定义为简单的函数/实例方法,则不会假定它是类方法。
__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__
的文档中看到的__init_subclass__
,它说:
类方法对象。
__init_subclass__
(cls)
它说“类方法”。
__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 中的方法是描述符。
观察以下代码行在实例方法方面的等效性。 呼叫的所述第一(较正常的)方式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
对Foo.__dict__
的未绑定方法调用__get__
每次都会生成一个新对象; 只有通过访问类上的实例方法,我们才能测试身份,就像您在问题中所做的那样。 但是,对于classmethod
s,即使从类访问方法也会在方法上调用__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
非常感谢@chepner和U12-Forward ,他们出色的回答帮助我解决了这个问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.