При написании классов Python обычно нет необходимости создавать собственную реализацию специального метода .__new__()
. В большинстве случаев базовой реализации из встроенного класса объектов достаточно для создания пустого объекта текущего класса.
Тем не менее, есть несколько интересных вариантов использования этого метода.
.__new__()
;.__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". В этой ситуации удобен метод .__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()
. Такое поведение может вызвать непредсказуемые эффекты инициализации и ошибки.