16. Разобрать метакласс с попыткой реализации Singleton
Условие задачи Разберите поведение метакласса и определите, какую задачу пытается решить данная реализация. Нужно понять, как работает переопределение new в метаклассе и к чему приведёт сохранение экземпляра в атрибуте класса.
class NewCls(type):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
Что выведут принты?
test = NewCls("X", (object,), {"a": 1, "b": 2})
print(test.a) >>
test2 = NewCls("X", (object,), {"a": 12, "b": 22})
print(test2.a) >>
Изменили код, что теперь выведут принты?
class NewCls(type):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self, *args):
self.a = args
test = NewCls("X", (object,), {"a": 1, "b": 2})
print(test.a) >>
test2 = NewCls("X", (object,), {"a": 12, "b": 22})
print(test2.a) >>
Спойлеры к решению
Подсказки
NewClsнаследуется отtype, значит это метакласс.- Вызов
NewCls("X", (object,), {...})динамически создаёт классX. - Метод
__new__отвечает за создание нового объекта-класса. - В
_instanceсохраняется первый созданный класс. - При повторном вызове новый класс уже не создаётся, возвращается тот же самый объект.
- Такая реализация пытается сделать singleton, но не для экземпляров обычного класса, а для классов, создаваемых этим метаклассом.
- Во втором примере добавлен
__init__, который выполняется при каждом вызовеNewCls(...)и перезаписывает атрибутa.
Решение
Первый вариант:
class NewCls(type):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
test = NewCls("X", (object,), {"a": 1, "b": 2})
print(test.a)
test2 = NewCls("X", (object,), {"a": 12, "b": 22})
print(test2.a)
Результат:
1
1
Почему так:
test = NewCls("X", (object,), {"a": 1, "b": 2})
При первом вызове _instance ещё равен None, поэтому создаётся новый класс X с атрибутами:
a = 1
b = 2
Поэтому:
print(test.a)
выведет:
1
Дальше выполняется:
test2 = NewCls("X", (object,), {"a": 12, "b": 22})
Но новый класс уже не создаётся, потому что NewCls._instance уже хранит первый созданный класс. Поэтому test2 получает ссылку на тот же самый класс, что и test.
Фактически:
test is test2 # True
Поэтому test2.a всё ещё равно 1.
Второй вариант:
class NewCls(type):
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self, *args):
self.a = args
test = NewCls("X", (object,), {"a": 1, "b": 2})
print(test.a)
test2 = NewCls("X", (object,), {"a": 12, "b": 22})
print(test2.a)
Результат:
('X', (<class 'object'>,), {'a': 1, 'b': 2})
('X', (<class 'object'>,), {'a': 12, 'b': 22})
Во втором варианте после __new__ вызывается __init__ метакласса:
def __init__(self, *args):
self.a = args
Здесь self — это созданный класс X, а args — аргументы, переданные в NewCls(...):
"X", (object,), {"a": 1, "b": 2}
Поэтому после первого создания test.a уже не равно 1. Атрибут a перезаписывается кортежем аргументов:
('X', (<class 'object'>,), {'a': 1, 'b': 2})
При втором вызове новый класс снова не создаётся, возвращается тот же объект. Но __init__ выполняется повторно и снова перезаписывает a:
('X', (<class 'object'>,), {'a': 12, 'b': 22})
Главная идея: __new__ в этом метаклассе пытается реализовать singleton для создаваемых классов. Но во втором варианте __init__ ломает ожидаемое поведение, потому что при каждом вызове заново меняет атрибут a у одного и того же сохранённого класса.