如何检测 `__init_subclass__` 是否已在子类中被覆盖?
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
继承的双下划线方法以及非双下划线方法:
>>> 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__
是一个特殊的 class 方法,无论你是否用 classmethod
修饰它。就像每次通过 class 实例访问属性时 Foo().mymethod
returns 一个新的 method
实例一样,Foo.__init_subclass__
每次都会生成一个新的 instance
方法通过 class 本身访问属性的时间。
__subclasshook__
,另一方面,必须声明为class方法才能正常工作。如果您将其定义为简单的 function/instance 方法,则不假定它是 class 方法。
__init_subclass__
和 __subclasshook__
是 class 方法。正如你在这里看到的:
>>> 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__
指的是它们的class是不同的实例,Bar
的十六进制是0x000002D70AAC5340
,Foo
的十六进制是0x000002D70AACAF70
.
正如您在 __init_subclass__
的文档中所见,它说:
classmethod object.__init_subclass__
(cls)
上面写着“class方法”。
__init_subclass__
是一种特殊的方法,因为即使在定义它时没有用 @classmethod
修饰它,它也隐含地是一个 classmethod
。但是,这里的问题并不是因为 __init_subclass__
是一种特殊方法。相反,您用于检测子class 中的方法是否被覆盖的技术存在一个根本错误:它根本不适用于任何 classmethod
s:
>>> 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 上的实例方法,我们才能像您在问题中所做的那样测试身份。但是,关于 classmethod
s,即使从 class 访问方法也会调用 __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
s,并且 __new__
当然 看起来 像 classmethod
。毕竟 __new__
的第一个参数名为 cls
!但是,Python documentation 清楚地表明情况并非如此:
object.__new__(cls[, ...])
Called to create a new instance of class cls
. __new__()
is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument.
是 staticmethod
,不是 classmethod
!谜底揭晓。
检测子class是否覆盖超类方法的更好方法class
确定某个方法是否已在子class 中被覆盖的唯一可靠方法是遍历方法解析顺序中每个 class 的 __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:
中被覆盖
>>> 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
非常感谢 and ,他们的出色回答帮助我解决了这个问题。
通常在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
继承的双下划线方法以及非双下划线方法:
>>> 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__
是一个特殊的 class 方法,无论你是否用 classmethod
修饰它。就像每次通过 class 实例访问属性时 Foo().mymethod
returns 一个新的 method
实例一样,Foo.__init_subclass__
每次都会生成一个新的 instance
方法通过 class 本身访问属性的时间。
__subclasshook__
,另一方面,必须声明为class方法才能正常工作。如果您将其定义为简单的 function/instance 方法,则不假定它是 class 方法。
__init_subclass__
和 __subclasshook__
是 class 方法。正如你在这里看到的:
>>> 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__
指的是它们的class是不同的实例,Bar
的十六进制是0x000002D70AAC5340
,Foo
的十六进制是0x000002D70AACAF70
.
正如您在 __init_subclass__
的文档中所见,它说:
classmethod object.
__init_subclass__
(cls)
上面写着“class方法”。
__init_subclass__
是一种特殊的方法,因为即使在定义它时没有用 @classmethod
修饰它,它也隐含地是一个 classmethod
。但是,这里的问题并不是因为 __init_subclass__
是一种特殊方法。相反,您用于检测子class 中的方法是否被覆盖的技术存在一个根本错误:它根本不适用于任何 classmethod
s:
>>> 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 上的实例方法,我们才能像您在问题中所做的那样测试身份。但是,关于 classmethod
s,即使从 class 访问方法也会调用 __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
s,并且 __new__
当然 看起来 像 classmethod
。毕竟 __new__
的第一个参数名为 cls
!但是,Python documentation 清楚地表明情况并非如此:
object.__new__(cls[, ...])
Called to create a new instance of class
cls
.__new__()
is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument.
是 staticmethod
,不是 classmethod
!谜底揭晓。
检测子class是否覆盖超类方法的更好方法class
确定某个方法是否已在子class 中被覆盖的唯一可靠方法是遍历方法解析顺序中每个 class 的 __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:
>>> 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
非常感谢