trying modify a decorator not to use a weakref
, I stumbled across the following behaviour:
import weakref
class descriptor(object):
def __get__(self, instance, owner):
return proxy(instance)
class proxy(object):
def __init__(self, instance):
self.instance = instance
def __iadd__(self, other):
return self
class A(object):
descr = descriptor()
def is_leaky(test_fn):
a = A()
wr = weakref.ref(a)
test_fn(a)
del a
return wr() is not None
def test1(a):
tmp = a.descr
tmp += object()
def test2(a):
a.descr += object()
print(is_leaky(test1)) # gives False
print(is_leaky(test2)) # gives True!!!
This seems very odd to me, as I'd expect both cases to behave the same. Furthermore, from my understanding of reference counting and object lifetime, I was convinced that in both cases the object should be freed.
I have tested it both on python2.7 and python3.3.
Is this a bug or intentional behaviour? Is there a way to get both calls to have the expected results (free the object in question)?
I do not want to use a weakref
in proxy
because this destroys the correct object lifetime semantics for bound methods:
a = A()
descr = a.descr
del a # a is kept alive since descr is a bound method to a
descr() # should execute a.descr() as expected
The two codepaths are not equivalent.
In-place add acts on two operators, the assignment target and the item added. In test1
that is temp
, a local variable, and the in-place addition is translated to following:
temp = temp.__iadd__(object())
and since you return self
and temp
refers to the same object, this becomes temp = temp
and that reference is cleaned up after the function exits.
In test2
, you complicated matters, since now descriptors are getting involved again:
a.descr += object()
becomes:
a.descr = A.__dict__['descr'].__get__(a, A).__iadd__(object())
so you assign the result of A.__dict__['descr'].__get__(a, A)
to the instance attribute a.descr
; the descriptor has no __set__()
method and is not consulted.
But, and here is the catch, the proxy
object holds a reference to a
itself, a.descr.instance
is a reference to a
! You created a circular reference.
This reference keeps the object alive long enough to show through the weak reference, but as soon as the garbage collection process runs and breaks this cycle, a
will be gone anyway.
Moral of this story? Don't use __iadd__
in combination with a non-data descriptor; include both __get__
and __set__
because you need to control what happens when the result is assigned back.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With