Comments 30
Вы сталкивались с метаклассами в Python? Выглядит как задача, где бы это пришлось к месту.
https://docs.python.org/3/reference/datamodel.html#metaclasses
Метод call метакласса как раз позволяет контролировать вызовы new и init без лишних телодвижений внутри самих классов.
Таки можно. Вам же указали: есть такой метод, __call__
. Будучи определен в метаклассе, он будет вызываться при создании объектов класса, вместо __new__
и __init__
вместе взятых.
Получается как-то вот так:
class cached(type):
def __new__(cls, name, bases, dct):
self = type.__new__(cls, name, bases, dct)
self.cache = dict()
return self
def __call__(self, id, *args, **kwargs):
if not id in self.cache:
self.cache[id]=type.__call__(self, id, *args, **kwargs)
return self.cache[id]
Пример: https://repl.it/MD9Z
Мне кажется, вы решаете не ту проблему.
Настоящая проблема в том, что вы пытаетесь избежать создания объектов с одинаковым id, но при этом создаете объекты разных классов с одним id. Это противоречивые требования, один объект не может быть сразу двух классов.
Вам надо разделить кеши — у каждого класса должен быть свой кеш.
Если вы не можете непродуманную и запутанную логику реализовать на питоне изящно и лаконично, то это не потому, что питон плохой, а потому, что логика непродуманная и запутанная.
Если вам нужно держать в одном кэше одинаковые айдишники для разных классов, и чтобы они не конфликтовали, то можно использовать класс как часть ключа:
class Animal:
pass
class Cat(Animal):
pass
cache = {
(Animal, 1): 'some_animal_object',
(Cat, 1): 'some_cat_object'
}
print(cache[Animal, 1])
# Напечатает: some_animal_object
print(cache[Cat, 1])
# Напечатает: some_cat_object
Сейчас проснутся pep8-nazi и за поля __cache__
и __tmp__
сожгут автора на костре.
Animal.__cache__[id].__class__.__init__=Animal.__fake_init__
Если да, то у кого-то еще есть претензии к БЭМ-у?
def singleton(cls):
instances = {}
def get_instance(id_, *args, **kwargs):
if id_ not in instances:
instances[id_] = cls(id_, *args, **kwargs)
return instances[id_]
return get_instance
Я как‐то не вижу проблемы вообще. Зачем запрещать __init__
, вызывая неочевидное поведение, просто напишите в аргументах __new__
*args, **kwargs
и спокойно игнорируйте эти аргументы? А проблема с __init__
легко решается метаклассом:
class MetaAnimal(type):
def __new__(cls, name, bases, namespace, **kwargs):
old_init = namespace.get('__init__')
if old_init:
def __init__(self, id, *args, **kwargs):
if not self._called_init:
old_init(self, id, *args, **kwargs)
self._called_init = True
namespace['__init__'] = __init__
return type.__new__(cls, name, bases, namespace)
class Animal(metaclass=MetaAnimal):
_cache = dict()
_called_init = False
def __new__(cls, id, *args, **kwargs):
if not id in Animal._cache:
Animal._cache[id] = super().__new__(cls)
return Animal._cache[id]
def __init__(self, id):
self.id=id
print('THERE')
class Cat(Animal):
data="data"
def __init__(self, id, b):
super(Cat, self).__init__(id)
print('HERE')
print(id(Animal(1)))
print(id(Animal(1)))
print(id(Cat(1)))
print(id(Cat(2, 1)))
print(id(Cat(2)))
Печатает
THERE
139883603787504
139883603787504
139883603787504
THERE
HERE
139883603787672
139883603787672
, что и нужно.
Animal.__tmp__=Animal.__cache__[id].__class__.__init__
Animal.__cache__[id].__class__.__init__=Animal.__fake_init__
Это баг.
Вы присваиваете __fake_init__ классу Animal и в дальнейшем Animal(id) у вас вызовет ошибку.
Выше в комментариях было правильно замечено, что вы решаете не ту проблему.
Достаточно просто добавить классметод Animal.get(id) который будет создавать и класть инстанс в кеш или брать из кеша, да вы потеряете при этом синтаксис Animal(id), но написать лишние 4 символа, я полагаю, не проблема, зато вы точно уверены, что конструктор создаёт инстанс, а не берёт уже существующий (что он и должен делать).
class Animal:
__cache__ = dict()
def __new__(cls, id, *args, **kwargs):
if id not in cls.__cache__:
cls.__cache__[id] = id
return super().__new__(cls)
else:
raise Exception('ID уже существует')
def __init__(self, id):
self.id = id
class Cat(Animal):
def __init__(self, id, a, b, c):
self.a = a
self.b = b
self.c = c
super().__init__(id)
if __name__ == '__main__':
for item in range(10000):
a = Animal(item)
print(a)
for item in range(10000, 20000):
b = Cat(item, 1, 2, 3)
print(b)
Если вы хотите поведение, когда можно создать Animal с id=1 и Cat с id=1:
class Animal:
__cache__ = dict()
def __new__(cls, id, *args, **kwargs):
if id not in cls.__cache__:
cls.__cache__[id] = id
return super().__new__(cls)
else:
raise Exception('ID уже существует')
def __init__(self, id):
self.id = id
class Cat(Animal):
__cache__ = dict()
def __init__(self, id, a, b, c):
self.a = a
self.b = b
self.c = c
super().__init__(id)
if __name__ == '__main__':
for item in range(10000):
a = Animal(item)
print(a)
for item in range(10000):
b = Cat(item, 1, 2, 3)
print(b)
class Animal(type):
ID_COUNTER = 0
def __new__(cls, name, bases, dct):
dct['_id'] = -1
return type.__new__(cls, name, bases, dct)
def __call__(self, *args, **kwargs):
inst = type.__call__(self, *args, **kwargs)
Animal.ID_COUNTER += 1
inst._id = Animal.ID_COUNTER
return inst
class Cat(metaclass=Animal):
pass
class Dog(metaclass=Animal):
pass
c1 = Cat()
c2 = Cat()
d1 = Dog()
d2 = Dog()
print(c1._id)
print(c2._id)
print(d1._id)
print(d2._id)
Хотелось красивого решения— людям данные нужны, а не красивые решения.
Я правильно понимаю, что если в Cat добавить метод meow, и попробовать его вызвать:
с1 = Cat(1)
c1.meow()
… то можно получить ошибку "'Animal' object has no attribute 'meow'"? А можно и не получить, если где-то ещё не был создан Animal с таким же идентификатором?
Мне бы не хотелось поддерживать такой код.
Верно, но это на самом деле вопрос того, как классы используются на самом деле. Стилем кодирования (и метаклассом чтобы наверняка) в принципе можно запретить определение новых методов, как и переопределение старых с несовместимыми сигнатурами. Хотя я лично всё же нахожу задачу несколько странной, и просто написал бы функцию получения животного по id, возможно даже вида def get_animal(cls, id, *args, **kwargs)
(дополнительные аргументы — для конструктора).
У вас с этой проблемой нет особого выбора. Просто запретите метаклассом создавать метод meow()
, лучше вы даже на уровне ниже¹ не напишете: не будет этой проблемы, будет проблема двойной инициализации, либо проблема её отсутствия, либо проблема гонки (кстати, ваш оригинальный код должен был удерживать блокировку в new и освобождать её в _fake_init (и не должен был использовать двойные подчёркивания где не надо)), либо проблема нарушения контракта «один id ссылается всегда на один и тот же объект».
¹ «На уровне ниже» можно существующему объекту и тип изменить, в т.ч. временно; при достаточном знании внутренностей CPython можно даже в процессе ничего не поломать до следующего релиза.
Варианта исправления тут два.
Просто откажитесь от единого хранилища всех объектов. Пусть
Animal(1)
иCat(1)
будут разными объектами.
- Сделайте класс Animal "абстрактным" — пусть
Animal(1)
кидает ошибку если нужный объект не лежит в кеше.
Судя по описанию исходной задачи с аккаунтами, у вас по смыслу должно происходить не создание объектов типа "аккаунт", а именно получение. Конструктор же подразумевает именно создание нового. Поэтому здесь всё же лучше подходит некий репозиторий аккаунтов — он как раз может нести семантику get-or-add. И тогда можно явно запретить ситуацию, когда объект с идентификатором X неявным образом меняет свой тип.
Если смена типа во время жизни объекта нужна, то можно
- Сделать некий метод вроде "become", но в этому случае все члены иерархии Animal должны знать друг о друге и уметь превращаться друг в друга. Опять же, будет проблема, если в одном месте объект будет сохранён как Dog с методом bark, а в другом из него сделают Cat с методом meow.
- Можно реализовать наследование не через систему типов, а полем внутри Animal. То есть вместо объявления разных классов просто сделать поле вида 0 — Animal, 1 — Cat, 2 — Dog и в публичных методах сделать if. Это позволит в одном месте — в классе Animal — однозначно определить, можно ли добавлять meow, bark и другие методы, и можно сделать более понятную ошибку, когда метод не поддерживается текущим типом.
На эту тему написано в Analysis Patterns Фаулера, глава 14.2.3.
Касательно замечания ZyXI про блокировку. Если надо обязательно сохранить синтаксис вызова конструктора, то можно сделать "теневую" иерархию и превратить Animal в прокси. Тогда подмена конструктора не понадобится и надо будет сделать только потоко-безопасный get-or-add для словаря-кеша:
import threading
class AnimalImpl:
def __init__(self, id):
self._id = id
self._name = None
def roar(self):
return '{}: {}'.format(self._id, self)
def tell_name(self):
if self._name is None:
raise Exception('I am nameless!')
return self._name
def give_name(self, name):
self._name = name
class Animal:
"""An animal proxy
Animal proxies with the same id are the same:
>>> a1 = Animal(1)
>>> a2 = Animal(1)
>>> a1.roar() == a2.roar()
True
>>> a1.give_name('Baloo')
>>> a2.tell_name()
'Baloo'
"""
__cache__ = dict()
__lock__ = threading.Lock()
def __init__(self, id):
Animal.__lock__.acquire()
try:
if id in Animal.__cache__:
self._impl = Animal.__cache__[id]
else:
impl = AnimalImpl(id)
Animal.__cache__[id] = impl
self._impl = impl
finally:
Animal.__lock__.release()
def roar(self):
self._impl.roar()
def tell_name(self):
return self._impl.tell_name()
def give_name(self, name):
self._impl.give_name(name)
if __name__ == "__main__":
import doctest
doctest.testmod()
Зачем мне гибкость Python, если мне запрещают ей пользоваться?