Python __new__ vs __init__ czyli pełna kontrola nad konstruktorem.

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:

  1. obiekt ma posiadać wszystkie właściwości tuple,
  2. będzie przechowywał tylko wartości typu float
  3. odfiltruje wartości mniejsze od zera
  4. 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.

Inne artykuły

Partycjonowanie bazy danych okładka

Partycjonowanie bazy danych

Partycjonowanie pozwala podzielić tabelę na mniejsze części, gdzie każda z nich może się znajdować na innym serwerze. Zobacz, jak to działa oraz dlaczego jest to …