При написании классов Python обычно нет необходимости создавать собственную реализацию специального метода obj.__new__(). В большинстве случаев базовой реализации из встроенного класса объектов достаточно для создания пустого объекта текущего класса.
Тем не менее, есть несколько интересных вариантов использования этого метода.
obj.__new__();.__new__() как фабрика случайных объектов;obj.__new__().Обычно, создание собственной реализации метода obj.__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, чтобы сделать метод более гибким и удобным в сопровождении, принимая любое количество аргументов. Всегда необходимо определять метод obj.__new__() с помощью *args и **kwargs, если только нет веской причины следовать другому шаблону.
Вызов super().__new__(cls) необходим, чтобы получить доступ к методу obj.__new__() родительского класса object, который является базовой реализацией метода obj.__new__() для всех классов Python. (Встроенный класс object является базовым классом по умолчанию для всех классов Python)
Важно отметить, что сама функция obj.__new__() принимает только один аргумент - класс для создания экземпляра. Если вызывать obj.__new__() с большим количеством аргументов, то получим исключение TypeError. Однако obj.__new__() по-прежнему принимает и передает дополнительные аргументы в конструктор obj.__init__(), если класс не имеет собственной реализации obj.__new__().
>>> a = SomeClass(7) >>> a.val # 7
Начнем с варианта использования obj.__new__(), который состоит из подкласса неизменяемого встроенного типа. В качестве примера предположим, что необходимо написать класс Distance как подкласс типа float Python. У класса Distance будет дополнительный атрибут для хранения единицы, которая используется для измерения расстояния.
Первый подход к проблеме с использованием метода obj.__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__() вызывается, как говориться "под капотом", и она не обрабатывает дополнительные аргументы так же, как obj.__new__(). Это то, что вызывает ошибку в этом примере.
Чтобы обойти эту проблему, можно инициализировать объект во время создания с помощью obj.__new__() вместо переопределения в obj.__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 является изменяемым.
obj.__new__() фабрика случайных объектов.Создание классов, возвращающие экземпляры другого класса может вызвать создание специальной реализации метода obj.__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 предоставляет метод obj.__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, то в результате получается случайный объект из другого класса. Такое возможно, т. к. нет ограничений на объект, который может возвращать obj.__new__(). Использование obj.__new__() таким образом превращает класс в гибкую и мощную фабрику объектов, не ограниченную экземплярами самого себя.
Наконец, обратите внимание, что метод класса Pet.__init__() никогда не запускается. Это происходит потому, что Pet.__new__() всегда возвращает объекты другого класса, а не самого Pet.
Иногда нужно реализовать класс, который позволяет создавать только один экземпляр. Этот тип класса широко известен как "Singleton". В этой ситуации удобен метод obj.__new__(), потому что он может помочь ограничить количество экземпляров, которые может иметь данный класс.
Примечание. Большинство опытных разработчиков утверждают, что не нужно реализовывать шаблон проектирования "Singleton" в Python, если уже нет рабочего класса и нужно добавить функциональность шаблона поверх него. В остальных случаях можно использовать константу уровня модуля, чтобы получить ту же функциональность "Singleton" без необходимости писать относительно сложный класс.
Пример класса Singleton с использованием метода obj.__new__(), который позволяет создавать только один экземпляр за раз. Для реализации такого поведения, метод obj.__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 и работает как кеш. Метод obj.__new__() проверяет, не существует ли предыдущий экземпляр, проверяя, что условие cls._instance равно None. Если это условие истинно, то блок кода if создает новый экземпляр Singleton и сохраняет его в cls._instance. Наконец, метод возвращает вызывающей стороне новый или существующий экземпляр.
Внимание! В приведенном выше примере
Singletonне предоставляет реализациюobj.__init__(). Если когда-нибудь понадобится такой класс с методомobj.__init__(), то имейте в виду, что этот метод будет запускаться каждый раз, когда вы вызываете конструкторSingleton(). Такое поведение может вызвать непредсказуемые эффекты инициализации и ошибки.