alxwrd
alxwrd

Reputation: 2470

How to remove classes from __subclasses__?

When inheriting from a class, the child class is accessible on the parent via the .__subclasses__() method.

class BaseClass:
    pass

class SubClass(BaseClass):
    pass

BaseClass.__subclasses__()
# [<class '__main__.SubClass'>]

However, deleting the child class doesn't seem to remove it from the parent.

del SubClass

BaseClass.__subclasses__()
# [<class '__main__.SubClass'>]

Or

Upvotes: 6

Views: 1748

Answers (2)

alxwrd
alxwrd

Reputation: 2470

Where does __subclasses__ get its information from?

For the CPython implementation of Python, the type object keeps a list of weak references under PyTypeObject.tp_subclasses. This is marked as "Not inherited. Internal use only" in the docs, so can be treated as an implementation detail of CPython. See also: How is __subclasses__ method implemented in CPython?.

And can I manipulate it?

Any class has a .__bases__ descriptor which, if changed, updates the references in PyTypeObject.tp_subclasses.

.__bases__ can only be manipulated when the class doesn't directly inherit from object. So while:

class BaseClass: pass
class OtherClass(BaseClass): pass

and

class BaseClass: pass
class OtherClass: pass

BaseClass.__bases__ = (OtherClass, )
# TypeError: __bases__ assignment: 'BaseClass' deallocator differs from 'object'

should be equivalent *. You will get an error. See: https://bugs.python.org/issue672115

You also can't use this to change a class to inherit from object.

class BaseClass: pass
class SubClass(BaseClass): pass

SubClass.__bases__ = (object,)
# TypeError: __bases__ assignment: 'type' object layout differs from 'BaseClass'

You can, however, change the bases of a class to be another class.

class BaseClass: pass
class SubClass(BaseClass): pass

class OtherClass: pass

SubClass.__bases__ = (OtherClass, )

# Or don't define it.
SubClass.__bases__ = (type("OtherClass", (object, ), {}), )

This all updates the parent class:

>>> BaseClass.__subclasses__()
[]

Upvotes: 1

Duncan
Duncan

Reputation: 95722

The subclass contains references to itself internally, so it continues to exist until it is garbage collected. If you force a garbage collection cycle it will disappear from the __subclasses__():

import gc
gc.collect()

and then it has gone.

However make sure you have deleted all other references to the class before you force the garbage collection. For example, if you do it interactively and the last output was the subclass list there will still be a reference to the class in _.

class BaseClass:
    pass

class SubClass(BaseClass):
    pass

print(BaseClass.__subclasses__())
# [<class '__main__.SubClass'>]
del SubClass

import gc
gc.collect()
print(BaseClass.__subclasses__())
# []

Output with python 3.7 is:

[<class '__main__.SubClass'>]
[]

I should probably also add that while garbage collection works for this simple case you probably shouldn't depend on it in real life: it would be far too easy to accidentally keep a reference to the subclass somewhere in your code and then wonder why the class never goes away.

What you are trying to do here is keep a registry of subclasses so that the factory can return an object of the appropriate class. If you want to be able to add and remove classes from the registry then I think you have to be explicit. You could still use __subclasses__ to find candidate classes, but keep a flag on each class to show whether it is enabled. Then instead of just deleting the subclass set the flag to show the class is no longer in use and then (if you want) delete it.

Upvotes: 6

Related Questions