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
cls
czyli 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:
# True
Typ 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 Singleton
Wracają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) # → True
Rejestracja 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) # True
Nastę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) # False
Dlaczego 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.