26 lutego 2011

OOP w Pythonie

Mam duży sentyment do Python-a. Pamiętam, że gdy go poznawałem, byłem nim oczarowany. Duck typing, funkcje wyższego rzędu, mixin – a wszystko to w języku obiektowym! Python pochłonął mnie bez reszty. Od tego momentu minęło już trochę czasu, emocje opadły, a listy składane nie powodują już ciarek na plecach. Dodatkowo, ostatnimi czasy wpadła w moje ręce książka Dusty Phillips-a „Python 3 Object Oriented Programming”. Była to wystarczająca motywacja, by przyjrzeć się obiektowości Pythona 3.

  • Klasy, metody, pola

Najprostsza klasa w Pythonie wygląda tak:
class ExampleClass:
    pass
Stworzenie zaś instancji powyższej klasy wygląda następująco :
my_class = ExampleClass()
Powyższy kod, choć wygląda trywialnie, sporo ukrywa. Za stworzenie nowej instancji klasy odpowiedzialna jest statyczna metoda __new__. Po zwróceniu przez nią obiektu, wywoływana jest jego metoda __init__ odpowiedzialna za zainicjowanie obiektu. Dopiero po tym wszystkim następuje przypisanie obiektu do zmiennej.

Nadpisywanie metody __init__ jest tym samym, czym nadpisywanie domyślnego konstruktora w Javie. Z kolei overriding metody __new__ rzadko kiedy znajduje uzasadnienie. Dołóżmy zatem do naszej klasy metodę inicjującą:
class ExampleClass:

    def __init__(self):
        pass
Każda metoda klasy, nie będąca metodą statyczną, musi jako swój pierwszy argument przyjmować self. Jest to referencja do aktualnej instancji klasy (odpowiednik Javowego this).
class ExampleClass:

    def __init__(self):
        pass

    def example_non_static_method(self):
        print('Example method !')
Oczywiście w Pythonie istnieją również metody statyczne. Wszystkich, którzy już pomyśleli, że metoda statyczna to taka, która nie posiada self jako pierwszy argument, muszę zmartwić. Otóż nie do końca! Należy jeszcze nad metodą postawić dekorator @staticmethod (w temat dekoratorów nie chciałbym się teraz zagłębiać, gdyż jest na tyle obszerny, że spokojnie można by o nim napisać osobny post...). A oto nasza klasa wzbogacona o metodę statyczną:
class ExampleClass:

    def __init__(self):
        pass

    def example_non_static_method(self):
        print('Example method !')

    @staticmethod
    def example_static_method():
        return 8
Skoro w klasach mamy metody, to muszą być i pola. Zaczniemy od pól statycznych, których deklaracja wygląda tak:
class ExampleClass:

    static_field = 8

    def __init__(self):
        pass

    def example_non_static_method(self):
        print('Example!')

    @staticmethod
    def example_static_method():
        return ExampleClass.static_field
Z polami niestatycznymi nie jest już tak łatwo. Nie istnieje sposób, który pozwoliłby na poziomie kodu powiedzieć, że klasa posiada pole niestatyczne. W związku z tym, pola związane z instancją klasy dodaje się dynamicznie. Możemy to zrobić w dowolnym miejscu w kodzie. Jeżeli jednak chcemy, żeby wszystkie instancje klasy posiadały pewne wspólne cechy, wówczas zalecanym miejscem jest metoda __init__, w której to do referencji self dodajemy nazwę pola.
class ExampleClass:

    static_field = 8

    def __init__(self):
        self.example_non_static_field = 'Example!'

    def example_non_static_method(self):
        print(self.example_non_static_field)

    @staticmethod
    def example_static_method():
        return static_field
  • Enkapsulacja

W pythonie w celu zapewnienia enkapsulacji mamy zarówno konwencje, jak i mechanizm. Konwencja mówi, że korzystanie z metod lub pól, których nazwy zaczynają się od podkreślnika , poza obrębem klasy w której się znajdują, jest niewskazane. Dla tych programistów, dla których konwencja to za mało, istnieje mechanizm – dwa podkreślniki. Sprawiają one, że metody i pola, których nazwy się od nich zaczynają, są niedostępne z zewnątrz klasy.
class ExampleClass:

    static_field = 8

    def __init__(self):
        self.__example_private_non_static_field = 'Example!'

    def example_non_static_method(self):
        print(self.__example_private_non_static_field)

    @staticmethod
    def example_static_method():
        return static_field

  • Dziedziczenie, nadpisywanie i przeciążanie metod

Python jako język obiektowy posiada mechanizm dziedziczenia, a konkretnie wielodziedziczenia. Żeby z niego skorzystać, wystarczy po nazwie klasy w nawiasach podać nazwy klas, po których chcemy dziedziczyć:
    class A:
        pass
    
    class B:
        pass
    
    class C(A, B):
        pass
    W związku z dynamicznym charakterem Python-a nie istnieje w nim polimorfizm inkluzywny. Gdy dziedziczymy, nie interesuje nas typ, tylko cechy i zachowania. Dlatego, w kontekście dziedziczenia na wszystkie klasy, możemy patrzeć jak na mixin-y. W przypadku metody takiej, jak ta poniżej nie interesuje nas po jakich klasach dziedziczy przekazany obiekt. To, co jest istotne, to to że potrafi latać :
    class Bird():
        def fly(self):
            pass
    
    class PaperPlane():
        def fly(self):
            pass
    
    def throw_up(object_to_throw):
        object_to_throw.fly()
    
    throw_up(Bird())
    throw_up(PaperPlane())
    Poniższy przykład pokazuje nadpisywanie metod (method overriding) w klasach dziedziczących:
    class A:
    
        def example_method(self):
            print('A method !')
    
    class B(A):
    
        def example_method(self, x):
            print('B method !')
            print ('x : ', x)
    Każda metoda w Python-owej klasie musi posiadać unikatową nazwę. W związku z tym, przesłanianie metod działa tylko i wyłącznie po ich nazwie, bez względu na parametry. Stąd płynie kolejny wniosek – w Pythonie nie można przeciążać metod (method overloading).
    O ile podejście takie w przypadku języków statycznie typizowanych wydaje się nie do pomyślenia, to w Pythonie jest ono całkiem naturalne. W związku z dynamiczną naturą języka nie istnieje sposób, żeby rozróżniać metody o tych samych nazwach na podstawie typów ich argumentów, czy typu wartości zwracanej. Liczba argumentów również nie jest odpowiednim kryterium do rozróżniania metod o tej samej nazwie w związku z tym, że metody mogą posiadać dowolną liczbę argumentów.
    def example_method(x, *args, **kwargs):
        pass
    
    example_method(2, 4, 5, 6, zzz = 5, qqq = 'So sick !')
    example_method('But it is so cool !')
    Ale wróćmy do dziedziczenia. Żeby dostać się do metody nadpisywanej z klasy, po której dziedziczymy, możemy użyć metody super():
    class A:
    
        def example_method(self):
            print('A method !')
    
    class B(A):
    
        def example_method(self, x):
            super().example_method()
            print('B method !')
            print('x : ',  x)
    Powyższa konstrukcja jest prosta – dziedziczymy po jednej klasie. W rzeczywistości, w przypadku użycia wielodziedziczenia, używanie metody super() nie jest już takie proste. Wystarczy rzut oka do dokumentacji Pythona, w której możemy znaleźć, że super() :
    „ Return a proxy object that delegates method calls to a parent or sibling class of type"
    Mechanizm ukryty pod super, w rzeczywistości jest tak skomplikowany, że można by tylko o nim stworzyć kolejny taki artykuł. Jeżeli ktoś jest zainteresowany, to odsyłam do strony James'a Knight'a, na której to autor omawia to zagadnienie dość dogłębnie. Gorąco polecam.

    • Podsumowanie

    Wiem, że artykuł ten nie jest kompletny i omija bardzo wiele aspektów Pythona związanych z obiektowością. Pisząc go, chciałem skupić się na tym, co jest esencją OOP według mnie. Jak widać w Pythonie esencja ta wygląda momentami mętnie. Nie twierdzę tak dlatego, że programowanie obiektowe w Pythonie wygląda inaczej niż w Javie. Oczywistym wydaje mi się fakt, iż w związku z różnym charakterem tych dwóch języków, mechanizmy w nich zastosowane powinny być inne. Powinny jednak pozwalać na korzystanie z nich w sposób czytelny i łatwy dla programisty, a z tym w Pythonie bywa różnie. Tym bardziej denerwuje zmarnowana przez twórców języka okazja, jaką było wydanie Python-a 3 i zerwanie z kompatybilnością wsteczną. Istniała możliwość, żeby uporządkować język i wykorzystać w pełni jego potencjał, a skończyło się jak zwykle na dobrych chęciach.

    Brak komentarzy:

    Prześlij komentarz