5. Objektově orientované programování (OOP)
Programovací paradigma, které zapouzdřuje vlastnosti a funkcionalitu do individuálních objektů. Fakticky jsme se s tímto paradigmatem potýkali celou dobou.
Objekt reprezentující řetězec "ahoj svete"
obsahuje rovněž funkcionalitu "ahoj svete".split()
, která vytvoří seznam obsahující jednotlivé řetězce, které odpovídají řětězcům vzniklým rozdělěním původního řětězce mezerami.
Třídy
Vytváření uživatelsky definovaných tříd (následně pak objektů) budeme v tuto chvíli chápat hlavně jako tvorbu vlastních datových struktur s navázanou funkcionalitou.
Sahat po vytváření vlastní datové struktury bychom měli pouze v případě, že vestavěné (primitivní) datové struktury nejsou dostatečné. Zkusme realizovat účet s kredity pomoci vestavěné datové struktury dict
.
1
2
3
4
5
6
7
8
credit_account_1 = {"owner": "Lukas Novak", "balance": 100000}
credit_account_2 = {"owner": "Pepa Novak", "balance": 10000}
# součet kreditů na dvou účtech
credit_account_1["balance"] + credit_account_2["balance"]
# odečet hodnoty od kreditů
credit_account_1["balance"] - 100
Třída (příkaz class
) tedy slouží k vytváření uživatelsky definovaných datových struktur. Určují jak má výsledná datová struktura vypadat a fungovat. Na základě třídy (předpis) můžeme vytvářet jednotlivé instance třídy (objekty).
Jako příklad si můžeme představit již dobře známý seznam. Jeden konkrétní seznam je instancí (objektem) třídy seznam, která popisuje jak seznamy vypadají a fungují. Třídy jsou tedy obecným předpisem, objekty pak konkrétní entity vytvořené na základě tohoto předpisu.
Třídy je dobré umisťovat do modulů stejného názvu.
V následujícím souboru credit_account.py
(modul credit_account
) si definujeme základní prázdnou třídu CreditAccount
:
1
2
3
# definice prázdné třídy
class CreditAccount:
pass
Následně nově vytvořenou třídu otestujeme:
1
2
3
4
5
6
7
8
9
10
11
12
from credit_account import CreditAccount
# dle předpisu třídy je následně možné vytvářet objekty
credit_account_1 = CreditAccount()
credit_account_2 = CreditAccount()
# ověření typu
type(credit_account_1)
# test zda je objekt instancí třídy
assert isinstance(credit_account_1, CreditAccount)
1
2
3
4
5
6
7
8
# PEP8 - název třídy používá CapWords konvenci
# správně
class CreditAccount:
pass
# špatně
class credit_account:
pass
Metody a vlastnosti
Vytvoření prázdné třídy neni moc praktické, pojdme definici rozšířit. Každá třída může obsahovat sadu funkcí které jsou s třídou úzce spjaty. Těmto funkcím říkáme metody. Již několikrát jsme používali metodu .split()
třídy řetězce, která umí daný řetězec rozdělit na seznam řetězců.
Všimněme si, že třída a její metody používají podobnou konvenci docstringů, budeme je tedy používat (a vyžadovat) i zde.
Nejdůležitější metodou každé třídy je konstruktor .__init__()
, zatím se nemusíme trápit z jakého důvodu název obsahuje podtržítka (to si vysvětlíme na dalším semináři). Hlavním úkolem konstruktoru je nastavit počáteční stav (initial state - proto název init) nově vytvořeného objektu.
Metoda .__init__()
může obsahovat libovolný počet parametrů (podobně jako funkce), prvním parametrem však vždy musí být parametr self
. Když je instance třídy vytvořena, je automaticky předána jako první parametr self
metodě .__init__()
. To je nutné pro nastavení počátečního stavu objektu (potřebujeme přístup k nově vytvořenému objektu aby jsme mohli počáteční stav nastavit).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreditAccount:
"""Account with stored credits."""
# metoda __init__ je volána při vzniku objektu třídy CreditAccount,
# obsahuje definici vlastností objektu a kód potřebný pro vytvoření instance
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
1
2
3
4
5
6
7
8
9
10
11
12
13
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1.owner
credit_account_1.balance
credit_account_1.owner = "Jaroslav Novak"
credit_account_1.balance += 300
assert credit_account_1.balance == 300
Vytvořili jsme tedy třídu CreditAccount
, která při vytvoření nové instance nastaví dvě vlastnosti CreditAccount.owner
a CreditAccount.balance
na hodnoty owner
a initial_credits
. Každá instance třídy CreditAccount
bude těmito vlastnosti disponovat.
Další metodou, kterou můžeme naši třídě CreditAccount
přidat je metoda CreditAccount.transfer_to(self, other, value)
. Narozdíl od dvou prázdných řádku mezi definicemi funkce, metody oddělujeme jedním prázdným řádkem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
# jeden prázdný řádek
def transfer_to(self, other, value):
"""Transfer money into another account. Negative balance is allowed.
Args:
other: Target of money transfer.
value: Amount of money to be transfered.
"""
self.balance -= value
other.balance += value
1
2
3
4
5
6
7
8
9
10
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1.transfer_to(credit_account_2, 200)
assert credit_account_1.balance == -200
assert credit_account_2.balance == 400
Asi nás nepřekvapí, že názvy metod podléhají doporučení PEP8.
1
2
3
4
5
6
7
8
9
10
# PEP8 - názvy metod a vlastností stejně jako u funkcí a proměnných
# správně
class TestClass:
def reverse_order(self):
pass
# špatně
class TestClass:
def reverseOrder(self):
pass
Vlastnosti třídy
Zatím jsme si ukázali, že vlastnosti jsou specifické pro jednotlivé objekty. Pokud zmeníme vlastnost u jednoho objektu, nezmení se u druhého. Vlastnosti definované jako vlastnosti třídy, můžeme využívat napříč všemi instancemi třídy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreditAccount:
"""Account with stored credits."""
max_balance = 1000
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
assert credit_account_1.max_balance == 1000
assert credit_account_2.max_balance == 1000
assert CreditAccount.max_balance == 1000
CreditAccount.max_balance = 10
assert credit_account_1.max_balance == 10
assert credit_account_2.max_balance == 10
assert CreditAccount.max_balance == 10
Vlastnosti třídy používejte k definovaní vlastností, které mají mít stejnou hodnoty pro všechny instance třídy. Vlastnosti instancí (objektů) používejte, pokud se mají objekt od objektu lišit.
Přístup k vlastnostem objektu
Narozdíl od jiných programovacích jazyků, jazyk Python přistupuje k vlastnostem objektu přímo (pomoci operátoru tečky). Programátor může modifikovat a číst libovolnou vlastnost/metodu objektu.
Není tedy třeba vytvářet přístupové metody (takzvané gettery a settery) jako v jiných jazycích. Na příštím semináři se k této problematice ještě vrátíme.
Pozor, s tímto faktem přichází velká zodpovědnost, programátor si musí sám uvědomit, jaké zásahy do objektů jsou validní a jaké mohou vést k problémům.
Existuje však způsob, kterým lze komunikovat, že uživatel přistupuje k vlastnosti/metodě, která není zamýšlena jako veřejná (je používaná například pouze interně v rámci objektu). Toho hojně využíváme v případě, že jsou naše metody komplikované a je nutné je rozdělit na několik dílčích metod (které však nejsou zamýšleny pro samotného uživatele).
1
2
3
4
5
6
7
8
9
10
11
12
13
# PEP8 - metody/vlastnosti, které nejsou zamýšlené jako veřejné
# pojmenujeme s prefixem podtržítka
class TestClass:
def __init__(self):
self._private_data = []
# většinou se jená o pomocné metody
def _reverse_order(self):
pass
# použité v jiné metodě téže třídy
def reverse(self):
return self._reverse_order()
Dědičnost
Koncept dědičnosti je jeden z hlavních konceptů objektově orientovaného programování. Ve zkratce si nyní ukážeme, jak může jedna třída dědit funkcionalitu od jiné.
Vraťme se k předchozímu příkladu. Představme si, že mimo CreditAccount
můžeme mít například i BankAccount
. Asi si umíme představit, že tyto třídy mohou sdílet učitou funkcionalitu a je tedy zbytečné ji implementovat vícekrát. Z toho důvodu se nabízí realizovat obecnou třídu Account
, od které bude dědit třída CreditAccount
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Account:
"""Represents an account."""
def __init__(self, owner, initial_balance=0):
"""Creates account with given owner and initial balance.
Args:
owner: owner of the account
initial_balance (optional): initial balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_balance
def transfer_to(self, other, value):
"""Transfer money into another account. Negative balance is allowed.
Args:
other: Target of money transfer.
value: Amount of money to be transfered.
"""
self.balance -= value
other.balance += value
U definice třídy CreditAccount
je nutné zdůraznit první řádek definice, do kulatých závorek uvádíme třídy (oddělené čárkou) z kterých má třída CreditAccount
dědit.
V konstruktoru třídy CreditAccount
si všimněme použití funkce super()
. Na technické úrovni se jedná o poměrně složitou věc, nám bude stačit vysvětlení, které říká, že funkce super()
umožňuje přístup k metodám z tříd od kterých dědíme.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from datetime import datetime
from account import Account
class CreditAccount(Account):
"""Represents a credit account."""
def __init__(self, owner, initial_balance=0):
# vyvolání konstruktoru předka
super().__init__(owner, initial_balance=initial_balance)
# dodané vlastnosti
self.expiration = datetime.now() + datetime.timedelta(days=365)
# dodaná funkcionalita
def expires_soon(self):
"""Checks if accounts validate within 30 days."""
return datetime.now() + datetime.timedelta(days=30) >= self.expiration
NamedTuple
Ne vždy je potřeba vytvářet celou třídu, takovým případem je reprezentace jednoduchých strukturovaných dat. Jestliže potřebujeme funkcionalitu tuple
s pojmenovaním jednotlivých uložených hodnot, můžeme použít collections.NamedTuple
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from collections import namedtuple
Person = namedtuple("Person", ["name", "phone", "email"])
owner = Person("Lukas Novak", "723812052", "novak@gmail.com")
owner.name
owner.phone
owner.email
# funguje jako klasický tuple
assert owner.name == owner[0]
# nelze, je to tuple
owner.name = "Pepa Novak"
Úkoly
Nevíte si rady? Přečtěte si “Jak pracovat s Github Classroom?”.
Při řešení úloh nepoužívejte pokročilejší funkcionalitu jazyka která nebyla ještě představena! Takové úkoly budou vráceny na přepracování bez ohledu na jejich funkčnost.
Skupina Mikula
- L05E01: Read points (NamedTuple) [Náhled], [Příjmout úkol]
- L05E02: Data [Náhled], [Příjmout úkol]
Skupina Petržela
- L05E01: Read points (NamedTuple) [Náhled], [Příjmout úkol]
- L05E02: Data [Náhled], [Příjmout úkol]