Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Úvod

Tento kurz je určený všem, kteří si chtějí osvojit základní principy programování. To se dá zobecnit do principů, které se poté aplikují v konkrétních programovacích jazycích. Podtrženo sečteno principy zůstávají stejné, co se ale mění je zápis. V tomto kurzu se seznámíme s jazykem Python, který bývá obecně označován za vhodný pro začátečníky; je avšak stále použitelný v reálném světě. Ze začátku kurzu budou některé koncepty zjednodušovány, aby byl kurz přístupný opravdu pro všechny. K jeho konci budou však vysvětleny v plné šíři.

Obsah tohoto kurzu může být libovolně používán k výuce programování na veřejných středních školách a gymnáziích. V roce 2026 slouží tento kurz k výuce seminářů programování na Gymnáziu Nad Alejí.

Algoritmus a program

Pod programem si představíme nějaký soubor s textem, který počítači říká, co má provést. Je to tedy analogie popisu pracovního postupu psaná v nějakém programovacím jazyce, ale místo prózy píšeme zkratkovité instrukce, nějaký kód. Za každým programem je nějaká úloha, tedy co chceme vypočíst – výpočet pětapadesátého prvočísla, výpis všech prvočísel menších než 100, a podobně. Obecnému postupu řešení úlohy, který se skládá z konečného počtu elementárních, myšleno nedělitelných, kroků říkáme algoritmus. Algoritmy můžeme popsat i přirozeným jazykem, třeba algoritmus pro ověření, zda je číslo dělitelné dvěma, můžeme zapsat takto:

Vezmi číslo a podívej se na zbytek po dělení dvěma. Pokud je zbytek 0, číslo je dělitelné dvěma. Pokud ne, číslo není dělitelné dvěma.

Také ho můžeme zapsat zkratkovitě jako

pokud
  (cislo mod 2 je rovno 0)
    vypis "cislo je delitelne dvema"
  jinak
    vypis "cislo neni delitelne dvema"

Pokud algoritmus zformulujeme v nějakém programovacím jazyce, nazveme tento program implementací algoritmu. Nutno podotknout, že implementace není nutně jedinečná; může existovat, a většinou existuje, více způsobů, jak algoritmus implementovat. Implementace se mezi sebou mohou zásadně lišit, například v době běhu programu. Vždy se samozřejmě snažíme implementovat algoritmus tak, aby běžel co nejrychleji.

Neexistuje jednoznačná definice pojmu “algoritmus”. Můžeme však definovat vlastnosti, které by měl algoritmus splňovat.

  • Konečnost zaručuje, že algoritmus skončí po vykonání konečného počtu kroků.
  • Správnost zaručuje, že algoritmus vypočítá správnou hodnotu pro nějaký vstup.
  • Determinismus znamená, že pro stejný vstup algoritmus vždy vypočítá stejnou hodnotu. Každý krok algoritmu je tedy jednoznačně definovaný v každé situaci.

Pokud máme algoritmus, který splňuje tyto vlastnosti, můžeme o něm jednodušeji uvažovat a analyzovat jeho chování. V rámci kurzu se občas s nějakým algoritmem setkáme, jejich implementace budou většinou ponechána jako domácí cvičení.

Často se také díváme na to, jaký má algoritmus vstup a jaký výstup. V příkladu s dělitelností dvěma bereme jako vstup nějaké číslo. Výstupem je pak informace o tom, zda je číslo dělitelné dvěma nebo není – pravda nebo nepravda. Této dvoustavové hodnotě se říká booleovská hodnota, také boolean. V anglických textech se běžně setkáme se jmény hodnot True a False.

Jak již bylo řečeno, základní koncepty samotného programování se mezi programovacími jazyky prakticky nemění. V další kapitole si představíme proměnné, tedy místo, kam si můžeme uložit hodnoty používané v programu.

Výstup

Když spustíme program, můžeme chtít, aby nám vydal výstup ve formě textu vypsaného na obrazovku, do terminálu. Nutno říci že není nutné, aby výstup byl ve formě textu na obrazovce – program může pracovat třeba jen se soubory a toto může být jeho výstupem.

V Pythonu můžeme z programu vypsat text pomocí slovíčka print. Za print následují závorky a v nich výraz, který chceme vypsat.

print("Ahoj, Pythone!")

Tento prográmek vypíše na obrazovku text Ahoj, Pythone!. Do uvozovek obalujeme jakýkoli souvislý text, který se vejde na jednu řádku; v programátorském žargonu těmto konstrukcím říkáme řetězec (anglicky string). Závorky jsou kolem hodnoty od toho, aby print věděl, co přesně má vypsat. Kdyby tam závorky nebyly, print by nevěděl, co všechno má vypsat. Magická slovíčka následovaná závorkami ještě s jistotou uvidíme.

Python neumí vypisovat pouze text. Umí třeba vypočítat aritmetický výraz. Vyzkoušejme si třeba spustit program

print(1 + 2 + 3)

Jak bychom asi očekávali, vypíše číslo šest. Aritmetika v Pythonu podporuje další operace, mimo jiné

  • sčítání (+), odčítání (-), násobení (*) a dělení (/),
  • celočíselné dělení (//),
  • zbytek po dělení (%)
  • a umocňování (**).

Tip

Zkuste si sami, co vypíší následující řádky kódu:

print(2 * 6 - 4)
print(15 // 4)
print(15 % 4)
print(2 ** 5)

Zkuste si také, co se stane, když vynásobíme číslo řetězcem, například 3 * "Ahoj".

Ve výrazech můžeme používat i závorky k tomu, abychom ovlivnili prioritu výpočtu, stejně jako v matematice na papíře.

Můžeme rovněž zapisovat desetinná čísla, jako oddělovač používáme narozdíl od české tradice psaní čárky západní způsob, tedy tečku. Šest desetin tedy zapíšeme jako 0.6, jednu polovinu jako 0.5. Aritmetické operace na nich fungují tak, jak bychom očekávali. Nutno podotknout, že pokud provádíme operace s celým a desetinným číslem, výsledkem je vždy desetinné číslo. Python rozděluje celá čísla (anglicky integer) a desetinná čísla (anglicky float).

Občas chceme slovíčkem print vytisknout více věcí, než jen text anebo číslo, třeba popis nějakého výpočtu. Následující program vypíše na výstup Šest plus pět se rovná 11.

print(f"Šest plus pět se rovná {6 + 5}")

Všimněte si písmena f před začínající uvozovkou; toto Pythonu říká, že se jedná o formátovací řetězec (formatting string). Všechny výrazy, které má Python vyhodnotit, píšeme do složených závorek. Po jejich vyhodnocení se výsledek vloží na místo složených závorek.

Jistě bychom mohli napsat jen následující, ale první možnost nám dává mnohem více možností pro výpis – občas se stává, že chceme v kódu zachovat právě formu výrazu, aby třeba kolega věděl, jak jsme k danému číslu došli (stejně tak je při počítání na papír důležitý postup).

print("Šest plus pět se rovná 11")

Více důvodů pro využití formátovacích řetězců uvidíme v dalších kapitolách.

Proměnné

Představme si program pro výpočet obvodu a obsahu obdélníka. Vstupem budou dvě kladná nenulová čísla: délka strany a výška. Výstupem budou také dvě čísla: obvod a obsah. Program by pro obdélník se stranami délky 12 a 7 mohl vypadat třeba takto:

print(f"Obvod: {2 * 12 + 2 * 7}, Obsah: {12 * 7}")

Během psaní programů se nám bude hodit dát hodnotám jméno, které nám umožní hodnoty znovupoužívat. Před výpisem výsledku vytvoříme dvě proměnné pojmenované obvod a obsah. Zde nastává kolize termínů – v matematice označujeme slovem proměnná buď číslo, které neznáme nebo číslo, které hledáme. Dá se na ně ale pohlížet i jako na parametry, které ovlivní výsledek; vzpoměňme si na vzorec pro výpočet kořenů kvadratické rovnice, kde tři koeficienty udávají kořeny.

V programování si pod proměnnými představíme hodnotu nebo výraz, který můžeme dál v programu využívat za pomocí nějakého jména. Pro náš případ z předchozího odstavce bude program vypadat následovně:

obvod = 2 * 12 + 2 * 7
obsah = 12 * 7

print(f"Obvod: {obvod}, Obsah: {obsah}")

Při vyhodnocování formátovaného řetězce Python zjistí, že potřebuje vyhodnotit proměnnou obvod. Podívá se na její definici, té odpovídá výraz 2 * 12 + 2 * 7, který výhodnotí. Pokračuje s proměnnou obsah. Při tisknutí pomocí print se podívá na hodnotu v proměnných obvod a obsah, ty umístí do formátovaného řetězce a následně jej vytiskne.

Můžeme jít ještě o krok dále. Všimněme si, že v posledním programu opakujeme hodnoty, které můžeme znovupoužívat na dalších místech – takové hodnoty by měly být skoro vždy vloženy do vlastní proměnné už jen proto, že se kód stane čitelnějším. Také jde i o praktičnost, neboť rozhodneme-li se změnit rozměry obdélníka, musíme změnit dohromady čtyři výskyty rozměrů v kódu. To otevírá prostor k chybám, protože můžeme na jeden výskyt zapomenout a hned dostaneme nesprávné výsledky. Ideálně bychom tedy chtěli změnit jen dva výskyty. Program tedy upravíme takto:

a = 12
b = 7

obvod = 2 * a + 2 * b
obsah = a * b

print(f"Obvod: {obvod}, Obsah: {obsah}")

Tento program už je velmi jednoduše změnitelný pro různé rozměry. Rozměry jsou na svém místě a pokud změníme hodnotu jedné strany a program spustíme, vypíše správný výsledek.

Úlohy

Obvod a obsah kruhu

Napište program, který vypočítá obvod a obsah kruhu ze zadaného poloměru. Konstantu potřebnou k výpočtu rozumně zaokrouhlete a umístěte do proměnné.

Teplota

Napište program, který pracuje s číslem v proměnné, které reprezentuje teplotu. Program vypíše převod mezi stupni Celsia a Fahrenheita, a to oběma směry. Například pro číslo 30 program vypíše

30 C je 86 F
30 F je -1.1 C

Vstup

Již v úvodní kapitole jsme zjistili, že do programu můžeme vnést nějaký vstup a program nám může vydat nějaký výstup. Výstupu jsme se již věnovali, teď se zaměříme na vstup. Dosud jsme vstup programu dávali tak, že jsme ručně měnili proměnné uvnitř programu. To je však velmi krkolomné, pokud si představíme, že bychom program rozeslali kolegům a ti by museli měnit jeho vnitřnosti – potřebovali by jistě nějaké instrukce, také se může jednoduše stát, že kolega je ne-programátor. Proto je jednodušší, aby se program uživatele na vstup zeptal přímo z příkazové řádky.

Získání vstupu z příkazové řádky

Použijeme k tomu magické slovíčko input(), které přečte vstup od uživatele a vrátí ho jako řetězec.

vstup = input()

print(vstup)

Po spuštění programu program čeká na vstup od uživatele. Po zadání vstupu a stisknutí klávesy Enter program pokračuje.

Uživatele se můžeme explicitně zeptat na otázku:

vstup = input("Zadejte jméno: ")

print(f"Vaše jméno je {vstup}.")

Podmíněný příkaz

Často se budeme chtít v programu rozhodovat, co vykonat v dalších krocích. Použijme příklad z první kapitolky, kde jsme zkoumali zápis algoritmu pro ověření sudosti čísla. Chceme, aby program vypsal Číslo je sudé pokud je číslo dělitelné dvěma beze zbytku. Jinak vypíšeme Číslo je liché.

Podmíněný příkaz zapíšeme pomocí klíčových slov if a else. Za slovo if patří podmínka, podle které se bude program rozhodovat, zdali vykoná řádky mezi if a else a přeskočí řádky pod else, nebo jestli přeskočí řádky mezi if a else a vykoná řádky pod else.

cislo = 19

if cislo % 2 == 0:
    print("Číslo je sudé")
else:
    print("Číslo je liché")

Povšimneme si také dvojteček, které jsou na konci řádků s if a else. Je také nesmírně důležité nezapomenout odsadit řádky, na které chceme, aby se podmínky vztahovaly, odsadit. Toho můžeme docílit vybráním textu s nimi a stisknutím tabulátoru.

Věnujme se teď části s podmínkou: cislo % 2 == 0. Nešálí vás zrak, opravdu vidíte dvě rovnítka za sebou. Jedno rovnítko slouží k přiřazení do proměnné (tak jak to je na prvním řádku). Dvojité rovnítko ověřuje rovnost a vrací boolean, tedy hodnotu pravda (True) nebo nepravda (False). Pokud je výsledek podmínky za if roven True, spustí se pouze odsazený kód mezi if a else.

Stejně jako hodnoty můžeme podmínky ukládat v proměnných – jsou to také výrazy. Podmínku z příkladu můžeme uložit do proměnné a náš program učiníme čitelnějším.

cislo = 19

cislo_je_sude = cislo % 2 == 0

if cislo_je_sude:
    print("Číslo je sudé")
else:
    print("Číslo je liché")

Je hezké, že program můžeme číst skoro jako přírozený jazyk, tedy “Pokud (if) je číslo sudé, vytiskni …, jinak vytiskni …”.

Zkusíme nyní napsat program, který odhalí nenulová čísla. Vytvoříme podmínku “číslo není nula” a pokud platí, vypíšeme text.

cislo = 18

cislo_neni_nula = cislo != 0

if cislo_neni_nula:
    print("Číslo není nulové")

Použili jsme podmiňovací operátor !=, tedy nerovná se. Také vidíme, že pokud je číslo nulové, nic nevypíšeme – celý blok else je totiž volitelný.

Tip

Pokud bychom nemohli použít operátor !=, můžeme použít klíčové slovo not. To podmínku v proměnné zneguje.


cislo = 18

cislo_je_nula = cislo == 0

if not cislo_je_nula:
    print("Číslo není nulové")

Podmínky také můžeme spojovat pomocí klasických logických spojek “a” a “nebo”. Těmto operacím odpovídají klíčová slova and a or. Program, který vypíše, jestli je číslo sudé a zároveň nenulové může vypadat takto:

cislo = 18

cislo_je_sude = cislo % 2 == 0
cislo_neni_nula = cislo != 0

if cislo_neni_nula and cislo_je_sude:
    print("Číslo není nulové a je sudé")

V konstrukci if-else můžeme použít ještě jedno klíčové slovo, a to je elif (zkrácenina slov else if). V klasickém if-else bloku se kód v if vykoná právě když je podmínka pravdivá, jinak se vykoná kód v else. Pokud bychom chtěli po neúspěšném if vykonat jiný kód, ale pouze pod nějakou jinou podmínkou, použili bychom elif.

score = 85

if score >= 90:
    grade = 1
elif score >= 80:
    grade = 2
elif score >= 70:
    grade = 3
elif score >= 60:
    grade = 4
else:
    grade = 5

print(f"Tvoje známka je {grade}.")

V podmínkách můžeme vytvářet nové proměnné a měnit proměnné, které již existují. Důležité je, že nové proměnné zůstávají existovat i mimo if-else blok; to nám umožňuje vypsat proměnnou grade až po vyhodnocení podmínky. Program bychom samozřejmě mohli napsat i tak, že by v každé podmínce byl samotný print, ale znovu platí to, že kdybychom chtěli změnit to, co vypisujeme, museli bychom změnit více výskytů.

Zde se také setkáváme s porovnávacím operátorem >= (větší nebo rovno). Pokud bychom chtěli ověřit, zdali je číslo striktně větší, použijeme operátor > (větší). Analogicky pro menší nebo rovno použijeme operátor <= (menší nebo rovno) a pro striktně menší použijeme operátor < (menší).

Úlohy

Liché
Napište program, pro číslo vytiskne “Liché”, pokud je číslo liché, nebo nic. Zkuste vhodně použít operátor pro negaci podmínky.
Kvadratická rovnice
Napište program, který řeší kvadratickou rovnici. Budou zadány koeficienty a, b a c a program vypíše reálná řešení. Pokud řešení neexistuje, program vypíše “Neexistuje řešení”.

Cyklus

Často se nám bude stávat, že budeme muset nějaký kus kódu zopakovat pro nějaké určité hodnoty. Představme si program pro vypisování všech násobků čísla tři – s našimi dosavadními znalostmi bychom tento program napsat nemohli.

K tomu nám poslouží koncept cyklu – blok kódu, který budeme opakovat pro každou hodnotu v nějaké posloupnosti. Abychom takové posloupnosti vygenerovali, pomůže nám magické slovíčko range. To nám umožní vytvořit jakoukoli posloupnost čísel s určitým počtem hodnot. Pro příklad

  • range(5) vytvoří posloupnost čísel od 0 do 4,
  • range(1, 6) vytvoří posloupnost čísel od 1 do 5,
  • range(5, 11) vytvoří posloupnost čísel od 5 do 10,
  • a range(1, 11, 2) vytvoří posloupnost čísel od 1 do 10 s krokem 2, tedy 1, 3, 5, 7, 9.

Přes hodnoty, které nám range vytvoří, budeme iterovat, neboli opakovat, blok kódu pro každou hodnotu. Tuto hodnotu můžeme v iteraci použít jako vstup, přičemž v každé iteraci bude mít jinou hodnotu podle toho, na jaké hodnotě se zrovna nacházíme v posloupnosti. Pro násobky čísla 3 tedy můžeme napsat následující program:

for cislo in range(1, 11):
    print(3 * cislo)

Posloupnost, kterou range vytvoří, jsou čísla 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. V každé iteraci se toto číslo vynásobí trojkou a výsledek vypíšeme na výstup programu.

Můžete však namítat, že tento program jde napsat takto:

print(3 * 1)
print(3 * 2)
print(3 * 3)
print(3 * 4)
print(3 * 5)
print(3 * 6)
print(3 * 7)
print(3 * 8)
print(3 * 9)
print(3 * 10)

To samozřejmě jde, ale má to řadu nevýhod a problémů:

  • Místo dvou řádků máme řádků deset.
  • Pokud chceme změnit to, jaké násobky generujeme, musíme upravit všechny řádky, případně řádky přidat nebo odebrat.
  • Neumíme ihned vyčíst krajní hodnoty, musíme projít řádky.
  • Neumíme generovat nekonečně mnoho násobků.

Důvodů je mnohem více, proto se odteď budeme držet jednoduchého pravidla: máme-li v našem kódu něco, co se dá napsat pomocí smyčky, použijeme ji. Samozřejmě i tato poučka má své výjimky, například pro dvě volání smyčku psát nebudeme.

Cyklus “dokud”

For cyklus pro každou hodnotu v posloupnosti vykoná nějaký blok kódu. Občas se stane, že chceme opakovat blok kódu, dokud nepřestane platit nějaká podmínka. K tomu slouží while cyklus. Vypišme třeba všechny násobky tak, že nejvyšší násobek je menší než 100.

i = 1
while i * 3 < 100:
    print(i * 3)
    i += 1

Pomocí while cyklu můžeme dokonce vytvořit i nekonečný cyklus:

while True:
    print("Nekonečný cyklus")

Pokud bychom cyklus chtěli opustit, můžeme použít klíčové slovo break:

i = 1

while True:
    i += 1
    if i > 100:
        break
    print(i)

Jak s for cykly tak i s while cykly se setkáme ještě mnohem více.

Převody datových typů

Abychom mohli na vstupu brát něco jiného, než řetězce, potřebujeme využít další magická slovíčka, podle toho, jaký datový typ chceme získat. Pokud požadujeme celé číslo (integer), použijeme int(...). Takto můžeme být pozdraveni tolikrát, kolikrát chceme.

pocet_pozdravu = int(input("Kolikrát chcete pozdravit?"))

for i in range(pocet_pozdravu):
    print(f"{i + 1} Ahoj!")

Všimněme si, že do závorek u int zabalíme input společně s jeho závorkami. Můžeme si to představit tak, že se nejdříve vezme vstup pomocí input a až poté se ho Python pokusí převést na celé číslo pomocí int(). Důraz klademe na pokusí, zkuste si, co se stane, když zadáme řetězec tam, kde se očekává celé číslo.

Pro přijetí desetinného čísla (float) použijeme float(...).

Tip

Zkuste si přepsat co nejvíce úloh z minulých kapitol tak, aby nevyžadovaly pro změnu vstupu změnu samotného zdrojového kódu programu. Použijte tedy konstrukce input a int, případně float.

Úlohy

Všechna sudá

Napište program, který vypíše všechna sudá čísla od 1 do 100. Program pak upravte tak, aby vypisoval čísla lichá.

Suma čísel

Napište program, který za pomocí cyklu sečte všechna čísla od zadaného počátku do zadaného konce. Program pak upravte tak, aby vypisoval součet všech čísel od 1 do 100.

Vlajka

Napište program, který vypíše následující obrazec pro libovolnou velikost:

*
**
***
****
Obrácená vlajka

Napište program, který vypíše obrácený obrazec pro libovolnou velikost:

****
***
  **
  *
Fibonacci

Napište program, který vypíše Fibonacciho posloupnost až po nějaké maximum.

Seznamy

Zatím jsme pracovali s čísly (jak celými tak desetinnými) a řetězci. Ty vždy reprezentovaly jednu konkrétní “věc”; co kdybychom ale chtěli reprezentovat soubor nějakých “věcí”, třeba jaké mince máme v peněžence nebo jména studentů ve třídě?

V Pythonu taková data můžeme zapsat pomocí seznamů (anglicky list, česky někdy též pole).

penezenka = [2, 2, 5, 5, 5, 20, 20, 50, 50]

studenti = ["Martin", "Dan", "Adam", "Edvard", "Richard", "Martina", "Káťa", "Ondřej"]

Pole zapisujeme pomocí hranatých závorek, ve kterých jsou jednotlivé prvky oddělené čárkou. V poli můžeme prvky libovolně opakovat, jak vidíme v případě s mincemi v peněžence (jejich vzestupné seřazení ale pole nezaručuje).

Užitečnou vlastností seznamů je, že jimi můžeme iterovat pomocí cyklů:

for mince in penezenka:
    print(mince)

V proměnné mince se postupně objevují všechny prvky seznamu penezenka.

Někdy se hodí znát délku seznamu, tedy kolik prvků obsahuje. K tomu slouží magické slovíčko len. Délka prázdného seznamu je 0.

print(penezenka)
print(studenti)

print(len(penezenka))
print(len(studenti))

K prvkům seznamu nemusíme přistupovat jen hromadně pomocí cyklu, ale i náhodně1. Každý prvek má svoje pořadní číslo, říkáme mu index, které začíná od 0. První prvek má tedy index 0, druhý index 1, atd. K získání hodnoty na daném indexu používáme hranaté závorky [] za jménem proměnné, ve které je náš seznam. Speciálně pak index hodnoty -1 vrací poslední prvek seznamu, -2 předposlední, a tak dále.

print(studenti[0])
print(studenti[1])
print(studenti[-1])

Takto můžeme iterovat polem i pomocí for cyklu s range. To se teď může zdát nepotřebné, ale občas potřebujeme ze seznamu získat všechny hodnoty na nějakých indexech, které se dají vyjádřit nějakou posloupností (buď vyjadřitelnou range, nebo seznamem indexů).

for i in range(len(studenti)):
    print(studenti[i])

Pro pole existuje spousta užitečných magických slovíček, které nám o polích řeknou tížené informace:

  • min(pole) nám vrátí nejmenší hodnotu v poli,
  • max(pole) nám vrátí největší hodnotu v poli
  • a sum(pole) nám vrátí součet všech hodnot v poli.

Prázdné pole můžeme založit pomocí přiřazení prázdného seznamu do proměnné:

znamky = []

Do pole můžeme přidávat prvky pomocí volání2 append:

znamky.append(2)
znamky.append(3)

Important

Zkuste si pomocí hranatých závorek zkusit přistoupit k prvku na indexu, který neexistuje (třeba proto, že je pole moc krátké). Python na vás zakřičí výjimku typu IndexError. Výjimky jsou prostředek komunikace chyb v programu, které nastaly během jeho běhu. V kurzu je ještě uvidíme.

Můžeme takto třeba přidat všem studentům známku pět, protože úkol nikdo neodevzdal:

for student in studenti:
    znamky.append(5)

Také by se hodilo umět ověřit, zda prvek je nebo není v seznamu. K tomu slouží klíčové slovo in a vrací hodnotu typu boolean (tedy podmínka). Můžeme tedy ověřit, jestli je někdo studentem nebo ne:

mozni_studenti = ["Jan", "Petr", "Honza", "Marie"]

for mozny_student in mozni_studenti:
    if mozny_student in studenti:
        print(f"{mozny_student} je studentem")
    else:
        print(f"{mozny_student} není studentem")

Pokud máme dva seznamy, můžeme je spojit pomocí operátoru +.

seznam1 = [1, 2, 3]
seznam2 = [4, 5, 6]
seznam3 = seznam1 + seznam2
print(seznam3)

Podívejme se nyní ještě jednou na řetězce (anglicky string). Jak název napovídá, řetězec není nic jiného než zřetězení znaků do jednoho uceleného objektu. Python na řetězce pohlíží jako na seznam znaků, a proto můžeme s řetězci dělat totéž, co se seznamy.

zprava = "Hello, world!"

print(zprava[0])
print(zprava[-1])

for znak in zprava:
    print(znak)

Úlohy

Important

Při řešení úloh se zamyslete nad tím, jaký vstup může váš program přijímat. Také se snažte o vstupu nic nepředpokládat, pokud to není explicitně zmíněno v zadání. Je také třeba myslet na takzvané edge cases, hraniční případy: třeba to, že by program měl fungovat správně i s prázdným seznamem.

Prázdné pole
Rozmyslete si, jakou podmínkou můžeme vyjádřit to, že je nějaké pole prázdné. Ověřte.
Poslední index
Pomocí délky pole vyjádřete, jaké hodnoty bude nabývat index posledního prvku (v závislosti na délce pole)
Analýza seznamu
Napište vlastní implementace min, max a sum pro seznamy. Použijte cyklus a nějakou podmínku. Stávájící hodnotu si pamatujte pomocí proměnné, kterou v cyklu buď aktualizujete, nebo ne.
Druhý nejvyšší prvek
Napište program, který najde druhý nejvyšší prvek v seznamu. Předpokládáme, že seznam bude mít alespoň dva prvky.
Deduplikace
Na vstupu dostane program seznam, kde se mohou vyskytovat duplicitní prvky. Napište program, který tento seznam deduplikuje, tj. takové prvky odstraní. Výsledek bude tedy znovu seznam, klidně seznam nový.

  1. Slovo “náhodně” zde není myšleno ve významu náhodnosti (například jako ve spojení “náhodné číslo”). Náhodný přístup k prvkům seznamu znamená, že jsme schopni se podívat na jakýkoli prvek seznamu.

  2. Zatím nerozebírejme, co znamená slovo “volání”. Berme zatím jako fakt, že můžeme napsat .append(...) za proměnnou, ve které je seznam.

Štábní kultura

Při psaní kódu se snažíme zachovat konzistentní styl psaní a formátování, stejně jako bychom psali slohovou práci nebo článek do školního časopisu. Během tohoto kurzu se tedy budeme držet následujících pravidel.

Jména proměnných

Proměnné budeme pojmenovávat buď anglicky nebo česky s tím, že píšeme vždy bez diakritiky a malými písmeny. Pokud chceme pojmenovat proměnnou více slovy, použijeme jako oddělovač podtržítka (_). Proměnné také pojmenováváme tak, aby bylo na první pohled jasné, co proměnná obsahuje.

pocet_lidi = 34
obvod = 3.5

Komentáře

Do kódu můžeme vpisovat komentáře, neboli poznámky pro nás a ostatní programátory. Při běhu programu je Python ignoruje. Pokud nejsme schopni podstatu programu vyjádřit jmény proměnných nebo jinak, použijeme komentáře. Co není žádoucí je popisovat komentáři věci, které jsou na první pohled jasné (tedy např. cykly skrz pole, apod.).

Odsazování bloků

V Pythonu se používá odsazování (indentace) pro označení bloků kódu, tedy řádky, na které se vztahuje nějaká podmínka, cyklus, a podobně. Indentace tedy není jen věc stylu kódu, ale také logické struktury programu.

Časová složitost

Když v programu začneme používat cykly, může se stát, že program bude běžet dlouho. Může se také stát, že program dokonce nikdy neskončí a bude běžet donekonečna. V úvodní kapitole jsme zmínili, že algoritmus je nějaký popis řešení úlohy a to, co úlohu řeší, je implementace v nějakém programovacím jazyce (tedy program, který lze spustit). Vzhledem k tomu, že implementací může být mnoho, může se stát, že jedna bude běžet rychleji a nějaká zas pomaleji. To svádí k zavedení konceptu časové složitosti algoritmu (anglicky time complexity), která nám umožní porovnávat rychlosti algoritmů. Budeme se zaměřovat na nejhorší případ, tedy situaci, kdy algoritmus provede nejvíce možný počet kroků a je tedy nejpomalejší. 1

Krokování algoritmů

Nejdříve se ale zaměříme na to, jak můžeme časovou složitost vůbec vyjádřit. Protože je algoritmus deterministický (neupravuje se za běhu), můžeme vždy na základě velikosti vstupu určit, kolik “kroků” algoritmus provede.2 Za krok budeme považovat nedělitelné operace, tedy například přiřazení do proměnné a aritmetické operace.

Pojďme zkusit analyzovat jednoduchý algoritmus, který hledá, jestli je zadané číslo v seznamu. Program tohoto algoritmu by vypadal takto:

hledane_cislo = 13

seznam = [...]

for cislo in seznam:
    if cislo == hledane_cislo:
        print("Hledané číslo je v seznamu.")
        break

Zde záměrně ponecháváme seznam neznámé délky (tento kód se nespustí, protože [...] je neplatný výraz), abychom nebyli ovlivněni konkrétní délkou seznamu; vždy se snažíme najít obecné řešení.

Označme délku seznamu písmenem , to je náš vstup. Podívejme se dovnitř smyčky: porovnáme jestli je momentální cislo rovno hledanému číslo, a pokud ano, ukončíme smyčku. Porovnání zabere jeden krok a dva řádky uvnitř zaberou dva kroky. I když se neprovedou vždy (jen pokud najdeme hledaný prvek), započítáme je vždy – to nám říká, kolik kroků algoritmus provede v nejhorším případě, což je to, co nás zajímá. Vnitřek smyčky se provede krát, protože procházíme seznam prvek po prvku, a pokud se znovu díváme na nejhorší případ, celkový počet kroků je .

Také bychom se na problém nejhoršího případu mohli podívat takto: najděte vstup, na kterém udělá algoritmus co nejvíce kroků. V tomto případě to znamená, že prvek v seznamu vůbec není, ale to my nezjistíme, dokud neprojdeme celý seznam.

Podívejme se na další příklad. Bude podobný prvnímu, ale vstup omezíme předpokladem uspořádanosti seznamu (tedy seznam je seřazený od nejmenšího po největší; pozor, neznamená to, že seznam obsahuje všechna čísla od 1 do ). Znovu hledáme, jestli je prvek v seznamu nebo ne.

U uspořádané posloupnosti můžeme použít trik: pokud se podíváme doprostřed seznamu, můžeme zjistit, jestli je náš hledaný prvek v prvcích nalevo nebo napravo od středu. Tím eliminujeme polovinu možností, které by algoritmus musel prohledávat a můžeme si to dovolit právě kvůli tomu, že seznam je uspořádaný. V této polovině se zase můžeme podívat doprostřed a postup opakovat. Tomuto postupu se říká binární vyhledávání. Napišme nejdříve program a poté ho analyzujme:

hledane_cislo = 13

seznam = [...]

levy = 0
pravy = len(seznam) - 1

while levy <= pravy:
    stred = (levy + pravy) // 2
    if seznam[stred] == hledane_cislo:
        print(f"Nalezeno na indexu {stred}.")
        break
    elif seznam[stred] < hledane_cislo:
        levy = stred + 1
    else:
        pravy = stred - 1

Program využívá takzvané metody dvou jezdců: udržujeme si indexy levy a pravy, které určují hranice toho, kde se má vyhledávat. V případě, že je střed právě prohledávaného podseznamu roven hledanému číslu, algoritmus končí úspěchem. Pokud je střed menší než hledané číslo (tedy hledané číslo je větší, než je střed), víme, že máme postup opakovat na pravé polovině, protože se v levé polovině nacházet nemůže. Analogicky pro hledání na levé polovině.

Přistupme k analýze. Uvnitř while smyčky se provedou nejhůře 3 kroky. Kolikrát se ale nejhůře provede smyčka sama? Označme písmenem počet provedených opakování cyklu a počet prvků v seznamu. Na začátku pracujeme se seznamem o velikosti , po jednom cyklu o velikosti , po druhém cyklu o velikosti , a tak dále. Můžeme tedy říct, že po opakováních cyklu pracujeme se seznamem o velikosti . Náš algoritmus se v nejhorším případě zastaví, když se dostaneme na seznam velikosti 1 (už nemůžeme seznam dělit dál). Chceme tedy vyjádřit v závislosti na tehdy, kdy se dostaneme na seznam velikosti 1. Řešíme tedy rovnici

To je zajímavé zjištění – počet opakování cyklu je závislé ne lineárně, ale logaritmicky na velikosti vstupního seznamu! Celkový počet kroků algoritmu je v nejhorším případě okolo .

Uvažme úpravu prvního algoritmu tak, aby byl omezen stejnou podmínkou pro vstup – používáme ho jen pro setřízené seznamy. Kód se nijak nezmění. Nyní máme dva programy pro stejnou úlohu hledání existence prvku v poli. Druhý algoritmus je ale výrazně rychlejší. Pro seznam o 10 000 prvcích provede první algoritmus okolo 20 000 kroků, zatímco druhý provede (po zaokrouhlení) pouze 42 kroků! To je výrazně rychlejší.

Od přesnosti k asymptoticitě

Dosud jsme rychlost algoritmu vyjadřovali v přesném počtu kroků. To může být ale občas krkolomné, protože nám vlastně nejde moc o přesný počet kroků, ale spíše o to, jak se bude rychlost algoritmu vyvíjet na velkých vstupech. Když poté do ruky dostaneme více algoritmů pro řešení stejné úlohy, vybereme ten, který úlohu s “velkým” zadáním bude řešit nejrychleji.

Potřeba analýzy pro velké vstupy donutila teoretické informatiky zavést koncept asymptotické3 časové složitosti. Spočívá v tom, že se na výrazy popisující počet kroků algoritmů budeme dívat jako na křivky a budeme pozorovat, jak rychle rostou.

Červenou křivkou je znázorněn první algoritmus s počtem kroků , modrou druhý s počtem kroků . Na ose je velikost vstupu, na ose počet kroků. Jak můžeme z grafu vidět, počet kroků binárního vyhledávání pro velké vstupy,4 a dokonce ani v nekonečnu, nikdy nepřesáhne počet kroků vyhledávání lineárního. To je definice toho, že funkce je asymptoticky menší, než funkce .

Ve světě asymptot můžeme dokonce ignorovat aditivní a multiplikativní konstanty a zaměřit se pouze na “tvar” funkce. Z funkce se tedy stane a z funkce se stane . Protože je základ logaritmu také nepotřebný, můžeme psát pouze . Abychom uvedli další příklady, z funkce se stane a z funkce se stane . Poslední způsob, jakým můžeme funkce pro náš účel takto “zjednodušit” je odstranění pomalu rostoucích členů. Funkci můžeme zjednodušit na prosté , protože část výrazu, který nejvíce přispívá růstu pro velké hodnoty , je právě člen .

Protože je toto zjednodušování běžné, zavedli pro něj pánové Bachmann a Landau speciální zápis (anglicky big O notation, my mu budeme říkat velké O). Pomocí něj můžeme napsat například toto:

Pojďme se také podívat na porovnání růstu běžných funkcí:

Za použití velkého O můžeme tedy napsat a také . Co už neplatí je , protože roste rychleji, než .

Symbol používáme proto, že velké O tvoří množinu funkcí, které rostou stejně nebo rychleji, než funkce nalevo od .

Pro naše dva algoritmy tedy můžeme říct, že první algoritmus běží v čase a druhý v čase . Protože funkce roste pomaleji, než funkce , je druhý algoritmus rychlejší.

Složitost běžných operací

Běžné aritmetické operace bereme jako operace s konstantní složitostí, tedy že provedou konstantní počet kroků bez ohledu na velikost vstupu. Tyto operace tedy budou velmi rychlé; konstantní složitost zapíšeme jako .

Operace s lineární časovou složitostí , kde je velikost vstupu, většinou tvoří iterace přes všechny prvky v nějakém datovém struktuře (seznam, slovník, atd.). Důležité je, že klíčové slovo in, které používáme ke zjištění toho, zda je prvek v seznamu či ne, má lineární časovou složitost, protože v nejhorším případě musí prohledat všechny prvky v seznamu.


  1. Může se zdát, že toto jde proti požadavku determinismu. To však není pravda, protože algoritmus sice může provést jen daný počet kroků v závislosti na vstupu, ale tento počet kroků se právě od tohoto vstupu odvíjí! Může se tedy stát, že pro nějaký vstup provede jeden krok, ale pro jiný tisíce – to nemusí být na škodu, my se jen snažíme o vyjádření toho, jak algoritmus funguje zaprvé v obecnosti a zadruhé pro nejhorší, nejpomalejší případ.

  2. Závislost na velikosti vstupu je zde velmi důležitá. Představte si, že dělíte dvě čísla pod sebou. Vydělit dvouciferné číslo jednociferným je jednodušší, než vydělit tisíciciferné číslo dvaceticiferným. Stále však opakujeme ty stejné kroky, jen nám to trvá déle, protože vstup je větší.

  3. Asymptotou v geometrii označujeme přímku, které se v nějaké křivce blíží, aniž by s ní měla společný bod. Asymptota může být libovolně blízko, ale nikdy se dané křivky nedotkne.

  4. Slovo velké je zde důležité, protože například třeba tyto naše dvě funkce se pro malá potkají, dvakrát protnou a modrá je dokonce větší, než červená. To nám však nevadí, protože se nesoustředíme na malé vstupy.

Slovník

V této kapitole si rozšíříme znalosti o datových typech, které umožňují uchovávat více dat najednou. Už jsme poznali seznamy (pole), tedy lineární kolekce, kterými můžeme iterovat a ke kterým můžeme přistupovat pomocí indexů.

Slovník (anglicky dictionary, ale také map, hashmap, hashtable, associative array; mapa, hashmapa, hashovací tabulka, asociativní seznam) je datová struktura, která umožňuje uchovávat páry klíč-hodnota (narozdíl od pole, které uchovává pouze hodnoty). Název “asociativní seznam” si získal proto, že se slovníkem dá iterovat podobně jako seznamem, ale zároveň asociuje hodnoty klíče s jejich přidruženými hodnotami. Český název “slovník” si získal proto, že funguje podobně jako slovník jazykový: pokud chceme přeložit slovo “apple” (klíč) do češtiny, nalistujeme v knize na stránku, kde se “apple” vyskytuje a přečteme překlad (hodnotu).

Ve slovnících můžeme uchovávat libovolné datové typy jako klíče nebo hodnoty.

Pořídíme si prázdný slovník, kterým budeme simulovat anglicko-český jazykový slovník:

slovnik = {}

Do slovníku následně přidáme některé anglicky-české překlady:

slovnik["apple"] = "jablko"
slovnik["banana"] = "banán"
slovnik["orange"] = "pomeranč"

Pokud nás zajímá překlad slova “banana”, můžeme ho získat pomocí tohoto klíče:

print(slovnik["banana"])

Záludnost nastává v momentě, kdybychom chtěli přeložit slovo “apricot”. Toto slovo ve slovníku není a Python na nás zakřičí výjimku (zkuste si to):

print(slovnik["apricot"])

Dostaneme výjimku KeyError, protože klíč “apricot” není v slovníku. Kdyby se toto stalo uprostřed běhu programu, běh by se zastavil a Python vypsal výjimku. Občas ale potřebujeme získat hodnotu pro klíč, který není v slovníku, a to bez zastavení programu možnou výjimkou. Můžeme to provést pomocí slovíčka get():

print(slovnik.get("apricot"))

Co ale get vrátí, když klíč není v slovníku? V tomto případě vrátí None, což je speciální hodnota, která reprezentuje “nic”. Ano, je to trochu zvláštní, že “nic” můžeme zhmotnit do hodnoty, ale co už… K hodnotě, o které si nejsme jisti, zdali ve slovníku je, můžeme bezpečně přistoupit takto:

chci_prelozit = "apricot"

hledane_slovo = slovnik.get(chci_prelozit)
if hledane_slovo is not None:
    print(f"{chci_prelozit} se přeloží na {hledane_slovo}.")
else:
    print(f"{chci_prelozit} není v slovníku.")

Pozor na to, jak ověřujeme to, jestli je v proměnné hodnotě None nebo ne: is not None a analogicky is None. Tuto konstrukci používáme výhradně pro None a nepoužíváme ji nikde jinde; pro ostatní případy použijeme klasické porovnávací operátory z kapitoly o podmíněných příkazech.

Použítí .get() se můžeme vyhnout tím, že předem ověříme, jestli je klíč ve slovníku:

chci_prelozit = "apricot"

if chci_prelozit in slovnik:
    hledane_slovo = slovnik[chci_prelozit]
    print(f"{chci_prelozit} se přeloží na {hledane_slovo}.")
else:
    print(f"{chci_prelozit} není v slovníku.")

Slovíčko get má další variantu, která nám umožní nastavit výchozí hodnotu, která se nám vrátí, pokud ve slovníku klíč není:

body = {"David": 10}

vysledek = body.get("David", 0)
print(vysledek)

vysledek = body.get("Jan", 0)
print(vysledek)

Slovníkem také můžeme iterovat pomocí .items():

for key, value in slovnik.items():
    print(f"{key}: {value}")

Pokud chceme iterovat pouze přes klíče nebo hodnoty, můžeme použít .keys() nebo .values():

for key in slovnik.keys():
    print(key)

for value in slovnik.values():
    print(value)

Jak items(), tak i keys() a values() vrací seznamy.

Úlohy

Počítání písmen
Na vstupu dostanete od uživatele řetězec. Váš úkol je pro každé písmeno v řetězci spočítat, kolikrát se v něm vyskytuje. Použijte slovník, kde klíče budou písmena a hodnoty jejich četnost v řetězci. Doporučuji použít variantu get s výchozí hodnotou 0. Řetězcem na vstupu můžete iterovat právě jednou.
Telefonní seznam
Od uživatele dostanete na vstupu jméno a telefonní číslo, přičemž byste měli uživateli umožnit přidat takových záznamů více. Po zadání ničeho do vstupu pro nové jméno do seznamu se ukončí zadávání a program se přepne do režimu pro vyhledávání. Na vstupu teď budete požadovat jen jméno, podle kterého program vyhledá telefonní číslo. Pokud neexistuje, vypište chybovou hlášku a nechte uživatele zadat jméno znovu.

Funkce

Pod sloven funkce si většina ze čtenářů představí funkce matematické, třeba , nebo . Jak bychom takové matematické funkce mohli popsat? Třeba tak, že vezmou nějaké číslo (nazvěme ho argumentem) a vypočítají, vrátí nám, hodnotu funkce , nebo . Předpis funkce si můžeme představit jako tabulku hodnot, ke kterým přiřazujeme jiné hodnoty.1

Funkce v programování jsou velmi podobná věc: je to nějaký kód, kterému předáme argumenty, kód něco provede a vrátí nám výsledek. Ne všechny funkce v Pythonu musí vracet hodnotu.

Vytvořme si jednoduchou funkci, která vrátí násobek dvou čísel:

def nasobek(a, b):
    return a * b

Definici funkce začneme klíčovým slovem def, následuje jméno funkce (řídí se stejnými pravidly, jako jména proměnných v kapitole Štábní kultura) a pak argumenty v závorkách. Potom následuje blok kódu, který funkce provede a vrátí nám výsledek pomocí klíčového slova return. Tělo funkce musí být jako u podmínek a cyklů patřičně odsazený.

Naši novou funkci nasobek následně můžeme zavolat pomocí jejího jména a předáním patřičných argumentů. Tím se kód v definici funkce spustí s předanými argumenty a vrátí nám výsledek.

vysledek = nasobek(3, 4)
print(vysledek)

Funkce nemusí brát žádné argumenty ani nemusí nic vracet, jako například tato funkce:

def pozdrav():
    print("Ahoj!")

Hlavní důvod proč používáme funkce je ten, že nám umožňují organizovat kód do menších, jednoduššeji spravovatelných částí. Ty poté můžeme navzájem volat a docílíme tím čitelnějšího kódu. Vraťme se k příkladu se zdravením lidí a napišme funkci, které předáme hodinu dne; funkce vrátí pozdrav vhodný pro danou denní dobu. Funkce je v tomto případě vhodná, protože logiku pozdravu přesuneme na jedno místo a navíc ho pojmenujeme. Tím docílíme toho, že kód neopakujeme a na místě volání víme, co se má provést.

def pozdrav_podle_hodiny(hodina):
    if hodina < 12:
        return "Dobrý den"
    elif hodina < 18:
        return "Dobré odpoledne"
    else:
        return "Dobrý večer"

print(pozdrav_podle_hodiny(9))
print(pozdrav_podle_hodiny(14))
print(pozdrav_podle_hodiny(22))

Ukažme si další příklad, tentokrát funkci pro ověření síly hesla.2 Ověříme, jestli má heslo alespoň 8 znaků a jestli obsahuje číslici.

def je_heslo_bezpecne(heslo):
    # zkontrolujeme delku
    if len(heslo) < 8:
        return False

    # zkontrolujeme, zda obsahuje cislici
    for char in heslo:
        if char.isdigit():
            return True

    return False

print(je_heslo_bezpecne("heslo123"))
print(je_heslo_bezpecne("heslo"))

Tato funkce funguje, ale její definice by se dala vylepšit. Ve funkci je_heslo_bezpecne kontrolujeme dvě podmínky – délku hesla a zda obsahuje číslici – a ty můžeme znovu vytáhnout do samostatných funkcí.

def je_slovo_dost_dlouhe(heslo, min_delka):
    return len(heslo) >= min_delka

def slovo_obsahuje_cislici(heslo):
    for char in heslo:
        if char.isdigit():
            return True
    return False

def je_heslo_bezpecne(heslo):
    return je_slovo_dost_dlouhe(heslo, 8) and slovo_obsahuje_cislici(heslo)

print(je_heslo_bezpecne("heslo123"))
print(je_heslo_bezpecne("heslo"))

Tato verze je mnohem čitelnější, protože jsme oběma podmínkám vytvořili vlastní funkce, které poté voláme pomocí jejich jména; pro programátora je to mnohem přehlednější.

Caution

V předchozích kapitolách jsme používali výraz “magické slovíčko” pro print, len, apod. Nyní si už můžeme říct, že to jsou ve skutečnosti funkce! Pozor, například int(...), float(...) a range(...) funkce nejsou; více si povíme a nějaké další kapitole. Volání jako .append(...) nebo .split(...) funkce jsou, ale volají se speciálním způsobem, protože se volají na nějakém objektu (pole, řetězec, apod.).

Úlohy

Nejdelší slovo

Napište funkci nejdelsi_slovo, která jako argument přijímá řetězec a vrátí nejdelší slovo v něm. Pro rozdělení řetězce na slova použijte slovíčko split():

veta = "Toto je veta"
slova = veta.split()
print(slova)
Slovo s největší počtem písmen

Napište funkci slovo_s_nejvetsim_poctem_pismen, která jako argument přijímá řetězec a písmeno a vrátí slovo ze vstupního řetězce s největším počtem písmen, které jsou stejné, jako písmeno předané druhám argumentem.

Slovní hodnocení

Napište funkci slovni_hodnoceni, která jako argument přijme známku 1 až 5 a vrátí slovní hodnocení této známky. Pro jiná čísla vrátí None.


  1. Ve skutečnosti to nemohou být libovolné hodnoty, ale to prozatím není důležité.

  2. Toto není moc dobrý způsob, jak sílu hesla měřit, vzhledem k tomu, že nejdůležitější faktor síly hesla je jeho náhodnost. Pro náš účel demonstrace práce s funkcemi však postačí.

Modul

Může se stát, že se kód, který píšeme, stane nepřehledným a chtělo by ho rozdělit do více souborů. Většinou platí, že každý soubor s kódem by měl být zodpovědný za jednu konkrétní věc nebo funkci našeho programu. Pokud takto rozdělujeme kód podle zodpovědností kódu, říkáme těmto souborům moduly. Ty pak můžeme importovat do jiných souborů a tam je používat.

Vlastním modulům se v této kapitole věnovat nebudeme, podíváme se však na moduly které nabízí sám Python – je jich totiž hodně a mohou se hodit.

math

Modul math obsahuje matematické funkce a konstanty, které se dají použít pro matematické výpočty. Protože je toto první modul, se kterým se setkáváme, nejdřív se podíváme na to, jak moduly importovat.

import math

Pomocí klíčového slova import se importuje celý modul, který pak můžeme používat v našem kódu. Všechny funkce a konstanty dostupné v modulu math jsou k přečtení na oficiální stránkách Python dokumentace modulu math. Pokud chceme použít konstantu , která je k dispozici v modulu math, můžeme to udělat takto:

import math

print(math.pi)

Třeba pro výpočet sinu použijeme funkci math.sin:

import math

print(math.sin(math.pi))

random

Modul random obsahuje funkce pro generování náhodných čísel a práci s nimi. Veškeré dostupné funkce v tomto modulu jsou k přečtení na oficiální stránkách Python dokumentace modulu random.

Hodí se však znát tyto funkce:

from random import randint

print(randint(1, 10))

Funkce randint vrací náhodné celé číslo v zadaném rozsahu (včetně obou krajních hodnot).

from random import choice

print(choice([1, 2, 3, 4, 5]))

Funkce choice vrací náhodný prvek z seznamu.

Zde vidíme použítí klíčových slov from ... import ..., což nám dovolí importovat funkci nebo konstantu z modulu bez nutnosti importovat celý modul a psát jméno modulu před každou funkcí nebo konstantou. U takto importovaných funkcí a konstant si dobře rozmyslete, jestli je opravdu takto chcete importovat – pokud budete mít stejně pojmenovanou funkci ve svém souboru, import nebude fungovat správně.

Podseznam (slice)

Již jsme si řekli, že seznamy (a seznamovými strukturami jako řetězec) můžeme iterovat. Také můžeme přistupovat ke specifickým prvkům pomocí indexu. Do seznamu můžeme přistupovat i jinak: pokud potřebujeme ze seznamu vyříznout souvislý kus, můžeme použít konstrukci podseznamu (slice).

Mějme slovo barbara. Z něj můžeme vyříznout slova bar, bara a barbar. Řez získáme pomocí hranatých závorek s indexy začátku a konce (pozor, index konce se nepočítá).

slovo = "barbara"

print(slovo[0:3])  # "bar"
print(slovo[0:4])  # "bara"
print(slovo[0:6])  # "barbar"

Indexy začátku nebo konce také můžeme vynechat, Python pak vezme podseznam od začátku nebo do konce.

print(slovo[:4])  # "bara"
print(slovo[3:])  # "bara"

Třída

Když vyvíjíme aplikaci nebo nějaký program, je občas úkolem programátora i modelovat data z reálného světa a definovat vztahy, které mezi nimi existují. V této kapitole si ukážeme, jak na to, a také zavedeme základy objektově orientovaného programování (OOP) v Pythonu.

Přenesme se do pozice vývojáře, který dostal na starost vytvořit systém pro psí útulek. Pro každého pejska si budeme chtít pamatovat jeho jméno a věk. Jak bychom tato data mohli uchovávat? Jednou možností je si data o jednom pejskovi uložit ve slovníku, a tyto slovníky následně v nějakém seznamu. Mohlo by to vypadat třeba takto:

pejsci = [
    {
        "name": "Azor",
        "age": 3
    },
    {
        "name": "Alík",
        "age": 4
    }
]

To by fungovalo, ale je to krkolomné. Pokaždé, co chceme vytvořit nového pejska, musíme vytvořit nový slovník s daty pejska. Důležitější však je, že tyto slovníky mohou v podstatě popisovat cokoli jiného – třeba kočku, ale i (poměrně zvláštně pojmenovaného) člověka. Chtěli bychom přímo v kódu popsat to, že nějaká data reprezentují právě psa.

Vytvoříme tedy třídu: tu si můžeme představit jako šablonu pro vytvářené objekty. Každý objekt psa bude obsahovat jméno a věk (a další, uvidíme zanedlouho); těm se říká atributy (anglicky attribute) nebo datové položky.

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Vytvořili jsme šablonu pro vytváření nových pejsků, kde si u každého budeme pamatovat jméno a věk. Jistě se ptáte – proč jsme vě třídě nadefinovali funkci se jménem __init__? Velkou výhodou tříd je, že můžeme definovat funkce, které můžeme volat jen a pouze na objektech, které pomocí třídy vytvoříme. Funkce __init__ je ale speciální funkce: zavolá se totiž tehdy, kdy pomocí třídy objekt vytváříme. Vytvořme tedy nového pejska a podívejme se, co funkce __init__ udělá.

pes1 = Pes("Azor", 3)
pes2 = Pes("Alík", 4)

Vytvořili jsme dva pejsky, Azora a Alíka. Po zavolání Pes("Azor", 3) Python vytvoří nový objekt (říkáme též instanci třídy) podle šablony (třídy) Pes. Poté, na prázném objektu Pes zavolá funkci __init__ z dané třídy, která vyplní prázdná pole name a age předanými hodnotami. Této funkci říkáme konstruktor.

Pokud bychom chtěli zjistit jméno a věk nějakého psa, můžeme toho docílit pomocí pes1.name nebo pes1.age.

Chceme nyní napsat funkci stekni, které předáme objekt nějakého psa a vypíše jeho jméno a text “haf”. Rozhodne bychom mohli tuto funkci napsat a volat takto:

def stekni(pes):
    print(f"{pes.name}: HAF!")

stekni(pes1)
stekni(pes2)

To ale není pohodlné, protože funkce existuje mimo definici třídy, tedy může být úplně na druhém konci souboru. To lze vyřešit rozšířením definice třídy právě o funkci stekni. Celá třída pak bude vypadat takto:

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")


pes1 = Pes("Azor", 3)
pes2 = Pes("Alík", 4)

pes1.stekni()
pes2.stekni()

Argument pes jsme přejmenovali na self, a to čistě z konvence: dáváme tím jasně najevo, že čteme objekt, na kterém jsme funkci zavolali.

Funkce, které definujeme na třídách, budeme odteď nazývat metody (anglicky method).

Tímto jsme popsali část první důležité vlastnosti, kterou nám třídy nabízí – zapouzdření (anglicky encapsulation). Všechny datové položky (jméno, věk) a metody jsou zabalené do jedné logické jednotky (třídy). K zapouzdření se ještě vrátíme v některé z dalších kapitol.


Podívejme se na další důležitou vlastnost tříd – dědičnost (anglicky inheritance). Ta nám umožňuje určit “předka” třídy, ze které třída nová zdědí všechny atributy a metody. Vytvoříme novou třídu Stene, která bude dědit ze třídy Pes:

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")

class Stene(Pes):
    ...

s1 = Stene("Azor", 3)
s1.stekni()

Třídu Stene teď můžeme používat úplně stejně, jako třídu Pes. Ve třídě Stene uděláme úpravu, a to přidání nového atribudu training_level.

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

s1 = Stene("Azor", 3, 0)
s1.stekni()

Lehce krkolomná konstrukce super().__init__(...) pouze volá konstruktor předka (kterého získáme pomocí super()). Ve třídě, která dědí z nějaké třídy, se vždy můžeme na předka odkázat. Zavolat konstruktor předka je důležité; kdybychom tak neučinili, atributy ze třídy Pes by se neinicializovaly a neměly by hodnotu.

Teď bychom chtěli změnit chování metody stekni v třídě Stene – stěňata přeci štěkají slaběji. Vytvoříme novou verzi metody stekni v třídě Stene:

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

    def stekni(self):
        print(f"{self.name}: haf.")

Tímto jsme přepsali metodu stekni ve třídě Stene, tradičně se tomuto řiká anglickým slovem override. Všechna štěňata odteď budou štěkat slabějším hláskem, než dospělí psi.

K čemu to vlastně je? Představme si, že máme pole psů a štěňat – tedy náš útulek. Pro každé zvíře bychom chtěli zavolat metodu stekni.

utulek = [
    Stene("Azor", 3, 0),
    Pes("Bella", 5),
    Stene("Cyril", 2, 1),
    Pes("Diana", 6)
]

for zvire in utulek:
    zvire.stekni()

Zde se na chvíli zastavíme: procházíme pole utulek a na každém prvku voláme metodu stekni, i když jsou ale prvky rozdílného typu (některé jsou instance třídy Stene, jiné instance třídy Pes). To je ale v pořádku, protože víme, že obě třídy obsahují metodu stekni.


V posledním odstavci jsme se setkali se třetí a poslední důležitou vlastností tříd, které říkáme polymorfismus. Do češtiny bychom toto slovo mohli přeložit jako mnohotvárnost. Říká nám, že třídy, které dědí z nějaké třídy (předka), mohou být použity na stejných místech, jako předek samotný.

To dává smysl, protože děděním můžeme vlastnosti a metody pouze přidávat nebo upravovat, nikoli mazat. I když se mohou metody chovat jinak, kontrakt (tedy definice metody ve třídě) je stále stejný, a můžeme ho bezpečně volat.

Třída II

V minulé kapitole jsme si představili třídy a tři hlavní principy – enkapsulace, dědičnost a polymorfismus – kterými se řídí. Nyní je čas podrobněji rozebrat všechny tři.

Enkapsulace

Metody ve třídách jsou funkce, které můžeme na objektech dané třídy zavolat. Co kdybychom ale chtěli metodu skrýt tak, aby byla volatelná jen v rámci třídy, nejen na objektech této třídy? K tomu slouží metody, kterým říkáme private.

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __stekni(self):
        print(f"{self.name}: HAF!")

    def pozdrav(self):
        self.__stekni()

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

    def __stekni(self):
        print(f"{self.name}: haf.")

    def __sedni(self):
        ...

    def pozdrav(self):
        self.__stekni()
        self.__sedni()

V Pythonu zavádíme private metody pomocí dvou podtržítek (__) před názvem metody. Tyto metody nejsou volatelné mimo jiné metody třídy. Slouží pro interní použití třídy, třeba k rozdělení nějaké velké funkce na menší části (jako v případě metody pozdrav, která u třídy Pes volá dvě private metody).

Private můžou být i atributy třídy, vytvoříme je v konstruktoru pomocí self. za použití dvou podtržítek (__).

Dědičnost

Když jsme pracovali se třídami Pes a Stene, stavěli jsme třídu Stene nad třídou Pes, a třída Stene zdědila všechny metody a datové položky třídy Pes.

Občas ale chceme, aby předek neměl žádné metody ani datové položky, ale jen tvořil společného předka pro všechny potomky. Zároveň ale tímto předkem určit rozhraní, tedy metody, které musí být na všech potomcích implementované. K tomu slouží abstraktní třídy (anglicky abstract class).

Uvažme třídu Tvar: to je nějaká abstraktní věc, která může popisovat nekonečně mnoho různých věcí, ale třeba kruh nebo čtverec tvarem bez problému nazvat můžeme. Pokud uvažujeme normálně se chovající tvary, můžeme u nich spočítat obsah a obvod. Toto bude naše rozhraní, které budeme chtít implementovat u všech potomků třídy Tvar.

from abc import ABC, abstractmethod

class Tvar(ABC):
    @abstractmethod
    def obsah(self):
        pass

    @abstractmethod
    def obvod(self):
        pass

Vytvořili jsme abstraktní třídu Tvar, která dědí z nějaké další třídy ABC.1 Definovali jsme také dvě metody obsah a obvod, které jsme označili jako abstraktní metody pomocí @abstractmethod. Klíčové slovo pass zde znamená, že metoda není implementována; pass samo o sobě nic nedělá, ale umožňuje nám definovat prázdné funkce. Místo pass můžeme použít tři tečky ....

Definujme třídy Kruh a Ctverec, které dědí z třídy Tvar a implementují metody obsah a obvod.

from abc import ABC, abstractmethod
import math

class Tvar(ABC):
    @abstractmethod
    def obsah(self):
        pass

    @abstractmethod
    def obvod(self):
        pass


class Kruh(Tvar):
    def __init__(self, r):
        self.r = r

    def obsah(self):
        return math.pi * self.r**2

    def obvod(self):
        return 2 * math.pi * self.r

class Ctverec(Tvar):
    def __init__(self, a):
        self.a = a

    def obsah(self):
        return self.a * 2

    def obvod(self):
        return 4 * self.a

Zde jde pěkně vidět použítí abstraktní třídy. Kruh je definovaný poloměrem r, ale čtverec je definovaný úplně něčím jiným (délkou strany a). Pro oba tvary však můžeme vypočítat obvod a obsah, proto dává smysl toto “rozhraní” zadefinovat pomocí abstraktní třídy. Konkrétní třídu jakožto předka zde použít nemůžeme kvůli důvodu z první věty.

Polymorfismus

Pracovali jsme s příkladem psů a štěnat:

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

    def stekni(self):
        print(f"{self.name}: haf.")

Polymorfismus nám říká, že tam, kde můžeme použít objekt třídy Pes, můžeme použít i objekt třídy Stene, protože třída Stene dědí od třídy Pes. Toto je specifický druh polymorfismu, kterému říkejme dědičný polymorfismus.

V Pythonu existuje ještě jeden druh polymorfismu, který se jmenuje duck typing. Název pochází z anglického přísloví “if it walks like a duck and quacks like a duck, it must be a duck” (doslovně přeloženo jako “pokud to chodí jako kachna a kváká jako kachna, musí to být kachna”).

Duck typing nevyžaduje dědičnost, ale stále nám v našem případě umožňuje použít objekt třídy Stene jako objekt třídy Pes.

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")

class Stene:
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

    def stekni(self):
        print(f"{self.name}: haf.")

utulek = [
    Stene("Azor", 3, 0),
    Pes("Bella", 5),
    Stene("Cyril", 2, 1),
    Pes("Diana", 6)
]

for zvire in utulek:
    zvire.stekni()

Všimněte si, že třída Stene již nedědí od třídy Pes, ale tento kód stále funguje. Je to z toho důvodu, že Python ve skutečnosti nehledí na dědičnost, ale jen na to, jak volaná metoda stekni vypadá. V našem případě nebere žádný argument a má stejné jméno a Python s voláním této metody nemá problém.

Ač duck typing není vyloženě antivzor, budeme se snažit co nejvíce používat polymorfismus založený na dědičnosti.


  1. Zkratka ABC ve skutečnosti znamená “abstract base class”, což je přeložitelné jako základní třída pro všechny abstraktní třídy.