Wednesday, January 7, 2009

Python - pierwszy parametr metody to obiekt i co z tego wynika

Kiedy definiujemy jakąś metodę, nie statyczną, w języku Python jako pierwszy parametr każdej metody podajemy obiekt klasy (zwyczajowo nazywamy tę zmienną self).

No tak. Działa to jak "this" z C++ no i pojawia się to dziwne self w deklaracji metody jednak - co z tego dla nas wynika ? Otóż pewna piękna rzecz :) Możemy wywołać metodę na klasie i podać obiekt, na której ma się wywołać. Wygląda to na przykład tak.
class Samochod(object):
    def dzwiek(self):
        print 'brum'

if __name__ == '__main__':
    s = Samochod()
    Samochod.dzwiek(s)
Jak widać stworzyliśmy obiekt i przekazaliśmy go jako pierwszy parametr do metody wywoływanej na klasie :) Ktoś powie "dziwne". I rzeczywiście dziwne. Sprawa zaczyna nabierać jednak nabierać sensu rumieńców w kilku przypadkach. Jednym z nich jest sytuacja gdy nasza klasa dziedziczy z wielu klas i chociaż dwie z nich mają metodę o tej samej nazwie. Wyobraź sobie jak mogłaby wyglądać amfibia: jedź jak samochód i pływa jak łódź. Umówmy się, że samochód wydaje dźwięk brum a łódź plum. Spróbujemy to jakoś ubrać w klasy. Oto moja propozycja.
class Samochod(object):
    def dzwiek(self):
        print 'brum'

class Lodz(object);
    def dzwiek(self):
        print 'plum'

class Amfibia(Samochod, Lodz):
    pass
Jak widać klasa Amfibia dziedziczy zarówno po Samochod jak i po Lodz :) i pięknie. Jak myślisz ? jaki dźwięk wyda amfibia gdy wywołamy na niej tą metodę ?

a = Amfibia()
a.dzwiek()
brum

Ale dlaczego akurat brum ? Odpowiedź jest bardzo prosta. Amfibia w kolejności dziedziczy najpierw po samochodzie później po łodzi. Zostanie wzięta funkcja klasy pierwszej od lewej. Możesz to łatwo sprawdzić definiując klasę Amfibia jako class Amfibia(Lodz, Samochod) :) teraz dźwięk będzie dźwiękiem łodzi.

Pozostańmy przy amfibii, która najpierw dziedziczy po samochodzie class Amfibia(Lodz, Samochod). O ile wydanie dźwięku samochodu jak widać nie sprawia problemu o tyle, nie za bardzo jest możliwość, w tradycyjny sposób, dostać się do dźwięku łodzi. Otóż gdy wywołasz z amfibii dźwięk "potomka" wywoła się dźwięk samochodu, gdyby zaś w procedurze samochodu został wywołany dźwięk potomka to wywołałby się dźwięk łodzi. Coś w tym stylu.
class Samochod(object):
    def dzwiek(self):
        print 'brum'
        super(Samochod, self).dzwiek()

class Lodz(object);
    def dzwiek(self):
        print 'plum'

class Amfibia(Samochod, Lodz):
    def dzwiek(self):
        super(Amfibia, self).dzwiek()

if __name__ == '__main__':
    a = Amfibia()
        a.dzwiek()
Metoda dźwięk z Amfibii odwołuje się do metody dźwięk pierwszego (od lewej) potomka - czyli u nas samochodu. Wywoływana jest metoda dźwięk z samochodu.
W metodzie samochodu znowu pobierany jest potomek. Samochód sam w sobie nie ma potomka, ale wywołujemy go "z amfibii" jest więc pobierany kolejny potomek (tutaj łódź) i wywoływany jest dla niego metoda dźwięk().

Z naszego punktu widzenia wielodziedziczenie działa tak: amfibia (klasa) dziedziczy po samochodzie i po łodzi. Już wiemy, że potomkiem amfibii jest pierwsza klasa od lewej wymieniona w dziedziczeniu (u nas klasa Samochod), a kolejnym potomkiem (potomkiem szukanym w metodzie klasy samochód) jest łódź. "Czyli co ?" ktoś zapyta "Amfibia dziedziczy po samochodzie, który dziedziczy po łodzi ?". Tak to właśnie wygląda od strony wywoływania metod. Aby dostać się do dźwięku łodzi trzeba go wywołać w samochodzie.

Jak dobrze pomyśleć to wydaje się to straszne. Mając taki kod kiedy teraz zrobimy:
s = Samochod()
s.dzwiek()
To wyda się dźwięk samochodu i będzie błąd ponieważ metoda super próbuje znaleźć potomka samochodu (jedynie object) i wydać na nim dźwięk - co jest niemożliwe. Potomek samochodu ma sens tylko w przypadku użycia amfibii. Tak więc klops. Klasa samochód sama w sobie, tak skonstruowana, będzie powodowała błędy.

Jeżeli byliście uważnymi czytelnikami użyłem na początku sformułowania "w tradycyjny sposób". To znaczy, że Python pomimo takiej swojej śmiesznej metody wielodziedziczenia daje mechanizmy aby zrobić klasę samochód i łódź takie aby działały samodzielnie, oraz amfibię w której będzie można wydać oba dźwięki. Cała sztuczka polega właśnie na tym :) że jako pierwszy parametr metody podajemy obiekt, na którym ma się wywołać.

Zdefiniujmy na początku samochód i łódź - najnormalniej na świecie:
class Samochod(object):
    def dzwiek(self):
        print 'brum'

class Lodz(object);
    def dzwiek(self):
        print 'plum'
Bardzo proste. Obydwie mogą wydać dźwięk i widzimy, że nie odwołują się do żadnych metod "potomków" więc mogą działać sobie naprawdę niezależnie. Dobrze. Troszkę zmian będzie w amfibii. Teraz nasza amfibia będzie wydawała dwa dźwięki jednocześnie. Jak to zrobimy. Otóż - Wywołamy metodę dźwiek na klasie (nie na obiekcie) Samochod i klasie (nie na obiekcie) Lodz podając jako obiekt, na którym należy wywołać metodę, amfibię. Wszystko dzięki temu iż składnia Python wymusza na nas aby pierwszy parametr każdej metody, nie statycznej, był obiekt na którym ma działać nasza metoda. Wszystko co opisaliśmy powyżej wygląda dokładnie tak:
class Amfibia(Samochod, Lodz):
    def dzwiek(self):
        Samochod.dzwiek(self)
        Lodz.dzwiek(self)

if __name__ == '__main__':
    a = Amfibia()
    a.dzwiek()
Co się tu dzieje ? Ponieważ już wiemy, że metodą super z amfibii dostaniemy się do pierwszej (od lewej) klasy po której dziedziczy nasza, użyliśmy innego zabiegu. Wywołaliśmy metodę dźwięk na klasie Samochod. Jednak ponieważ metoda ta nie jest statyczna i trzeba ją wywołać na obiekcie jako pierwszy parametr podaliśmy obiekt, na którym operacja ma się wykonać - self czy w tym przypadku naszą amfibię. To samo zrobiliśmy dla łodzi. Ponieważ amfibia dziedziczy i po samochodzie i po łodzi będzie dobrze zachowywała się zarówno z metodami pierwszej jak i drugiej klasy.

Taka składnia wywołania to jeden z wielu smaczków Python-a wynikającej z troistości jego natury: strukturalne, obiektowej i tej którą widać tutaj - funkcjonalnej :] Nie oceniam czy to jak rozwiązane jest w Pythonie sprawdza się czy nie - czas pokaże.

Uwaga !
Użycie metody print wewnątrz klas jest zabiegiem upraszczającym zagadnienie mającym na celu ułatwienie zrozumienia problemu i kodu w celu zwiększenia jego walorów dydaktycznych. Autor świadomie pogwałcił zasady dotyczących I/O klasy i obiektów w celu IMHO uczynienia przykładu czytelniejszym i przejrzystszym.

1 comment:

Anonymous said...

Jestem zupełnie początkującą osobą bawiącą się programowaniem, więc wybacz być może za brednie, ale konstruując metodę w ten sposób:

def dzwiek(self):
a = Samochod()
b = Lodz()
a.dzwiek()
b.dzwiek()

i wywołując

a = Amfibia()
a.dzwiek()

również osiągnę zamierzony efekt, bez potrzeby wywoływania metod na klasie tylko na konkretnych obiektach. Wtedy wiadomo która metoda dzwiek() odnosi sie do którego obiektu.