Разобрать метакласс с попыткой реализации Singleton

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 у одного и того же сохранённого класса.