#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, поскольку у него есть параметр usecls
.
Ответ №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)