wanncy
wanncy

Reputation: 77

member variable's reference count does not decrement after object was deleted

Background truths(?): When python program exits, gc(garbage collection) works to collect memory in a common way reference counting.

So when an object was deleted, the reference count of it's member object should decrement 1. But I find the reference count does not minus 1, unless calling del in __del__ function

import sys

class B():
    def __init__(self):
        self.name = 'little b construct'
    def __del__(self):
        print(sys.getrefcount(self))
        print('little b was deleted')

class A():
    def __init__(self):
        print('little a construct')
        self._b = B()

    def __del__(self):
        # del self._b   # makes B' reference minus 1
        print('little a was deleted')

if __name__ == '__main__':
    a = A()

Result:

# without del self._b
little a construct
little a was deleted
4
little b was deleted
# with del self._b
little a construct
3
little b was deleted
little a was deleted

Question:

  1. Why B's reference count does not minus 1 automatically after A was deleted?

  2. what B's reference count 4 composed of?

Thanks for your help

Upvotes: 2

Views: 896

Answers (1)

progmatico
progmatico

Reputation: 4964

I have simplified your example to make it easier for mentally counting the references.

What happens is that you have to count a reference for the object, another for the self inside the method, but also other references for the same object in the arguments of any function call running at the moment you invoke the sys.getrefcount, including that function too, because it also holds a copy of the reference in its own local scope.

So what you are missing is that calling functions, that require the object passed as argument, makes new names to be bound to the formal parameters in the local function/method scope. There is always a +1 offset, even when you just have a name pointing the object, exactly because of this new local reference inside sys.getrefcount:

import sys

class B():
    def __init__(self):
        print('setting up b...')
    def counter(self):
        return sys.getrefcount(self)

class A():
    def __init__(self):
        print('setting up a...')

if __name__ == '__main__':
    a = A()
    b = B()
    print(sys.getrefcount(a))
    print(sys.getrefcount(b))

    print(b, b.counter())  # 5 refs to b. Why 5?
    print(b, B.counter(b)) # Because this is what really happens.
    print(b.counter(), b ) # 4 refs to b

    print(b,b,b.counter()) # 6 refs to b

Output:

setting up a...
setting up b...
2
2
<__main__.B object at 0x7f11df905940> 5
4 <__main__.B object at 0x7f11df905940>
<__main__.B object at 0x7f11df905940> <__main__.B object at 0x7f11df905940> 6

Apart from this __init__ is not a constructor, that's __new__ that you usually don't need, __init__ setups the object that is already created and accessible via self, see another answer of mine here, and __del__ is usually not needed also, unless you need some special resource cleanup just before object destruction (destroying contained objects is not needed).

Finally del does not delete an object it just removes a name that binds to it; it decreases the refcount and after refcount reaches 0 it eventually gets collected by the GC.

My explanation to Q1 is that a still exists. As mentioned in your comment link to __del__ docs, __del__ is a finalizer not a destructor. Refcount being 0 means __del__ is invoked, does not mean the object gets destroyed, and __del__does not destroy it. So the messages you print are not accurate (to the destruction moment). It will be destroyed by the GC, but you don't know exactly when (it is not specified in the language, depends on implementation).

Experiment replacing the statement

a = A()

by just

A()

Then no reference to A() object is kept, and this also decreases your refcount of b by one (while maintaining the rest of the output the same), meaning that there is no more a._b. So it should be the existance of a in memory with a._b that explains this 4 instead of 3. ais still alive when the B.__del__ is running. The same happens (count decrease of b) if you instead add a del a as the last statement.

Upvotes: 1

Related Questions