GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.03k stars 231 forks source link

Descriptors don't seem to work, if they're mixed in #227

Open AlexanderFarkas opened 1 year ago

AlexanderFarkas commented 1 year ago

class memoized_property(object):
    """A read-only @property that is only evaluated once."""

    def __init__(self, fget, doc=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        obj.__dict__[self.__name__] = result = self.fget(obj)
        return result

class B:
    @memoized_property
    def c(self):
        return WeakKeyDictionary()

class Proxy(wrapt.ObjectProxy, B):
    pass

if __name__ == '__main__':
    b = ProxySon(B())
    print(b.c)
    print(b.c)

Output:

<WeakKeyDictionary at 0x1109a50d0>
<WeakKeyDictionary at 0x1109a51d0>
!!! not the same
GrahamDumpleton commented 1 year ago

Sorry for slow reply, was on holiday at the time and just catching up.

As far as I can tell your implementation of __get__() is wrong as you don't check to see whether you already cached the value and return that. Instead you call self.fget() every time. I would expect something like:

import wrapt

from weakref import WeakKeyDictionary

class memoized_property(object):
    """A read-only @property that is only evaluated once."""

    def __init__(self, fget, doc=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        if not self.__name__ in obj.__dict__:
            result = obj.__dict__[self.__name__] = self.fget(obj)
        else:
            result = obj.__dict__[self.__name__]
        return result

class B:
    @memoized_property
    def c(self):
        return WeakKeyDictionary()

class Proxy(wrapt.ObjectProxy, B):
    pass

if __name__ == '__main__':
    b = Proxy(B())
    print(b.c)
    print(b.c)
AlexanderFarkas commented 1 year ago

It's the implementation taken directly from the SQLAlchemy library. Descriptors are not called if there is an item in __dict___ with the corresponding name - so implementation is perfectly valid.