Count number of instances using a metaclass in Python

606 Views Asked by At

I've written a metaclass which counts the number of instances for each of its children class. So my meta has a dict like {classname: number_of_instances}.

This is the implementation:

class MetaCounter(type):
    
    _counter = {}

    def __new__(cls, name, bases, dict):
        cls._counter[name] = 0
        return super().__new__(cls, name, bases, dict)

    def __call__(cls, *args, **kwargs):
        cls._counter[cls.__name__] += 1
        print(f'Instantiated {cls._counter[cls.__name__]} objects of class {cls}!')
        return super().__call__(*args, **kwargs)



class Foo(metaclass=MetaCounter):
    pass


class Bar(metaclass=MetaCounter):
    pass


x = Foo()
y = Foo()
z = Foo()
a = Bar()
b = Bar()
print(MetaCounter._counter)
# {'Foo': 3, 'Bar': 2} --> OK!
print(Foo._counter)
# {'Foo': 3, 'Bar': 2} --> I'd like it printed just "3"
print(Bar._counter) 
# {'Foo': 3, 'Bar': 2} --> I'd like it printed just "2"

It works fine but I want to add a level of information hiding. That is, classes whom metaclass is MetaCounter should not have the counter of instances of other classes with the same meta. They should just have the information regarding the number of their instances. So MetaCounter._counter should display the entire dict, but Foo._counter (let Foo be a class with MetaCounter as its metaclass) should just return the number of Foo instances.

Is there a way to achieve this?

I've tried to override __getattribute__ but it ended in messing around with the counting logic.

I've also tried to put _counter as attribute in the dict parameter of the __new__ method, but then it became also an instance member and I didn't want so.

2

There are 2 best solutions below

6
On BEST ANSWER

I do not know enough metaclasses to say if it is a bad idea to do what you are trying but it is possible.

You can make MetaCounter just keep references to its "metaclassed" classes and add a specific instance counter to each one by updating the dict in MetaCounter.__new__ method:

class MetaCounter(type):
    _classes = []

    def __new__(cls, name, bases, dict):
        dict.update(_counter=0)
        metaclassed = super().__new__(cls, name, bases, dict)
        cls._classes.append(metaclassed)
        return metaclassed

Each time a new MetaCounter class is instantiated, you increment the counter

    def __call__(cls, *args, **kwargs):
        cls._counter += 1
        print(f'Instantiated {cls._counter} objects of class {cls}!')
        return super().__call__(*args, **kwargs)

This ensures each MetaCounter class owns its instance counter and does not know anything about other MetaCounter classes.

Then, to get all the counters, you can add a _counters static method to MetaCounter, which returns the counter for each MetaCounter class:

    @classmethod
    def _counters(cls):
        return {klass: klass._counter for klass in cls._classes}

Then you are done:

print(MetaCounter._counters())  # {<class '__main__.Foo'>: 3, <class '__main__.Bar'>: 2}
print(Foo._counter)  # 3
print(Bar._counter)  # 2
0
On

This is a working example with the use of a descriptor to modify the behavior of the _counter attribute based on whether it's accessed by instance (the class) or by class (the metaclass in this case)

class descr:

    def __init__(self):
        self.counter = {}
        self.previous_counter_value = {}

    def __get__(self, instance, cls):
        if instance:
            return self.counter[instance]
        else:
            return self.counter

    def __set__(self, instance, value):
        self.counter[instance] = value
    

class MetaCounter(type):
    
    _counter = descr()

    def __new__(cls, name, bases, dict):
        new_class = super().__new__(cls, name, bases, dict)
        cls._counter[new_class] = 0
        return new_class


    def __call__(cls, *args, **kwargs):
        cls._counter += 1
        print(f'Instantiated {cls._counter} objects of class {cls}!')
        return super().__call__(*args, **kwargs)