Using metaclass to keep track of instances in python
Question:
I need to keep tracks of instances of some classes (and do other stuff with those classes). I would like to not have to declare any extra code in the classes in question, thus everything should ideally be handled in the metaclass.
What I can’t figure out is how to add a weak reference to each new instance of those classes. For example:
class Parallelizable(type):
def __new__(cls, name, bases, attr):
meta = super().__new__(cls, name, bases, attr)
# storing the instances in this WeakSet
meta._instances = weakref.WeakSet()
return meta
@property
def instances(cls):
return [x for x in cls._instances]
class Foo(metaclass=Parallelizable)
def __init__(self, name):
super().__init__()
self.name = name
# I would like to avoid having to do that - instead have the metaclass manage it somehow
self._instances.add(self)
Any ideas? I can’t seem to find a hook on the metaclass side to get into the __init__
of Foo….
Answers:
You could decorate the attrs['__init__']
method in Parallizable.__new__
:
import weakref
import functools
class Parallelizable(type):
def __new__(meta, name, bases, attrs):
attrs['__init__'] = Parallelizable.register(attrs['__init__'])
cls = super().__new__(meta, name, bases, attrs)
cls._instances = weakref.WeakSet()
return cls
@classmethod
def register(cls, method):
@functools.wraps(method)
def newmethod(self, *args, **kwargs):
method(self, *args, **kwargs)
self._instances.add(self)
return newmethod
@property
def instances(cls):
return [x for x in cls._instances]
class Foo(metaclass=Parallelizable):
def __init__(self, name):
"Foo.__init__ doc string"
super().__init__()
self.name = name
# Notice that Foo.__init__'s docstring is preserved even though the method has been decorated
help(Foo.__init__)
# Help on function __init__ in module __main__:
#
# __init__(self, name)
# Foo.__init__ doc string
stilton = Foo('Stilton')
gruyere = Foo('Gruyere')
print([inst.name for inst in Foo.instances])
# ['Gruyere', 'Stilton']
del stilton
print([inst.name for inst in Foo.instances])
# ['Gruyere']
The method on the metaclass that is called when each new instance of its “afiliated” classes is __call__
. If you put the code to record the instances in there, that is all the work you need:
from weakref import WeakSet
# A convenient class-level descriptor to retrieve the instances:
class Instances:
def __get__(self, instance, cls):
return [x for x in cls._instances]
class Parallelizable(type):
def __init__(cls, name, bases, attrs, **kw):
super().__init__(name, bases, attrs, **kw)
cls._instances = WeakSet()
cls.instances = Instances()
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
cls._instances.add(instance)
return instance
The same code will work without the descriptor at all – it is just a nice way to have a class attribute that would report the instances. But if the WeakSet is enough, this code suffices:
from weakref import WeakSet
class Parallelizable(type):
def __init__(cls, name, bases, attrs, **kw):
super().__init__(name, bases, attrs, **kw)
cls.instances = WeakSet()
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
cls.instances.add(instance)
return instance
How about this, its a class to inherit from, instead of a metaclass. I think its simpler but achieves the same point:
class AutoDiscovered:
instances = []
def __new__(cls, *args, **kwargs):
obj = super().__new__(cls)
cls.instances.append(obj)
return obj
Usage:
class Foo(AutoDiscovered):
pass
a = Foo()
b = Foo()
print(Foo.instances) # [<__main__.Foo object at 0x7fdabd345430>, <__main__.Foo object at 0x7fdabd345370>]
I need to keep tracks of instances of some classes (and do other stuff with those classes). I would like to not have to declare any extra code in the classes in question, thus everything should ideally be handled in the metaclass.
What I can’t figure out is how to add a weak reference to each new instance of those classes. For example:
class Parallelizable(type):
def __new__(cls, name, bases, attr):
meta = super().__new__(cls, name, bases, attr)
# storing the instances in this WeakSet
meta._instances = weakref.WeakSet()
return meta
@property
def instances(cls):
return [x for x in cls._instances]
class Foo(metaclass=Parallelizable)
def __init__(self, name):
super().__init__()
self.name = name
# I would like to avoid having to do that - instead have the metaclass manage it somehow
self._instances.add(self)
Any ideas? I can’t seem to find a hook on the metaclass side to get into the __init__
of Foo….
You could decorate the attrs['__init__']
method in Parallizable.__new__
:
import weakref
import functools
class Parallelizable(type):
def __new__(meta, name, bases, attrs):
attrs['__init__'] = Parallelizable.register(attrs['__init__'])
cls = super().__new__(meta, name, bases, attrs)
cls._instances = weakref.WeakSet()
return cls
@classmethod
def register(cls, method):
@functools.wraps(method)
def newmethod(self, *args, **kwargs):
method(self, *args, **kwargs)
self._instances.add(self)
return newmethod
@property
def instances(cls):
return [x for x in cls._instances]
class Foo(metaclass=Parallelizable):
def __init__(self, name):
"Foo.__init__ doc string"
super().__init__()
self.name = name
# Notice that Foo.__init__'s docstring is preserved even though the method has been decorated
help(Foo.__init__)
# Help on function __init__ in module __main__:
#
# __init__(self, name)
# Foo.__init__ doc string
stilton = Foo('Stilton')
gruyere = Foo('Gruyere')
print([inst.name for inst in Foo.instances])
# ['Gruyere', 'Stilton']
del stilton
print([inst.name for inst in Foo.instances])
# ['Gruyere']
The method on the metaclass that is called when each new instance of its “afiliated” classes is __call__
. If you put the code to record the instances in there, that is all the work you need:
from weakref import WeakSet
# A convenient class-level descriptor to retrieve the instances:
class Instances:
def __get__(self, instance, cls):
return [x for x in cls._instances]
class Parallelizable(type):
def __init__(cls, name, bases, attrs, **kw):
super().__init__(name, bases, attrs, **kw)
cls._instances = WeakSet()
cls.instances = Instances()
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
cls._instances.add(instance)
return instance
The same code will work without the descriptor at all – it is just a nice way to have a class attribute that would report the instances. But if the WeakSet is enough, this code suffices:
from weakref import WeakSet
class Parallelizable(type):
def __init__(cls, name, bases, attrs, **kw):
super().__init__(name, bases, attrs, **kw)
cls.instances = WeakSet()
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
cls.instances.add(instance)
return instance
How about this, its a class to inherit from, instead of a metaclass. I think its simpler but achieves the same point:
class AutoDiscovered:
instances = []
def __new__(cls, *args, **kwargs):
obj = super().__new__(cls)
cls.instances.append(obj)
return obj
Usage:
class Foo(AutoDiscovered):
pass
a = Foo()
b = Foo()
print(Foo.instances) # [<__main__.Foo object at 0x7fdabd345430>, <__main__.Foo object at 0x7fdabd345370>]