Konstruktor w Python tworzą dwie metody __new__ oraz __init__, wykonywane jedna po drugiej. __new__ tworzy instancję, która następnie jest przekazywana do __init__ .
Zapraszam Cię do przeglądu własności i zastosowania tej nieco orientalnej części konstruktora.
Podstawowe informacje o Python __new__
- jest metoda statyczną, w związku z tym jej pierwszym argumentem jet
clsczyli referencja do klasy na której jest wywoływana.
- pozostałe argumenty wynikają z wywołania samej klasy, zatem jeśli
__init__przyjmuje argumenty, musisz zdefiniować parametry w__new__. Z tego powodu kolejny blok kodu rzuci wyjątkiem:TypeError: new() takes 1 positional argument but 2 were given
class Point:
def __new__(cls):
...
def __init__(self, x):
self.x = x
Point(5)- wartość którą zwraca
__new__jest wartością zwróconą z wywołania samej klasy co reprezentuje:
class Dummy:
def __new__(cls):
return 1
print(Dummy() == 1)
# Outputs:
# True- jeśli
__new__nie zwróci instancji klasy na której jest wywołany__init__nie zostanie wywołany.
class Dummy:
...
class MyClass:
def __new__(cls):
return Dummy()
def __init__(self):
print('Will not be printed')
print(isinstance(MyClass(), Dummy))
# Outputs:
# TrueTyp instancji uzależniony od argumentów konstruktora
Pierwszy przykład trąci antywzorcem, ponieważ zwracana wartość jest innego typu niż klasa, która jest wywoływana. Skupmy się na ten moment na samej mechanice.
Jeśli boki prostokąta są równe to czy nie lepiej zwrócić kwadrat?
class Square:
def __init__(self, side_length):
self.side_length = side_length
class Rectangle:
def __new__(cls, width: float, height: float):
if width == height:
return Square(side_length=width)
return object.__new__(Rectangle)
def __init__(self, width: float, height: float):
self.width = width
self.height = height
r1 = Rectangle(2, 3)
r2 = Rectangle(2, 2)
print(type(r1))
print(type(r2))
# Outputs:
# <class '__main__.Rectangle'>
# <class '__main__.Square'>Wykorzystanie we wzorcu Singleton
Celem wzorca Singleton jest ograniczenie ilości tworzonych instancji. Istnieje mnóstwo jego implementacji korzystające w większości z __init__.
Jedna z ciekawszych implementacji tego wzorca, czerpiąca z credo Pythona „Simple is better than complex”, polega na natychmiastowym usuwaniu klasy po stworzeniu instancji. Oczywiście, ma to swoją wadę, ponieważ nie pozwala na lazy loading.
class Singleton:
def __init__(self, *args, **kwargs):
pass
singleton_instance = Singleton()
del SingletonWracając do tematu, oto implementacja Singleton przy użyciu __new__
class Singleton:
_instance = None # Keep instance reference
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 == s2) # → TrueRejestracja pochodzenia podczas dziedziczenia.
Poprzednie przykłady można zaimplementować w inny sposób, nie mając świadomości o samym __new__. Teraz natomiast przedstawię Ci sytuacje, gdzie __new__ jest niezastąpiony.
Załóżmy, że SubClass dziedziczy z BaseClass:
class BaseClass:
pass
class SubClass(BaseClass):
pass
isinstance(SubClass(), BaseClass) # TrueNastępnie chcesz uzyskać informację czy wywołanie instancji BaseClass pochodzi z niej samej czy klasy dziedziczącej.
Rozwiązanie wygląda tak:
class BaseClass:
def __new__(cls):
obj = super(BaseClass, cls).__new__(cls)
obj._from_base_class = type(obj) == BaseClass
return obj
class SubClass(BaseClass):
...
base_instance = BaseClass()
sub_instance = SubClass()
print(base_instance._from_base_class) # True
print(sub_instance._from_base_class) # FalseDlaczego nie można tego zaimplementować używając __init__? Ponieważ self w __init__, jak ustaliliśmy wcześniej, zawsze jest instancją o typie klasy w której się mieści.
Dziedziczenie z typów immutable
__new__ pozwala na modyfikację zwracanej wartości. Jeśli chcemy zmodyfikować tworzenie typów immutable, __init__ nam się nie przyda, ponieważ w tej części konstruktora już jest za późno, dostaliśmy gotową instancję. Dlatego potrzebujemy metody __new__.
Stwórzmy zatem pokazową klasę PositiveNumberTuple spełniającą założenia:
- obiekt ma posiadać wszystkie właściwości
tuple, - będzie przechowywał tylko wartości typu
float - odfiltruje wartości mniejsze od zera
- będzie przechowywał informację ile zostało pominiętych
class PositiveNumberTuple(tuple): # 1
def __new__(cls, *numbers):
skipped_values_count = 0 # 4
positive_numbers = []
for x in numbers:
if x >= 0: # 2, 3
positive_numbers.append(x)
else:
skipped_values_count += 1
instance = super().__new__(cls, tuple(positive_numbers))
instance.skipped_values_count = skipped_values_count
return instance
positive_ints_tuple = PositiveNumberTuple(-2, -1, 0, 1, 2)
print(positive_ints_tuple) # -> (0, 1, 2)
print(type(positive_ints_tuple)) # -> <class '__main__.PositiveNumberTuple'>
print(positive_ints_tuple.skipped_values_count) # -> 2
Wnioski
Podsumowując łatwo o nadużycia korzystając z __new__. Jego zastosowanie, którego nie można zastąpić w inny, bardziej powszechny sposób jest bardzo wąskie. Jednak jak się przekonałeś istnieje nie bez powodu.
Spotkałem się również z opinią że w przypadku meta klas, logika z __init__ zostaje przeniesiona właśnie do __new__ aby nie została łatwo nadpisana.
Udostępnij ten wpis
Dobrnąłeś do końca. Jeśli ten artykuł był dla Ciebie wartościowy i chcesz otrzymywać informacje o kolejnych, to zapraszam Cię do zapisania się do listy mailingowej. Gwarantuję zero spamu.
Radek.

