1

I have been wondering about this behaviour when replacing a class method I observe in Python 3.8 and concluded I do not understand it. I suspect it may have something to do with loosing the @classmethod decorator or something similar, but am at a loss.

  1. What exactly is going wrong there?

  2. What is a good way to make the last case work without using mocking libraries?

Note: I know the code below is not the best practice. This is about trying to learn more about Python and understanding what is going on under the hood.

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
vch
  • 131
  • 8

1 Answers1

1

Welcome to the Descriptor protocol!

Consider this code:

hello1 = SomeClass.hello
hello2 = DerivedClass.hello
print(hello1())  # 'SomeClass'
print(hello2())  # 'DerivedClass'

hello1 and hello2 are different even though they both are retrieved from the same definition of hello.

This is because both ordinary functions defined inside classes and classmethods implement the descriptor protocol, which is used whenever a value is retrieved from a class or an object.

SomeClass.hello (as well as SomeClass().hello) returns the underlying function with the cls argument (or self if it weren't a classmethod) to the class (or instance) it was retrieved from. Let's check:

print(SomeClass.hello)  # <bound method SomeClass.hello of <class '__main__.SomeClass'>>
print(DerivedClass.hello)  # <bound method SomeClass.hello of <class '__main__.DerivedClass'>>

If you want to save and restore the original value of hello for SomeClass, you cannot use the object access. Let's use __dict__ instead:

hello = SomeClass.__dict__['hello']
print(hello)  # <classmethod at 0x12345678>
SomeClass.hello = lambda: 'replacement'
print(DerivedClass.hello())  # 'replacement'
SomeClass.hello = hello
print(DerivedClass.hello())  # 'DerivedClass'

(And yes, of course, mocking is a code smell - all code for explanatory purposes only.)

Koterpillar
  • 7,883
  • 2
  • 25
  • 41