Paradygmaty programowania
Paradygmat programowania to styl programowania. Istnieje wiele paradygmatów. Aby profesjonalnie zajmować się programowaniem, należy poznać bądź to paradygmat programowania funkcyjnego, bądź paradygmat programowania obiektowego.
Stan
Jedną z podstawowych różnic pomiędzy paradygmatami programowania jest sposób obsługi stanu. Stan to wartości zmiennych używanych w programie podczas jego działania. Stan globalny to wartości zmiennych globalnych przyjmowane podczas wykonywania programu.
Programowanie proceduralne
Kiedy stosujemy programowanie proceduralne, piszemy kod, który „najpierw robi to, a następnie coś innego”:
2
3 x = 2
4 y = 4
5 z = 8
6 xyz = x + y + z
7 xyz
>> 14
Każdy wiersz kodu w tym przykładzie zmienia stan programu. Najpierw definiujemy zmienną x, potem y, a potem z. W końcu definiujemy zmienną xyz. W przypadku programowania proceduralnego dane są przechowywane w zmiennych globalnych, a operacje na nich wykonywane przy użyciu funkcji:
2
3 rock = []
4 country = []
5
6
7 def collect_songs():
8 song = "Wpisz tytuł piosenki."
9 ask = "Naciśnij r lub c albo q, by wyjść."
10
11 while True:
12 genre = input(ask)
13 if genre == "q":
14 break
15
16 if genre == "r":
17 rk = input(song)
18 rock.append(rk)
19
20 elif genre ==("c"):
21 cy = input(song)
22 country.append(cy)
23
24 else:
25 print("Nieprawidłowe polecenie.")
26 print(rock)
27 print(country)
28
29 collect_songs()
>> Naciśnij r lub c albo q, by wyjść.
Programowanie proceduralne można stosować podczas pisania niewielkich programów, takich jak prezentowane w tej książce, jednak w czasie pisania dużych programów zaczynają pojawiać się problemy ze względu na to, że stan programu jest przechowywany w zmiennych globalnych. Problem ze stosowaniem zmiennych globalnych polega na tym, że mogą one powodować występowanie nieoczekiwanych błędów. Wraz z powiększaniem się programu zaczynamy używać zmiennych globalnych w coraz to większej liczbie funkcji w całym kodzie programu i niemal niemożliwe staje się śledzenie wszystkich miejsc, w których zmienne te mogą być modyfikowane. Przykładowo wartość zmiennej globalnej może być modyfikowana w jednej funkcji, a następnie, dalej w kodzie programu ta sama zmienna może być modyfikowana przez inną funkcję, gdyż programista zapomniał, że została ona zmieniona już wcześniej. Takie sytuacje występują bardzo często i mogą prowadzić do uszkodzenia danych programu.
Wraz ze wzrostem złożoności programu rośnie także liczba używanych w nim danych globalnych. Jeśli połączymy to ze zwiększaniem się ilości funkcji, niezbędnych do zapewniania nowych możliwości programu, z których wszystkie modyfikują jakieś zmienne globalne, to okaże się, że kod programu bardzo szybko stanie się niezwykle trudny do zarządzania i modyfikacji. Co więcej, ten styl programowania bazuje na efektach ubocznych. Efekt uboczny to zmiana stanu zmiennej globalnej. W programowaniu proceduralnym efekty uboczne, takie jak niezamierzona dwukrotna inkrementacja jakiejś zmiennej, mogą występować całkiem często. Wszystkie problemy związane z programowaniem proceduralnym doprowadziły do opracowania paradygmatów programowania obiektowego oraz funkcyjnego, a każdy z nich stara się rozwiązać te problemy w inny sposób.
Paradygmat programowania funkcyjnego
Programowanie funkcyjne wywodzi się od rachunku lambda: najmniejszego, uniwersalnego języka programowania na świecie (opracowanego przez matematyka Alonzo Churcha). Programowanie funkcyjne rozwiązuje problemy występujące w programowaniu proceduralnym poprzez
wyeliminowanie stanu globalnego. Programowanie funkcyjne bazuje na wykorzystaniu funkcji, które bądź to w ogóle nie używają, bądź też nie modyfikują stanu globalnego — jedynym stanem, z którego korzystają, jest stan przekazany do nich w formie parametrów. Wynik zwracany przez
funkcję jest zazwyczaj przekazywany do innej funkcji. Dzięki temu programiści korzystający z paradygmatu programowania funkcyjnego mogą unikać stosowania stanu globalnego poprzez przekazywanie go z jednej funkcji do drugiej. Wyeliminowanie stosowania stanu globalnego
wyklucza występowanie efektów ubocznych oraz wszystkich związanych z nimi problemów.
Programowanie funkcyjne cechuje się swoistym żargonem, lecz Mary Rose Cook odrzuca go całkowicie w swojej krótkiej definicji: „Kod funkcyjny charakteryzuje się jedną podstawową cechą: brakiem efektów ubocznych. W żadnym stopniu nie jest on zależy od jakichkolwiek danych spoza aktualnie wykonywanej funkcji, jak również nie modyfikuje żadnych danych spoza niej.
Definicja ta jest ilustrowana przykładem funkcji mającej efekty uboczne:
2
3 a = 0
4
5 def increment():
6 global a
7 a += 1
A to funkcja, w której efekty uboczne nie będą występować:
2
3 def increment(a):
4 return a + 1
W pierwszej z tych funkcji efekty uboczne występują, gdyż bazuje ona na danych spoza niej samej, jak również zmienia dane spoza swojego kodu — konkretnie rzecz biorąc, inkrementuje wartość zmiennej globalnej. Natomiast w przypadku drugiej funkcji efekty uboczne nie występują, gdyż ani
nie używa, ani nie modyfikuje danych spoza swojego kodu.
Ogromną zaletą programowania funkcyjnego jest to, że umożliwia ono wyeliminowanie całej kategorii błędów powodowanych przez stan globalny (w przypadku programowania funkcyjnego stan globalny nie istnieje). Jednak z drugiej strony, programowanie funkcyjne ma także swoją wadę: otóż okazuje się, że niektóre problemy łatwiej można sobie wyobrazić, posługując się stanem. Łatwiej na przykład można sobie wyobrazić projektowanie interfejsu użytkownika posiadającego stan globalny niż interfejsu, który będzie takiego stanu pozbawiony. Gdybyśmy mieli sobie wyobrazić program z przyciskiem, który na przemian wyświetla i ukrywa obrazek, znacznie łatwiej będzie nam wyobrazić sobie utworzenie takiego przycisku, gdy napiszemy program mający stan globalny. W takim przypadku moglibyśmy utworzyć zmienną globalną przyjmującą wartości True lub False, która — w zależności od swojej bieżącej wartości — będzie wyświetlać lub ukrywać obrazek. Wyobrażenie sobie przycisku bez takiego stanu globalnego jest znacznie trudniejsze.
Paradygmat programowania obiektowego
Także paradygmat programowania obiektowego eliminuje problemy występujące w programowaniu proceduralnym poprzez wyeliminowanie stanu globalnego, jednak zamiast przenoszenia tego stanu do funkcji zapisuje go w obiektach. W programowaniu obiektowym stosowane jest pojęcie klas, które definiują zestaw obiektów, a te z kolei mogą prowadzić ze sobą interakcje. Klasy są mechanizmem pozwalającym programistom na klasyfikację i grupowanie obiektów o podobnych cechach. Wyobraźmy sobie torbę pomarańczy. Każda z tych pomarańczy jest obiektem. Wszystkie pomarańcze mają te same atrybuty, takie jak kolor oraz waga, jednak wartości tych atrybutów mogą się zmieniać w zależności od konkretnego owocu. Możemy użyć klasy do opracowania ogólnego modelu pomarańczy, a następnie utworzyć obiekty tej klasy różniące się od siebie konkretnymi wartościami atrybutów. Możemy na przykład zdefiniować klasę, a następnie użyć jej do utworzenia obiektu pomarańczy limety o wadze 280 g, jak również obiektu pomarańczy olbrzymiej o wadzie 340 g.
Każdy obiekt jest instancją klasy. Jeśli zdefiniujemy klasę Orange i utworzymy dwa obiekty Orange, każdy z nich będzie instancją klasy Orange — oba będą miały ten sam typ, czyli Orange. Terminów obiekt oraz instancja można używać zamiennie. Po zdefiniowaniu klasy wszystkie jej instancje będą identyczne. Wszystkie będą mieć te same atrybuty zdefiniowane w klasie, na przykład dla klasy reprezentującej pomarańcze mogą to być kolor i waga — jednak w każdym z obiektów wartości tych atrybutów mogą być inne.
W języku Python klasa jest instrukcją złożoną, składającą się z nagłówka oraz zestawów. Do definiowania klas jest używana składnia class [nazwa]: [zestawy]; gdzie [nazwa] to nazwa klasy, a [zestawy] to definiowane przez nas zestawy klasy. Konwencja stosowana w języku Python nakazuje, by nazwy wszystkich klas zaczynały się od wielkiej litery, a w przypadku nazw składających się z kilku słów należy stosować notację CamelCase, czyli pierwsze litery poszczególnych słów trzeba zapisywać wielką literą (tak jak OtoPrzykład), a nie rozdzielać znakiem podkreślenia (jak nakazuje konwencja określająca postać nazw funkcji). Zestaw wchodzący w skład definicji klasy może być zwyczajną instrukcją bądź też instrukcją złożoną nazywaną metodą. Metody przypominają funkcje, jednak są definiowane wewnątrz klas i można je wywoływać wyłącznie na rzecz obiektów utworzonych przy użyciu danej klasy (jak robiliśmy wcześniej w tej książce, na przykład wywołując "Witaj".upper() na rzecz obiektu łańcucha znaków). Nazwy metod, podobnie jak nazwy funkcji, należy zapisywać wyłącznie małymi literami, a poszczególne słowa oddzielać od siebie znakami podkreślenia.
Metody są definiowane przy wykorzystaniu dokładnie takiej samej składni, jakiej używa się do tworzenia funkcji, choć występują pomiędzy nimi dwie różnice. Po pierwsze, metody mogą być definiowane wyłącznie jako zestawy w klasie, a po drugie, każda metoda musi mieć przynajmniej jeden parametr (choć i od tej reguły istnieją wyjątki). Zwyczajowo, pierwszy parametr metody zawsze ma nazwę self. To definiowanie przynajmniej jednego parametru w metodach wynika z faktu, że podczas ich wywoływania na rzecz jakiegoś obiektu Python automatycznie przekazuje ten obiekt do metody jako jej parametr:
2
3 class Orange:
4 def __init__(self):
5 print("Utworzono!")
Parametru self można użyć do definiowania zmiennych instancyjnych, czyli zmiennych należących do obiektu. Jeśli utworzymy więcej obiektów, każdy z nich może mieć inne wartości zmiennych instancyjnych. Zmienne instancyjne można tworzyć, używając składni self.[nazwa_zmiennej] =
[wartość_zmiennej]. Zmienne instancyjne zazwyczaj definiuje się wewnątrz specjalnej metody o nazwie __init__ (co stanowi skrót od słowa „inicjalizacja”), którą Python wywołuje podczas tworzenia obiektu.
Poniżej przedstawiony został przykład klasy reprezentującej pomarańczę:
2
3 class Orange:
4 def __init__(self, w, c):
5 self.weight = w
6 self.color = c
7 print("Utworzono!")
Kod metody __init__ jest wykonywany podczas opracowania obiektu Orange (co nie następuje w tym przykładzie) i tworzy dwie zmienne instancyjne, takie jak weight oraz color. Tych zmiennych można używać jak zwyczajnych zmiennych we wszystkich metodach danej klasy. Podczas tworzenia obiektu Orange kod umieszczony w metodzie __init__ wyświetla jeszcze komunikat Utworzono!. Wszystkie metody, których nazwy są umieszczone pomiędzy podwójnym znakami podkreślenia, tak jak __init__, są nazywane metodami magicznymi; są to metody, których Python używa w specjalnych celach, takich jak tworzenie obiektów.
Obiekt Orange można utworzyć, posługując się tym samym zapisem, który jest używany do wywoływania funkcji — [nazwa_klasy]([parametry]), przy czym wyrażenie [nazwa_klasy] należy zastąpić nazwą klasy, której obiekt chcemy utworzyć, a [parametry] — parametrami wymaganymi przez metodę __init__. Do tej metody nie trzeba przekazywać parametru self — Python zrobi to automatycznie. Tworzenie obiektu nazywamy także tworzeniem instancji klasy:
2
3 class Orange:
4 def __init__(self, w, c):
5 self.weight = w
6 self.color = c
7 print("Utworzono!")
8
9 or1 = Orange(280, "ciemnopomarańczowy")
10 print(or1)
>> Utworzono!
>> <__main__.Orange object at 0x101a787b8>
W tym przykładzie poniżej definicji klasy tworzymy instancję klasy Orange, używając wywołania w postaci Orange(410, "pomarańcza limeta"). W efekcie utworzenia instancji i wywołania metody __init__ zostaje wyświetlony komunikat Utworzono!. Następnie wyświetlamy sam utworzony obiekt — spowoduje to, że Python wyświetli informację, iż jest to obiekt Orange, oraz jego lokalizację w pamięci (jak można się spodziewać, przedstawiona w książce będzie inna od tej, którą będziesz mógł zobaczyć na ekranie swojego komputera). Po utworzeniu obiektu możemy pobrać wartości jego zmiennych instancyjnych; do tego celu służy składnia w postaci [nazwa_obiektu].[nazwa_zmiennej]:
2
3 class Orange:
4 def __init__(self, w, c):
5 self.weight = w
6 self.color = c
7 print("Utworzono!")
8
9 or1 = Orange(280, "ciemnopomarańczowy")
10 print(or1.weight)
11 print(or1.color)
>> Utworzono!
>> 280
>> ciemnopomarańczowy
Wartość zmiennej instancyjnej można zmienić, posługując się zapisem [nazwa_obiektu].[nazwa_zmiennej] = [nowa_wartość]:
2
3 class Orange:
4 def __init__(self, w, c):
5 self.weight = w
6 self.color = c
7 print("Utworzono!")
8
9 or1 = Orange(280, "ciemnopomarańczowy")
10 or1.weight = 650
11 or1.color = "jasnopomarańczowy"
12
13 print(or1.weight)
14 print(or1.color)
>> Utworzono!
>> 650
>> jasnopomarańczowy
Choć zmienne instancyjne color i weight miały początkowo wartości "ciemnopomarańczowy" i 280, to jednak zmieniliśmy je w programie na "jasnopomarańczowy" i 390. Klasy Orange można użyć do utworzenia wielu pomarańczy:
2
3 class Orange:
4 def __init__(self, w, c):
5 self.weight = w
6 self.color = c
7 print("Utworzono!")
8
9 or1 = Orange(120, "jasnopomarańczowy")
10 or2 = Orange(240, "ciemnopomarańczowy")
11 or3 = Orange(390, "żółty")
>> Utworzono!
>> Utworzono!
>> Utworzono!
Jednak pomarańcza to nie tylko jej cechy fizyczne, takie jak kolor i waga. Pomarańcze wykonują także pewne czynności, które można zamodelować przy użyciu metod; przykładem takiej czynności może być psucie się. Poniższy przykład pokazuje, w jaki sposób możemy zapewnić pomarańczy możliwość zepsucia się:
2
3 class Orange():
4 def __init__(self, w, c):
5 """waga jest podawana w gramach"""
6 self.weight = w
7 self.color = c
8 self.mold = 0
9 print("Utworzono!")
10
11 def rot(self, days, temp):
12 self.mold = days * temp
13
14 orange = Orange(170, "pomarańczowy")
15 print(orange.mold)
16 orange.rot(10, 29)
17 print(orange.mold)
>> Utworzono!
>> 0
>> 290
Metoda rot pobiera dwa parametry: liczbę dni od momentu zerwania owocu oraz średnią temperaturę w ciągu dnia. Po wywołaniu metoda ta używa określonego wzoru matematycznego, by zmienić wartość zmiennej instancyjnej mold, co jest możliwe, gdyż w dowolnej metodzie klasy można zmienić wartość dowolnej zmiennej instancyjnej. Teraz już pomarańcza może się psuć. W klasie można definiować dowolnie wiele metod. Poniżej przedstawiony został przykład klasy reprezentującej prostokąt wraz z metodą obliczającą jego powierzchnię oraz drugą — do zmiany jego wielkości:
2
3 class Rectangle():
4 def __init__(self, w, l):
5 self.width = w
6 self.len = l
7
8 def area(self):
9 return self.width * self.len
10
11 def change_size(self, w, l):
12 self.width = w
13 self.len = l
14
15 rectangle = Rectangle(10, 20)
16 print(rectangle.area())
17 rectangle.change_size(20, 40)
18 print(rectangle.area())
>> 200
>> 800
Zastosowany w tym przykładzie obiekt Rectangle ma dwie zmienne instancyjne — len i width. Metoda area zwraca pole danego prostokąta, mnożąc jego długość przez szerokość; z kolei metoda change_size zmienia wartości tych zmiennych instancyjnych, przypisując im nowe wartości przekazane jako parametry. Programowanie obiektowe ma kilka zalet. Wspomaga i zachęca do wielokrotnego stosowania kodu, a co za tym idzie, ogranicza czas poświęcany na pisanie i pielęgnację kodu. Dodatkowo ułatwia dzielenie dużych problemów na mniejsze, co także ułatwia późniejszą pielęgnację kodu.
Z kolei wadą programowania obiektowego jest to, że pisanie programów wymaga dodatkowego wysiłku, który zazwyczaj należy włożyć w etap projektowania programów pisanych obiektowo.
Słownictwo
Efekt uboczny. Zmiana stanu zmiennej globalnej.
Instancja. Każdy obiekt jest instancją klasy. Wszystkie instancje danej klasy są tego samego typu.
Klasy. Mechanizm pozwalający programistom na klasyfikowanie i grupowanie podobnych obiektów.
Metody magiczne. Metody, których Python używa w różnych sytuacjach, takich jak inicjalizacja obiektu.
Metody. Metody są zestawami w klasach. Przypominają nieco funkcje, jednak definiujemy je wewnątrz klas i możemy je wywoływać wyłącznie na rzecz obiektów danej klasy.
Paradygmat programowania. Styl programowania.
Programowanie funkcyjne. Programowanie funkcyjne rozwiązuje problemy występujące w programowaniu proceduralnym, gdyż eliminuje stan globalny — w programowaniu funkcyjnym stan jest przekazywany z funkcji do funkcji.
Programowanie obiektowe. Paradygmat programowania polegający na tworzeniu obiektów, które prowadzą ze sobą interakcje.
Programowanie proceduralne. Styl programowania, w którym programista pisze sekwencję czynności wykonywanych w celu dotarcia do rozwiązania — każda z tych czynności zmienia stan programu.
Stan globalny. Wartości zmiennych globalnych programu podczas jego działania.
Stan. Wartości zmiennych programu w trakcie jego działania.
Tworzenie instancji. Proces tworzenia nowego obiektu danej klasy.
Zmienne instancyjne. Zmienne należące do obiektu.