Programowanie zorientowane obiektowo

Wprowadzenie

Wszystkie programy, jakie dotąd napisaliśmy, budowaliśmy wokół funkcji, czyli prostych bloków poleceń, które przetwarzały dane. Na tym właśnie polega technika programowania proceduralnego. Istnieją również inne paradygmaty programowania, a nas interesuje konkretnie programowanie obiektowo orientowane. Pomysł w skrócie polega na tym, aby powiązać ściśle ze sobą dane i zachowania i zamknąć je w czymś, co nazywamy obiektem. Zazwyczaj wystarczy programować w sposób proceduralny, ale podczas pisania dużych programów wygodniej jest skorzystać z techniki programowania obiektowo orientowanego. Zapewne nieraz spotkasz się z sytuacjami (nie muszą to być nawet duże programy), gdzie zastosowanie tej techniki samo będzie się nasuwało.

Dwoma podstawowymi aspektami programowania orientowanego obiektowo są klasy i obiekty. Klasa tworzy nowy typ, podczas gdy obiektyinstancjami danej klasy. Analogicznie możesz utworzyć zmienne typu całkowitego int, które należy rozumieć jako konkretne instancje (obiekty) klasy int.

Uwaga dla programistów języków statycznych
Zauważ, że nawet liczby całkowite są traktowane jako obiekty (klasy int). Python różni się tym np. od C++ czy Javy (przed wersją 1.5), gdzie liczby całkowite są prymitywnymi typami wbudowanymi. W poszukiwaniu większej ilości informacji na temat klasy int, przeczytaj help(int).

Obiekty mogą przechowywać dane w zwykłych zmiennych, które należą do obiektu. Zmienne należące do obiektu lub klasy nazywamy polami. Obiekty mogą posiadać również zachowania, które są realizowane przez funkcje należące do danej klasy. Funkcje takie nazywami metodami klasy. Taka terminologia jest o tyle ważna, że pomaga rozróżnić niezależne funkcje i zmienne od tych należących do jakichś klas czy obiektów. Metody oraz pola klasy nazywamy jej atrybutami.

Rozróżniamy pola dwóch typów: mogą one należeć do każdego obiektu klasy z osobna (zmienne instancji) lub do całej klasy (zmienne klasy).

Klasę tworzymy za pomocą słowa kluczowego class. Pola i metody klasy są wypisane wewnątrz przeznaczonego dla nich bloku.

Adres zwrotny

Metody klas różnią się od zwykłych funkcji tylko jednym detalem — muszą mieć dodatkową zmienną umieszczaną na początku listy parametrów, której jednak nie przypisuje się żadnej wartości podczas wywoływania metody — Python sam ją podaje. Ta konkretna zmienna odnosi się do samego obiektu i, zgodnie z konwencją, nazywana jest self.

Mimo że możesz użyć dowolnej nazwy dla tego parametru, zalecane jest, by używać nazwy self — każda inna nazwa jest niemile widziana. Używanie standardowej nazwy niesie ze sobą wiele zalet — każda osoba czytająca Twój kod od razu się w nim połapie, a specjalistyczne środowiska programistyczne (IDE — Integrated Development Environment) mogą Ci dodatkowo pomóc, jeśli używasz self.

Uwaga dla programujących w C++/Javie/C#
Pythonowy self jest równoważny wskaźnikowi self w C++ i referencji this w Javie i C#.

Pewnie zastanawiasz się, jak Python ustala wartość self i dlaczego sam nie musisz jej podawać. Najlepiej wyjaśni do przykład. Załóżmy, że masz klasę MojaKlasa oraz instancję tej klasy o nazwie mojobiekt. Gdy wywołujesz metodę tego obiektu jako mojobiekt.metoda(arg1, arg2), Python automatycznie tłumaczy to na MojaKlasa.metoda(mojobiekt, arg1, arg2) — właśnie po to jest self.

Oznacza to także, że jeśli napiszesz metodę, która nie będzie przyjmowała żadnych argumentów, to i tak musisz ją zdefiniować jako przyjmującą jeden argument — adres zwrotny self.

Klasy

Oto najprostsza możliwa klasa:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: najprostszaklasa.py

class Osoba:
    pass # Pusty blok

o = Osoba()
print o

Rezultat:

$ python najprostszaklasa.py
<__main__.Osoba instance at 0xb7cb542c>

Jak to działa?

Nową klasę tworzymy za pomocą polecenia class i nazwy klasy. Potem następuje wcięty blok poleceń, które składają się na ciało klasy. W naszym przypadku blok ten jest pusty, co sygnalizujemy poleceniem pass.

Następnie tworzymy obiekt tej klasy przez użycie nazwy klasy i pary okrągłych nawiasów. (Dokładniej o tworzeniu obiektów klas porozmawiamy w jedenj z następnych sekcji). Dla sprawdzenia potwierdzamy typ zmiennej przez wypisanie jej na ekran. Python mówi nam, że mamy instancję klasy Osoba w module __main__.

Zauważ, że jest wypisywany także adres w pamięci komputera, gdzie ten obiekt jest przechowywany. Oczywiście jeśli sam uruchomisz program, adres ten będzie inny, ponieważ Python może przechowywać obiekty, gdzie tylko znajdzie na nie miejsce.

Metody obiektowe

Powiedzieliśmy już sobie, że klas/obiekty mogą posiadać metody, które są niczym innym, jak funkcjami z dodatkowym argumentem self. Teraz zobaczymy przykład takiej metody.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: metoda.py

class Osoba:
    def przywitajSie(self):
        print 'Witaj, jak się masz?'

o = Osoba()
o.przywitajSie()

# Ten krótki przykład można także zapisać jako Osoba().przywitajSie()

Rezultat:

$ python metoda.py
Witaj, jak się masz?

Jak to działa?

W tym przykładzie widzimy adres zwrotny self w akcji. Zauważ, że metoda przywitajSie nie przyjmuje żadnych argumentów, a mimo to w jej definicji widnieje self.

Metoda __init__

Istnieje wiele nazw metod, które mają specjalne znaczenie dla klas w Pythonie. Teraz zobaczymy działanie jednej z tych metod: __init__.

Metoda __init__ jest wywoływana w momencie, kiedy tworzony jest obiekt danej klasy. Jest ona przydatna, kiedy chcesz zainicjalizować obiekt w jakiś sposób. Zauważ podwójne podkreślniki na początku i na końcu nazwy.

Przykład:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: klasa_init.py

class Osoba:
    def __init__(self, imie):
        self.imie = imie
    def przywitajSie(self):
        print 'Witaj, mam na imię', self.imie

o = Osoba('Swaroop')
o.przywitajSie()

# Ten krótki przykład można także zapisać jako Osoba('Swaroop').przywitajSie()

Rezultat:

$ python klasa_init.py
Witaj, mam na imię Swaroop

Jak to działa?

Definiujemy metodę __init__, jako przyjmującą parametr imie (oprócz obowiązkowego self). Tworzymy w niej także pole o nazwie imie. Zauważ, że są to dwie różne zmienne, mimo że mają tę samą nazwę. Użycie notacji z kropką umożliwia nam rozróżnienie ich od siebie.

Najważniejsze w tym przykładzie jest jednak to, że nie wywołujemy jawnie metody __init__, ale tylko podczas tworzenia nowego obiektu podajemy argumenty owej metody w nawiasach po nazwie klasy. Na tym właśnie polega specjalne znaczenie metody __init__.

Dzięki przeprowadzeniu inicjalizacji naszego obiektu, odtąd możemy używać pola self.imie w metodach tej klasy, jak zrobiliśmy to w metodzie przywitajSie.

Zmienne klas i obiektów

Omówiliśmy już zachowania klas i obiektów (czyli metody), teraz porozmawiamy więc o danych, jakie mogą one przechowywać. Dane te, czyli pola, są niczym innym jak zwykłymi zmiennymi przywiązanymi do przestrzeni nazw danej klasy. Oznacza to, że możemy używać ich nazw tylko w kontekście danej klasy czy obiektu.

Istnieją dwa typy pól — zmienne klas i zmienne obiektów, które rozróżniamy po tym, czy dana zmienna należy do całej klasy, czy też do poszczególnych obiektów.

Zmienne klasy są dzielone, co oznacza, że są dostępne dla wszystkich instancji danej klasy. Istnieje tylko jedna kopia zmiennej klasy, czyli jeśli jeden obiekt zmieni w jakiś sposób tę zmienną, to zmiana ta będzie widziana również przez wszystkie pozostałe instancje.

Zmienne obiektów należą do poszczególnych obiektów danej klasy. Oznacza to, że każdy obiekt posiada własną kopię takiej zmiennej, czyli nie są one dzielone ani w żadnej sposób powiązane ze sobą w różnych instancjach danej klasy. Następujący przykład pomoże Ci to zrozumieć.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: zmobj.py

class Robot:
    u'''Reprezentuje robota, z nazwą.'''

    # Zmienna klasy pokazująca liczbę robotów
    populacja = 0

    def __init__(self, nazwa):
        u'''Inicjalizuje dane.'''
        self.nazwa = nazwa
        print '(Inicjalizacja %s)' % self.nazwa

        # Kiedy nowy robot jest tworzony, zwiększamy
        # licznik populacji o 1
        Robot.populacja += 1

    def __del__(self):
        u'''Umieram.'''
        print '%s jest niszczony!' % self.nazwa

        Robot.populacja -= 1

        if Robot.populacja == 0:
            print '%s był ostatnim robotem.' % self.nazwa
        else:
            print 'Postały %d roboty.' % Robot.populacja

    def przywitajSie(self):
        u'''Powitanie robota.

        Tak, one naprawdę to potrafią.'''
        print 'Melduję się, moi panowie nazywają mnie %s.' % self.nazwa

    def jakDuzo():
        u'''Wypisuje aktualną populację.'''
        print 'Mamy %d roboty.' % Robot.populacja
    jakDuzo = staticmethod(jakDuzo)

droid1 = Robot('R2-D2')
droid1.przywitajSie()
Robot.jakDuzo()

droid2 = Robot('C-3PO')
droid2.przywitajSie()
Robot.jakDuzo()

print "\nUmówmy się, że roboty wykonują tutaj jakąś pracę.\n"

print "Roboty zakończyły swoje zadania, więc możemy się ich pozbyć."
del droid1
del droid2

Robot.jakDuzo()

Rezultat:

$ python zmobj.py
(Inicjalizacja R2-D2)
Melduję się, moi panowie nazywają mnie R2-D2.
Mamy 1 roboty.
(Inicjalizacja C-3PO)
Melduję się, moi panowie nazywają mnie C-3PO.
Mamy 2 roboty.

Umówmy się, że roboty wykonują tutaj jakąś pracę.

Roboty zakończyły swoje zadania, więc możemy się ich pozbyć.
R2-D2 jest niszczony!
Postały 1 roboty.
C-3PO jest niszczony!
C-3PO był ostatnim robotem.
Mamy 0 roboty.

Jak to działa?

To był dosyć długi przykład, ale z pewnością pomógł Ci w zrozumieniu natury zmiennych klasy i obiektów. W naszym przykładzie populacja należy do klasy Robot, więc jest zmienną klasy. Zmienna nazwa należy do obiektu (jest to zaznaczone przez użycie self), więc jest zmienną obiektu.

Tak więc do zmiennej populacja, która jest zmienną klasy, odnosimy się przez Robot.populacja, a nie przez self.populacja. Natomiast do zmiennej obiektu nazwa odnosimy się w metodach tego obiektu przez self.nazwa. Zapamiętaj tę różnicę pomiędzy zmiennymi klas i zmiennymi obiektów. Zapamiętaj też, że zmienna obiektu o takiej samej nazwie, jak zmienna klasy, zasłoni zmienną klasy!

Metoda jakDuzo jest przykładem metody należącej do klasy, a nie do obiektu (nie posiada self). Oznacza to, że możemy zdefiniować ją jako classmethod lub staticmethod w zależności od tego, czy chcemy zachować informację, do jakiej klasy ta metoda należy. Ponieważ nie potrzebujemy w naszym programie takiej informacji, użyjemy staticmethod.

Dokładnie to samo możemy osiągnąć za pomocą dekoratorów:

@staticmethod
def jakDuzo():
    u'''Wypisuje aktualną populację.'''
    print 'Mamy %d robotów.' % Robot.populacja

Dekoratory można sobie wyobrazić jako skróty do wywołania jawnego polecenia, jak to widzieliśmy w przykładzie.

Zauważ, że metoda __init__ inicjalizuje instancję klasy Robot nazwą podaną przez argument. Zwiększa ona także o 1 licznik populacja. Zauważ też, że wartość self.name jest inna dla każdego obiektu, ponieważ jest to zmienna obiektu.

Zapamiętaj, że do metod i pól tego samego obiektu możesz odnosić wyłącznie za pomocą self.

W tym programie zobaczyliśmy także użycie docstringów dla klas i metod. Możemy dostać się do docstringa klasy podczas działania programu przez Robot.__doc__, a do docstringa metody przez Robot.przywitajSie.__doc__.

Jak wspomniałem, oprócz __init__, istnieją również inne specjalne metody. Jedną z nich jest __del__, która jest wywoływana w momencie, w którym obiekt umiera, to znaczy kiedy już nigdy nie będzie użyty i zostaje zwrócony systemowi, aby ten mógł zwolnić zajmowaną przezeń pamięć. W tej metodzie po prostu zmniejszamy licznik Robot.populacja o 1.

Metoda __del__ jest uruchamiana, kiedy obiekt już nie jest w użyciu, ale nie ma żadnej gwarancji, kiedy to nastąpi. Jeśli chcesz jawnie ją wywołać, możesz użyć polecenia del, jak to zrobiliśmy w naszym przykładzie.

Uwaga dla programistów C++/Javy/C#
W Pythonie wszystkie składniki klas (włączając w to pola) są publiczne, a wszystkie metody są wirtualne. Istnieje od tego jeden wyjątek: Jeśli nazwiesz jakieś pole używając jako prefiksu dwa podkreślniki (np. __zmiennaprywatna), Python zrobi z tego zmienną prywatną. Tak więc, zgodnie z konwencją, jeśli jakaś zmienna ma być używana tylko w obrębie danej klasy czy obiektu, powinna zaczynać się od dwóch podkreślników, a pozostałe nazwy są publiczne i inne klasy i obiekty mogą się do nich dostać. Pamiętaj jednak, że to tylko konwencja i Python w żaden sposób jej nie wymusza (za wyjątkiem działania dwóch podkreślników).

Dziedziczenie

Jedną z najważniejszych korzyści wynikających z programowania obiektowo orientowanego jest możliwość ponownego wykorzystania już raz napisanego kodu. Osiągamy to poprzez mechanizm dziedziczenia. Dziedziczenie można sobie łatwo wyobrazić jako tworzenie typów pochodnych od istniejących klas.

Załóżmy, że musisz napisać program, który będzie zbierał dane na temat wykładowców i studentów na uczelni. Mają oni pewne wspólne cechy, jak imię, wiek, czy adres. Mają także cechy specyficzne, jak pensja, szkolenia i urlopy dla wykładowców oraz oceny i czesne dla studentów.

Możesz oczywiście stworzyć dwie niezależne klasy dla każdego typu, ale wtedy będziesz musiał zawrzeć cechy wspólne w ciałach obu tych klas. Takie postępowanie szybko okaże się nieporęczne.

O wiele lepiej byłoby stworzyć ogólną klasę SchoolMember, z której klasy dla wykładowców i studentów by dziedziczyły, czyli stałyby się typami pochodnymi od tego typu (klasy), do których moglibyśmy dodać specyficzne dla nich dane i zachowania.

Jest wiele zalet takiego podejścia. Jeśli dodamy lub zmodyfikujemy jakąś funkcjonalność w klasie SchoolMember, zmiany te zostaną oczywiście automatycznie uwzględnione przez klasy pochodne. Można na przykład wyobrazić sobie taką sytuację, w której na uczelni wprowadzono wspólne dla wykładowców i studentów karty identyfikacyjne. Wtedy wystarczy, że dodasz odpowiednie pole z numerem karty do klasy SchoolMember, dzięki czemu otrzymają je zarówno wykładowcy, jak i studenci. W przeciwieństwie do zmian w typach podstawowych, zmiany w typach pochodnych nie wpływają w żaden sposób na inne typy pochodne.

Kolejną zaletą jest fakt, że możesz traktować obiekty studentów i wykładowców wspólnie jako obiekty klasy SchoolMember, co może się okazać bardzo użyteczne w pewnych sytuacjach, na przykład podczas liczenia wszystkich ludzi związanych z uczelnią. Taka właściwość klas nazywa się polimorfizmem. Polimorfizm w skrócie polega na tym, że zawsze, kiedy oczekiwany jest typ podstawowy, możesz zamiast niego podstawić typ pochodny od danego typu podstawowego, czyli dowolny obiekt może być traktowany jak instancja jego klasy podstawowej.

Zauważ też, że wielokrotnie wykorzystujemy kod zawarty w klasie podstawowej, podczas gdy bez zastosowania mechanizmu dziedziczenia musielibyśmy pisać ten kod dla każdej klasy osobno.

Klasa SchoolMember jest w naszym przypadku klasą podstawową albo superklasą. Klasy Wykladowca i Studentklasami pochodnymi albo podklasami.

Zobaczmy teraz, jak wygląda wyżej opisany przykład zapisany jako program w Pythonie.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Nazwa pliku: dziedziczenie.py

class SchoolMember:
    u'''Reprezentuje człowieka związanego z uczelnią.'''
    def __init__(self, imie, wiek):
        self.imie = imie
        self.wiek = wiek
        print '(Inicjalizacja SchoolMember: %s)' % self.imie

    def powiedz(self):
        u'''Opowiedz o sobie.'''
        print 'Imię:"%s" Wiek:"%s"' % (self.imie, self.wiek),

class Wykladowca(SchoolMember):
    u'''Reprezentuje wykładowcę.'''
    def __init__(self, imie, wiek, pensja):
        SchoolMember.__init__(self, imie, wiek)
        self.pensja = pensja
        print '(Inicjalizacja Wykladowcy: %s)' % self.imie

    def powiedz(self):
        SchoolMember.powiedz(self)
        print 'Pensja: "%d"' % self.pensja

class Student(SchoolMember):
    '''Reprezentuje studenta.'''
    def __init__(self, imie, wiek, oceny):
        SchoolMember.__init__(self, imie, wiek)
        self.oceny = oceny
        print '(Inicjalizacja Studenta: %s)' % self.imie

    def powiedz(self):
        SchoolMember.powiedz(self)
        print 'Oceny: "%d"' % self.oceny

w = Wykladowca('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)

print # wypisuje pustą linię

osoby = [w, s]
for osoba in osoby:
    osoba.powiedz() # działa zarówno dla Wykładowców, jak i Studentów

Rezultat:

$ python dziedziczenie.py
(Inicjalizacja SchoolMember: Mrs. Shrividya)
(Inicjalizacja Wykladowcy: Mrs. Shrividya)
(Inicjalizacja SchoolMember: Swaroop)
(Inicjalizacja Studenta: Swaroop)

Imię:"Mrs. Shrividya" Wiek:"40" Pensja: "30000"
Imię:"Swaroop" Wiek:"25" Oceny: "75"

Jak to działa?

Aby użyć dziedziczenia, w linijce, w której definiujemy klasę, za jej nazwą zamieszczamy w krotce nazwy klas, z których dziedziczymy. W innym miejscu w kodzie widzimy jawne wywołanie metody __init__ z klasy podstawowej, co było możliwe przez przekazanie jej self. Tym sposobem możemy małym nakładem pracy zainicjalizować tę część klasy pochodnej, która została odziedziczona z klasy podstawowej. Musisz tutaj zapamiętać jedną ważną rzecz: Python nie wywoła automatycznie konstruktora (metody __init__) klasy podstawowej w konstruktorze klasy pochodnej — musisz zrobić to samodzielnie.

Zobaczyliśmy w tym przykładzie, że wywoływanie metod z klasy podstawowej polega na poprzedzeniu ich nazwą klasy podstawowej i kropką oraz przekazaniu im zmiennej self jako pierwszy argument.

Zwróć uwagę na to, że możemy traktować instancje klas Wykladowca oraz Student jak instancje klasy SchoolMember, kiedy używamy metody powiedz z klasy SchoolMember. Zauważ jednak, że wywoływana jest nie metoda powiedz z klasy podstawowej, ale jej wersja z klasy pochodnej, której obiekt aktualnie kryje się pod maską klasy podstawowej. Dzieje się tak dlatego, że Python zawsze rozpoczyna szukanie metod od klasy, z której obiektem w danej chwili ma do czynienia. Dopiero kiedy nie znajdzie żądanej metody, szuka jej w klasach podstawowych w takiej kolejności, w jakiej zostały one zamieszczone w krotce w definicji klasy pochodnej.

Jeszcze jedna uwaga co do terminologii — jeśli w krotce z klasami dziedziczonymi znajduje się więcej niż jedna klasa, takie dziedziczenie nazywamy dziedziczenie wielokrotnym albo wielodziedziczeniem.

Podsumowanie

Poznaliśmy w tym rozdziale różne oblicza klas i obiektów oraz terminologię z nimi związaną. Zobaczyliśmy także, na czym polegają korzyści wynikające z programowania obiektowo orientowanego oraz pułapki z nim związane. Zapamiętaj, że Python jest językiem silnie zorientowanym obiektowo i zrozumienie tej techniki programowania jest kluczem do sukcesów w przyszłości.

Teraz zobaczymy, jak obchodzić się z plikami oraz operacjami wejścia–wyjścia w Pythonie.

results matching ""

    No results matching ""