Сообщить об ошибке.

Использование метода .__new__() в классах Python

Примеры использования метода .__new__() при создании классов

При написании классов Python обычно нет необходимости создавать собственную реализацию специального метода .__new__(). В большинстве случаев базовой реализации из встроенного класса объектов достаточно для создания пустого объекта текущего класса.

Тем не менее, есть несколько интересных вариантов использования этого метода.


Общий принцип использования метода .__new__().

Обычно, создание собственной реализации метода .__new__() необходима только тогда, когда нужно управлять созданием нового экземпляра класса на низком уровне. Теперь, если нужна кастомная реализация этого метода, то следует выполнить несколько шагов:

  • Создать новый экземпляр, вызвав super().__new__() с соответствующими аргументами.
  • Настроить новый экземпляр в соответствии с конкретными потребностями.
  • возвратить новый экземпляр, чтобы продолжить процесс создания экземпляра.

С помощью этих трех кратких шагов можно настроить этап создания экземпляра в процессе создания экземпляра Python.

class SomeClass:
    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        # В этом месте можно настроить свой экземпляр...
        return instance
    def __init__(self, val):
        self.val = val

В этом примере представлена ​​своего рода реализация шаблона .__new__(). Как обычно, .__new__() принимает текущий класс в качестве аргумента, который обычно называется cls.

Обратите внимание, что используются *args и **kwargs, чтобы сделать метод более гибким и удобным в сопровождении, принимая любое количество аргументов. Всегда необходимо определять метод .__new__() с помощью *args и **kwargs, если только нет веской причины следовать другому шаблону.

Вызов super().__new__(cls) необходим, чтобы получить доступ к методу object.__new__() родительского класса object, который является базовой реализацией метода .__new__() для всех классов Python. (Встроенный класс object является базовым классом по умолчанию для всех классов Python)

Важно отметить, что сама функция object.__new__() принимает только один аргумент - класс для создания экземпляра. Если вызывать object.__new__() с большим количеством аргументов, то получим исключение TypeError. Однако object.__new__() по-прежнему принимает и передает дополнительные аргументы в конструктор .__init__(), если класс не имеет собственной реализации .__new__().

>>> a = SomeClass(7)
>>> a.val
# 7

Создание подклассов неизменяемых встроенных типов.

Начнем с варианта использования .__new__(), который состоит из подкласса неизменяемого встроенного типа. В качестве примера предположим, что необходимо написать класс Distance как подкласс типа float Python. У класса Distance будет дополнительный атрибут для хранения единицы, которая используется для измерения расстояния.

Первый подход к проблеме с использованием метода .__init__():

class Distance(float):
    def __init__(self, value, unit):
        super().__init__(value)
        self.unit = unit

>>> in_miles = Distance(42.0, "Miles")
# Traceback (most recent call last):
#    ...
# TypeError: float expected at most 1 argument, got 2

Когда создается подкласс неизменяемого встроенного типа данных, то получаем ошибку. Часть проблемы в том, что значение задается при создании, а менять его при инициализации уже поздно. Кроме того, функция float.__new__() вызывается, как говориться "под капотом", и она не обрабатывает дополнительные аргументы так же, как object.__new__(). Это то, что вызывает ошибку в этом примере.

Чтобы обойти эту проблему, можно инициализировать объект во время создания с помощью .__new__() вместо переопределения в .__init__().

class Distance(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        return instance

>>> in_miles = Distance(42.0, "Miles")
>>> in_miles
# 42.0
>>> in_miles.unit
# 'Miles'
>>> in_miles + 42.0
# 84.0

В этом примере .__new__() выполняет три шага:

  • метод создает новый экземпляр текущего класса cls, вызывая super().__new__(). На этот раз вызов возвращается к float.__new__(), который создает новый экземпляр и инициализирует его, используя значение в качестве аргумента.
  • метод настраивает новый экземпляр, добавляя к нему атрибут .unit.
  • возвращает новый экземпляр.

Теперь класс Distance работает, как и ожидалось, позволяя использовать атрибут экземпляра для хранения единиц измерения. В отличие от значения float, хранящегося в экземпляре Distance атрибут .unit является изменяемым.

Метод .__new__() фабрика случайных объектов.

Создание классов, возвращающие экземпляры другого класса может вызвать создание специальной реализации метода .__new__(). При создании классов подобного рода нужно быть осторожны, потому что в этом случае Python полностью пропускает этап инициализации. Таким образом, вы будете нести ответственность за перевод вновь созданного объекта в допустимое состояние, прежде чем использовать его в своем коде.

Следующий пример демонстрирует возврат экземпляров случайно выбранных классов:

# pets.py
from random import choice

class Pet:
    def __new__(cls):
        # выбираем класс случайным образом
        other = choice([Dog, Cat, Python])
        # подставляем вместо собственного класса `cls` 
        # случайно выбранный `other`
        instance = super().__new__(other)
        print(f{type(instance).__name__}!")
        return instance

    def __init__(self):
        print("Класс `Pet` никогда не запустится!")

class Dog:
    def communicate(self):
        print("Гав! Гав!")

class Cat:
    def communicate(self):
        print("Мяу! Мяу!")

class Bird:
    def communicate(self):
        print("Чик! Чирик!")

В этом примере класс Pet предоставляет метод .__new__(), который создает новый экземпляр, случайным образом выбирая класс из списка существующих классов.

Использование класса Pet в качестве фабрики объектов домашних питомцев:

>>> from pets import Pet
>>> pet = Pet()
# Я Dog!
>>> pet.communicate()
# Гав! Гав!
>>> isinstance(pet, Pet)
# False
>>> isinstance(pet, Dog)
# True
>>> another_pet = Pet()
# Я Bird!
>>> another_pet.communicate()
# Чик! Чирик!

Каждый раз, когда создается экземпляр Pet, то в результате получается случайный объект из другого класса. Такое возможно, т. к. нет ограничений на объект, который может возвращать .__new__(). Использование .__new__() таким образом превращает класс в гибкую и мощную фабрику объектов, не ограниченную экземплярами самого себя.

Наконец, обратите внимание, что метод класса Pet.__init__() никогда не запускается. Это происходит потому, что Pet.__new__() всегда возвращает объекты другого класса, а не самого Pet.

Создание "Singleton" класса.

Иногда нужно реализовать класс, который позволяет создавать только один экземпляр. Этот тип класса широко известен как "Singleton". В этой ситуации удобен метод .__new__(), потому что он может помочь ограничить количество экземпляров, которые может иметь данный класс.

Примечание. Большинство опытных разработчиков утверждают, что не нужно реализовывать шаблон проектирования "Singleton" в Python, если уже нет рабочего класса и нужно добавить функциональность шаблона поверх него. В остальных случаях можно использовать константу уровня модуля, чтобы получить ту же функциональность "Singleton" без необходимости писать относительно сложный класс.

Пример класса Singleton с использованием метода .__new__(), который позволяет создавать только один экземпляр за раз. Для реализации такого поведения, метод .__new__() проверяет наличие предыдущих экземпляров, кэшированных в атрибуте класса:

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

>>> a = Singleton()
>>> b = Singleton()
>>> a is b
# True

В этом примере класс Singleton имеет атрибут с именем ._instance, который по умолчанию имеет значение None и работает как кеш. Метод .__new__() проверяет, не существует ли предыдущий экземпляр, проверяя, что условие cls._instance равно None. Если это условие истинно, то блок кода if создает новый экземпляр Singleton и сохраняет его в cls._instance. Наконец, метод возвращает вызывающей стороне новый или существующий экземпляр.

Внимание! В приведенном выше примере Singleton не предоставляет реализацию .__init__(). Если когда-нибудь понадобится такой класс с методом .__init__(), то имейте в виду, что этот метод будет запускаться каждый раз, когда вы вызываете конструктор Singleton(). Такое поведение может вызвать непредсказуемые эффекты инициализации и ошибки.