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

#python #encapsulation #python-3.8 #metaclass #information-hiding

#python #инкапсуляция #python-3.8 #метакласс #скрытие информации

Вопрос:

Я написал метакласс, который подсчитывает количество экземпляров для каждого из его дочерних классов. Итак, моя мета имеет 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"
  

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

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

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

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

Комментарии:

1. Этот тип счетчика экземпляров для всего класса почти всегда является плохой идеей в первую очередь. Это не потокобезопасно, оно становится небезопасным для GC, если вы пытаетесь сделать его потокобезопасным, для него почти нет применений, любое использование лучше обрабатывается другими методами, и это добавляет ненужные накладные расходы, когда вы его не используете.

2. Кроме того, выполнение этого с метаклассом добавляет проблемы конфликта метаклассов, когда вы хотите наследовать от класса, который использует другой метакласс.

3. @user2357112supportsMonica 1 потому что я согласен с вами, но это только для дидактических целей

Ответ №1:

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

Вы можете MetaCounter просто сохранить ссылки на его «метаклассовые» классы и добавить к каждому из них определенный счетчик экземпляров, обновив метод dict in 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
  

Комментарии:

1. Спасибо, все в порядке. Это добавит _counter атрибут, доступный также для экземпляров, не так ли? —> x = Foo() print(x._counter) --> "1" . Что, если я хочу сохранить эту информацию доступной только на уровне класса?

2. @dc_Bita98 почему? Как правило, экземпляр содержит всю информацию, доступную классу … это своего рода центральный аспект ООП на основе классов. Я полагаю, вы могли бы создать какой-то дескриптор для _counter , который предотвращает доступ при использовании экземпляром. Хотя это становится крайне непрактичным

3. AFAIK невозможно сделать атрибуты класса недоступными для экземпляров

4. У меня была просто мысль. Я собираюсь принять этот ответ, потому что он решил проблему (протестирован сейчас)

5. Это почти невозможно исправить, но из-за того факта, что у _counters метода в MetaCounter нет никаких оснований быть «staticmethod» — он может — и должен — быть classmethod, поскольку у него есть параметр use cls .

Ответ №2:

Это рабочий пример с использованием дескриптора для изменения поведения _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)