Dlaczego metody __init__ nadklasy nie są wywoływane automatycznie?

155

Dlaczego projektanci Pythona zdecydowali, że __init__()metody podklas nie wywołują automatycznie __init__()metod ich nadklas, jak w niektórych innych językach? Czy idiom w języku Pythonic i zalecane jest naprawdę podobny do następującego?

class Superclass(object):
    def __init__(self):
        print 'Do something'

class Subclass(Superclass):
    def __init__(self):
        super(Subclass, self).__init__()
        print 'Do something else'
jrdioko
źródło
2
Możesz napisać dekorator, który odziedziczy __init__metodę, a nawet może automatycznie wyszukać podklasy i je ozdobić.
osa
2
@osa, to naprawdę dobry pomysł. czy chciałbyś opisać trochę więcej w części dekoratorskiej?
Diansheng
1
@osa tak, opisz więcej!
Charlie Parker

Odpowiedzi:

163

Kluczowa różnica między Pythona __init__i tych innych języków konstruktorów jest to, że __init__to nie konstruktor: to jest inicjator (rzeczywisty konstruktor (jeśli w ogóle, ale zobaczymy później ;-) jest __new__i działa zupełnie inaczej znowu). Podczas konstruowania wszystkie superklas (i bez wątpienia robi tak „przed” ty kontynuować budowy w dół) jest oczywiście częścią mówiąc jesteś konstruowaniu instancji podklasy jest, że z pewnością nie jest przypadek inicjalizacji, ponieważ istnieje wiele przypadków użycia, w których inicjalizacja nadklas musi zostać pominięta, zmieniona, kontrolowana - dzieje się, jeśli w ogóle, „w środku” inicjalizacji podklasy i tak dalej.

Zasadniczo, super-klasa delegacja inicjatora nie jest automatyczna w Pythonie dla dokładnie tych samych powodów takie przekazanie nie jest także automatyczne do jakichkolwiek innych metod - i zauważ, że te „inne języki” nie rób automatycznego delegację super-klasę dla każdego inna metoda albo ... tylko dla konstruktora (i jeśli to możliwe, destruktora), który, jak wspomniałem, nie jest tym, czym jest Python __init__. (Zachowanie __new__jest również dość osobliwe, chociaż tak naprawdę nie jest bezpośrednio związane z twoim pytaniem, ponieważ __new__jest tak osobliwym konstruktorem, że właściwie nie musi niczego konstruować - może z powodzeniem zwrócić istniejącą instancję lub nawet nie instancję ... oczywiście Python oferuje wielewiększą kontrolę nad mechaniką niż w przypadku „innych języków”, o których myślisz, co oznacza również brak możliwości automatycznego delegowania __new__zadań! -).

Alex Martelli
źródło
7
Za przyjęciem, ponieważ dla mnie prawdziwą korzyścią jest możliwość wywołania funkcji _ init () _ nadklasy w dowolnym momencie inicjalizacji podklasy (lub wcale).
kindall
53
-1 dla " __init__nie jest konstruktorem ... rzeczywisty konstruktor ... jest __new__". Jak sam zauważyłeś, __new__nie zachowuje się jak konstruktor z innych języków. __init__jest w rzeczywistości bardzo podobny (jest wywoływany podczas tworzenia nowego obiektu, po jego przydzieleniu, aby ustawić zmienne składowe w nowym obiekcie) i prawie zawsze jest miejscem do zaimplementowania funkcjonalności, które w innych językach byś umieścił konstruktor. Więc nazwij to konstruktorem!
Ben
8
Uważam również, że jest to raczej absurdalne stwierdzenie: „ konstruowanie wszystkich nadklas ... jest oczywiście częścią stwierdzenia, że konstruujesz instancję podklasy, co oczywiście nie dotyczy inicjowania ”. W słowach „budowa / inicjalizacja” nie ma nic, co czyni to „oczywistym” dla każdego. I __new__nie wywołuje też automatycznie superklasy __new__. Więc twoje twierdzenie, że kluczową różnicą jest to, że konstrukcja koniecznie obejmuje budowę nadklas, podczas gdy inicjalizacja nie jest niezgodna z twoim twierdzeniem, że __new__jest konstruktorem.
Ben
36
W rzeczywistości, od docs Pythona __init__na docs.python.org/reference/datamodel.html#basic-customization : „Jako specjalny przymusu na konstruktorów , bez wartości mogą zostać zwrócone; spowoduje to spowodować TypeError zostać podniesiona przy starcie „(podkreślenie moje). Więc to oficjalne, __init__jest konstruktor.
Ben
3
W terminologii Python / Java __init__jest nazywany konstruktorem. Ten konstruktor jest funkcją inicjalizacyjną wywoływaną po całkowitym skonstruowaniu obiektu i zainicjowaniu go do stanu domyślnego, w tym jego ostatecznego typu środowiska uruchomieniowego. Nie jest to odpowiednik konstruktorów C ++, które są wywoływane na statycznie typowanym, przydzielonym obiekcie o niezdefiniowanym stanie. Są one również różne od __new__, więc tak naprawdę mamy co najmniej cztery różne rodzaje funkcji alokacji / konstrukcji / inicjalizacji. Języki używają mieszanej terminologii, a ważną częścią jest zachowanie, a nie terminologia.
Elazar
36

Jestem trochę zawstydzony, gdy ludzie papugują „zen Pythona”, jakby to wszystko usprawiedliwiało. To filozofia projektowania; poszczególne decyzje projektowe zawsze można wyjaśnić bardziej konkretnymi terminami - i tak musi być, bo inaczej „Zen Pythona” staje się wymówką do robienia czegokolwiek.

Powód jest prosty: niekoniecznie konstruujesz klasę pochodną w sposób podobny do tego, jak konstruujesz klasę bazową. Możesz mieć więcej parametrów, mniej, mogą być w innej kolejności lub w ogóle nie być powiązane.

class myFile(object):
    def __init__(self, filename, mode):
        self.f = open(filename, mode)
class readFile(myFile):
    def __init__(self, filename):
        super(readFile, self).__init__(filename, "r")
class tempFile(myFile):
    def __init__(self, mode):
        super(tempFile, self).__init__("/tmp/file", mode)
class wordsFile(myFile):
    def __init__(self, language):
        super(wordsFile, self).__init__("/usr/share/dict/%s" % language, "r")

Dotyczy to wszystkich metod pochodnych, a nie tylko __init__.

Glenn Maynard
źródło
6
Czy ten przykład oferuje coś wyjątkowego? języki statyczne też to potrafią
jean
18

Java i C ++ wymagają wywołania konstruktora klasy bazowej ze względu na układ pamięci.

Jeśli masz klasę BaseClassz członkiem field1i tworzysz nową klasę, SubClassktóra dodaje członka field2, to wystąpienie SubClasszawiera miejsce na field1i field2. Potrzebujesz konstruktora BaseClassdo wypełnienia field1, chyba że chcesz, aby wszystkie klasy dziedziczące powtarzały BaseClassinicjalizację we własnych konstruktorach. A jeśli field1jest prywatny, dziedziczenie klas nie może zostać zainicjowane field1.

Python to nie Java ani C ++. Wszystkie wystąpienia wszystkich klas zdefiniowanych przez użytkownika mają ten sam „kształt”. Są to po prostu słowniki, w których można wstawiać atrybuty. Przed wykonaniem jakiejkolwiek inicjalizacji wszystkie wystąpienia wszystkich klas zdefiniowanych przez użytkownika są prawie dokładnie takie same ; to tylko miejsca do przechowywania atrybutów, które jeszcze nie przechowują.

Dlatego też dla podklasy Pythona ma sens, aby nie wywoływać konstruktora klasy bazowej. Mógłby po prostu dodać atrybuty, gdyby chciał. Nie ma miejsca zarezerwowanego dla danej liczby pól dla każdej klasy w hierarchii i nie ma różnicy między atrybutem dodanym przez kod z BaseClassmetody a atrybutem dodanym przez kod z SubClassmetody.

Jeśli, co jest powszechne, SubClassfaktycznie chce mieć wszystkie BaseClassniezmienniki ustawione przed przystąpieniem do własnych dostosowań, to tak, możesz po prostu zadzwonić BaseClass.__init__()(lub użyć super, ale jest to skomplikowane i czasami ma swoje własne problemy). Ale nie musisz. Możesz to zrobić przed, po lub z różnymi argumentami. Do diabła, gdybyś chciał, mógłbyś wywołać the BaseClass.__init__z innej metody całkowicie niż __init__; może masz jakieś dziwne, leniwe inicjowanie.

Python osiąga tę elastyczność dzięki prostocie. Obiekty inicjalizujesz, pisząc __init__metodę, która ustawia atrybuty self. Otóż ​​to. Zachowuje się dokładnie jak metoda, ponieważ jest dokładnie metodą. Nie ma innych dziwnych i nieintuicyjnych zasad dotyczących rzeczy, które trzeba zrobić najpierw lub rzeczy, które wydarzy się automatycznie, jeśli nie zrobisz innych rzeczy. Jedynym celem, jaki musi spełniać, jest być punktem zaczepienia do wykonania podczas inicjalizacji obiektu w celu ustawienia początkowych wartości atrybutów, i właśnie to robi. Jeśli chcesz, aby robił coś innego, napisz to wyraźnie w swoim kodzie.

Ben
źródło
2
Jeśli chodzi o C ++, nie ma to nic wspólnego z „układem pamięci”. Ten sam model inicjalizacji, jaki oferuje Python, mógłby zostać zaimplementowany w C ++. Jedynym powodem, dla którego konstrukcja / niszczenie są takie, jakie są w C ++, jest decyzja projektowa o zapewnieniu niezawodnych i dobrze zachowujących się narzędzi do zarządzania zasobami (RAII), które równie dobrze można wygenerować automagicznie (co oznacza mniej kodu i błędów ludzkich) przez kompilatory, ponieważ ich reguły (kolejność wywołań) są rygorystycznie zdefiniowane. Nie jestem pewien, ale Java najprawdopodobniej po prostu zastosowała to podejście, aby być kolejnym językiem podobnym do POLA C.
Alexander Shukaev,
10

„Wyraźne jest lepsze niż ukryte”. To samo rozumowanie wskazuje, że powinniśmy wyraźnie napisać „ja”.

Myślę, że w końcu jest to korzyść - czy możesz wyrecytować wszystkie zasady Java dotyczące wywoływania konstruktorów nadklas?

Mike Axiak
źródło
8
W większości się z tobą zgadzam, ale reguła Javy jest w rzeczywistości dość prosta: wywoływany jest konstruktor bez argumentu, chyba że wyraźnie poprosisz o inny.
Laurence Gonsalves,
1
@Laurence - Co się dzieje, gdy klasa nadrzędna nie definiuje konstruktora bezargumentowego? Co się dzieje, gdy konstruktor bez argumentów jest chroniony lub prywatny?
Mike Axiak
8
Dokładnie to samo, co by się stało, gdybyś próbował nazwać to wprost.
Laurence Gonsalves,
8

Często podklasa ma dodatkowe parametry, których nie można przekazać do nadklasy.

John La Rooy
źródło
7

W tej chwili mamy dość długą stronę opisującą kolejność rozwiązywania metod w przypadku wielokrotnego dziedziczenia: http://www.python.org/download/releases/2.3/mro/

Gdyby konstruktory były wywoływane automatycznie, potrzebna byłaby kolejna strona o co najmniej tej samej długości, wyjaśniająca kolejność tego procesu. To byłoby piekło ...

viraptor
źródło
To była właściwa odpowiedź. Python musiałby zdefiniować semantykę przekazywania argumentów między podklasą a nadklasą. Szkoda, że ​​ta odpowiedź nie jest przegłosowana. Może gdyby pokazał problem na kilku przykładach?
Gary Weiss,
5

Aby uniknąć nieporozumień, warto wiedzieć, że można wywołać __init__()metodę base_class , jeśli child_class nie ma __init__()klasy.

Przykład:

class parent:
  def __init__(self, a=1, b=0):
    self.a = a
    self.b = b

class child(parent):
  def me(self):
    pass

p = child(5, 4)
q = child(7)
z= child()

print p.a # prints 5
print q.b # prints 0
print z.a # prints 1

W rzeczywistości MRO w Pythonie będzie szukał __init__()w klasie nadrzędnej, gdy nie może go znaleźć w klasie dzieci. Jeśli masz już __init__()metodę w klasie podrzędnej, musisz bezpośrednio wywołać konstruktor klasy nadrzędnej .

Na przykład następujący kod zwróci błąd: class parent: def init (self, a = 1, b = 0): self.a = a self.b = b

    class child(parent):
      def __init__(self):
        pass
      def me(self):
        pass

    p = child(5, 4) # Error: constructor gets one argument 3 is provided.
    q = child(7)  # Error: constructor gets one argument 2 is provided.

    z= child()
    print z.a # Error: No attribute named as a can be found.
Keyvanrm
źródło
3

Może __init__jest to metoda, którą podklasa musi zastąpić. Czasami podklasy wymagają uruchomienia funkcji rodzica przed dodaniem kodu specyficznego dla klasy, a innym razem muszą skonfigurować zmienne instancji przed wywołaniem funkcji nadrzędnej. Ponieważ nie ma sposobu, aby Python mógł wiedzieć, kiedy byłoby najbardziej odpowiednie wywołanie tych funkcji, nie powinien zgadywać.

Jeśli to Cię nie kołysze, pomyśl, że __init__to tylko inna funkcja. Jeśli dostuffzamiast tego byłaby ta funkcja , czy nadal chciałbyś, aby Python automatycznie wywoływał odpowiednią funkcję z klasy nadrzędnej?

Kirk Strauser
źródło
2

Uważam, że jedną z bardzo ważnych kwestii jest tutaj to, że w przypadku automatycznego wywołania super.__init__(), zgodnie z projektem, wykluczasz, kiedy ta metoda inicjalizacji jest wywoływana i z jakimi argumentami. unikanie automatycznego wywoływania go i wymaganie od programisty jawnego wykonania tego wywołania wiąże się z dużą elastycznością.

w końcu tylko dlatego, że klasa B wywodzi się z klasy A, nie oznacza, że A.__init__()można lub należy ją wywołać z tymi samymi argumentami, co B.__init__(). uczynienie wywołania jawnym oznacza, że ​​programista może np. zdefiniować B.__init__()z zupełnie innymi parametrami, wykonać jakieś obliczenia z tymi danymi, wywołać A.__init__()z argumentami stosownie do tej metody, a następnie wykonać pewne przetwarzanie końcowe. ten rodzaj elastyczności byłby niewygodny do osiągnięcia, gdyby A.__init__()był wywoływany B.__init__()niejawnie, albo przed B.__init__()wykonaniem, albo zaraz po nim.

pływ
źródło