Skip to content

Latest commit

 

History

History
924 lines (584 loc) · 24.2 KB

File metadata and controls

924 lines (584 loc) · 24.2 KB

Урок 5. Магический метод __new__ и управление созданием объектов. Пример паттерна Singleton

На прошлом занятии мы познакомились с магическим методом __init__. Он вызывается после создания объекта и используется для его начальной настройки: сохранения параметров, создания атрибутов и подготовки объекта к работе.

Но в Python существует ещё один метод, который участвует в процессе создания объекта. Причём он вызывается ещё раньше, чем __init__.

Этот метод называется:

__new__

На первый взгляд может возникнуть закономерный вопрос:

Зачем нужен ещё один метод, если уже существует __init__?

Ответ связан с тем, что в Python создание объекта и его инициализация — это два разных этапа.

Разберёмся, как именно Python создаёт объект.


Как Python создаёт объект

Когда вы пишете код:

p = Point(1, 2)

на самом деле происходит несколько последовательных шагов:

  1. Вызывается метод __new__
  2. Создаётся новый объект в памяти
  3. Вызывается метод __init__
  4. Объект возвращается в переменную

Схематично процесс выглядит так:

Point(1, 2)
    │
    ▼
__new__(cls, 1, 2)
    │
    ▼
создание объекта в памяти
    │
    ▼
__init__(self, 1, 2)
    │
    ▼
готовый объект

То есть:

  • __new__ создаёт объект
  • __init__ настраивает объект

Это очень важное различие.


Первый пример работы __new__

Давайте посмотрим, как работает этот метод на практике.

Создадим простой класс Point.

class Point:

    def __new__(cls, *args, **kwargs):
        print("Вызов метода __new__")
        print("cls =", cls)

    def __init__(self, x=0, y=0):
        print("Вызов метода __init__")
        self.x = x
        self.y = y

Теперь попробуем создать объект:

p = Point(1, 2)
print(p)

Результат выполнения программы будет примерно таким:

Вызов метода __new__
cls = <class '__main__.Point'>
None

Обратите внимание на две вещи:

  1. Метод __init__ не вызвался
  2. Переменная p получила значение None

Почему объект не был создан?

Причина заключается в том, что метод __new__ обязан вернуть новый объект.

Если метод __new__ ничего не возвращает, Python автоматически возвращает None. Это означает, что объект не был создан. Поэтому p = None.

А раз объекта нет — метод __init__ вызываться не может.


Как правильно реализовать __new__

Чтобы объект был создан, нужно вернуть новый экземпляр класса.

Но возникает вопрос:

Откуда взять новый объект?

Ответ простой — его создаёт базовый класс object.

Поэтому стандартная реализация метода __new__ выглядит так:

class Point:

    def __new__(cls, *args, **kwargs):
        print("Вызов метода __new__")

        obj = super().__new__(cls)
        return obj

    def __init__(self, x=0, y=0):
        print("Вызов метода __init__")

        self.x = x
        self.y = y

Теперь создадим объект:

p = Point(1, 2)

Результат:

Вызов метода __new__
Вызов метода __init__

Теперь оба метода вызываются.


Почему используется cls, а не self

Обратите внимание на сигнатуру метода:

def __new__(cls, *args, **kwargs):

Первый параметр называется cls, а не self.

Причина проста:

  • self — это уже созданный объект
  • cls — это класс, из которого нужно создать объект

В момент вызова __new__ объекта ещё не существует, поэтому передаётся ссылка на класс.


Что делает super().__new__(cls)

Строка:

obj = super().__new__(cls)

делает следующее:

  1. обращается к базовому классу
  2. вызывает его метод __new__
  3. создаёт новый объект
  4. возвращает его

Важно понимать одну вещь:

Если метод __new__ не вернуть объект — объект создан не будет.


Откуда берётся базовый класс object

Вы могли заметить, что мы используем super().

Но мы нигде не писали наследование:

class Point:

Откуда тогда появляется базовый класс?

Начиная с Python 3 все классы автоматически наследуются от object.

То есть код:

class Point:

на самом деле интерпретируется как:

class Point(object):

Схема наследования выглядит так:

Point
  │
  ▼
object

И именно класс object содержит базовую реализацию:

__new__
__init__
__str__
__repr__

и многие другие магические методы.


Передача аргументов в __new__

Обратите внимание на параметры:

*args
**kwargs

Зачем они нужны?

Когда мы создаём объект:

p = Point(1, 2)

аргументы 1 и 2 передаются:

сначала в __new__
потом в __init__

То есть Python делает примерно следующее:

__new__(Point, 1, 2)
__init__(p, 1, 2)

В большинстве случаев метод __new__ просто игнорирует аргументы, а основная логика выполняется в __init__.

Но иногда аргументы используются, чтобы решить — нужно ли создавать объект вообще.

Именно здесь появляется одна очень интересная возможность.


Когда используется __new__

Метод __new__ используется редко, но он становится незаменимым в нескольких случаях:

  • управление созданием объектов
  • изменение логики создания объекта
  • создание не более одного экземпляра класса
  • работа с неизменяемыми типами
  • реализация паттернов проектирования

Самый известный пример — паттерн Singleton.


Использование __new__ для контроля создания объектов

Теперь, когда мы понимаем, как Python создаёт объект, можно задать следующий важный вопрос:

Можно ли сделать так, чтобы объект создавался не всегда?

Оказывается — да.

Метод __new__ позволяет контролировать процесс создания объекта.

Мы можем:

  • запретить создание объекта
  • вернуть уже существующий объект
  • создать объект другого класса
  • изменить стандартную логику создания

Что такое паттерн Singleton?

Singleton (Одиночка) — это паттерн проектирования, который гарантирует, что у класса существует только один экземпляр. И к этому экземпляру можно получить доступ из любой части программы.

Проще говоря:

Сколько бы раз мы ни создавали объект, мы всегда будем получать один и тот же экземпляр


Зачем это нужно

Singleton применяется в ситуациях, когда создание нескольких объектов может привести к ошибкам.

Например:

  • подключение к базе данных
  • работа с конфигурацией программы
  • управление логированием
  • управление кешем
  • управление доступом к оборудованию

Представьте, что каждый модуль программы создаёт своё подключение к базе данных.

Это может привести к:

  • избыточным соединениям
  • конфликтам
  • утечкам ресурсов

Гораздо правильнее создать одно соединение, которое будет использоваться во всей программе.


Пример класса для работы с базой данных

Создадим простой класс DataBase.

class DataBase:

    def __init__(self, user, password, port):
        self.user = user
        self.password = password
        self.port = port

    def connect(self):
        print(f"Подключение к БД: {self.user}, {self.password}, {self.port}")

    def close(self):
        print("Соединение закрыто")

    def read(self):
        return "данные из базы"

    def write(self, data):
        print(f"Запись данных: {data}")

Создадим два объекта:

db1 = DataBase("root", "1234", 3306)
db2 = DataBase("admin", "5678", 5432)

print(id(db1))
print(id(db2))

Результат будет примерно таким:

140485739124560
140485739124720

Это два разных объекта.

Но иногда нам нужно запретить такую ситуацию.


Реализация Singleton

Чтобы гарантировать существование только одного объекта, нам понадобится:

  • атрибут класса для хранения экземпляра
  • метод __new__, который будет контролировать создание объекта

Добавим в класс специальный атрибут.

class DataBase:

    __instance = None

Он будет хранить ссылку на единственный объект класса.

Теперь добавим метод __new__.

class DataBase:

    __instance = None

    def __new__(cls, *args, **kwargs):

        if cls.__instance is None:
            cls.__instance = super().__new__(cls)

        return cls.__instance

Как работает этот код

Мы проверяем создавался ли объект раньше.

if cls.__instance is None:

Если значение None, значит объект ещё не существует.


Если объект ещё не создан:

cls.__instance = super().__new__(cls)

мы создаём новый объект стандартным способом.

И сохраняем его в атрибуте класса.


Если же объект уже существует, то код:

return cls.__instance

просто возвращает тот же самый объект.


Проверим работу Singleton

Теперь попробуем создать два объекта.

db1 = DataBase("root", "1234", 3306)
db2 = DataBase("admin", "5678", 5432)

print(id(db1))
print(id(db2))

Результат:

140485739124560
140485739124560

ID одинаковый.

Это означает, что db1 и db2 указывают на один и тот же объект


Важная проблема реализации Singleton

Теперь выполним следующий код.

db1 = DataBase("root", "1234", 3306)
db2 = DataBase("admin", "5678", 5432)

db1.connect()
db2.connect()

Результат будет неожиданным:

Подключение к БД: admin, 5678, 5432
Подключение к БД: admin, 5678, 5432

Почему?

Мы же создавали первый объект с параметрами:

root 1234 3306

Причина проблемы

Проблема связана с тем, что метод __init__ вызывается каждый раз при создании объекта.

Даже если объект уже существует.

То есть Python делает примерно следующее:

__new__ → возвращает существующий объект
__init__ → выполняется снова

И второй вызов __init__ перезаписывает атрибуты объекта.


Возможное решение (упрощённое)

Иногда разработчики добавляют специальный флаг.

class DataBase:

    __instance = None
    __initialized = False

    def __new__(cls, *args, **kwargs):

        if cls.__instance is None:
            cls.__instance = super().__new__(cls)

        return cls.__instance

    def __init__(self, user, password, port):

        if self.__initialized:
            return

        self.user = user
        self.password = password
        self.port = port

        self.__initialized = True

Теперь __init__ выполнится только один раз.


Но это не идеальное решение

Подобный подход иногда называют "костылём".

Потому что он:

  • усложняет код
  • добавляет дополнительное состояние
  • может приводить к ошибкам

В более серьёзных реализациях используются:

  • метаклассы
  • переопределение __call__
  • фабричные функции

Но эти темы мы будем изучать намного позже.

Сейчас наша цель — понять принцип работы __new__.


Когда реально используется __new__

В обычных классах этот метод переопределяется крайне редко.

Но он используется в следующих ситуациях:

1. Паттерны проектирования

Например:

Singleton
Flyweight

2. Работа с неизменяемыми типами

Например:

tuple
str
int

Поскольку такие объекты нельзя изменить после создания, их инициализация происходит именно через __new__.


3. Контроль создания объектов

Например:

  • кеширование объектов
  • повторное использование экземпляров
  • ограничение количества объектов

Итоги урока

Теперь вы знаете важную особенность создания объектов в Python.

Создание объекта — это двухэтапный процесс.

1. __new__  → создаёт объект
2. __init__ → инициализирует объект

Метод __new__:

  • вызывается раньше __init__
  • получает ссылку на класс (cls)
  • обязан вернуть объект

Если объект не вернуть — он не будет создан.

Также мы познакомились с паттерном:

Singleton

который позволяет гарантировать существование только одного экземпляра класса.


Вопросы

  1. В каком порядке вызываются методы __new__ и __init__ при создании объекта?
  2. Какой параметр передаётся первым в метод __new__?
  3. Чем отличается cls от self?
  4. Что произойдёт, если метод __new__ не вернёт объект?
  5. Почему в реализации __new__ часто используется super().__new__(cls)?
  6. Что гарантирует паттерн Singleton?
  7. Почему при реализации Singleton метод __init__ может вызываться несколько раз?
  8. Почему реализация Singleton через флаг считается упрощённой?
  9. В каких случаях разработчики действительно переопределяют метод __new__?

Задачи

Задача 1

Объявите класс AbstractClass, объекты которого создавать нельзя.

При выполнении команды:

obj = AbstractClass()

переменная obj должна ссылаться не на объект класса, а на строку:

"Ошибка: нельзя создавать объекты абстрактного класса"

Задача 2

Объявите класс OnlyNumbers, объекты которого создаются командой:

obj = OnlyNumbers(value)

Если value — число (int или float), то создаётся объект класса. Если передано что-то другое — возвращается строка:

"Передано не число"

Задача 3

Создайте класс для кэширования объектов по значению.

Объявите класс CachedData, объекты которого создаются командой:

obj = CachedData(data)

Если объект с таким же значением data уже создавался ранее, то должен возвращаться тот же самый объект.

Атрибут класса для хранения уже созданных экземпляров должен называться cache и быть единым для всех объектов (атрибутом класса).

Тип данных кэша должен быть словарь.

Пример использования:

a = CachedData("hello")
b = CachedData("hello")

id(a) == id(b)  # True

Задача 4

Объявите класс LimitedString, объекты которого создаются командой:

obj = LimitedString(text, max_length)

Если длина строки text превышает max_length, то в объекте должна сохраняться только обрезанная строка. Для сохранения строки используйте атрибут text.

Пример использования:

obj = LimitedString("HelloWorld", 5)

Результат:

obj.text == "Hello"

Задача 5

Создайте класс для создания объекта другого класса (упрощённая фабрика).

Объявите два класса:

class Integer:
    def __init__(self, value):
        self.value = int(value)


class String:
    def __init__(self, value):
        self.value = str(value)

Объявите класс Converter, который создаётся командой:

obj = Converter(value)

Если value — число → вернуть объект Integer, иначе → объект String

Пример использования:

a = Converter(10)
b = Converter("hello")

print(type(a), a.value)
print(type(b), b.value)

Задача 6

Нужно создать класс с ограниченным количеством объектов.

Объявите класс SingletonFive, объекты которого создаются командой:

obj = SingletonFive(name)

Класс должен:

  • создать первые 5 объектов
  • начиная с 6-го — всегда возвращать пятый объект

Пример использования:

objs = [SingletonFive(str(i)) for i in range(10)]

id(objs[4]) == id(objs[5]) == id(objs[9])

Задача 7

Создать Фабрику объектов в зависимости от системы.

В программе есть переменная:

TYPE_OS = 1  # 1 - Windows, 2 - Linux

И два класса:

class DialogWindows:
    name_class = "DialogWindows"


class DialogLinux:
    name_class = "DialogLinux"

Необходимо объявить класс Dialog, который создаётся так:

dlg = Dialog(name)

Класс должен:

  • создавать объект DialogWindows, если TYPE_OS == 1
  • создавать объект DialogLinux, если TYPE_OS != 1
  • добавлять созданному объекту атрибут name

Пример использования:

dlg1 = Dialog("Первый")

TYPE_OS = 2

dlg2 = Dialog("Второй")

print(type(dlg1), dlg1.name)
print(type(dlg2), dlg2.name)

Задача 8 (на повторение)

Объявите класс Point, который используется для представления точки на плоскости.

Объекты этого класса должны создаваться командой:

pt = Point(x, y)

где:

  • x — координата по оси X (число)
  • y — координата по оси Y (число)

Внутри каждого объекта должны храниться эти значения в атрибутах:

self.x
self.y

Добавьте в класс Point метод:

clone(self)

Этот метод должен:

  • создавать новый объект класса Point
  • копировать в него значения x и y текущего объекта

Пример использования:

pt = Point(2, 3)
pt_clone = pt.clone()

print(pt.x, pt.y)           # 2 3
print(pt_clone.x, pt_clone.y)  # 2 3
print(id(pt) == id(pt_clone))  # False

Предыдущий урок | Следующий урок