Магический метод __new__. Пример паттерна Singleton

Курс по Python ООП: https://stepik.org/a/116336

На этом занятии мы познакомимся с еще одним магическим методом __new__, который вызывается непосредственно перед созданием объекта класса. Я напомню, что другой магический метод __init__ вызывается после создания объекта (о нем мы говорили на предыдущем занятии).

Здесь у вас может сразу возникнуть вопрос, зачем нужно было определять два разных метода, которые последовательно вызываются при создании экземпляров классов? Разве не достаточно одного __init__, чтобы выполнять начальную инициализацию объекта? Конечно, нет. В практике программирования встречаются самые разнообразные задачи и иногда нужно что-то делать и до создания объектов. Например, реализация известного паттерна Singleton в Python, как раз делается через метод __new__ и мы с ним позже познакомимся.

А для начала нам нужно познакомиться с работой самого метода __new__. Давайте добавим его в наш класс Point. Я его перепишу в сокращенной форме:

class Point:
    def __new__(cls, *args, **kwargs):
        print("вызов __new__ для " + str(cls))
 
    def __init__(self, x=0, y=0):
        print("вызов __init__ для " + str(self))
        self.x = x
        self.y = y

Смотрите, здесь записан метод __new__, у которого первым идет обязательный параметр cls – это ссылка на текущий класс Point, а затем, указываются коллекции из фактических и формальных параметров, которые может принимать данная функция. Это стандартное определение метода __new__ в классах. В теле функции я просто сделал вывод сообщения и переменной cls.

Если теперь попробовать создать экземпляр класса:

pt = Point(1, 2)

то мы в консоли увидим только одно сообщение от метода __new__. То есть, второй метод __init__ не был вызван и, кроме того, если мы распечатаем переменную pt:

print(pt)

то увидим значение None, то есть, объект не был создан. Почему так произошло? В Python магический метод __new__ должен возвращать адрес нового созданного объекта. А в нашей программе он ничего не возвращает, то есть, значение None, что эквивалентно отказу в создании нового объекта. Именно поэтому переменная pt принимает значение None.

Хорошо, давайте адрес нового объекта. Но откуда мы его возьмем? Для этого можно вызвать аналогичный метод базового класса и делается это, следующим образом:

    def __new__(cls, *args, **kwargs):
        print("вызов __new__ для " + str(cls))
        return super().__new__(cls)

Здесь функция super() возвращает ссылку на базовый класс и через нее мы вызываем метод __new__ с одним первым аргументом. Но, подождите! Что это за базовый класс? Мы наш класс Point ни от какого класса не наследовали? Да и вообще еще не изучали тему наследования! Да, поэтому, забегая вперед, скажу, что, начиная с версии Python 3, все классы автоматически и неявно наследуются от базового класса object:

И уже из этого базового класса мы вызываем метод __new__. Кстати, если метод __new__ не прописывать в классе Point, то будет автоматически запускаться версия базового класса. То есть, этот метод всегда вызывается при создании нового объекта. При необходимости, мы можем его переопределять, добавляя новую логику его работы. И то же самое относится ко всем магическим методам. Они всегда существуют у всех классов. Но переопределяем мы лишь те, что необходимо, а остальные работают по умолчанию. В этом сила базового класса object. В нем уже существует программный код, общий для всех классов языка Python. Иначе, нам пришлось бы его каждый раз прописывать заново.

Итак, теперь мы знаем откуда берется и вызывается магический метод __new__. Запустим программу и видим в консоли, что были вызваны оба метода __new__ и __init__ нашего класса Point, а также был успешно сформирован новый объект.

Возможно, здесь у вас остался один вопрос: а зачем нужны списки параметров *args, **kwargs в методе __new__? Мы, вроде, их нигде не используем? В действительности, здесь хранятся дополнительные параметры, которые мы можем указывать при создании объекта. Например, строчка:

pt = Point(1, 2)

создает объект с двумя числовыми значениями, то есть, *args будет содержать эти два числа. По идее, мы можем реализовать в методе __new__ какую-либо логику с учетом значений этих аргументов. Но, в данном случае, просто игнорируем. Используем их дальше в методе __init__ при инициализации объекта. То есть, аргументы 1 и 2 передаются и в метод __new__ и в метод __init__.

Пример паттерна Singleton (учебный)

Думаю, вы в целом теперь представляете себе работу магического метода __new__, но остается вопрос: зачем все же он нужен? В качестве ответа я приведу пример очень известного паттерна проектирования под названием Singleton. Этот паттерн будет представлен в учебном варианте, то есть, мы его реализуем не полностью, т.к. пока отсутствуют достаточные знания.

Итак, давайте предположим, что мы разрабатываем класс для работы с БД. В частности, через него можно будет подключаться к СУБД, читать и записывать информацию, закрывать соединение:

class DataBase:
    def __init__(self, user, psw, port):
        self.user = user
        self.psw = psw
        self.port = port
 
    def connect(self):
        print(f"соединение с БД: {self.user}, {self.psw}, {self.port}")
 
    def close(self):
        print("закрытие соединения с БД")
 
    def read(self):
        return "данные из БД"
 
    def write(self, data):
        print(f"запись в БД {data}")

И далее полагаем, что в программе должен существовать только один экземпляр этого класса в каждый момент ее работы. То есть, одновременно два объекта класса DataBase быть не должно. Чтобы это обеспечить и гарантировать, как раз и используется паттерн Singleton. Реализуем его для класса DataBase.

Я пропишу в нем специальный атрибут (на уровне класса):

__instance = None

который будет хранить ссылку на экземпляр этого класса. Если экземпляра нет, то атрибут будет принимать значение None. А, затем, чтобы гарантировать создание строго одного экземпляра, добавим в класс магический метод __new__:

    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
 
        return cls.__instance

Работает этот метод очевидным образом. Мы проверяем атрибут класса __instance. Причем, для обращения к нему используем параметр cls – ссылку на текущий класс. Подробнее я еще освещу этот момент. Далее, проверяем, если значение равно None, то вызываем метод __new__ базового класса и тем самым разрешаем создание объекта. Иначе, просто возвращаем ссылку на ранее созданный экземпляр. Как видите, все достаточно просто.

И пропишем еще один магический метод – финализатор __del__, который будет обнулять атрибут __instance перед уничтожением объекта, чтобы мы могли, при необходимости, создать новый.

Все, простейший вариант паттерна Singleton готов. Правда он имеет один изъян. Смотрите, если попробовать создать два экземпляра:

db = DataBase('root', '1234', 80)
db2 = DataBase('root2', '5678', 40)
print(id(db), id(db2))

то их id ожидаемо будут равны. То есть, ссылки db и db2 действительно ведут на один объект. Но, если выполнить метод:

db.connect()
db2.connect()

то увидим значения: 'root2', '5678', 40 – аргументы при повторном создании класса. По идее, если объект не создается, то и локальные свойства его также не должны меняться. Почему так произошло? Все просто. Мы здесь действительно видим первый объект. Но при повторном вызове DataBase() также был вызван магический метод __init__ с новым набором аргументов и локальные свойства изменили свое значение. Конечно, мы можем здесь поставить «костыль» (как говорят в программисты) и дополнительно в классе прописать флаговый атрибут, например:

__is_exist = False

специально для метода __init__, чтобы не выполнять его если объект уже создан. Но я даже не буду дописывать такую программу. Слишком уж костыльно получается. Правильнее было бы здесь переопределить еще один магический метод __call__, о котором мы еще будем говорить. А пока оставим нашу реализацию паттерна Singleton в таком виде.

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

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме