Подсчитайте количество экземпляров, используя метакласс в Python

Я написал метакласс, который подсчитывает количество экземпляров для каждого дочернего класса. Итак, моя мета имеет dict, например {classname: number_of_instances}.

Это реализация:

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"

Он отлично работает, но я хочу добавить уровень сокрытия информации. То есть классы, для которых metaclass является MetaCounter, не должны иметь счетчик экземпляров других классов с такой же мета. У них должна быть только информация о количестве их экземпляров. Таким образом, MetaCounter._counter должен отображать весь dict, а Foo._counter (пусть Foo будет классом с MetaCounter в качестве его метакласса) должен просто возвращать количество экземпляров Foo.

Есть ли способ добиться этого?

Я пытался переопределить __getattribute__, но это закончилось возни с логикой подсчета.

Я также пытался поместить _counter в качестве атрибута в параметр dict метода __new__, но тогда он также стал членом экземпляра, а я этого не хотел.


person dc_Bita98    schedule 15.09.2020    source источник
comment
Такой счетчик экземпляров на уровне класса почти всегда является плохой идеей. Он не является потокобезопасным, он становится небезопасным для GC, если вы пытаетесь сделать его потокобезопасным, для него почти нет применения, любое использование лучше обрабатывается другими методами, и он добавляет ненужные накладные расходы, когда вы его не используете.   -  person user2357112 supports Monica    schedule 15.09.2020
comment
Кроме того, выполнение этого с метаклассом добавляет проблемы конфликта метакласса, когда вы хотите наследовать от класса, который использует другой метакласс.   -  person user2357112 supports Monica    schedule 15.09.2020
comment
@ user2357112supportsMonica +1, потому что я согласен с вами, но это только в дидактических целях.   -  person dc_Bita98    schedule 15.09.2020


Ответы (2)


Я не знаю достаточно метаклассов, чтобы сказать, является ли плохой идеей делать то, что вы пытаетесь, но это возможно.

Вы можете сделать так, чтобы MetaCounter просто сохранял ссылки на его метаклассированные классы и добавлял конкретный счетчик экземпляров к каждому из них, обновляя метод dict в MetaCounter.__new__:

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

Каждый раз, когда создается экземпляр нового класса MetaCounter, вы увеличиваете счетчик

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

Это гарантирует, что каждый класс MetaCounter владеет своим счетчиком экземпляров и ничего не знает о других классах MetaCounter.

Затем, чтобы получить все счетчики, вы можете добавить статический метод _counters к MetaCounter, который возвращает счетчик для каждого класса MetaCounter:

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

Тогда вы сделали:

print(MetaCounter._counters())  # {<class '__main__.Foo'>: 3, <class '__main__.Bar'>: 2}
print(Foo._counter)  # 3
print(Bar._counter)  # 2
person Tryph    schedule 15.09.2020
comment
Спасибо, все в порядке. Это добавит атрибут _counter, доступный также для инстансов, не так ли? --› x = Foo() print(x._counter) --> "1". Что, если я хочу, чтобы эта информация была доступна только на уровне класса? - person dc_Bita98; 15.09.2020
comment
@dc_Bita98 почему? Как правило, экземпляр имеет всю информацию, доступную классу... это своего рода центральный аспект ООП на основе классов. Я полагаю, вы могли бы создать какой-то дескриптор для _counter, который предотвращает доступ при использовании экземпляром. Хотя это становится крайне непрактичным - person juanpa.arrivillaga; 15.09.2020
comment
AFAIK невозможно сделать атрибуты класса недоступными для экземпляров - person Tryph; 15.09.2020
comment
Моя была просто мыслью. Я приму этот ответ, потому что он решил проблему (сейчас проверено) - person dc_Bita98; 15.09.2020
comment
Это почти не ретушируется, но поскольку метод _counters в MetaCounter не имеет никаких причин быть статическим методом, он может и должен быть методом класса, поскольку он использует параметр cls. - person jsbueno; 15.09.2020
comment
@jsbueno хм... На самом деле я просто забыл декоратор @classmethod (позор: x). Так что вы правы, я обновлю ответ - person Tryph; 15.09.2020

Это рабочий пример с использованием дескриптора для изменения поведения атрибута _counter в зависимости от того, осуществляется ли доступ к нему экземпляром (классом) или классом. (в данном случае метакласс)

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)
person dc_Bita98    schedule 16.09.2020