TI/Wstęp do programowania obiektowego

Z Brain-wiki
Wersja z dnia 10:07, 23 maj 2015 autorstwa Jarekz (dyskusja | edycje) (Utworzono nową stronę " ==The Zen of Python== Wpisz w interpreterze Pythona <source lang="pycon">import this</source> Programowanie obiektowe, czy szerzej programowanie zorientowane obiektowo...")
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)

The Zen of Python

Wpisz w interpreterze Pythona

import this

Programowanie obiektowe, czy szerzej programowanie zorientowane obiektowo, jest pewnym sposobem organizacji kodu. Pomysł polega na umieszczeniu kodu obsługującego dane blisko tych danych. W jakim sensie „blisko”? Nie fizycznej bliskości, czyli nie w sensie bliskiego umiejscowienia w pamięci, ani niekoniecznie w sensie „blisko” w tekście programu. Chodzi raczej o bliskość logiczną, podkreśloną przez wygodną notację. Co to wszystko dokładnie znaczy łatwiej będzie opisać po wprowadzeniu odpowiednich pojęć.


Szybkie wprowadzenie do obiektów i klas

Obiekt

Zmienne przechowujące dane zawarte w obiekcie nazywa się polami, natomiast funkcje związane z obiektem, nazywa się metodami.

Ważną cechą obiektów jest to, że dzielą się one na grupy charakteryzujące się podobnym zachowaniem. Wynika to z tego, że każdy obiekt ma swoją klasę, lub bardziej poprawnie gramatycznie, jest pewnej klasy.

Klasa
definiuje wspólne własności i zachowanie obiektów. Klasa zawiera funkcje przeznaczone do używania z obiektami tej klasy — metody.

Klasa jest opisem, definicją. Obiekt jest konkretną zmienną zbudowaną i zachowującą się zgodnie z definicją klasy.

Gdy tworzymy zmienną i przypisując jej, powiedzmy, liczbę 5, to tworzymy obiekt o nazwie i, klasy int.

i = 5

Notacja obiektowa

Ważnym elementem używania obiektów jest notacja obiektowa. Do pól i metod obiektów dostajemy się pisząc nazwę zmiennej dowiązanej do obiektu, kropkę i nazwę atrybutu obiektu.

Na przykład dla klasy list istnieje metoda append, która pozwala na dopisywanie elementów na końcu każdej listy. Wywołanie mojalista.append('rzecz') doda napis 'rzecz' na koniec listy mojalista.

Jako przykład pól rozważmy liczbę: obiekt klasy int.

>>> a = 1
>>> print 'a = ', a.numerator, '/', a.denominator
a =  1 / 1
>>> print 'a = ', a.real, ' + i * ', a.imag
a =  1  + i *  0
>>> print 'a jest typu', a.__class__.__name__
a jest typu int

Jak widać, obiekt klasy int ma (przynajmniej) cztery pola — licznik {{ang|numerator i mianownik {{ang|denominator oraz część rzeczywistą {{ang|real i urojoną {{ang|imaginary.

Notację z kropką stosuje się tak samo by uzyskać dostęp do pól jak i do metod, bo tak naprawdę różnica między polami a metodami jest znacznie mniejsza niż się na pierwszy rzut oka wydaje.

Jak w Pythonie tworzy się nowe klasy?

Zacznijmy od przykładu:

class Wektor(object):
    "Dwuwymiarowy wektor"

    def __init__(self, x, y):
         self.a = x
         self.b = y
         print "wektor został stworzony!"

    def dlugosc(self):
         "Zwraca długość wektora."
         return (self.a ** 2 + self.b ** 2) ** 0.5

w1 = Wektor(5, 7)        # pisze: wektor został stworzony!
print w1.dlugosc()       # pisze: 8.6...
Jak to działa?

Nasza nowa klasa Wektor ma dwie metody: Wektor.__init__ i Wektor.dlugosc. Nazwa i znaczenie pierwszej z nich będzie wyjaśnione poniżej, na razie powiemy tylko, że jej zadaniem jest zapisanie odpowiednich wartości w polach obiektu i że zostaje ona wywołana automatycznie w momencie tworzenia obiektu. Natomiast funkcja dlugosc oblicza długość wektora korzystając ze zmiennych przechowywanych w polach wektora. Jak to zostało pokazane w przykładzie, dysponując obiektem, metodę można wywołać dzięki notacji z kropką.

Zmienna self
W definicji funkcji dlugosc podany jest jeden parametr — self. Niemniej, w momencie wywołania, piszemy w1.dlugosc(), nie podając żadnych argumentów. Wewnątrz metod, zmienna self odnosi się do samego obiektu. Dzięki temu możliwy jest dostęp do pól obiektu, np. self.a. W momencie wywołania metody obiektu, zostaje on automatycznie wstawiony jako pierwszy argument metody i użytkownik podaje o jeden mniej argument niż metoda wymaga.

Specjalne metody w Pythonie

Pewne funkcje zostają wywołane w sposób automatyczny. Aby podkreślić fakt, że dana funkcja może zostać wywołana automatycznie, mają one nazwy zaczynające i kończące się dwoma podkreśleniami. W Pythonie są trzy różne kategorie nazw zaczynających się od podkreślenia, ale tak naprawdę nie mają one ze sobą dużo wspólnego. Najważniejsze są właśnie funkcje wywoływane automatycznie opisanej powyżej, które mają po parze podkreśleń na początku i na końcu. Oprócz tego występują zmienne prywatne, zaczynające się od jednego lub dwóch podkreśleń. Ich przeznaczeniem jest przechowanie zmiennych, do których nie należy się bezpośrednio odwoływać spoza klasy czy modułu.

Metoda __init__

Ważną funkcją specjalną jest metoda __init__. Python sam wywołuje ją automatycznie kiedy tworzymy nowy obiekt danego typu. Programista nie musi podać nazwy funkcji do wywołania, bo interpreter wie, że do konstrukcji obiektu służy metoda o tej właśnie nazwie. W tym właśnie sensie metoda __init__ jest specjalna. Metoda ta powinna wykonywać wszystkie operacje potrzebne do zainicjowania nowego obiektu, w szczególności powinna ona nadawać wartości jego polom. Pod innymi względami jest ona zupełnie zwyczajna, w szczególności można ją wywołać drugi i trzeci raz podając explicite jej nazwę (jeśli chcemy zmienić nasz wektor, na przykład wydłużyć dwa razy to możemy napisać w1.__init__(10, 14)).

Metoda __str__

Oprócz wcześniej wspomnianej metody __init__ przydatna jest metoda __str__. Służy ona do wytwarzania tekstowej reprezentacji obiektu. Jest automatycznie wywoływana np. przez polecenie print. Można ją dodać do naszego przykładowego wektora i zostanie ona automatycznie użyta do uzyskania reprezentacji tekstowej wektora kiedy tylko zajdzie taka potrzeba.

class Wektor(object):
    "Dwuwymiarowy wektor"

    def __init__(self, x, y):
         self.a = x
         self.b = y
         print "wektor został stworzony!"

    def dlugosc(self):
         "Zwraca długość wektora."
         return (self.a ** 2 + self.b ** 2) ** 0.5

    def __str__(self):
         """Zwraca napis, reprezentację tekstową obiektu.

         Ta metoda jest wywoływana automatycznie, gdy ktoś napisze
             str(wektor)
         lub
             print wektor
         """
         return "Wektor x={0.a} y={0.b}".format(self)

w1 = Wektor(5, 7)     
print w1        # wypisuje: Wektor x=5 y=7

Operatory dwuargumentowe

Operatory dwuargumentowe są kolejnym typem metod specjalnych.

  • Ich nazwy zawierają po dwa znaki podkreślenia na początku i na końcu.
  • Wywoływane są gdy w kodzie pomiędzy dwoma obiektami pojawia się symbol operatora, np. +
  • Z każdym operatorem związana jest domyślna nazwa metody, która definiuje co programista przewidywał zrobić gdy np. dodawał do siebie dwa wektory

Wracając do naszgo przykładowego Wektora: jeśli zechcemy dodać dwa Wektory korzystając ze znaku +, nie będzie to wykonalne, otrzymamy TypeError. Stanie się to możliwe dopiero kiedy napiszemy metodę __add__, czyli implementację operatora + dla klasy Wektor.

Aby zrozumieć działanie operatora dwuargumentowego, trzeba wiedzieć, że Python po napotkaniu x+y wywołuje metodę x.__add__(y) (mówimy, że wykonanie operatora zostaje delegowane do metody). Zatem implementując operator dwuargumentowy musimy napisać funkcję, której pierwszym argumentem jest obiekt znajdujący się na lewo od operatora (jest to aktualny obiekt i mamy dostęp do niego przez self) zaś drugim argumentem jest obiekt znajdujący się na prawo od operatora. Poniżej prezentujemy przykład implementacji dodawania dla rozważnej klasy Wektor:

class Wektor(object):
 #    
 # dotychczasowy kod klasy wektor
 #
   def __add__(self, other):
         """Zwraca nowy wektor, sumę self i other.

         Ta metoda jest wywoływana automatycznie, gdy ktoś napisze
             w1 + w2
         """
         return Wektor(self.a + other.a, self.b + other.b)
w1=Wektor(5,7)
print w1 + w1       # wypisuje: Wektor x=10 y=14

Inne funkcje specjalne

Funkcji specjalnych jest sporo, bo tabela operatorów jest długa, a należy pamiętać, że dochodzą jeszcze operatory skrócone +=, -=, ... Każdy z nich ma odpowiadającą mu funkcję specjalną, którą można napisać by umożliwić wykonywanie tej operacji na obiektach danej klasy. Wszystkie funkcje specjalne są opisane w dokumentacji języka, http://docs.python.org/dev/reference/datamodel.html#special-method-names. W trakcie pracy w interaktywnym interpreterze przydatny jest moduł operator, który zawiera funkcje specjalne do arytmetyki na liczbach. Można skorzystać z jego dokumentacji, która zawiera opisy funkcji specjalnych do operacji matematycznych (import operator; help(operator)).

Życie obiektu

Stworzenie w wyniku wywołania klasy

Aby stworzyć obiekt, trzeba wcześniej mieć klasę. Samo polecenie stworzenia obiektu wygląda identycznie jak wywołanie funkcji. W naszym przykładzie

instancja_wektora = Wektor(11, -4)

Inicjalizacja zawartości obiektu

Bezpośrednio po utworzeniu obiektu, zmienne wewnątrz obiektu (pola) jeszcze nie istnieją. Dodanie ich do obiektu przez przypisanie im wartości jest zadaniem funkcji zwanych konstruktorami. Po stworzeniu obiektu zostają wywołane jego konstruktory, czyli metody __new____init__. W obiektach klas stworzonych przez użytkownika języka ich rola się dubluje, o pierwszej zapominamy, i należy używać tylko drugiej z nich.

Przechodząc do inicjalizacji obiektu, następuje ona przez wywołanie funkcji __init__ z argumentami przekazanymi przy „wywołaniu” klasy opisanym w poprzednim punkcie.

Rozważmy szczegółowo inicjalizację Wektora wyrażeniem Wektor(11, -4).
Powoduje to wywołanie funkcji Wektor.__init__ z trzema argumentami:

  1. self → nowo stworzony obiekt, jeszcze bez pól
  2. a → 11, argument przekazany w wywołaniu
  3. b → −4, argument przekazany w wywołaniu

Na początku funkcji __init__ nasz obiekt jest wydmuszką, która nawet nie ma swoich pól ab. Ma jedynie metody zdefiniowane w klasie, ale nie można by ich użyć bo nie ma danych potrzebnych do ich wykonania.

Ponieważ Wektor po prostu przechowuje podane wartości współrzędnych to są one zapisanywane do zmiennych self.aself.b bez żadnej obróbki. Gdybyśmy chcieli nałożyć jakieś warunki, np. aby wektor zawierał się w I ćwiartce, to w funkcji __init__ powinniśmy dodać test na współrzędne i rzucić ValueError w wypadku gdy nasze żądania co do argumentów inicjalizujących Wektor nie zostały spełnione.

Po zakończeniu działania funkcji __init__ wektor ma nowo stworzone zmienne self.aself.b, oraz metody __init__, dlugosc, i inne zdefiniowane w klasie. Metody są takie same dla każdego obiektu, są więc przechowywane w klasie, natomiast zmienne obiektu są przechowywane razem z nim, ze względu na to, że każdy wektor może mieć inne współrzędne. Taki podział działa automatycznie w ten sposób, że Python najpierw poszukuje atrybutu, pola lub metody, w obiekcie, a dopiero potem w jego klasie. Są też możliwe bardzie wyrafinowane przypisania pól i metod do obiektów, ale nie będziemy o nich tutaj pisać. [staticmethod, classmethod, funkcje w obiektach, zmienne klasowe]

Życie obiektu

Teraz obiekt jest gotowy do użycia. Aby było to możliwe, musi być przypisany do jakiejś zmiennej. Po tym jak żadne zmienne nie wskazują na obiekt, przestaje on być potrzebny i będzie później zniszczony. Innymi słowy, jeśli chcemy żeby nasz obiekt żył szczęśliwie, to musimy go mieć przypisanego przez cały czas do przynajmniej jednej zmiennej, dzięki temu wiemy, że nie zostanie zniszczony.

Śmierć obiektu

Python niszczy obiekty kiedy stają się niepotrzebne. Niepotrzebnym jest obiekt, który nie jest przypisany do żadnej nazwy. Obiekt ma wbudowany licznik referencji. Przypisanie obiektu do dowolnej zmiennej zwiększa ten licznik o 1, a usunięcie zmiennej (skasowanie zmiennej przez del, przypisanie do zmiennej innego obiektu lub „zniknięcie” zmiennej po zakończeniu funkcji) — zmniejsza o 1.

Obiekt żyje co najmniej tak długo, jak długo jego licznik referencji jest większy od 0.

Zanim obiekt zostanie zniszczony, zostanie wywołana jego metoda __del__. Jej zadaniem jest wykonanie działań takich jak zamknięcie plików, które muszą być zamknięte wraz ze zniszczeniem obiektu. Niemniej, moment destrukcji obiektu jest trudny do przewidzenia, więc mechanizm __del__ jest bardzo zawodny. Nie należy go używać.

Przykład: historia życia dwóch obiektów

Podstawą zrozumienia tego, jak to wszystko działa, jest to, że w Pythonie prawie wszystko jest obiektem. Zmienna jest nazwą, która wskazuje na obiekt, czyli mówiąc bardziej precyzyjnie, kluczem w pewnym słowniku wewnątrz Pythona (zwanym namespace), który wiąże nazwy z obiektami.

Poniżej szczegółowo przedstawiona jest operacją przypisania dwóch zmiennych.

Stwórzmy pierwszy obiekt (klasy Obiekt) i zachowajmy go w zmiennej nazwa.

  nazwa = Obiekt('niebieski')
Obiekt z nazwą.png

Teraz przypiszmy obiektowi drugą nazwę.

  nowa_nazwa = nazwa
Obiekt z nazwą i nazwą.png


Teraz stwórzmy nowy obiekt (klasy Obiekt) i zachowajmy go w zmiennej nazwa.

  nazwa = Obiekt('zielony')
Obiekty dwa.png

Teraz użyjmy operatora del.

  del nowa_nazwa
Obiekty przed usunięciem.png

Jak widać, operator del usuwa zmienną, nie obiekt na który ta zmienna wskazuje.

Co się stanie z obiektem, na który wskazywała nazwa? W tym momencie jego licznik referencji {{ang|reference count, czyli sposobów na który można dotrzeć do obiektu, wynosi 0. Obiekt zostanie niedługo zlikwidowany.

Przykład: kompletna definicja klasy Wektor

# -*- coding: utf-8 -*-
class Wektor(object):
    """Dwuwymiarowy wektor."""

    _ile_nas = 0
    
    def __init__(self, a, b):
        self.a = a
        self.b = b
        Wektor._ile_nas += 1

    def dlugosc(self):
        """Zwraca liczbę, długość Wektora."""
        return (self.a**2 + self.b**2)**0.5

    def obroc(self):
        """Odwraca wektor w miejscu, czyli zamienia
           wartości jego współrzędnych na przeciwne.
        """
        self.a *= -1
        self.b *= -1

    def obrocony(self):
        """Zwraca nowy wektor odwrócony."""
        return Wektor(-self.a, -self.b)

    def __str__(self):
        """Zwraca reprezentację Wektora w formie (x, y)"""
        return "(" + str(self.a) + ", " + str(self.b) + ")"

    def __repr__(self):
        """Zwraca reprezentacje Wektora w formie <Wektor(x, y) @id>"""
        return "<Wektor{0} @{1}>".format(self.__str__(), id(self))

    def __add__(self, other):
        """Zwraca nowy Wektor będący sumą self i other."""
        print "dodawanie", self, other
        return Wektor(self.a + other.a,
                      self.b + other.b)

    def dodawanie(self, other):
        """Zwraca nowy Wektor będący sumą self i other."""
        print "dodawanie'", self, other
        return Wektor(self.a + other.a,
                      self.b + other.b)

    def __mul__(self, other):
        """Zwraca liczbę będącą iloczynem skalarnym self i other."""
        print "mnozenie", self, other
        return self.a * other.a + self.b * other.b

    def __del__(self):
        Wektor._ile_nas -= 1

Plik rozpoczyna się standardowym nagłówkiem informującym o kodowaniu — dzięki temu możemy użyć polskie literki w napisach w programie bez ryzyka, że zostaną wyświetlone jako krzaczki pod innym systemem. Informacja o kodowaniu jest zapisana w komentarzu (zaczynającym się od #).

Wszystkie nowe klasy powinny pochodzić od typu object lub innej klasy. Jeśli nie chcemy zmieniać działania innej klasy tylko „zacząć od zera” to powinniśmy zrobić to tak jak w tym przykładzie.
Na początku klasy, podobnie jak na początku funkcji, możemy dodać docstring. Ten napis zostanie wyświetlony jeśli ktoś napisze help(Wektor).

Dodamy zmienną klasową liczącą stworzone wektory. Ta zmienna, w przeciwieństwie do zwykłych pól, występuje tylko raz i  jest przypisana do samej klasy, a nie obiektów tej klasy. Pozwala to na przechowywanie informacji wspólnych dla wszystkich obiektów, w tym wypadku ich całkowitej liczby.

Metoda __init__ ma za zadanie stworzyć pola obiektu. Aktualizujemy również licznik stworzonych wektorów.

Metoda dlugosc wykorzystuje pola obiektu (self.aself.b),
aby wykonać niezbędne obliczenia i zwrócić długość wektora.

Dwa sposoby wykonania operacji: w miejscu i ze stworzeniem nowego obiektu.

  1. obroc zmienia obiekt
  2. obrocony tworzy nowy, stary pozostawiajac bez zmian

Nazwy w formie niedokonanej i dokonanej są pewną konwencją — podobnie sort sortuje listę zmieniając kolejność jej elementów, a sorted zwraca nowy obiekt z posortowanymi elementami.

Metody __str____repr__ wytwarzają napis będący reprezentacją Wektora — zwięzłą, przeznaczoną do wypisywania wyników dla użytkownika, oraz nieco bardziej dokładną zawierającą informacje przydatne dla programisty. Dobrze, by __repr__ zwracało napis pozwalający na skopiowanie i wykorzystanie jako fragment programu.

Dwa sposoby wykonania operacji arytmetycznej: metoda o specjalnej nazwie wywoływana automatycznie przy wykonywaniu operacji +, oraz metoda wywoływana explicité przez podanie nazwy.

  1. __add__ pozwala na dodawanie dwóch Wektorów. Tworzony jest nowy obiekt.
  2. dodawanie robi dokładnie to samo, tylko sposób wywołania jest inny.

Metoda __mul__ realizuje mnożenie dwóch Wektorów. Metoda ta jest wywoływana automatycznie po użyciu operatora *. Wykorzystując różne obiekty można łatwo zrealizować mnożenie dla różnych typów. Niestety, jest tylko jeden operator mnożenia i nie można wygodnie zrealizować różnych mnożeń (np. wektorowego i skalarnego) dla jednego typu.

Metoda __del__ jest wywoływana automatycznie przy destrukcji obiektu. Normalnie nie jest specjalnie przydatna, ale tutaj używamy jej do aktualizacji licznika wystąpień obiektów tej klasy.

Dziedziecznie

Simple inheritance.svg

Dziedziczeniem nazywamy sytację, w której definiujemy nową klasę jako rozwinięcie istniejącej. Tę nową klasę nazywamy dzieckiem, a starą rodzicem. Jeszcze inne, alternatywne, nazewnictwo mówi o podklasach i nadklasach. Pochodzenie tych słów będzie wyjaśnione poniżej.

Załóżmy, że mamy klasę B, która dziedziczy po klasie A. Dziedziczenie oznacza, że metody i pola dostępne w klasie-rodzicu A, są również dostępne w klasie-dziecku B. Innymi słowy, wszystkie obiekty klasy B mają takie same atrybuty jak obiekty klasy A. Powoduje to, że w ogólności, tam gdzie mogliśmy użyć obiektu klasy A, możemy również użyć obiektu klasy B. Oznacza to, że obiekty typu B są też obiektami typu A, czyli B jest podtypem czy też podklasą A. Symetrycznie, A jest nadklasą B.

Sytuację kiedy obiektów dwóch różnych klas mają wspólny zestaw metod i pól i można je używać zamiennie nazywamy polimorfizmem.

Polimorfizm jest częstszy, niż by się na pierwszy rzut oka mogło wydawać. Na przykład różne rodzaje liczb (klasy int, float czy complex) stosujemy w tym samym miejscu w zasadzie bez zastanowienia. Ponieważ możemy dodać dwie liczby zmienno-przecinkowe pisząc 1.5 + 2.0, to oczekujemy też, że podobna opercja będzie możliwa i da identyczny wynik jeśli za jedną z nich podstawimy liczbę całkowitą pisząć 1.5 + 2.

Polimorfizm jest ważny, gdyż znacznie ułatwia wykorzystanie obiektów różnych klas. Wygodniej jest myśleć o operacji dodawania zdefiniowanej dla wszystkich liczb, niż o opercji dodawania liczb zmienno-przecinkowych, operacji dodawania liczb całkowitych, operacji dodawania liczb zespolonych, itd.

Dziedziczenie jest mechanizmem, który pozwala na uzyskanie polimorfizmu w bardzo łatwy sposób. Ponieważ klasy potomne na wstępie otrzymują komplet metod i pól rodzica, to możliwość podstawienia obiektu klasy dziecka za rodzica otrzymuje się automatycznie.

Duck-typing

W Pythonie dziedziczenie nie jest jednym sposobem na uzyskanie wymienności obiektów różnych klas. Tym co decyduje, jakiego typu obiekt możemy wykorzystać np. jako argument funkcji, nie jest jego klasa, tylko zestaw pól i metod obiektu wykorzystywanych w funkcji. Popatrzmy na przykład


>>> class Hello(object):
...     def hello(self): return 'hello'
>>> class Bye2(object):
...     def hello(self): return 'bye, bye'
>>> def print_greeting(obj):
...     print type(obj), obj.hello()
>>> print_greeting(Hello())
<class '__main__.Hello'> hello
>>> print_greeting(Bye2())
<class '__main__.Bye2'> bye, bye

Mamy tutaj obiekty dwóch różnych klas, i przekazujemy je do tej samej funkcji print_greeting. Od obiektu przekazanego jako parametr, funkcja wymaga tylko tyle, by miał on metodę hello. Taki "leniwy polimorfizm" nazywa się {{ang|duck-typing, co oznacza, że definiujemy klasy które niekoniecznie mają wspólnego przodka, a jedynie definiują wszystkie atrybuty potrzebne do wykonania danej operacji.

Wielodziedziczenie

Diamond inheritance.svg

W typowym przypadku, klasy które definiujemy mają jednego tylko rodzica. Niemniej, nie musi tak być. Jeśli podamy więcej niż jednego rodzica, to obiekty naszej nowej klasy odziedziczą atrybuty po wszystkich rodzicach.

>>> class A(object):
...     def a(self):
...         print 'A'
>>> class B(object):
...     def b(self):
...         print 'B'
>>> class C(A, B):
...     def c(self):
...         print 'C'
>>> c = C()
>>> c.a()
A
>>> c.b()
B
>>> c.c()
C

W przypadku gdy zażądamy dostępu do metody czy pola, które występuje tylko w jednym z rodziców, to sytuacja jest prosta — przeszukiwany jest najpierw pierwszy rodzic, potem drugi,... dopóki nie natrafimy na atrybut o żądanej nazwie.

W sytuacji kiedy zażądamy dostępu do atrybutu, dostępnego u więcej niż jednego rodzica, to decyduje kolejność dziedziczenia. W przykładzie powyżej, najpierw przeszukiwane jest C, potem A, potem B, a na końcu object.

Wielodziedziczenie jest bardzo ciekawą techniką programistyczną, lecz niestety w praktyce w miarę skomplikowaną. Problemem jest to, że implementacje wszystkich klas w hierarchii muszą ze sobą współgrać. W szczególności, gdyby jeden z rodziców miał metodę o pewnej nazwie, a drugi z rodziców przypadkowo miałby pod taką samą nazwą zdefiniowaną metodę, która wykonuje zupełnie inną operację, to poprzez dziedziczenie po obydwu rodzicach, możemy np. doprowadzić do sytuacji, że omyłkowo wywołamy metodę z pierwszego rodzica chcąc wywołać metodę z pierwszego rodzica. Musimy sprawdzać nie tylko czy pożądana metoda jest zdefiniowana, ale przypadkiem również czy nie jest zdefiniowana inna metoda o tej samej nazwie. Takie ścisłe zespolenie różnych klas nie jest dobre, i często lepiej wykorzystać kompozycję zamiast (wielo-)dziedziczenia.


Funkcja super

Wiemy już, że jeśli zdefiniowaliśmy w klasie-dziecku metodę o danej nazwie, to zastępuje ona metodę o takiej samej nazwie zdefiniowaną w klasie-rodzicu. Niemniej, czasami chcielibyśmy wywołać metodę nadrzędną, czyli tą odziedziczoną po rodzicu. Możemy to zrobić na dwa sposoby: podając explicite nazwę klasy rodzica, lub wykorzystując funkcję super. To pierwsze rozwiązanie jest nieco prostsze, to drugie nieco lepsze.

W pierwszym rozwiązaniu, do metody rodzicielskiej odwołujemy się tak samo jak do każdej innej funkcji, podając ścieżkę do niej.

Diamond inheritance+super.svg
>>> class Welcome(object):
...     def hello(self):
...         return 'Hello'
>>> class WarmWelcome(Welcome):
...     def hello(self):
...         return Welcome.hello(self) + ", you're welcome"
>>> print Welcome().hello()
Hello
>>> print WarmWelcome().hello()
Hello, you're welcome

W drugim rozwiązaniu, do metody rodzicielskiej odwołujemy się poprzez pomocniczy obiekt zwracany przez super. Funkcja super bieże dwa argumenty — klasę od której zaczynamy poszukiwania (czyli generalnie klasę wewnątrz której definicji wywołujemy super) oraz argument self. Brzmi to trochę karkołomnie, ale w praktyce nie jest specjalnie skomplikowane.

>>> class HeartyWelcome(Welcome):
...     def hello(self):
...         return super(HeartyWelcome, self).hello() + ", you're heartily welcome"
>>> print HeartyWelcome().hello()
Hello, you're heartily welcome

W takich prostych przypadkach jak ten, i jeden, i drugi sposób odwołania się do Welcome.hello będzie skuteczny. Można nawet powiedzieć, że pierwszy jest bardziej klarowny. Niemniej, ze względu na to, że w bardziej skomplikowanych przypadkach drugi jest lepszy, dla spójności (i celem wyrabiania dobrych nawyków), lepiej stosować ten drugi.

Czym różnią się te dwa sposoby? Wykorzystując super podajemy nazwę bieżącej klasy. Wykorzystując bezpośrednie odwołanie podajemy nazwę klasy macierzystej. W przypadku gdy zmieniamy hierarchię dziedziczenia, np. czyniąc HeartyWelcome dzieckiem Welcome, nie musimy zmieniać wszystkich odwołań do odziedziczonych metod. Jeśli odwoływaliśmy się do metod rodzica przez super, to automatycznie zaczniemy się odwoływać do metod nowego rodzica. Jest to szczególnie ważne, gdy klasa-rodzic jest zdefiniowana w innym module. Wykorzystanie super zmniejsza prawdopodobieństwo pomyłek przy przerabianiu kodu.

Po drugie, funkcja super zachowuje się w szczególny sposób w sytuacji wielokrotnego dziedziczenia (czyli w sytuacji, gdy dana klasa ma więcej niż jednego rodzica). O ile każda z metod w hierarchii wywołuje metodę "nadrzędną" przez super, to wszystkie metody w hierarchii zostaną wywołane w pewnym ściśle określonym porządku.

>>> class A(object):
...   def __init__(self):
...     super(A, self).__init__()
...     print 'init A'
...
>>> class B(object):
...   def __init__(self):
...     super(B, self).__init__()
...     print 'init B'
...
>>> class C(A, B):
...   def __init__(self):
...     super(C, self).__init__()
...     print 'init C'
...
>>> c = C()
init B
init A
init C

Zauważmy, że A.__init__ wywołuje B.__init__, mimo że klasa A zupełnie nic o klasie B nie wie. Niemniej, z punktu widzenia klasy C, to by obydwoje jej rodzice zostali poprawnie zainicjalizowani, jest absolutnie kluczowe.

Po trzecie, nie musimy przekazywać self jako pierwszego argumentu wywołania metody. Oczywiście niewiele na tym zyskujemy, bo musimy przekazać self jako argument funkcji super. Możnaby pomyśleć, że takie przekazywanie oczywistych informacji, np. tego wewnątrz jakiej klasy metoda jest zdefiniowana, jest niepotrzebne i interpreter Pythona mógłby sam to stwierdzić. Tak się dzieje, ale dopiero w Pythonie 3. Piszemy po prostu super().

Stare i nowe klasy w Pythonie 2

Przykładowe klasy definiowane powyżej zawsze dziedziczą po czymś. Często jest to klasa object. Niemniej wydawałoby się, że to dziedzicznie nie jest do niczego potrzebne (bo faktycznie klasa object nie definiuje żadnych pól, a jedynie parę metod typu __str__, do których rzadko się odwołujemy z klas potomnych). Dziedziczenie po object jest potrzebne z innych przyczyn.

W Pythonie 2 mamy dwa rodzaje klas — tak zwane {{ang|old-style classes oraz {{ang|new-style classes. Te drugie otrzymujemy, gdy definiujemy klasy dziedziące bo object, bezpośrednio lub pośrednio, tak jak w przykładach w tym skrycie. Te pierwsze otrzymujemy, gdy definiujemy klasy gołe, nie dziedziczące po niczym, lub klasy dziedziczące po klasach starego typu. Klasy starego typu są reliktem historii i pochodzą z czasów, gdy klasy definiowane przez użytkownika tworzyły zupełnie oddzielną hierarchię od typów macierzystych języka. Ta dychotomia jest od dawna zniesiona, ale oba typy klas różnią się nieco zachowaniem, o czym poniżej, i ze względu na potrzebę zachowania poprawności działania istniejącego kodu, można definiować klasy zarówno starego jak i nowego typu. W Pythonie 3 klasy starego typu już nie istnieją. Z tego względu w Pythonie 3, niezależnie od tego, czy się w jawny sposób zadeklaruje dziedziczenie po object, czy też nie, takie dziedziczenie jest automatyczne i zawsze otrzymuje się klasę nowego typu.

W przypadku klas starego i nowego typu, nieco inaczej zachowuje się operator super. Przy rekurencyjnych wywołaniach metody nadrzędnej, w klasach nowego typu przechodzimy wszystkich rodziców w pewnym porządku. W klasach starego typu możemy jednego rodzica odwiedzić więcej niż raz. [1] [2]

W klasach nowego typu możemy definiować tzw. deskryptory. Deskryptory pozwalają na definiowanie specjalnego typu atrybutów. Dostęp do atrybutów zdefiniowanych przez deskryptory do których dostęp wygląda tak samo jak dostęp do zwykłych statycznych pól, ale faktycznie powoduje wywołanie metody na deskryptorze. Użycie deskryptora pozwala np. na zastąpenie zwykłego pola przez parę funkcji {{ang|getter/{{ang|setter, czyli np. dodanie sprawdzania poprawności wartości wstawianej do pola, bez zmiany sposobu dostępu do tego pola. Bywa to niezwykle wygodne w przypadku przerabiania kodu. Ta funkcjonalność jest opisana tutaj.

Niemniej, deskryptory działają tylko w przypadku klas nowego typu. Podobnych drobnych niekompatybilności między klasami starego i nowego typu jest więcej. Generalnie, są one w miarę subtelne i naprawdę trudno jest je wszystkie pamiętać. Oznacza to, że jeśli nie chcemy mieć niespodzianek, powinniśmy zacisnąć zęby i zawsze dopisywać te osiem znaków potrzebnych po to, by definiować klasy nowego typu. Jako dodatkowy bonus, nasz kod jest znacznie łatwiej przerobić na Pythona 3, gdzie klasy nowego typu nazywają się po prostu klasami.


Przestrzenie nazw

Zastanówmy się, czy możemy mieć więcej niż jedną zmienną o tej samej nazwie? Gdyby pod tą samą nazwą kryły się dwie różne rzeczy, to mielibyśmy problem. Z tego względu nasuwa się odpowiedz negatywna. Niemniej, żądanie, by nigdy nazw nie powtarzać, idzie bardzo daleko. Jeśli mamy dwie funkcje, i jeśli użyjemy zmiennej n w obydwu, to nie powinno być problemów. Chcielibyśmy móc odpowiedzieć 'tak!'.

Rozwiązaniem problemu są przestrzenie nazw. Nazwy nie mogą się powtarzać wewnątrz jednej przestrzeni nazw, ale to ograniczenie nie dotyczy różnych przestrzeni nazw. Czym dokładnie są przestrzenie nazw i w jaki sposób i kiedy owe tworzymy?

Przestrzeń nazw {{ang|namespace tworzy po prostu odwzorowanie z nazw do obiektów. Jak każde odwzorowanie, również to oznacza, że jeśli mamy jakąś nazwę w naszym wyjściowym zbiorze zdefiniowanych nazw, to mamy jeden (i dokładnie jeden) odpowiadający jej obiekt.

W Pythonie przestrzenie nazw są zaimplementowane po prostu jako słowniki, w których kluczami są napisy --- nazwy zmiennych --- a wartościami obiekty. Jak wiemy, klucze w słowniku nie mogą się powtarzać (czyli mamy jednoznaczność odzworowania) i jeśli jakiś klucz się w słowniku znajduje, to możemy uzyskać dostęp do odpowiadającej mu wartości (czyli odwzorowanie jest zapamiętane jako zestaw par klucz:wartość). Każda rzecz, która ma własną przestrzeń nazw, ma przypisany słownik, do którego interpreter zagląda szukając zmiennych.

Każdy moduł ma swoją przestrzeń nazw. Każda klasa ma oddzielną przestrzeń nazw. Każdy obiekt ma swoją własną przestrzeń nazw. Każde wywołanie funkcji tworzy nową przestrzeń nazw.

Przestrzeń nazw w funkcji

Każda funkcja posiada własną przestrzeń nazw, nazywaną lokalną przestrzenią nazw, a która śledzi zmienne funkcji, włączając w to jej argumenty i lokalnie zdefiniowane zmienne. W momencie kiedy funkcja zaczyna wykonywanie, to w tej lokalnej przestrzeni nazw znajdują się tylko parametry określone w nagłówku funkcji. W miarę wykonywania funkcji, lokalnie zdefiniowane zmienne są dopisywane do tej przestrzeni nazw. Wraz z zakończeniem wykonywania funkcji ta przestrzeń nazw znika.

Dostęp do najbardziej wewnętrzej przestrzeni nazw można uzyskać (pomijając bezpośrednie użycie zmiennych), poprzez funkcję locals. Funkcja ta zwraca słownik, który jest właśnie tym słownikiem który pamięta co lokalna przestrzeń nazw zawiera.

Przestrzeń nazw w module

Każdy moduł, czyli każdy pythonowy plik, posiada własną przestrzeń nazw. Na początku jest ona pusta, a w miarę wykonywania poleceń w pliku (czyli wczytywania definicji klas czy funkcji, a nie wykonywania tych funkcji!) odpowiednie nazwy są do niej dopisywane. Ostatecznie znajdują się w niej funkcje, klasy i inne zaimportowane moduły, a także zmienne zdefiniowane na poziomie modułu.

Przestrzeń nazw w module jest nazywana globalną przestrzenią nazw, bo nazwy w niej zdefiniowane są widoczne również w funkcjach i klasach zdefiniowanych wewnątrz, o czym za chwilę.

Dostęp do przestrzeni nazw modułu można uzyskać poprzez funkcję globals.

Przestrzeń nazw w klasie i w obiekcie

Rozpoczęcie definicji klasy słowem class, tworzy również nową przestrzeń nazw. W zasadzie jest to przestrzeń nazw jak każda inna, i mogą się w niej znaleźć dowolne rzeczy, ale w praktyce są to głównie funkcje, a rzadziej zmienne klasowe, czyli zmienne wspólne dla wszystkich obiektów tej klasy.

Po stworzeniu obiektu danej klasy, tworzymy również nową przestrzeń nazw dla tego indywidualnego obiektu. Żyje ona tak samo długo jak ten obiekt, i zostaje zniszczona razem z nim.

Zagnieżdżenie przestrzeni nazw

Gdybyśmy będąc w obrębie jednej przestrzeni nazw, na przykład w trakcie wykonywania funkcji, nie mogli w żaden sposób odwołać się do zmiennych zdefiniowanych w innych przestrzeniach nazw, na przykład dla modułu, to programowanie byłoby znacznie utrudnione. Okazuje się, że możemy uzyskać dostęp do innych przestrzeni nazw na dwa sposoby.

Pierwszy z tych dwóch sposobów to automatyczne przeszukiwanie kilku przestrzeni nazw po kolei. W przypadku gdy zażądamy dostępu do obiektu pod pewną nazwą która nie istnieje w naszej lokalnej przestrzeni nazw, to interpreter poszuka nazwy w kilku innych miejscach.

Przeszukiwane przestrzenie nazw w znacznej mierze odpowiadają porządkowi w jakim definiowane są moduły i funkcje w plikach. Jeśli funkcja jest zdefiniowana w jakimś module, to jej przestrzeń nazw jest zagnieżdzona w przestrzeni nazw modułu. Oznacza to, że jeśli użyjemy w tej funkcji nazwy która nie jest zdefiniowana w tej funkcji (albo jako zwykła zmienna, albo jako parametr), to również moduł zostanie sprawdzony pod kątem tej nazwy.

Jeśli jakaś nazwa nie jest zdefiniowana ani w funkcji (czyli lokalnej przestrzeni nazw), ani w zawierającym tę funkcję module (czyli globalnej przestrzeni nazw), to sprawdzana jest jeszcze jedna przestrzeń nazw, a dokładnie jeszcze jeden moduł. Jest to moduł __builtins__, który zawiera wszystkie nazwy które są normalnie dostępne, takie jakie len, range, int, str, Exception, ...

Drugim sposobem na dostęp do różnych przestrzeni nazw jest bezpośrednie się do nich odwołanie przez użycie kropki, czyli tzw. notacji obiektowej. Jeśli np. mamy jakiś obiekt, to pisząc nazwa-obiektu -- kropka -- nazwa-zmiennej, szukamy tej zmiennej w zmiennych zdefiniowanych dla tego obiektu. Oznacza to, że sprawdzamy zarówno słownik nazw dla tego obiektu, jak i dla jego klasy.


Zadanie 1

Wyświetl w interpretrze globalną i lokalną przestrzeń nazw.


Zadanie 2

Za pomocą funkcji dir() wyświetl przestrzeń nazw modułu numpy.


Zadanie 3

Dopisz zmienną xxx do lokalnej przestrzeni nazw poprzez operację bezpośrednio na słowniku zwracanym przez locals. Dopisz zmienną yyy do globalnej przestrzeni nazw poprzez operację bezpośrednio na słowniku zwracanym przez globals. Sprawdź, czy masz dostęp do zmiennych xxx i yyy.


Zadanie 4

Zadeklaruj funkcję foo, która bierze jeden parametr (o nazwie arg) i w której zainicjowana zostanie zmienna x o wartości 1. W funkcji wypisz zawartość globalnej i lokalnej przestrzeni nazw.


Abstrakcyjne spojrzenie na programowanie obiektowe

Obiekty

Abstrakcyjną ideą programowania obiektowego jest powiązanie stanu (danych, które określane są zwykle polami) i zachowania (algorytmy związane ze stanem i całym obiektem, określane słowem metody). Program korzystający z obiektowości wyrażony jest jako zbiór takich obiektów, komunikujących się pomiędzy sobą w celu wykonywania zadań. W pewien sposób jest to najbardziej intuicyjne podejście do rozwiązywania problemów, bo w taki sposób — traktując zagadnienia jako obiekty wraz ze stanem i metodami; do rozwiązywania problemów podchodzi ludzki mózg. Typy obiektów nazywamy klasami.

Klasa umożliwia zdefiniowanie tego "jak działa pies" (Rys. Figure 1) i wykorzystania tego w przypadku innych psów. Zresztą mówiąć "Burek jest psem", mówimy tak naprawdę, że "Obiekt Burek jest instancją klasy pies".

Enkapsulacja (Kapsułkowanie bądź hermatyzacja)

Enkapsulacja polega na tym, że szczegóły implementacji są ukryte. Dzięki temu obiekt nie może zmieniać stanu wewnętrznego innych obiektów w nieoczekiwany sposób. Tylko wewnętrzne metody danego obiektu są uprawnione do zmiany jego stanu. Każdy typ obiektów ma swój interfejs, który określa dopuszczalne metody współpracy.

Jesteśmy jakimś obiektem, dla ustalenia A. Widzimy drugi obiekt, B. Obiekt B wie o sobie wszystko, my wiemy o nim tylko tyle, ile on nam udostępnia. W szczególności nie mamy dostępu do wielu zmiennych tego obiektu, możemy natomiast go "poprosić" żeby coś zrobił z tymi zmiennymi, lub podał nam ich wartość, wywołująć metodę, jaką obiekt nam udostępnia.

Analogia z życia:

Idziemy do apteki, chcemy kupić jakiś lek — nie bierzemy go z półki sami, tylko wywołujemy określoną metodę obiektu Apteka, prosząc Panią Sprzedawczynię, aby ten lek nam podała. Nie interesuje nas w jaki sposób ona to zrealizuje — tzn czy będzie np. musiała poczukać go w magazynie, czy też wejść na stołek bo lek stoi na górnej półce. My tego sami robić nie musimy, to już nie nasz problem, leży to w gestii drugiego obiektu.

Dziedziczenie

Tworzenie nowej klasy na bazie klasy już istniejącej. Dziedziczenie korzysta z klasy, a także rozszerza ją. W przypadku zmiany funkcjonalności część metod można nadpisać nowymi.

Tak, jak na Rys. Figure 1 klasy pies i owca dziedziczą po klasie Ssak. Mają na nowo zdefiniowane metody związane z dźwiękiem, który wydają, z szybkością biegania, a także odziedziczone i niezmienione metody zwracające kolor obiektu.

Zasada podstawienia Liskov

(wersja potoczna)

To, co działa dla klasy ssak powinno także działać dla klasy pies.

Polimorfizm

Zapewń powszechny sposób użycia dla różnych klas z odpowiednim zachowaniem dla danej klasy.

Przykłady:

  • operatory matematyczne (np. +, - itp.)
  • funkcja len(), którą można użyć i dla list i dla tablic

Abstrakcja

Przykład

Spróbujmy opisać kawałek otaczającej nas rzeczywistości. W naszym otocze Jesteśmy na uczelni. Na uczelni jest dużo studentów. Wszyscy studenci mają coś takiego jak Imię, Nazwisko oraz nr indeksu. Opiszmy to w pythonie:

class Student:
	''' '''
        znizka_na_pkp = 50 # zmienna klasowa, wspolna dla wszystkich obiektow klasy Student
	def __init__(self, imie, nazwisko, nr_indeksu): # znaczenie specjalnego słówka self poznamy za chwilę, każda klasa musi zawierać specjalną metodę __init__() która jest zwana konstruktorem, służy do tworzenia obiektów danej klasy. 
                # zmienne obiektow, przybierajace rozne wartosci dla konkretnych instancji
		self.__imie = name # pole "prywatne"
		self.__nazwisko = nazwisko 
		self.nr_indeksu = nr_indeksu # pole "publiczne"

	def lubisz_studia(self):
		print "no nie wiem"

        def przedstaw_sie(self):
		print "Nazywam się ", self.__imie, self.__nazwisko

        def zmien_nazwisko(self, nazwisko):
                self.__nazwisko = nazwisko

Spróbujmy teraz opisać Dobrego Studenta. Dobry student, tak samo jak każdy inny, ma Imie, Nazwisko oraz nr_indeksu. Czym się wyróżnia dobry student spośród pozostałych studentów? Dla uproszczenia przyjmijmy, że dobry student ma stypendium. Dodatkowo, dobry student na pytanie czy lubi studia po chwili zastanowienia odpowie, że jednak tak. Zobaczmy jak to będzie wyglądało w pythonie:

class DobryStudent(Student): # klasa dobry student dziedziczy po klasie Student, bo jest jej uszczegółowieniem
	def __init__(self, imie, nazwisko, nr_indeksu, stypendium):
		Student.__init__(self, imie, nazwisko, nr_indeksu) # pierw tworzymy zwykłego studenta
		self.stypendium = stypendium # potem wzbogacamy go o stypendium

	def lubisz_studia(self):
		Student.lubisz_studia(self) # zastanawia się chwilę, jak każdy student
		print "raczej tak" # ale dodaje, że lubi studia

Teraz spróbujmy opisać Złego Studenta. Dla uproszczenia niech zły student to będzie taki, który powtarza rok.

class ZlyStudent(Student):
	def __init__(self, imie, nazwisko, nr_indeksu, aktualnie_powtarzany_rok):
		Student.__init__(self, imie, nazwisko, nr_indeksu)
		self.aktualnie_powtarzany_rok = aktualnie_powtarzany_rok

	def lubisz_studia(self):
		Student.lubisz_studia(self) # wacha się chwilę jak każdy student
		print "studia sa do chrzanu" # w przeciwieństwie do dobrego studenta odpowiada, że nie lubi studiów

To pobawmy się teraz chwilę tymi obiektami:


s = Student("jan", "kowalski", "222")
print s.__imie
# widzimy, że takie odwołanie nie jest możliwe, gdyż umowilismy sie, ze __imie jest zmienna prywatna 
# i nie chcemy, zeby ktos je zmienial poza klasa
s.przedstaw_sie()
print s.nr_indeksu
s.nr_indeksu = 111222
s.__nazwisko = "Kowalski"

Teraz zobaczmy, gdzie tu są podstawowe cechy programowania obiektowego:

  • Enkapsulacja: nie odwołujemy się sami bezpośrednio do pól imie, nazwisko, tylko prosimy studenta, żeby się przedstawił.
  • Dziedziczenie: DobrzyStudenci i ŹliStudenci są uszczegółowieniem Studenta.
  • Polimorfizm
l = []
l.append(DobryStudent("jan", "kowalski", "222"))
l.append(ZlyStudent("zly", "student", "111"))
for s in l:
 s.lubisz_studia()

Utworzylismy listę studentów — przemiszanych dobrych i złych. Mimo że nie wiemy do jakiego w danej chwili studenta trzymamy referencję, jeśli wywołamy dowolną metodę, jaką ten student ma zaimplementowaną, to wywoła się właśnie ta metoda.

Uwaga dla programujących w innych językach

W Pythonie polimorfizm jest tak naturalny, że nawet się tego nie zauważa. W językach silnie typowanych przedstawia się trochę bardziej skomplikowanie i jest silnie związany z dziedziczeniem. W Javie, żeby zilustrować polimorfizm, musięlibysmy tworzyć referencje do obiektów klasy Student, i wskazywać tą referencją na obiekt klasy DobryStudent/ ZlyStudent i cieszylibyśmy się, że wywołanie metody lubisz_studia wywoluje metodę obiektu klasy DobryStudent/ZlyStudent a nie nadklasy, czyli Student. W C++ musięlibysmy metodę lubisz_studia zadeklarować jako wirtualną, żeby osiągnąć ten efekt. W Pythonie wszystkie metody zachowują się jak wirtualne.