如何在不模拟的情况下临时替换 Python class 方法?
How to temporarily replace Python class method without mocking?
在替换 Python 3.8 中观察到的 class 方法时,我一直想知道这种行为,并得出结论,我不理解它。我怀疑这可能与丢失 @classmethod
装饰器或类似的东西有关,但我不知所措。
到底哪里出了问题?
在不使用模拟库的情况下使最后一个案例工作的好方法是什么?
注意:我知道下面的代码不是最佳实践。这是关于尝试更多地了解 Python 并了解幕后发生的事情。
from unittest import mock
class SomeClass:
@classmethod
def hello(cls) -> str:
return cls.__name__
class DerivedClass(SomeClass):
pass
def test_works_as_expected():
assert SomeClass.hello() == 'SomeClass' # True
assert DerivedClass.hello() == 'DerivedClass' # True
def test_replace_with_mock():
# This works just fine
assert DerivedClass.hello() == 'DerivedClass' # True
with mock.patch.object(SomeClass, 'hello', new=lambda: 'replacement'):
assert DerivedClass.hello() == 'replacement' # True
assert DerivedClass.hello() == 'DerivedClass' # True
def test_this_does_not_work():
assert DerivedClass.hello() == 'DerivedClass' # True
original_fn = SomeClass.hello
SomeClass.hello = lambda: 'replacement'
assert DerivedClass.hello() == 'replacement' # True
SomeClass.hello = original_fn # This should put things back in order, but does not
assert DerivedClass.hello() == 'DerivedClass' # AssertionError: assert 'SomeClass' == 'DerivedClass'
# After executing the above DerivedClass.hello() no longer works correctly in this module or in any other
欢迎来到Descriptor protocol!
考虑这段代码:
hello1 = SomeClass.hello
hello2 = DerivedClass.hello
print(hello1()) # 'SomeClass'
print(hello2()) # 'DerivedClass'
hello1
和 hello2
是不同的,即使它们都是从 hello
.
的相同定义中检索到的
这是因为在 classes 和 classmethod
s 中定义的普通函数 都实现了描述符协议,每当从一个 class 或一个对象。
SomeClass.hello
(以及 SomeClass().hello
)returns 带有 cls
参数的基础函数(或者 self
如果它不是 classmethod
) 到从中检索到的 class (或实例)。让我们检查一下:
print(SomeClass.hello) # <bound method SomeClass.hello of <class '__main__.SomeClass'>>
print(DerivedClass.hello) # <bound method SomeClass.hello of <class '__main__.DerivedClass'>>
如果要为 SomeClass
保存和恢复 hello
的原始值,则不能使用对象访问。让我们使用 __dict__
代替:
hello = SomeClass.__dict__['hello']
print(hello) # <classmethod at 0x12345678>
SomeClass.hello = lambda: 'replacement'
print(DerivedClass.hello()) # 'replacement'
SomeClass.hello = hello
print(DerivedClass.hello()) # 'DerivedClass'
(是的,当然,模拟是一种代码味道 - 所有代码仅用于解释目的。)
在替换 Python 3.8 中观察到的 class 方法时,我一直想知道这种行为,并得出结论,我不理解它。我怀疑这可能与丢失 @classmethod
装饰器或类似的东西有关,但我不知所措。
到底哪里出了问题?
在不使用模拟库的情况下使最后一个案例工作的好方法是什么?
注意:我知道下面的代码不是最佳实践。这是关于尝试更多地了解 Python 并了解幕后发生的事情。
from unittest import mock
class SomeClass:
@classmethod
def hello(cls) -> str:
return cls.__name__
class DerivedClass(SomeClass):
pass
def test_works_as_expected():
assert SomeClass.hello() == 'SomeClass' # True
assert DerivedClass.hello() == 'DerivedClass' # True
def test_replace_with_mock():
# This works just fine
assert DerivedClass.hello() == 'DerivedClass' # True
with mock.patch.object(SomeClass, 'hello', new=lambda: 'replacement'):
assert DerivedClass.hello() == 'replacement' # True
assert DerivedClass.hello() == 'DerivedClass' # True
def test_this_does_not_work():
assert DerivedClass.hello() == 'DerivedClass' # True
original_fn = SomeClass.hello
SomeClass.hello = lambda: 'replacement'
assert DerivedClass.hello() == 'replacement' # True
SomeClass.hello = original_fn # This should put things back in order, but does not
assert DerivedClass.hello() == 'DerivedClass' # AssertionError: assert 'SomeClass' == 'DerivedClass'
# After executing the above DerivedClass.hello() no longer works correctly in this module or in any other
欢迎来到Descriptor protocol!
考虑这段代码:
hello1 = SomeClass.hello
hello2 = DerivedClass.hello
print(hello1()) # 'SomeClass'
print(hello2()) # 'DerivedClass'
hello1
和 hello2
是不同的,即使它们都是从 hello
.
这是因为在 classes 和 classmethod
s 中定义的普通函数 都实现了描述符协议,每当从一个 class 或一个对象。
SomeClass.hello
(以及 SomeClass().hello
)returns 带有 cls
参数的基础函数(或者 self
如果它不是 classmethod
) 到从中检索到的 class (或实例)。让我们检查一下:
print(SomeClass.hello) # <bound method SomeClass.hello of <class '__main__.SomeClass'>>
print(DerivedClass.hello) # <bound method SomeClass.hello of <class '__main__.DerivedClass'>>
如果要为 SomeClass
保存和恢复 hello
的原始值,则不能使用对象访问。让我们使用 __dict__
代替:
hello = SomeClass.__dict__['hello']
print(hello) # <classmethod at 0x12345678>
SomeClass.hello = lambda: 'replacement'
print(DerivedClass.hello()) # 'replacement'
SomeClass.hello = hello
print(DerivedClass.hello()) # 'DerivedClass'
(是的,当然,模拟是一种代码味道 - 所有代码仅用于解释目的。)