Search code examples
pythonunit-testinggarbage-collectionsubclassing

Garbage collection does not recognize del of a class after instance assignment


For testing purposes, I'm creating temporary classes which I want to delete (before other test methods run). Trouble is, [superclass].__subclasses__() still lists the deleted classes, even after running garbage collection.

Here's what my test method looks like:

class Apple(Fruit):
    @staticmethod
    def mass(size):
        return size

class Orange(Fruit):
    @staticmethod
    def mass(size):
        return size

try:
    Apple()
    Orange()
    a1 = Apple(type='fuji')
finally:
    if 'a1' in locals():
        print 'del a1'
        del a1
    print gc.get_referrers(Apple)
    print gc.get_referrers(Orange)
    del Apple
    del Orange
    print Fruit.__subclasses__()
    gc.collect()
    print Fruit.__subclasses__()

The output is as follows:

del a1
[<frame object at 0xabcdef0>, (<class 'Apple'>, <class 'Fruit'>, <type 'object'>), <Apple object at 0x4443331>, {'a1': <Apple object at 0x4443331, 'self': <FruitTests testMethod=test_pass_Fruit_core>, 'Orange': <class 'Orange'>, 'Apple': <class 'Apple'>}]    
[<frame object at 0xabcdef0>, (<class 'Orange'>, <class 'Fruit'>, <type 'object'>), {'a1': <Apple object at 0x4443331, 'self': <FruitTests testMethod=test_pass_Fruit_core>, 'Orange': <class 'Orange'>, 'Apple': <class 'Apple'>}]
[<class 'Apple'>, <class 'Orange'>]
[<class 'Apple'>, <class 'Orange'>]

None of the classes involved have an explicitly-defined __del__(), although Fruit does use __metaclass__ = abc.ABCMeta and a @abc.abstractmethod decorator on Fruit.mass().

The remaining class reference has something to do with the assignment of the Fruit instance to a variable: If I remove all the lines containing a1, the final Fruit.__subclasses__() returns [] - even though the bare constructor Apple() still runs.

This is a problem for me because another test is concerned with fruit interactions (call the relevant method-to-be-tested blends()), and that uses a Fruit.__subclasses__() call to check combinations of different types of Fruit. I haven't bothered to define interactions with these test classes, and that's confusing blends().

Any hints on why these references are sticking around would be appreciated.

Edit: If I call gc.get_referrers(Apple) after gc.collect(), I get an "UnboundLocalError: local variable 'Apple' referenced before assignment" Fruit defines a number of methods with the "@classmethod "and "@property" decorators, and references another class which handles "blends()"...

After garbage collection, gc.get_referrers(Fruit.__subclasses__()[0]) returns

[{'a1': <Apple object at 0x4443331>, 'self': <FruitTests testMethod=test_pass_Fruit_core>, 'Orange': <class 'Orange'>, 'Apple': <class 'Apple'>}, <Apple object at 0x4443331>, (<class 'Apple'>, <class 'Fruit'>, <type 'object'>)]

Edit: The problem occurs when I run just this one test method. (It also occurs when I queue up multiple tests.) I tried rebooting my IDE (PyCharm) and running "./manage.py test FruitTests.test_pass_Fruit_core " from the command line. All cases yield the same results, although the particular memory addresses vary. locals() is being called directly - I don't have it aliased anywhere.

Edit: The entire module defining Fruit:

from abc import abstractmethod, ABCMeta


class Fruit(object):
    __metaclass__ = ABCMeta

    def __init__(self, **kwargs):
        super(Fruit, self).__init__()

    @abstractmethod
    def mass(self, size):
        raise NotImplementedError

In the test method, test_pass_Fruit_core(), "a1 = Apple()" and "a1 = Apple(type='fuji')" produce the same results. Dropping the assignment to "a1" makes no difference, but if I drop the call to "locals()", garbage collection works as expected - Apple is no longer available as a subclass of Fruit at the end of the method.


Solution

  • The persistent reference is being created in the call to locals(). To guarantee "del a1" does not generate an error if one was created inside the "try:" block, assign "a1 = None" before the block and skip the call to locals().

    Final, working code follows. Compare with the first code block above: class Apple(Fruit): @staticmethod def mass(size): return size

    class Orange(Fruit):
        @staticmethod
        def mass(size):
            return size
    
    a1 = None
    try:
        Apple()
        Orange()
        a1 = Apple(type='fuji')
    finally:
        del a1
        print gc.get_referrers(Apple)
        print gc.get_referrers(Orange)
        del Apple
        del Orange
        print Fruit.__subclasses__()
        gc.collect()
        sc = Fruit.__subclasses__()
        print sc
        if len(sc) > 0:
            print 42, gc.get_referrers(sc[0])