Blog

Junior, Python

__init__(self) a klasa bazowa

28 Wrz 2018

Python jest językiem obiektowym, pozwalającym na wielodziedziczenie. Jak w przypadku klas pochodnych zachowuje się metoda __init__? Omówimy to na przykładach, jakie często pojawiają się w trakcie rozmów.

Jaki będzie widoczny efekt działania tego programu?

class Base(object):
  def __init__(self):
    print("Base.__init__")
 
class Derived(Base):
  def __init__(self):
    print("Derived.__init__")
 
derived = Derived()

Oczywiście poprawna odpowiedź to:

> Derived.__init__

repl.it

To najprostszy możliwy przypadek. Hierarchia dziedziczenia nie jest rozbudowana. Mamy dwie klasy i prostą zależność pomiędzy nimi. To co może nas tutaj zgubić to znajomość innych języków obiektowych takich jak C++ czy Java, oraz traktowanie metody __init__ jako odpowiednika konstruktora z tych języków. Tutaj metody klasy bazowej nie są wołane automagicznie. Cytując dokumentację:

If a base class has an __init__() method, the derived class’s __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance

Mamy już poprawną odpowiedź, oraz jej bardzo dokładne uzasadnie. To jednak zwykle nie wystarczy na rozmowie. Pada pytanie:

Jak zapewnić, że zostanie wywołana również metoda klasy bazowej?

Tutaj jest kilka poprawnych odpowiedzi. Nie wszystkie są równie dobre, za to każdą warto znać.

Bezpośrednie wywołanie metody __init__

Oczywiście każdą metodę możemy wywołać bezpośrednio.

class Derived(Base):
  def __init__(self):
    Base.__init__(self)
    print("Derived.__init__")

repl.it

Co nie sprawdza się zbytnio przy wielokrotnym dziedziczeniu. Wystarczy delikatnie rozbudować nasz przykład, by zobaczyć jak uciążliwe jest wołanie metody dla każdej klasy, po której dziedziczymy.

class BaseA(object):
  def __init__(self):
    print("BaseA.__init__")
 
class BaseB(object):
  def __init__(self):
    print("BaseB.__init__")

class Derived(BaseA, BaseB):
  def __init__(self):
    BaseA.__init__(self)
    BaseB.__init__(self)
    print("Derived.__init__")

derived = Derived()

repl.it

Każda zmiana hierarchii dziedziczenia niesie tutaj za sobą ryzyko przeoczenia tych wywołań. Jak można to poprawić?

Wykorzystanie funkcji super

Funkcja super powstała, by ułatwić korzystanie z metod nadpisanych w klasie pochodnej i uczynić kod łatwiejszym w utrzymaniu.

class Derived(Base):
  def __init__(self):
    super(Derived, self).__init__()
    print("Derived.__init__")

repl.it

Tutaj wszystko okazało się bezproblemowe. Wróćmy teraz do naszego przykładu z dziedziczeniem wielokrotnym i trochę go poprawmy.

class BaseA(object):
  def __init__(self):
    print("BaseA.__init__")
 
class BaseB(object):
  def __init__(self):
    print("BaseB.__init__")

class Derived(BaseA, BaseB):
  def __init__(self):
    super(Derived, self).__init__()
    print("Derived.__init__")

derived = Derived()

Wynik jest jednak daleki od naszych oczekiwań.

> BaseA.__init__
> Derived.__init__

repl.it

Jak działa super? Wywołuje następną funkcję w łańcuchu MRO. Sam mechanizm MRO jest materiałem na kolejny artykuł, tutaj wystarczy nam przypilnowanie, aby kod był spójny. Każda klasa powinna wykorzystywać ten sam mechanizm.

class BaseA(object):
  def __init__(self):
    super(BaseA, self).__init__()
    print("BaseA.__init__")
 
class BaseB(object):
  def __init__(self):
    super(BaseB, self).__init__()
    print("BaseB.__init__")

class Derived(BaseA, BaseB):
  def __init__(self):
    super(Derived, self).__init__()
    print("Derived.__init__")

derived = Derived()

Dzięki temu metody __init__ zostaną poprawnie wywołane dla każdej klasy.

> BaseB.__init__
> BaseA.__init__
> Derived.__init__

 

repl.it

Czy można tutaj jeszcze coś poprawić?

New Super

Korzystając z pythona 3 można wykorzystać uproszczoną składnię super, rezygnując z podawania nazwy klasy i instancji (jeśli kogoś interesuje konkretniejszy dokument – PEP 3135 — New Super).

class Base(object):
  def __init__(self):
    print("Base.__init__")
 
class Derived(Base):
  def __init__(self):
    super().__init__()
    print("Derived.__init__")
 
derived = Derived()

repl.it

Pomijając fakt, że takie wywołanie jest przyjemniejsze dla oka – jest ono również mniej podatne na błędy. Gdy tylko można, należy unikać hardcodowania wartości. Także nazwy klasy.

Dawid Trendota

Software Engineer po przejściach. Na swojej drodze miał okazję zmierzyć się zarówno z rozwiązaniami wbudowanymi, aplikacjami bazodanowymi jak i kernelem Linuksa. Prywatnie użytkownik i fan Gentoo Linux. W wolnym czasie programuje w C#/Unity.

@Dawid Trendota
Referencja kontra Wskaźnik

Przygotowanie do rozmowy kwalifikacyjnej – pułapki

Rozmowa kwalifikacyjna – porady

Angielski (3)
C++ (7)
Junior (11)
Python (3)
Regular (6)
ReverseEngineering (1)
Rozmowa (5)
Różne (4)
Senior (1)
Tips&Tricks (8)
angielski c++ junior python regular rozmowa rozmowa kwalifikacyjna