[Python-ideas] Weak-referencing/weak-proxying of (bound) methods

Jan Kaliszewski zuo at chopin.edu.pl
Sat Jun 16 10:42:57 CEST 2012


Jan Kaliszewski dixit (2012-06-16, 01:41):

> Tal Einat dixit (2012-06-15, 15:41):
> 
> > On Mon, Jun 11, 2012 at 2:16 AM, Jan Kaliszewski <zuo at chopin.edu.pl> wrote:
> [snip]
> > >    >>> import weakref
> > >    >>> class A:
> > >    ...     def method(self): print(self)
> > >    ...
> > >    >>> A.method
> > >    <function method at 0xb732926c>
> > >    >>> a = A()
> > >    >>> a.method
> > >    <bound method A.method of <__main__.A object at 0xb7326bec>>
> > >    >>> r = weakref.ref(a.method)  # creating a weak reference
> > >    >>> r                          # ...but it appears to be dead
> > >    <weakref at 0xb7327d9c; dead>
> > >    >>> w = weakref.proxy(a.method)  # the same with a weak proxy
> > >    >>> w
> > >    <weakproxy at 0xb7327d74 to NoneType at 0x829f7d0>
> > >    >>> w()
> > >    Traceback (most recent call last):
> > >      File "<stdin>", line 1, in <module>
> > >    ReferenceError: weakly-referenced object no longer exists
> > >
> > > This behaviour is perfectly correct -- but still surprising,
> > > especially for people who know little about method creation
> > > machinery, descriptors etc.
> > >
> > > I think it would be nice to make this 'trap' less painful --
> [snip]
> > > A prototype implementation:
> > >
> > >    class InstanceCachedMethod(object):
> > >
> > >        def __init__(self, func):
> > >            self.func = func
> > >            (self.instance_attr_name
> > >            ) = '__{0}_method_ref'.format(func.__name__)
> > >
> > >        def __get__(self, instance, owner):
> > >            if instance is None:
> > >                return self.func
> > >            try:
> > >                return getattr(instance, self.instance_attr_name)
> > >            except AttributeError:
> > >                method = types.MethodType(self.func, instance)
> > >                setattr(instance, self.instance_attr_name, method)
> > >                return method
> [snip]
> > I was bitten by this issue a while ago as well. It made working with
> > weakref proxies much more involved than I expected it would be.
> > 
> > Wouldn't it be better to approach the issue from the opposite end, and
> > improve/wrap/replace weakref.proxy with something that can handle bound
> > methods?
> 
> Indeed, probably could it be done by wrapping weakref.ref()/proxy()
> with something like the following:
> 
>     # here `obj` is the object that is being weak-referenced...
>     if isinstance(obj, types.MethodType):
>         try:
>             cache = obj.__self__.__method_cache__
>         except AttributeError:
>             cache = obj.__self__.__method_cache__ = WeakKeyDictionary()
>         method_cache.setdefault(obj.__func__, set()).add(obj)
> 
> (Using WeakKeyDictionary with corresponding function objects as weak
> keys -- to provide automagic cleanup when a function is deleted, e.g.
> replaced with another one.  In other words: the actual weak ref/proxy
> to a method lives as long as the corresponding function does).

On second thought -- no, it shouldn't be done on the side of
weakref.ref()/proxy().

Why?  My last idea described just above has such a bug: each time
you create a new weak reference to the method another method object
is cached (added to __method_cache__[func] set).

You could think that caching only one object (just in
__method_cache__[func]) would be a better idea, but it wouldn't:
such a behaviour would be strange and unstable: after creating
a new weakref to the method, the old weakref would became invalid...

And yes, we can prevent it by ensuring that each time you take
the method from a class instance you get the same object (per class
instance) -- but then we come back to my previous idea of a
descriptor-decorator.  And IMHO such a decorator should not be
applied on the class dictionary implicitly by weakref.ref()/proxy()
but explicitly in the class body with the decorator syntax
(applying such a decorater, i.e. replacing a function with a
caching descriptor is a class dict, is too invasive operation to
be done silently).

So I renew (and update) my previous descriptor-decorator that
could be added to functools (or to weakref as a helper?) and
applied explicitly by programmers, when needed:

    class CachedMethod(object):

        def __init__(self, func):
            self.func = func

        def __get__(self, instance, owner):
            if instance is None:
                return self.func
            try:
                cache = instance.__method_cache__
            except AttributeError:
                # not thread-safe :-(
                cache = instance.__method_cache__ = WeakKeyDictionary()
            return cache.setdefault(
                  self.func,
                  types.MethodType(self.func, instance))

Usage:

    class MyClass(object):
        @CachedMethod
        def my_method(self):
            ...

    instance = MyClass()
    method_weak_proxy = weakref.proxy(instance.my_method)
    method_weak_proxy()  # works!

It should be noted that caching a reference to a method in an
instance causes circular referencing (class <-> instance).
However, ofter it is not a problem and can help avoiding
circular references involving other objects which we want to
have circular-ref-free (typical use case: passing a bound
method as a callback).

Cheers.
*j




More information about the Python-ideas mailing list