Monkey patching a @property

Is it at all possible to monkey patch the value of a @property of an instance of a class that I do not control?

class Foo:
    @property
    def bar(self):
        return here().be['dragons']

f = Foo()
print(f.bar)  # baz
f.bar = 42    # MAGIC!
print(f.bar)  # 42

Obviously the above would produce an error when trying to assign to f.bar. Is # MAGIC! possible in any way? The implementation details of the @property are a black box and not indirectly monkey-patchable. The entire method call needs to be replaced. It needs to affect a single instance only (class-level patching is okay if inevitable, but the changed behaviour must only selectively affect a given instance, not all instances of that class).


Subclass the base class (Foo) and change single instance's class to match the new subclass using __class__ attribute:

>>> class Foo:
...     @property
...     def bar(self):
...         return 'Foo.bar'
...
>>> f = Foo()
>>> f.bar
'Foo.bar'
>>> class _SubFoo(Foo):
...     bar = 0
...
>>> f.__class__ = _SubFoo
>>> f.bar
0
>>> f.bar = 42
>>> f.bar
42

from module import ClassToPatch

def get_foo(self):
    return 'foo'

setattr(ClassToPatch, 'foo', property(get_foo))

To monkey patch a property, there is an even simpler way:

from module import ClassToPatch

def get_foo(self):
    return 'foo'

ClassToPatch.foo = property(get_foo)

Idea: replace property descriptor to allow setting on certain objects. Unless a value is explicitly set this way, original property getter is called.

The problem is how to store the explicitly set values. We cannot use a dict keyed by patched objects, since 1) they are not necessarily comparable by identity; 2) this prevents patched objects from being garbage-collected. For 1) we could write a Handle that wraps objects and overrides comparison semantics by identity and for 2) we could use weakref.WeakKeyDictionary. However, I couldn't make these two work together.

Therefore we use a different approach of storing the explicitly set values on the object itself, using a "very unlikely attribute name". It is of course still possible that this name would collide with something, but that's pretty much inherent to languages such as Python.

This won't work on objects that lack a __dict__ slot. Similar problem would arise for weakrefs though.

class Foo:
    @property
    def bar (self):
        return 'original'

class Handle:
    def __init__(self, obj):
        self._obj = obj

    def __eq__(self, other):
        return self._obj is other._obj

    def __hash__(self):
        return id (self._obj)


_monkey_patch_index = 0
_not_set            = object ()
def monkey_patch (prop):
    global _monkey_patch_index, _not_set
    special_attr = '$_prop_monkey_patch_{}'.format (_monkey_patch_index)
    _monkey_patch_index += 1

    def getter (self):
        value = getattr (self, special_attr, _not_set)
        return prop.fget (self) if value is _not_set else value

    def setter (self, value):
        setattr (self, special_attr, value)

    return property (getter, setter)

Foo.bar = monkey_patch (Foo.bar)

f = Foo()
print (Foo.bar.fset)
print(f.bar)  # baz
f.bar = 42    # MAGIC!
print(f.bar)  # 42