如何在不模拟的情况下临时替换 Python class 方法?

How to temporarily replace Python class method without mocking?

在替换 Python 3.8 中观察到的 class 方法时,我一直想知道这种行为,并得出结论,我不理解它。我怀疑这可能与丢失 @classmethod 装饰器或类似的东西有关,但我不知所措。

  1. 到底哪里出了问题?

  2. 在不使用模拟库的情况下使最后一个案例工作的好方法是什么

注意:我知道下面的代码不是最佳实践。这是关于尝试更多地了解 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'

hello1hello2 是不同的,即使它们都是从 hello.

的相同定义中检索到的

这是因为在 classes 和 classmethods 中定义的普通函数 都实现了描述符协议,每当从一个 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'

(是的,当然,模拟是一种代码味道 - 所有代码仅用于解释目的。)