... ale báli jste se zeptat na hodině ;)
from utils import TableOfContents
TableOfContents("python_crash_course.ipynb")
Objekty¶
Programování v Pythonu spočívá v manipulaci s různými objekty -- čísli, řetězci (písmen), seznamy, funkcemi... Zastřešující pojem "objekt" naznačuje analogii s fyzickými předměty. Každý předmět slouží k něčemu jinému, něco s ním lze udělat jednoduše, něco hůř, něco vůbec; taky případně může obsahovat další předměty. Podobně v Pythonu: čísla se hodí na sčítání, ale nemůžeme změřit jejich délku; a seznam jakožto objekt je vlastně kolekcí dalších objektů, jeho prvků.
Když Python spustíte, načte se poměrně velké množství zabudovaných objektů (funkcí a konstant), se kterými můžete rovnou začít pracovat. Např. funkce sorted
vytvoří z libovolného iterovatelného objektu (= objektu, ze kterého lze jeden po druhém tahat nějaké jeho dílčí prvky) seřazený seznam:
# cokoli za znakem # na jakémkoli řádku Python ignoruje,
# jsou to tzv. komentáře, které slouží jen lidem
sorted
(Seznam všech zabudovaných funkcí -- built-in functions -- v Pythonu)
Konstanta True
zas označuje "pravdu" (v logickém smyslu)...
True
... takže ji Python vrátí jako výslednou hodnotu při vyhodnocení pravdivého výroku:
2 + 2 == 4
Ale další objekty si můžeme libovolně vytvářet.
# čísla
42
# řetězce
"Hello, world!"
Proměnné¶
Abychom s objekty mohli dál pracovat, je potřeba je nějak pojmenovat, jinak na ně nemůžeme odkazovat. Štítek se jménem můžeme na objekt pověsit pomocí přiřazovacího operátoru =
.
vzdálenost = 10
čas = 20
rychlost = vzdálenost / čas
rychlost
Těm štítkům se jmény se říká proměnné, protože je lze libovolně kdykoli pověsit na jiný objekt.
vzdálenost = 5
rychlost = vzdálenost / čas
rychlost
Jsou i jiné způsoby, jak pověsit štítek se jménem na nějaký objekt. Např. když teď vytvoříme funkci a pojmenujeme ji rychlost
, tak štítek rychlost
strhneme z čísla 0.25, na které jsme ho naposledy navěsili, a nalepíme ho na tu funkci (která je z hlediska Pythonu zase jen dalším typem objektu, jako čísla, řetězce atp.).
def rychlost(vzdálenost, čas):
return vzdálenost / čas
rychlost
Nyní tedy už proměnná rychlost
neodkazuje na číslo 0.25, ale na funkci, kterou jsme právě vytvořili.
Funkci můžeme zavolat, tj. spustit "recept" (sérii kroků, malý dílčí program), který je v ní uložený:
rychlost(vzdálenost, čas)
Nic nebrání tomu, abychom na existující objekty navěsili štítků víc, a můžeme k tomu použít stávající štítky:
distance = vzdálenost
time = čas
speed = rychlost
speed(distance, time)
Jak to funguje? Python nejprve vyhodnotí výraz napravo od operátoru =
, tj. v tomto případě jen dohledá objekty, na které odkazují původní proměnné, a pak na ty samé objekty navěsí další nové štítky (uvedené nalevo od operátoru =
).
Všimněte si, že jsme tímto způsobem jednoduše přiřadili další jméno i funkci rychlost
:
speed
To, zda dvě jména (dvě proměnné) odkazují na ten samý objekt, jednoduše zjistíme pomocí operátoru is
:
distance is vzdálenost
distance is čas
distance = čas
distance is čas
speed is rychlost
POZN.: Rovnost (x == y
) a identita (x is y
) jsou dvě různé věci. Dva různé objekty (z hlediska identity) jsou si rovné, pokud reprezentují tu samou hodnotu. Např. dva různé seznamy čísel od jedné do tří:
x = [1, 2, 3]
y = [1, 2, 3]
x == y
x is y
Je to podobně jako s jednovaječnými dvojčaty: vypadají stejně, takže třeba pro účel focení jsou zaměnitelná, ale to neznamená, že je to ten samý člověk.
Ještě pár slov ke konvencím pojmenovávání proměnných v Pythonu:
- u drtivé většiny proměnných (vč. funkcí) je zvykem používat jen malá písmena, např.
proměnná
- občas se používají i číslice, např.
x1
, ale jméno proměnné číslicí nesmí začínat - když pojmenování sestává z více slov, oddělují se pomocí podtržítka, např.
víceslovná_pojmenování
(tzv. snake case) - proměnné, které se používají napříč celým programem (často je definujeme na začátku programu a pak na ně odkazujeme v růzých jeho částech), se někdy odlišují pomocí velkých písmen, např.
ABECEDA
neboRYCHLOST_SVĚTLA
- jména tříd (viz níže) jsou většinou v tzv. camel case s velkým počátečním písmenem, např.
Třída
čiJináTřída
Jméno proměnné by nemělo být moc dlouhé, ale zároveň by mělo být deskriptivní -- mělo by srozumitelně naznačovat, k čemu proměnná slouží, jak se používá. Hezky a trefně pojmenovat proměnnou je často těžší, než by se na první pohled mohlo zdát. Když píšete program na jedno použití, je to jedno, ale pokud po sobě budete program ještě někdy číst (nebo někdo jiný), budete za každou smysluplně pojmenovanou proměnnou (a každý vhodně umístěný komentář) vděční.
Typy a třídy¶
Každý objekt v Pythonu spadá do nějaké kategorie objektů -- např. číslo, řetězec, seznam apod. -- podobně jako předměty v reálném světě spadají do různých kategorií (všechny kočky do kategorie "kočka", všechny tužky do kategorie "tužka" apod.). Těmto kategoriím s v Pythonu říká typy a u libovolného objektu lze zjistit jeho typ pomocí funkce type
:
obj = 1
type(obj)
obj = "ahoj"
type(obj)
obj = [1, "ahoj"]
type(obj)
type(1) == type(2)
type("a") == type("b")
# ovšem pozor
type("1")
type(1) == type("1")
Typy jako int
, str
nebo list
jsou v Pythonu přímo zabudované, jsou k dispozici vždycky. Ale každý programátor si může definovat vlastní typy a dále s nimi pracovat. Těmto uživatelsky definovaným typům se říká třídy, podle klíčového slovíčka class
, pomocí něhož se definují:
class FooBar:
# klíčové slovíčko pass je prázdný příkaz (Python na jeho základě nic
# nevykoná)
pass
Instanci třídy, tj. konkrétní objekt, jehož typem bude daná třída, vytvoříme tak, že zavoláme tzv. konstruktor, tj. funkci, která nám instanci "postaví", "zkonstruuje". Počáteční sestavení a nastavení objektu někdy nazýváme inicializací. Jako konstruktor většinou slouží třída samotná, kterou zavoláme jako funkci:
obj = FooBar()
type(obj)
type(obj) is FooBar
Oproti tomu instance zabudovaných typů můžeme vytvářet i pomocí speciálních zápisů, tzv. literálů, nepotřebujeme k tomu nutně funkce. V konstruktoru je proces vytváření objektu svým způsobem skrytý, není na první pohled jasné, co se v něm odehrává (obecně může konstruktorem být libovolná funkce), kdežto v literálu doslova (angl. literally) prostě jen vypíšeme, jak má nějaký objekt vypadat, a Python nám ho podle toho poskládá:
# číselný literál
42
# řetězcový literál
"Hello, world!"
Literály jsou součástí syntaxe Pythonu, tj. pravidel pro skládání programů tak, aby jim Python rozuměl a mohl je vykonávat. Důležité jsou ještě literály pro vytváření kolekcí (viz odd. Kolekce).
Chceme-li ověřit, zda je nějaký objekt instancí nějaké dané třídy či obecněji typu, můžeme použít funkci isinstance
.
isinstance(obj, FooBar)
isinstance(1, int)
Třídy se hodí pro některé pokročilejší programátorské účely (viz např. notebook object_oriented.ipynb
). V tomto semestru se jim blíže nevěnujeme.
Importování knihoven¶
Knihovna (též balíček, angl. package) je v programování soubor funkcí, tříd atp., které jsou často užitečné, takže nemá smysl, aby si je každý programátor implementoval znovu a znovu, když je potřebuje. Zároveň ale nejsou potřeba úplně pořád, takže taky nemá smysl, aby byly do Pythonu přímo zabudované (tj. aby byly k dispozici pokaždé, když Python spustíme, podobně třeba jako funkce sorted
). Vždy jsou ale na dosah ruky, stačí je importovat.
Např. knihovna random
obsahuje funkcionalitu související s generováním náhodných čísel:
import random
# následujícímu příkazu není potřeba věnovat přílišnou
# pozornost, jen zajistí, abychom pokaždé dostali stejnou
# sérii pseudonáhodných čísel; viz:
# https://docs.python.org/3/library/random.html#random.seed
random.seed(42)
K objektům, které knihovna definuje, se pak dostaneme přes tečku. Např. pokud chceme pomocí funkce randint
vygenerovat náhodné číslo mezi jednou a deseti:
random.randint(1, 10)
Při importu si lze knihovnu přejmenovat pomocí klíčového slovíčka as
, např. pokud jméno random
už v programu používáme pro něco jiného.
random = "random. angl. náhodný"
import random as rnd
rnd.randint(1, 10), random
Taky je možné si z knihovny naimportovat přímo jeden konkrétní objekt (funkci, třídu atp.):
from random import randint
randint(1, 10)
To se hodí zejm. v případě, kdy objekt budeme používat často a nechce se nám pokaždé zdlouhavě psát jméno_knihovny.jméno_objektu
. Pokud chceme, můžeme si jich rovnou naimportovat i víc zároveň:
from random import random, randint
random(), randint(1, 10)
Větší knihovny sestávají z dílčích modulů, které při importu oddělujeme tečkami.
import os.path
this_file = "python_crash_course.ipynb"
os.path.isfile(this_file)
import os.path as osp
osp.realpath(this_file)
from os.path import splitext
splitext(this_file)
Funkce¶
Funkce je jako recept -- série příkazů, malý dílčí program, který má jméno, pomocí něhož ho můžeme kdykoli spusit, tzv. zavolat. Funkci definujeme pomocí klíčového slovíčka def
...
def hoď_kostkou(): # hlavička
return randint(1, 6) # tělo
... a voláme tak, že napíšeme její jméno a za ně kulaté závorky:
hoď_kostkou()
Recept uložený ve funkci se vykonává krok po kroku (řádek po řádku), dokud Python nenarazí na klíčové slovíčko return
. V tu chvíli funkci ukončí a hodnotu výrazu, který se nachází za return
, vrátí jako výsledek celé funkce. Tento výsledek můžeme uložit do proměnné:
výsledek_hodu_kostkou = hoď_kostkou()
výsledek_hodu_kostkou
Do těla funkce patří vše, co následuje po hlavičce a je odsazené alespoň o jednu úroveň dál než samotná hlavička:
def function():
print("Tohle patří do těla funkce.")
print("Tohle taky.")
print("Tohle už ne.")
function()
Funkce často mají jeden nebo více parametrů či argumentů, tj. proměnných, které deklarujeme v hlavičce funkce. Např. následující funkce má parametry počet_stěn
a násobek
.
def hoď_kostkou_a_vynásob(počet_stěn, násobek):
return randint(1, počet_stěn) * násobek
Když funkci voláme, je potřeba za parametry dosadit konkrétní objekty, které funkce pak v rámci receptu použije:
hoď_kostkou_a_vynásob(20, 100)
POZN.: Přísně vzato bychom měli takto rozlišovat mezi (formálními) parametry (= proměnnými, se kterými pracujeme při definici funkce a které v tu chvíli nereprezentují žádnou konkrétní hodnotu) a (konkrétnimi) argumenty (= konkrétními objekty, které funkci předáme ve chvíli, kdy ji voláme). Nicméně často tento rozdíl není příliš důležitý, takže v praxi se oba termíny používají zaměnitelně.
Parametry mohou mít defaultní hodnotu:
def hoď_kostkou_a_vynásob(počet_stěn, násobek=100):
return randint(1, počet_stěn) * násobek
Pokud nám defaultní hodnota daného parametru vyhovuje, nemusíme ho pak při volání specifikovat:
hoď_kostkou_a_vynásob(20)
Pokud nám naopak nevyhouje, nic nám v tom nebrání:
hoď_kostkou_a_vynásob(20, 0.01)
Parametrům bez defaultní hodnoty se říká povinné (je potřeba je specifikovat vždy), parametrům s defaultní hodnotou pak volitelné.
Parametry můžeme funkci předávat buď na základě pořadí (tzv. pozičně)...
# počet_stěn = 4, násobek = 10
hoď_kostkou_a_vynásob(4, 10)
# počet_stěn = 10, násobek = 4
hoď_kostkou_a_vynásob(10, 4)
... nebo pomocí jejich jmen (pak hovoříme o pojmenovaných argumentech, angl. named nebo též keyword arguments):
hoď_kostkou_a_vynásob(počet_stěn=4, násobek=10)
# v tomto případě na pořadí nezáleží
hoď_kostkou_a_vynásob(násobek=10, počet_stěn=4)
Oba způsoby můžeme i kombinovat, dokonce je to velmi časté. V Pythonu je většinou zvykem, že se povinné parametry předávají pozičně (bývá jich málo a je potřeba je zadávat pokaždé, takže si člověk jejich pořadí zapamatuje) a volitelné parametry jsou pojmenované (může jich být mnoho a používají se příležitostně, takže si člověk jejich pořadí nezapamatuje; navíc často chceme specifikovat jen jeden a ostatním nechat defaultní hodnotu). V případě naší funkce by to vypadalo takto:
hoď_kostkou_a_vynásob(4, násobek=10)
Lokální vs. globální proměnné¶
Parametry plus jakékoli další proměnné, které v rámci funkce vytvoříme, jsou soukromé, dostupné čistě jen funkci -- jsou to tzv. lokální proměnné. Též hovoříme o tom, že tyto proměnné mají lokální dosah (angl. local scope). To znamená, že s nimi můžeme pracovat pouze v rámci těla funkce.
def function(parameter):
local_variable = 1
print("My parameter:", parameter)
print("My local variable:", local_variable)
function(0)
Mimo tělo funkce function
nejsou proměnné parameter
a local_variable
definované:
parameter
local_variable
TIP: Naučit se číst chybové hlášky je k nezaplacení, Python se vám s jejich pomocí snaží ze všech sil poradit, v čem je problém. Zkuste si každou chybovou hláškou pečlivě přečíst, zamyslet se nad ní, pochopit, v čem je problém a kde přesně nastal.
Funkce mohou taky pracovat s globálními proměnnými, tj. s proměnnými definovanými samostatně, mimo nějaké funkce či třídy.
global_variable = 2
def function(parameter):
local_variable = 1
print("My parameter:", parameter)
print("My local variable:", local_variable)
print("My global variable:", global_variable)
function(0)
Taková funkce se ale snadno může rozbít -- stačí dotyčnou globální proměnnou smazat...
del global_variable
... a když funkci příště zavoláme, tak zkolabuje:
function(0)
Snad ještě horší je to, že u takové funkce není možné jen na základě jejího volání odhadnout, co přesně udělá:
global_variable = 512
function(0)
global_variable = 1024
function(0)
V obou případech funkci voláme jako function(0)
, ale výsledek je pokaždé trochu jiný.
Mnohem lepší je tedy psát funkce tak, aby používaly jen lokální proměnné a nebyly závislé na těch globálních. Nejde to vždycky, ale většinu času ano. Místo přímého odkazování na globální proměnné je lepší dát funkci více parametrů, přes něž jí můžeme globální proměnné zprostředkovaně předávat, což je mnohem bezpečnější a přehlednější.
def function(parameter1, parameter2):
local_variable = 1
print("My first parameter:", parameter1)
print("My local variable:", local_variable)
print("My other parameter:", parameter2)
global_variable = 2
function(0, global_variable)
print vs. return¶
Jaký je rozdíl v tom, když zavolám následující dvě funkce?
def funkce1():
print(1)
def funkce2():
return 1
funkce1()
funkce2()
V obou případech se mi zobrazí číslo 1, v druhém je navíc vedle něj číslo v hranatých závorkách a dvojtečka. V čem se to liší?
Rozdíl se lépe projeví, když se pokusíme výsledek funkce uložit do proměnné:
výsledek1 = funkce1()
výsledek1
výsledek2 = funkce2()
výsledek2
funkce1
číslo 1 jen v rámci svého běhu vytiskne na obrazovku, kdežto funkce2
ho vrátí jako svůj výsledek, takže ho můžeme uložit do proměnné a dál s ním pracovat. Možná pomůže, když se podíváme na funkci, která jedno čísle tiskne a jiné vrací:
def funkce3():
print(1)
return 2
výsledek3 = funkce3()
výsledek3
Zbývá tedy jen otázka: co vrací funkce1
? Nic, když vůbec ani neobsahuje klíčové slovíčko return
? Svým způsobem ano, ale i nic je v Pythonu něco ;) Což zní krypticky, ale je to jednoduché: Python má speciální objekt (konstantu) None
, která reprezentuje "nic". Protože None
je nic, tak se vám po vyhodnocení ani neukáže:
None
A kdykoli nějaká funkce doběhne na konec svého receptu, aniž by potkala return
, tak implicitně automaticky vrátí jako svůj výsledek None
.
Vím, že je trochu záludné, že ono None
není "vidět", ale v praxi si můžeme to, že funkce1
skutečně vrátila None
, lehce ověřit:
výsledek1 is None
Pokročilé způsoby předávání argumentů¶
V Pythonu lze také vytvořit funkce, které pracují s libovolným počtem pozičních nebo pojmenovaných argumentů. Slouží k tomu speciální operátory *
a **
:
def flexibilní_funkce(
poziční_argument,
*zbytek_pozičních_argumentů,
pojmenovaný_argument,
**zbytek_pojmenovaných_argumentů
):
print("poziční argument:", poziční_argument)
print("zbytek pozičních argumentů:", zbytek_pozičních_argumentů)
print("pojmenovaný argument:", pojmenovaný_argument)
print("zbytek pojmenovaných argumentů:", zbytek_pojmenovaných_argumentů)
flexibilní_funkce(1, 2, 3, 4, a=5, b=6, pojmenovaný_argument=7, c=8)
Parametr *zbytek_pozičních_argumentů
sesbírá zbylé poziční argumenty, sestaví z nich n-tici a tu uloží do proměnné zbytek_pozičních_argumentů
; podobně parametr **zbytek_pojmenovaných_argumentů
sesbírá zbylé pojmenované argumenty, sestaví z nich slovník a uloží ho do proměnné zbytek_pojmenovaných_argumentů
(víc o n-ticích a slovnících viz odd. Kolekce).
Jak vidno, můžeme tyto speciální parametry pojmenovat libovolně, speciální chování jim propůjčuje operátor *
, resp. **
. Nicméně konvenčně se v Pythonu používají jména *args
a **kwargs
(jako keyword arguments).
A aby toho nebylo málo, funguje to i "naopak": máme-li seznam/n-tici (nebo slovník) a chceme jeho položky předat funkci jako poziční (nebo pojmenované) argumenty, můžeme taky využít operátoru *
(nebo **
):
def nudná_funkce(a, b):
return a + b
poziční_argumenty = [1, 2]
nudná_funkce(*poziční_argumenty)
pojmenované_argumenty = {"a": 1, "b": 2}
nudná_funkce(**pojmenované_argumenty)
U pozičních argumentů musí pochopitelně sedět počet...
poziční_argumenty = [1, 2, 3]
nudná_funkce(*poziční_argumenty)
... a u pojmenovaných jména:
pojmenované_argumenty = {"a": 1, "c": 2}
nudná_funkce(**pojmenované_argumenty)
Metody¶
Metody jsou funkce úzce spjaté s objekty. Seznam metod, které daný objekt podporuje, získáme tak, že napíšeme jméno proměnné, která objekt obsahuje, za ně tečku a stiskneme tabulátor:
obj.<TAB>
obj = "ahoj"
obj.upper()
Formálně je metoda jen funkce "navěšená" na třídě:
class FooBar:
def get_my_type(self):
return type(self)
obj = FooBar()
obj.get_my_type()
První argument metody (self
) odkazuje na instanci třídy, na které jsme metodu zavolali, tj. v příkladu výše na ten samý objekt, na který odkazuje proměnná obj
. Na rozdíl od případných dalších argumentů se metodě předává pomocí propojení přes tečku, ne v závorkách za metodou, nepíšeme tedy obj.get_my_type(obj)
.
Čísla¶
V Pythonu jsou dva základní typy čísel:
- celá čísla (angl. integer) -- typ
int
- reálná čísla, reprezentovaná pomocí tzv. pohyblivé řádové čárky (angl. floating point) -- typ
float
Literály celých čísel vypadají např. takhle:
3
type(3)
-5
9543761
Pro lepší čitelnost mohou obsahovat i podtržítka:
9_543_761
Literály reálných čísel se většinou poznají podle toho, že obsahují desetinnou čárku... tedy tečku protože je to podle angličtiny:
3.14
type(3.14)
-2.72
0.1
# nulu můžeme vynechat
.1
Ovšem ne nutně -- Python má speciální syntax pro tzv. vědecký zápis čísel, a čísla zapsaná tímto způsobem jsou vždycky typu float
, i když desetinnou tečku neobsahují.
# 1 × 10³
1e3
# 1 × 10⁻³
1e-3
# 2.34 × 10²
2.34e2
Kromě celých a reálných čísel disponuje Python i komplexními čísly:
3 + 4j
type(3 + 4j)
Jestli jste o nich nikdy neslyšeli, tak je zase pusťte z hlavy :)
Kromě desítkové (decimální) soustavy, na kterou jsme všichni zvyklí, mají celá čísla v Pythonu literály i v soustavách jiných. Rozpoznáme je pomocí prefixů:
0b
→ binární (dvojková) soustava -- používá číslice 0 a 10o
→ oktální (osmičková) soustava -- používá číslice 0–70x
→ hexadecimální (šestnáctková) soustava -- používá číslice 0–9 a a–f
0b11010
0o32
0x1a
Jak vidíme, převod na desítkovou soustavu je jednoduchý -- stačí vyhodnotit literál v soustavě jiné a Python nám ho v odpovědi převede do desítkové. K převodu opačným směrem existují funkce, které nám vrátí řetězec obsahující zápis čísla v požadované soustavě:
bin(26)
oct(26)
hex(26)
S čísly jdou provádět různé výpočty pomocí následujících operátorů, které snad díky zkušenosti s kalkulačkou budou působit převážně povědomě.
# sčítání
3 + 4
# odečítání
3 - 4
# násobení
3 * 4
# exponenciace
2**3
# dělení
5 / 3
U dělení si všimněte, že výsledek je vždy float
, i když dělíme dva int
y a výsledkem je něco, co my lidé chápeme jako celé číslo:
4 / 2
# celočíselné dělení
4 // 2
5 // 3
# modulo (= zbytek po celočíselném dělení)
5 % 3
Když chceme zároveň celočíselné dělení i zbytek po něm, můžeme použít zabudovanou funkci divmod
.
divmod(5, 3)
divmod(5, 3) == (5 // 3, 5 % 3)
To se hodí např. při časových převodech -- kolik je 143 vteřin minut?
divmod(143, 60)
→ 2 minuty a 23 vteřin.
Zabudovaná funkce abs
vrátí absolutní hodnotu čísla:
abs(-4.1)
Pokud máme nějakou kolekci čísel (viz odd. Kolekce), můžeme identifikovat nejmenší, resp. největší z nich pomocí zabudovaných funkcí min
a max
.
from math import inf
# inf reprezentuje nekonečno
inf
čísla = [0, -inf, 3.14, -2.72, inf]
min(čísla)
max(čísla)
Operátory mají různou prioritu, stejně jako v matematice:
2**3 + 4 * 5
Pokud si nejsme pořadím operací jisti, můžeme ho pro jistotu specifikovat pomocí kulatých závorek:
(2**3) + (4 * 5)
Nebo pochopitelně i změnit:
2**(3 + 4) * 5
Všechny výše uvedené typy i zápisy čísel můžeme při výpočtech libovolně kombinovat, jen je potřeba mít na paměti, že jakmile se nám jako dílčí výsledek objeví float
, i celkový výsledek bude float
.
-0x1f + 165 * -0b1001 + 1.0
Ke konverzi z int
u na float
slouží funkce float
:
i = 3
float(i)
Dokáže též převést textový zápis čísla (řetězec) na reálné číslo:
float("3.14")
float("3")
Analogicky funguje funkce int
:
int(2.0)
int("2")
int(2.72)
Všimněte si, že funkce int
prostě jen uřízne desetinná místa. Pokud chceme zaokrouhlit podle matematických zvyklostí, je potřeba použít funkci round
:
round(2.72)
Při počítání s float
y je nutná jistá obezřetnost. Kvůli tomu, jak jsou v paměti počítače reprezentovány, není jejich zachycení úplně přesné, a některé výsledky tak můžou být překvapivé...
.1 + .2
... což vede až k tomu, že některé očekávané rovnosti neplatí:
.1 + .2 == .3
Při porovnávání float
ů je bezpečnější místo operátoru ==
(striktní rovnosti) používat funkci isclose
z knihovny math
(přibližná rovnost).
from math import isclose
isclose(.1 + .2, .3)
S číselnými proměnnými často narážíme na následující situaci: vytvoříme proměnnou...
i = 0
i
... a následně ji pak pomocí nějaké aritmetické operace potřebujeme aktualizovat na základě staré hodnoty -- např. přičíst k původnímu číslu nějaké nové a výsledek znovu uložit do té samé proměnné:
i = i + 2
i
Python umožňuje zápis i = i + x
zkrátit na i += x
, abychom nemuseli psát i
dvakrát:
i += 7
i
Tento zkrácený zápis funguje s libovolným operátorem.
i **= 2
i
Řetězce¶
Řetězce (angl. strings, v Pythonu typ str
) nám umožňují reprezentovat text jako uspořádanou sérii (řetězec) znaků. Literály řetězců mají různé varianty, vždy jsou ale ohraničené jednoduchými nebo dvojitými uvozovkami.
"hello"
'hello'
type("hello")
Chceme-li do řetězce uvozeného jednoduchými (dvojitými) uvozovkami vložit jednoduchou (dvojitou) uvozovku, je potřeba zrušit její speciální význam "ukončovač řetězce". K tomu slouží zpětné lomítko. Angl. se říká the backslash escapes the next character.
"\"Hello,\" I said."
'\'Hello,\' I said.'
Někdy je ale jednodušší prostě jen prostřídat druh uvozovek, které slouží k delimitaci řetězce, jak nám to ve svých odpovědích naznačuje sám Python.
Zpětné lomítko ale neslouží jen k rušení speciálního významu uvozovek -- jiným znakům speciální význam naopak dodává. Slouží tak spíš jako přepínač mezi doslovným a speciálním významem. Speciálním sekvencím znaků, které tvoří zpětné lomítko + jeden či více dalších znaků a mají jiný než doslovný význam, se angl. říká escape sequences.
Např. sekvence \n
se promění ve znak nového řádku, \t
pak ve znak tabulátoru:
"a\t1\naa\t11"
Na první pohled to tak nevypadá, ale to je jen kvůli tomu, že nás Python chce na tyto speciální znaky upozornit (což je dobře), takže když řetězec jen zobrazuje, reprezentuje tyto znaky pomocí dobře čitelných speciálních sekvencí. Aby se nové řádky a tabulátory projevily, musíme řetězec vytisknout:
print("a\t1\naa\t11")
Což odpovídá zamýšlené podobě, ale je mnohem těžší poznat, co je v řetězci skutečně za znaky. Úplně jiný řetězec může totiž vytištěný vypadat zcela identicky:
print("a 1 \naa 11 ")
Speciální sekvence existují též např. pro vložení libovolného unicodového znaku, ve formátu \uXXXX
nebo \UXXXXXXXX
, kde XXX...
je hexadecimální zápis pořadového čísla znaku v unicodové tabulce:
"asi nějaký čínský znak...? \u4e3e"
Pořadové číslo znaku lze v Pythonu získat pomocí funkce ord
...
ord("č")
... takto získáme jeho hexadecimální zápis...
hex(269)
... a ten pak můžeme použít ve speciální sekvenci:
"tohle by mělo být č: \u010d"
Inverzní funkcí k funkci ord
je funkce chr
-- dáme jí pořadové číslo znaku a ona nám vrátí odpovídající znak.
chr(269)
chr(0x10d)
Více o znacích, Unicodu a kódování textu obecně viz samostatný notebook unicode.ipynb
.
Python pozná, když zpětné lomítko nepředchází znaku či znakům, s nímž/nimiž by tvořilo speciální sekvenci, a vloží na takovém místě doslovné zpětné lomítko, ale zároveň nám jemně naznačí, že by byl radši, kdybychom speciální přepínací význam zpětného lomítka v literálu explicitně vypnuli... pomocí zpětného lomítka:
"a \ b"
print("a \ b")
print("a \\ b")
"a \ b" == "a \\ b"
Z toho mj. plyne, že když chceme zpětných lomítek napsat víc za sebou, je jich potřeba vždy dvojnásobek:
"a \\\\ b"
print("a \\\\ b")
Podobné literály začnou brzy vypadat nepřehledně. Člověk pak zatouží všechny speciální sekvence zrušit, jen aby všechny znaky fungovaly jednoduše doslovně. I to je možné, pomocí tzv. surových řetězců (angl. raw strings). V nich je zpětné lomítko jen další znak:
r"\t \\\\ \n"
print(r"\t \\\\ \n")
Surové řetězce se velmi dobře hodí pro práci s regulárními výrazy (viz notebook regex.ipynb
), kde se lomítka často používají a hodí se nemuset přemýšlet o tom, jestl nám je Python nějak nepomíchá.
Formátovací řetězce (angl. format strings) umožňují pomocí složených závorek vkládat do řetězců libovolné výrazy:
x = 1
y = 2
f"{x} + {y} = {x + y}"
Místo pár jednoduchých/dvojitých uvozovek můžeme řetězce též delimitovat párem trojic jednoduchých/dvojitých uvozovek. Tyto literály můžou obsahovat doslovné znaky nového řádku (ne jen speciální sekvenci \n
) jednoduché/dvojité uvozovky bez lomítek (jen ne tři za sebou).
"""a
b
c"""
"""
a
b
c
"""
s = """"Hello," I said.
"Hi," she replied."""
print(s)
Jinak v nich platí stejná pravidla jako v běžných řetězcích, můžou být taktéž surové nebo formátovací nebo obojí zároveň atp.
Řetězce mají mnoho užitečných metod. Několik jich začíná na is...
a informují nás o obsahu řetězce:
"hello".islower()
"HELLO".isupper()
"Hello".istitle()
"12".isnumeric()
Jiné vytvoří nový pozměněný řetězec:
"hello".upper()
"hello".title()
Metoda strip
oseká z okrajů řetězce prázdné znaky (angl. whitespace)...
" \n ZZZ\n \t \n".strip()
... popřípadě libovolné znaky, které jí zadáme:
"bbabZZZaabaaa".strip("ab")
Metoda split
naseká řetězec na dílčí řetězce na prázdných znacích...
"a b c".split()
... nebo na každém výskytu zadaného podřetězce:
"a, b, c".split(", ")
Prázdných znaků může být libovolné množství a pokud dělením vznikne prázdný řetězec, tak je z výsledku odebrán...
" a\n b \t c\n ".split()
... ale pokud zadáme podřetězec, hledá metoda doslova a do písmene přesně jeho výskyty a případné prázdné řetězce ve výstupu zachovává:
" a, b, , c".split(", ")
Opakem metody split
je metoda join
, která pospojuje kolekci řetězců, kterou jí předáme jako argument:
"-".join(["a", "b", "c"])
# i řetězec lze chápat jako kolekci jednoznakových řetězců
"-".join("abc")
Důležité je, že řetězce patří k tzv. nemodifikovatelným typům, tj. ať s nimi děláme co děláme...
s = " a,b,c "
s
s.strip().split(",")
... původní řetězec vždy zůstane nedotčený:
s
Více o (ne)modifikovatelných typech viz odd. Kolekce.
Ještě pár slov k funkci print
. Tato funkce vytiskne všechny svoje poziční argumenty oddělené mezerami a na závěr přilepí znak nového řádku.
print(1, "a", [])
Oddělovač (mezeru) i ukončovač (nový řádek) si můžeme upravit pomocí pojmenovaných argumentů sep
a end
-- může to být libovolný jiný řetězec.
print(1, "a", [], sep="__ODDĚLOVAČ__", end="__UKONČOVAČ__")
Jak to, že print
umí vytisknout i jiné objekty než jen řetězce? Ve skutečnosti neumí, jen na každý poziční argument zavolá funkci str
, která ho právě na řetězec převede.
str(1)
str([])
U objektů, které již řetězci jsou, žádný převod pochopitelně není potřeba, str
jako výsledek vrátí nezměněný argument:
str("a")
Kolekce¶
Kolekce je objekt, který obsahuje další objekty, které z něj lze jeden po druhém vytáhnout. Mezi základní zabudované typy kolekcí patří:
- seznamy -- typ
list
- n-tice -- typ
tuple
- množiny -- typ
set
- slovníky -- typ
dict
Svým způsobem můžeme i na řetězce nahlížet jako na kolekce znaků -- jak uvidíme níže, jde s nimi provádět stejné typy operací. Stejně jako ostatní kolekce např. reagují na funkci len
, která vrací počet prvků v kolekci...
len("abc")
... nebo na operátor in
, který testuje přítomnost prvku v kolekci...
"b" in "abc"
... nebo na funkci sorted
, která umí na základě kolekce vyrobit seřazený seznam jejích prvků:
sorted("bca")
Seznamy¶
Klíčové vlastnosti:
- uspořádanost (angl. se uspořádaná kolekce řekne ordered collection): zachovává pořadí prvků
- prvky se mohou opakovat
- modifikovatelnost (angl. mutability): seznam lze kdykoli upravovat (přidávat/ubírat prvky)
Literál seznamu sestává z hranatých závorek, mezi něž vypíšeme počáteční prvky seznamu oddělené čárkami:
[1, "a"]
# opakované prvky
[1, 1, 1]
# prázdný seznam
[]
Kromě literálu můžeme seznam vytvořit ještě pomocí funkce list
, např. proměnit řetězec na seznam znaků:
list("abc")
Argument pro funkci list
může být jakýkoli iterovatelný objekt, tj. objekt, ze kterého lze opakovaně tahat další objekty jako králíky z klobouku (iterace = opakování). Většinou se jedná o různé typy kolekcí, ale ne nutně. Např. funkce range
umí vytvořit objekt, z nějž lze postupně tahat čísla v zadaném rozpětí:
list(range(3))
# prázdný seznam
list()
Vytvořme si nyní seznam na hraní:
zvířata = "kočka pes morče slon žirafa kočka".split()
zvířata
Díky tomu, že je seznam uspořádaný, má každý jeho prvek pořadového číslo, tzv. index (číslováno od nuly). Pomocí indexů lze k jednotlivým prvkům seznamu přistupovat ("ukázat" si na ně -- index pochází z latinského slova pro ukazováček). Této operaci se říká indexace a vypadá tak, že za seznam napíšeme hranaté závorky s požadovaným indexem:
zvířata[0]
Záporná čísla indexují odzadu:
zvířata[-1]
Teoreticky lze indexaci přilepit přímo za literál seznamu, byť to v praxi není moc užitečné a zápis vypadá zvláštně:
["kočka", "pes", "morče"][2]
Dohledat index nějakého prvku můžeme pomocí metody index
:
zvířata.index("morče")
zvířata.index("orangutan")
Spočítat počet výskytů nějakého prvku můžeme pomocí metody count
:
zvířata.count("kočka")
Když chceme vytáhnout ze seznamu ne jeden prvek, ale podseznam, použijeme při indexaci tzv. výřez (angl. slice):
zvířata[2:4]
První číslo je index prvku, jímž podseznam začíná, druhé číslo je index prvního prvku, který již do podseznamu nepatří. To je na první pohled možná trochu zvláštní, ale v kombinaci s tím, že vypuštěním prvního/druhého čísla dosáhneme toho, že výřez bude od začátku/do konce, nám to umožňuje jednoduše rozříznout seznam na nepřekrývající se části:
hranice = 3
zvířata[:hranice]
zvířata[hranice:]
Když vynecháme obě čísla, vytvoříme podseznam odpovídající původnímu seznamu:
zvířata[:]
# což je to samé jako toto
zvířata[0:len(zvířata)]
Ve výřezu můžeme volitelně specifikovat ještě třetí číslo, které stanovuje, že do seznamu má být zahrnutý jen každý x-tý prvek (např. každý druhý):
zvířata[1:4:2]
Když je toto číslo záporné, můžeme podseznam vyříznou v opačném směru (zprava doleva):
zvířata[4:1:-2]
# takto lze tedy převrátit pořadí seznamu
zvířata[::-1]
# ale srozumitelnější je asi tento ekvivalentní zápis
# pomocí funkce reversed
list(reversed(zvířata))
Kromě vyřezávání lze nové seznamy vytvářet i spojováním seznamů pomocí operátoru +
...
zvířata + ["mastodont"]
... nebo klonováním pomocí operátoru *
:
2 * zvířata
Říkali jsme si, že seznamy jsou modifikovatelné, že lze prvky libovolně ubírat a přidávat. Nicméně všechno, co jsme zatím s naším seznamem zvířata
prováděli, na něm nezkřivilo ani vlásek:
zvířata
Zatím jsme tedy tento seznam nijak nemodifikovali, všechny operace, které jsme na něj aplikovali, vedly k tomu, že na jeho základě vznikl nový, který z toho původního nějakým způsobem vycházel.
K modifikaci seznamu slouží některé jeho metody. Ukažme si to na příkladu seřazování. Samostatná funkce sorted
vytvoří na základě původního seznamu nový seznam, který obsahuje stejné prvky, ovšem seřazené, a vrátí ho jako výsledek:
seřazená_zvířata = sorted(zvířata)
seřazená_zvířata
Můžeme si jednoduše ověřit, že zvířata
a seřazená_zvířata
jsou dva různé objekty...
zvířata is seřazená_zvířata
... a ani si nejsou rovné (protože záleží na pořadí):
zvířata == seřazená_zvířata
Naopak metoda sort
seřadí původní seznam, na kterém ji zavoláme, a vrátí None
:
# v tuto chvíli poprvé modifikujeme seznam zvířata...
zvířata.sort()
# ... což si můžeme jednoduše ověřit (změnilo se pořadí)
zvířata
Seznamy zvířata
a seřazená_zvířata
jsou nadále různé objekty...
zvířata is seřazená_zvířata
... ale v tuto chvíli už jsou si rovné (protože teď už obsahují stejné prvky ve stejném pořadí):
zvířata == seřazená_zvířata
Některé další užitečné modifikující metody objektů typu list
:
# přidání jednoho prvku na konec
zvířata.append("užovka")
zvířata
# pomocí indexace lze nahradit jeden prvek...
zvířata[4] = "slonice"
zvířata
# ... nebo celý podseznam:
zvířata[4:6] = ["slon", "žirafáč"]
zvířata
# přidání jednoho prvku na libovolný index
zvířata.insert(1, "kocour")
zvířata
# přidání celé kolekce prvků na konec
zvířata.extend(["kapr", "štika", "candát"])
zvířata
# odebrání posledního prvku
zvířata.pop()
zvířata
# odebrání prvku na libovolném indexu
zvířata.pop(0)
zvířata
# odebrání prvního výskytu konkrétního prvku
zvířata.remove("pes")
zvířata
# převrácení pořadí prvků
zvířata.reverse()
zvířata
Důležitým důsledkem modifikovatelnosti je, že pokud na ten samý seznam odkazuje více proměnných (= má více jmen), jakákoli modifikace se projeví pod všemi jmény. Představte si analogii: pokud mám bratra, který se jmenuje Jan, tak když se Jan nechá ostříhat, bude ostříhaný i můj bratr, protože je to jedna a tatáž osoba. Podobně se seznamy:
jan = ["nohy", "trup", "hlava", "vlasy"]
bratr = jan
jan is bratr
# teď Jana "ostříháme"
jan.pop()
jan
bratr
Modifikace je ošemetná zejména v případě, kdy je skrytá v nějaké funkci a týká se argumentů, které funkci předáváme zvenčí. Pak nám vůbec nemusí dojít, že argumenty vlastně modifikujeme:
def zkrášlit(osoba):
# osobu zkrášlíme tak, že ji ostříháme
osoba.pop()
josef = ["nohy", "trup", "hlava", "vlasy"]
zkrášlit(josef)
Nic není vidět, zafungovala to vůbec, podařilo se nám Josefa zkrášlit? No pro jistotu to zkusíme ještě jednou, tím přece nemůžeme nic zkazit, že...
zkrášlit(josef)
Tak co teď? Podívejme se na Josefa...
josef
Ježišmarja chudák Josef, kde má hlavu?!
(Je to trochu fórek pórek, ale snad si rozumíme, v čem tkví nebezpečí :) )
Závěrem -- s modifikací je potřeba být opatrný a raději s ní šetřit:
- pokud možno přímo nemodifikovat argumenty funkcí
- kde se to hodí, používat místo seznamů nemodifikovatelné n-tice
n-tice¶
Klíčové vlastnosti jsou stejné jako u seznamů jen s tím rozdílem, že jsou nemodifikovatelné. Lze s nimi tedy provádět ty samé operace, s výjimkou těch modifikujících.
Literály n-tic v zásadě mohou sestávat jen z prvků oddělených čárkami:
1, "a"
Nicméně v praxi je často potřeba obalit n-tici do kulatých závorek, protože čárka má jako operátor velmi nízkou prioritu (viz odd. Čísla). Je to podobné, jako když musíme použít závorky, aby sčítání proběhlo před dělením -- např. 2 + 3 × 4 = 14 vs. (2 + 3) × 4 = 20. Kdo se tím nechce moc zabývat, může závorky okolo n-tic používat pořád.
(1, "a")
Specifickou syntax má prázdná n-tice...
()
type(())
len(())
... a jednoprvková n-tice (čárka před uzavírací závorkou je povinná):
("a",)
Jiný iterovatelný objekt na n-tici proměníme pomocí funkce tuple
:
tuple([1, "a"])
tuple("abc")
tuple(range(3))
Jedním z důsledků nemodifikovatelnosti n-tic je, že do nich lze sice indexovat...
ntice = tuple("abc")
ntice[1]
... ale už nelze pomocí indexace prvky nahrazovat jinými:
ntice[1] = "Z"
Podobně řetězce, které jsou také nemodifikovatelné:
řetězec = "abc"
řetězec[1]
řetězec[1] = "Z"
Množiny¶
Klíčové vlastnosti:
- neuspořádanost: prvky nemají dané pořadí
- prvky se nesmí opakovat
- modifikovatelnost
Literály vypadají následovně:
{1, "a"}
Ovšem pozor, {}
není prázdná množina, ale prázdný slovník (viz níže)! Prázdnou množinu získáme pomocí funkce set
...
set()
... která nám poslouží i k převodu jiné kolekce na množinu:
set("kočička")
Prvky v množině se nesmějí opakovat v tom smyslu, že do množiny nelze vložit dva prvky a
a b
, o nichž platí, že a == b
. Nicméně není třeba se bát, když se o to pokusíme, nenastane chyba, množina prostě druhý pokus jednoduše ignoruje:
{1, 2, 1}
set([1, 2, 1])
Vytvořme si dvě množiny na hraní:
set1 = {1, 2, 3}
set2 = {2, 3, 4}
Důležitým důsledkem neuspořádanosti je, že do množiny nelze indexovat -- ptát se po "prvním" prvku množiny nedává smysl:
set1[0]
Množiny podporují různé množinové operace. Některé jsou nemodifikující...
# sjednocení
set1.union(set2)
# též možno pomocí operátoru |
set1 | set2
# průnik
set1.intersection(set2)
# též možno pomocí operátoru &
set1 & set2
# rozdíl
set1.difference(set2)
# též možno pomocí operátoru -
set1 - set2
# vztahy mezi množinami
set1.issubset(set2)
... jiné jsou modifikující:
# přidání prvku
set1.add(4)
set1
# zde k modifikaci nedojde, protože set1 už prvek 4 obsahuje
set1.add(4)
set1
# odebrání prvku
set1.remove(4)
set1
# přidání více prvků, ovšem znovu pochopitelně s vyřazením duplicit
set1.update([0, 1, 2])
set1
# průnik, přičemž výsledek operace se uloží do původní množiny set1
set1.intersection_update(set2)
set1
Teď už tedy platí:
set1.issubset(set2)
# též možno pomocí operátoru < ("menší než")
set1 < set2
set2.issuperset(set1)
# též možno pomocí operátoru > ("větší než")
set2 > set1
Kromě toho, že nám množiny dobře poslouží, když chceme kolekci s duplicitami zredukovat na kolekci unikátních objektů (např. když chceme seznam tokenů v textu převést na množinu typů), mají ještě jednu výhodu: díky tomu, že prvky nemusí respektovat pořadí zadané uživatelem, můžou být interně uspořádané tak, aby umožňovaly velmi rychle zjistit, zda množina daný prvek obsahuje či ne.
V případě seznamu je potřeba ho procházet prvek po prvku, od začátku do konce, abychom zjistili, zda daný prvek obsahuje či ne. Čím je seznam delší, a čím dál v něm prvek je (nebo pokud v něm prvek vůbec není), tím déle to trvá:
# seznam prvního milionu čísel
seznam = list(range(1_000_000))
# Python musí seznam projít celý, prvek po prvku, aby zjistil,
# že řetězec "a" v našem seznamu prvního milionu čísel není
"a" in seznam
%%timeit
# speciální direktiva %%timeit není přímo součástí Pythonu,
# poskytuje ji prostředí Jupyter; slouží k tomu, že spustí
# danou buňku opakovaně, pokaždé změří, jak dlouho trvá
# buňku vykonat, a pak nám ukáže průměrný čas
"a" in seznam
Oproti tomu v množině lze díky jejímu internímu uspořádání dohledat prvek velmi rychle:
množina = set(seznam)
"a" in množina
%%timeit
"a" in množina
U seznamu trvá naše operace v řádu desítek milisekund (10⁻³ s), kdežto u množiny jsou to desítky nanosekund (10⁻⁹ s) -- je tedy milionkrát rychlejší!
Nemodifikovatelným protějškem typu set
je typ frozenset
.
Slovníky¶
Slovníky se od ostatních kolekcí, které jsme doposud potkali, liší v tom, že obsahují dva typy prvků:
- klíče (angl. keys), podle nichž se ve slovníku hledá
- a hodnoty (angl. values), které pod klíči dohledáme
Je to podobně jako s jazykovými slovníky (proto se tak v Pythonu jmenují): když ve slovníku hledám slovo "kočka", tak "kočka" je klíč, a definice, kterou najdu, je jeho odpovídající hodnota.
Stěžejní vlastnosti slovníků:
- neuspořádanost: prvky nemají dané pořadí
- klíče se nesmí opakovat, ale hodnoty klidně můžou
- modifikovatelnost
Literály vypadají následovně:
{"a": 1, "b": 2}
Prvek před dvojtečkou je vždy klíč, za dvojtečkou pak jeho odpovídající hodnota.
Když stejný klíč specifikujeme víckrát, nenastane chyba, jen poslední asociovaná hodnota přemaže ty předchozí:
{"a": 1, "a": 2, "a": 3}
# prázdný slovník
{}
type({})
Slovníky lze vytvářet i pomocí funkce dict
, a to v zásadě dvěma způsoby. První možnost je, že do slovníku "nasypeme" kolekci sestávající z dvojic objektů, z nichž první vždy bude brán jako klíč a druhý jako jeho asociovaná hodnota:
# např. seznam n-tic o dvou položkách...
dict([("a", 1), ("b", 2)])
# ... nebo klidně seznam seznamů o dvou položkách...
dict([["a", 1], ["b", 2]])
# ... nebo třeba n-tice řetězců o dvou znacích atp.
dict(("ab", "cd"))
Druhá možnost je, že využijeme pojmenované argumenty -- jména argumentů pak skončí jako klíče, argumenty samotné jako hodnoty:
dict(a=1, b=2)
Vytvořme si slovník na hraní:
slovník = {
"pes": "nejlepší přítel člověka",
"kočka": "vypočítavá potvora"
}
Slovníky jsou sice stejně jako množiny neuspořádané, ale indexaci na rozdíl od nich podporují. Jen se jako index nepoužívá pořadové číslo (protože žádné pořadí neexistuje), ale klíč:
slovník["kočka"]
Přes metodu keys
se dostaneme ke všem klíčům ve slovníku...
slovník.keys()
... přes metodu values
k hodnotám...
slovník.values()
... a přes metodu items
k uspořádaným dvojicím (klíč, hodnota)
:
slovník.items()
Když se pokusíme indexovat podle klíče, který ve slovníku není, Python zkolabuje:
slovník["morče"]
Abychom se kolapsu (a tím i zastavení běhu programu) vyhnuli, můžeme použít metodu get
, která v případě absence klíče vrátí místo hodnoty ve slovníku nějaký jiný objekt (defaultně je to None
, ale můžeme specifikovat libovolný objekt).
slovník.get("pes")
slovník.get("morče")
slovník.get("morče", "MORČE VE SLOVNÍKU NENÍ!")
Díky tomu, že jsou slovníky modifikovatelné, můžeme hodnoty odpovídající jednotlivým klíčům libovolně měnit...
slovník["kočka"] = "chlupatý mazlíček"
slovník
... nebo přidávat nové:
slovník["rybička"] = "němá slizká tvář"
slovník
V prvním případě jsme přepsali původní hodnotu odpovídající klíči "kočka"
, ve druhém jsme přidali nový klíč "rybička"
a k němu odpovídající hodnotu. Syntax (zápis) ale vypadá v obou případech stejně.
Občas potřebujeme zajistit, aby za žádnou cenu nedošlo k přepsání existující hodnoty (viz první případ výše). K tomu poslouží metoda setdefault
-- pokud klíč už ve slovníku existuje, původní hodnotu nezmění, jen ji vrátí...
slovník.setdefault("kočka", "tuhle novou hodnotu do slovníku neprocpu :(")
slovník
... ale pokud neexistuje, klíč a odpovídající hodnotu do slovníku přidá (a hodnotu taky vrátí):
slovník.setdefault("morče", "ale tuhle procpu :)")
slovník
Metoda setdefault
tedy funguje podobně jako metoda get
s tím rozdílem, že v případě nepřítomnosti klíče ve slovníku defaultní hodnotu nejen vrátí, ale i uloží do slovníku.
Metoda update
, která přidává nové prvky do slovníku / upravuje stávající, nabízí co do argumentů, které zvládne zpracovat, podobné možnosti jako samotná funkce dict
:
# libovolná kolekce dvojic prvků...
slovník.update([("a", 1), ("b", 2)])
slovník
slovník.update(("ab", "cd"))
slovník
# ... jiný slovník...
slovník.update({"b": "z"})
slovník
# ... nebo pojmenované argumenty:
slovník.update(žralok="dravá mořská paryba", kůň="tam někde v pastvinách")
slovník
Odebírat prvky ze slovníku lze různými způsoby, podle toho, zda chceme odebrat konkrétní klíč a chceme ještě pracovat s jeho hodnotou...
# metoda pop smaže prvek podle klíče a vrátí odpovídající hodnotu
slovník.pop("a")
slovník
... nebo zda hodnotu k ničemu nepotřebujeme...
# operátor del smaže prvek podle klíče a nevrátí nic
del slovník["b"]
slovník
... nebo zda je nám jedno, který prvek odebereme (necháme Python vybrat za nás):
# metoda popitem odebere nějaký prvek (nevíme předem jaký)
# a vrátí klíč a hodnotu jako n-tici
slovník.popitem()
slovník
Díky neuspořádanosti a zákazu opakování je dohledávání klíčů ve slovníku velmi rychlé (stejně jako u množin) a zapisuje se velmi jednoduše:
"kočka" in slovník
Hodnoty ale ve slovníku takhle jednoduše nedohledáme:
"nejlepší přítel člověka" in slovník
Je potřeba je projít jednu po druhé pomocí metody values
, což na rozdíl od klíčů trvá stejně dlouho jako u seznamu (asi jako kdybychom v jazykovém slovníku hledali tak, že bychom pročítali jen definice):
"nejlepší přítel člověka" in slovník.values()
Můžeme si to ověřit na libovolném velkém slovníku. K vytvoření takového velkého testovacího slovníku můžeme použít zabudovanou funkci zip
, která "sezipuje" dohromady prvky několika iterovatelných objektů. Zní to složitě, ale když se na ni podíváme v akci, je to myslím jednoduché:
# vše obalíme do funkce list, aby se výsledné "sezipované" prvky
# uložily do seznamu
list(zip("abc", [1, 2, 3]))
Pomocí funkce zip
jednoduše můžeme vytvořit slovník, v němž namapujeme první milion čísel na sebe sama, ve stylu...
dict(zip(range(3), range(3)))
... akorát větší. Takový slovník samozřejmě není k ničemu užitečný, jen je velký, takže dobře poslouží k ilustraci rozdílu rychlosti v hledání mezi klíči a hodnotami.
libovolný_velký_slovník = dict(zip(range(1_000_000), range(1_000_000)))
Hledání v hodnotách:
"a" in libovolný_velký_slovník.values()
%%timeit
"a" in libovolný_velký_slovník.values()
Hledání v klíčích:
"a" in libovolný_velký_slovník
%%timeit
"a" in libovolný_velký_slovník
Znovu pozorujeme rozdíl šesti řádů: hledání v klíčích je milionkrát rychlejší než hledání v hodnotách.
A když už máme slovníku plné zuby, můžeme jeho obsah vymazat a začít nanovo:
libovolný_velký_slovník.clear()
libovolný_velký_slovník
Vnořené kolekce¶
Jak už jsme ostatně v některých příkladech výše naznačili, kolekce lze do sebe libovolně zanořovat, čímž můžeme reprezentovat různé složité strukturní vztahy. Literální zápis vnořených kolekcí je celkem intuitivní:
slovník = {
"hesla": {
"kočka": {
"definice": "chlupatý mazlíček",
"příklad": "Na okně seděla kočka...",
},
"pes": {
"definice": "nejlepší přítel člověka",
"příklad": "... a venku štěkal pes.",
},
},
"autoři": [
{"jméno": "John", "příjmení": "Doe"},
{"jméno": "Jane", "příjmení": "Doe"},
],
"datum": (2018, 11, 7),
}
Naproti tomu indexace do vnořených kolekcí občas lidem činí při prvním setkání potíže. Chceme-li např. vytáhnout ze slovníku definici kočky, někdo má tendenci zkoušet následující zápis, který svým vnořením zrcadlí vnoření literálu:
slovník["hesla"["kočka"]]
Chybová hláška zní trochu krypticky, ale můžeme z ní odvodit, že se Python zřejmě snaží indexovat do nějakého řetězce (tj. vytáhnout znak z řetězce), což jsme rozhodně nezamýšleli, takže zápis musí být špatně.
Co se tedy děje? Pojďme si výraz rozebrat tak, jak ho vidí Python. Nejdřív vidí proměnnou slovník
, za ní hranaté závorky. Usoudí tedy (správně), že se snažíme přistoupit k nějakému klíči ve slovníku. K jakému klíči? To je potřeba zjistit na základě obsahu hranatých závorek. Jinými slovy, Python v tuto chvíli vidí slovník[X]
, kde X
je zástupný znak pro klíč, který chceme, aby ve slovníku dohledal.
Dobře, tak dál. Abych mohl X
dohledat, uvažuje Python, musím nejdřív zjistit, co je zač. Hm, podle všeho mám za X
dosadit výraz "hesla"[Y]
. Ten musím tedy vyhodnotit jako první, abych mohl posléze vyhodnotit výraz slovník[X]
.
Co je "hesla"[Y]
za výraz? Je to výraz, v němž máme řetězec "hesla"
, a pomocí indexace se z něj snažíme vytáhnout znak, který odpovídá indexu Y
.
Skvěle, tetelí se Python, už tedy stačí jen zjistit, jaká je hodnota indexu Y
, a můžu začít celý výraz vyhodnocovat, pěkně zvnitřka k vnějšku. Jenže ouha, Y
by mělo být pořadové číslo požadovaného znaku, ale místo toho je to řetězec "kočka"
. Chudák Python neví, co to znamená, vytáhnout "kočka"
-tý znak z řetězce "hesla"
, a tak to vzdá a jen si postěžuje, že indexy do řetězců by měly být celá čísla ("string indices must be integers").
Jinými slovy, vyhodnocování celého výrazu selže na tomto podvýrazu:
"hesla"["kočka"]
Jak vidno, chyba je stejná.
Jak tedy na to? Vždycky je možné rozdělit indexaci do vnořené kolekce na víc jednoduchých indexací s použitím pomocných proměnných:
hesla = slovník["hesla"]
kočka = hesla["kočka"]
kočka
Z výše uvedeného zápisu ale plyne, že se meziproměnné hesla
můžeme zbavit pomocí dosazování. Začněme od proměnné kočka
, která obsahuje zamýšlený výsledek (slovníkové heslo pro kočku; mezery slouží jen ke zvýraznění toho, jak probíhá dosazování):
kočka
Za výraz kočka
lze dosadit výraz hesla["kočka"]
(protože kočka = hesla["kočka"]
):
hesla ["kočka"]
Výborně, výsledek zůstal stejný, takže dosazení platí. Dále můžeme za výraz hesla
dosadit výraz slovník["hesla"]
(protože hesla = slovník["hesla]
), přičemž útržek kódu ["kočka"]
zůstane tam, kde byl:
slovník["hesla"] ["kočka"]
Skvěle, výsledek je stále stejný! Teď už se jen zbavíme mezer, aby byl zápis konvenčnější, a vymysleli jsme způsob, jak indexovat do vnořených kolekcí.
slovník["hesla"]["kočka"]
# rok
slovník["datum"][0]
# příkladová věta pro psa
slovník["hesla"]["pes"]["příklad"]
# jméno druhého autora
slovník["autoři"][1]["jméno"]
A tak podobně.
Nezabudované kolekce¶
Existuje samozřejmě mnoho nezabudovaných kolekcí -- např. třída FreqDist
z knihovny nltk
je taky kolekce (obsahuje dílčí prvky). Nezabudované kolekce pochopitelně nemají literály, je potřeba je inicializovat pomocí příslušného konstruktoru třídy (viz odd. Typy a třídy):
from nltk import FreqDist
FreqDist("aababc")
Opakování operací: for-cyklus¶
Často chceme nějakou operaci provést opakovaně, ale pokaždé s trochu jinými daty. Např. chceme vytisknout čísla od 0 do 2. Mohli bychom samozřejmě použít copy-paste, tj. řádek třikrát zkopírovat a pokaždé jen nahradit číslo...
print(0)
print(1)
print(2)
... jenže pak je problém, že jakmile chceme akci trochu upravit, třeba vytisknout "Číslo: 0" místo jen "0", a podobně pro ostatní čísla, musíme provést úpravu na každém řádku:
print("Číslo:", 0)
print("Číslo:", 1)
print("Čísloo:", 2)
Při takovém opisování / kopírování je snadné udělat na jednom místě chybu (viz výše "Čísloo" místo "Číslo") a najednou místo toho, abychom provedli třikrát tu samou operaci, provedeme dvakrát jednu a potřetí trochu jinou.
Proto je lepší se zamyslet, co se při každém opakování mění a co naopak zůstává stejné, a zaznamenat to pomocí tzv. for-cyklu (angl. for-loop):
for i in [0, 1, 2]: # hlavička
print("Číslo:", i) # tělo
Takový for-cyklus můžeme parafrázovat následovně: každé číslo ze seznamu [0, 1, 2]
postupně ulož do proměnné i
a proveď sérii operací popsaných v těle cyklu. Zkráceně: pro každé (angl for each) číslo ze seznamu proveď následující operaci/operace.
Hlavička for-cyklu popisuje proměnlivou část akce, kterou provádíme (zde: proměnná i
postupně nabývá různých hodnot, které čerpáme ze seznamu). Tělo for-cyklu popisuje repetitivní část (zde: pokaždé proměnnou i
vytiskneme, spolu s prefixem "Číslo: "). Podobně jako u funkcí poznáme to, co ještě patří do těla for-cyklu, a to, co už ne, podle odsazení:
for i in [0, 1, 2]:
print("Číslo:", i)
print("Já ještě do těla cyklu patřím!")
print("Já už ne :(")
Obecně můžeme for-cyklus charakterizovat takto:
for item in iterable:
# do something with item
Proměnná iterable
může obsahovat jakýkoli iterovatelný objekt (= objekt, ze kterého lze tahat další objekty, viz odd. Kolekce). Seznamy už jsme v roli iterable
viděli v akci výše, n-tice fungují stejně. Řetězce taky, přičemž ve for-cyklu je procházíme znak po znaku:
řetězec = "abc"
for znak in řetězec:
print(znak)
Množiny fungují podobně, jen není zaručené pořadí, v němž budeme prvky vytahovat:
množina = {2, 3, 1}
for prvek in množina:
print(prvek)
U slovníků je to trochu složitější -- záleží, zda chceme procházet klíče...
slovník = {"a": 1, "b": 2, "c": 3}
for klíč in slovník:
print(klíč)
# nebo též explicitněji
for klíč in slovník.keys():
print(klíč)
... hodnoty...
for hodnota in slovník.values():
print(hodnota)
... nebo obojí zároveň:
# jak přesně tato syntax funguje si vysvětlíme o kousek níž
for klíč, hodnota in slovník.items():
print(f"Pod klíčem {klíč!r} je uložená hodnota {hodnota}.")
Ale iterovatelný objekt nerovná se nutně jen kolekce. V kombinaci s for-cyklem se často hodí funkce range
:
for i in range(3):
print("Číslo:", i)
Funkce range
vytvoří objekt, který v hlavičce for-cyklu postupně generuje čísla v zadaném rozpětí (v našem případě od 0 do 3, horní hranici vyjímaje). Proč je to užitečné? Představte si, že bychom chtěli operaci print("Číslo:", i)
provést pro prvních milion nezáporných čísel. Oproti seznamu má funkce range
dvě výhody:
- pro nás je výhoda, že nemusíme ručně vypisovat seznam s milionem čísel
- pro počítač je výhoda, že nemusí vytvářet celý seznam najednou a pak ho uchovávat v paměti (milion čísel sice dnešní počítače zvládnou hravě, ale obecně platí, že paměti nikdy není neomezeně) -- čísla tiskne jedno po druhém a jakmile jedno zpracuje, tak ho může zapomenout
Když proměnnou z hlavičky for-cyklu nikde v těle nepoužijeme, bývá zvykem jí dát speciální jméno _
, čímž ostatním programátorům naznačíme, že se nemají divit, že není použitá.
# tři hody kostkou
for _ in range(3):
print(randint(1, 6))
Občas se stane, že potřebujeme for-cyklem projít kolekci, jejímiž prvky jsou n-tice nebo seznamy, které v rámci for-cyklu potřebujeme dále rozebrat na dílčí prvky. Můžeme samozřejmě použít indexaci:
# Větu "Prší." máme reprezentovanou jako seznam dvou tokenů. Každý token
# je trojice řetězců: první označuje slovní tvar, druhý lemma (=
# slovníkovou podobu), třetí slovní druh (V = sloveso, Z = interpunkce).
věta = [("Prší", "pršet", "V"), (".", ".", "Z")]
for token in věta:
word = token[0]
lemma = token[1]
tag = token[2]
print(f"WORD: {word!r}, LEMMA: {lemma!r}, TAG: {tag!r}")
Práci nám ale může ušetřit tzv. destrukturace (angl. destructuring, též se tomu někdy říká unpacking nebo pattern matching): máme-li uspořádanou kolekci (seznam nebo n-tici) o X prvcích, můžeme prvky rovnou namapovat na X proměnných:
a, b, c = [1, 2, 3]
a
b
c
Jak to funguje? Strukturní vzorec (pattern) nalevo od =
obsahuje tři volné "sloty" (odpovídající třem proměnným). Python se tento vzorec pokusí přiložit na datovou strukturu napravo od =
(představte si průhledný pauzovací papír), namapovat (match) objekty, které datová struktura obsahuje, na volné sloty, a strukturu tak rozebrat na dílčí prvky (proto hovoříme o destrukturaci nebo unpacking -- rozbalení).
Ne vždycky se to samozřejmě povede -- např. když je slotů víc než prvků...
a, b, c = (1, 2)
... nebo naopak:
a, b = (1, 2, 3)
Strukturní vzorec může obsahovat proměnnou s hvězdičkou (viz odd. Funkce), pak na sebe tato proměnná naváže zbývající prvky kolekce:
a, *rest = (1, 2, 3, 4, 5, 6)
a
rest
Když je kolekce vnořená, můžeme strukturní vzorec namapovat jen na nejvyšší úroveň...
a, b = (1, (2, 3))
a
b
... nebo může vzorec být taktéž vnořený:
a, (b, c) = (1, (2, 3))
a
b
c
Když dáme destrukturaci dohromady s for-cyklem, zkrátí a zpřehlední se nám zápis:
for token in věta:
word, lemma, tag = token
print(f"WORD: {word!r}, LEMMA: {lemma!r}, TAG: {tag!r}")
A dokonce můžeme ubrat ještě jeden řádek, protože destrukturaci lze provést přímo v rámci hlavičky for-cyklu:
for word, lemma, tag in věta:
print(f"WORD: {word!r}, LEMMA: {lemma!r}, TAG: {tag!r}")
Varianta s vnořeným strukturním vzorcem může nastat např. v případě, že si jednotlivé tokeny očíslujeme pomocí funkce enumerate
, která ke každému prvku ve zdrojové kolekci přidá pořadové číslo. Takhle to vypadá, když si výsledek uložíme do seznamu:
list(enumerate(věta))
A takhle bychom ji spolu s vnořenou destrukturací mohli použít ve for-cyklu:
for i, (word, lemma, tag) in enumerate(věta):
print(f"{i + 1}. WORD: {word!r}, LEMMA: {lemma!r}, TAG: {tag!r}")
V prvním opakování for-cyklu provede Python tuto destrukturaci...
i, (word, lemma, tag) = (0, ("Prší", "pršet", "V"))
... v druhém pak tuto:
i, (word, lemma, tag) = (1, (".", ".", "Z"))
Na závěr příklad použití destrukturace s hvězdičkou -- může se hodit, když jsou jednotlivé dílčí kolekce, které postupně ve for-cyklu zpracováváme, různě dlouhé:
for i, *rest in [(1,), (2, 3), (4, 5, 6)]:
print(i)
print(rest)
Občas se v rámci for-cyklů můžou hodit klíčová slovíčka break
a continue
. break
okamžitě ukončí for-cyklus. Následující for-cyklus by měl sice teoreticky projít čísla od 0 do 4, ale zastaví se u čísla 3:
for i in range(5):
# syntax podmínek viz odd. Podmínky
if i == 3:
break
print(i)
continue
ukončí současnou iteraci for-cyklu a zahájí další. Následující for-cyklus tedy projde čísla od 0 do 4, ale vytiskne jen ta lichá, protože u sudých se k řádku s funkcí print
vůbec nedostane:
for i in range(5):
if i % 2 == 0:
continue
print(i)
POZN.: V tělech for-cyklů jsme všude použili funkci print
, abychom mohli nahlédnout do jejich průběhu, tj. do toho, jak se jednotlivé operace opakují. Bez funkce print
to jde samozřejmě taky, jen se nám nedostane žádné vizuální zpětné vazby, která by nám pomohla pochopit, co se po spuštění for-cyklu odehrává:
num = 0
for x in range(101):
num += x
num
Jen ještě doplním, že elegantněji můžeme čísla od 0 do 100 sečíst pomocí funkce sum
:
sum(range(101))
Logika¶
V Pythonu lze pracovat i s logickými výroky. Základem pro to jsou konstanty True
a False
, které označují pravdu, resp. nepravdu, a které Python vrací, když posuzuje pravdivost nějakého logického výroku:
# rovnost
2 + 2 == 4
list("abc") == sorted("cab")
# negace rovnosti
2 + 2 != 5
# přítomnost prvku v kolekci
"a" in "abc"
# přítomnost celého čísla v rozpětí celých čísel
7 in range(5, 10)
# identita objektu
jan = bratr = ["nohy", "trup", "hlava", "vlasy"]
jan is bratr
# různé typy nerovností -- menší než
2 < 3
# menší nebo rovno
3 <= 3
# větší
2 > 1
# větší nebo rovno
1 >= 1
1 < 3 > 2
# výroky o řetězcích
"abc".islower()
A tak podobně, viz příklady operací, které vrací True
nebo False
, v předchozích oddílech.
Pravdivostní hodnotu ale mají všechny objekty v Pythonu. Můžeme ji zjistit pomocí funkce bool
:
bool("abc")
Pravdivé jsou skoro všechny objekty kromě:
# čísla 0
bool(0)
# prázdných kolekcí
bool([])
bool({})
bool("")
# None
bool(None)
# a pochopitelně samotné konstanty False
bool(False)
Sestavovat z jednoduchých výroků složitější lze pomocí operátorů and
, or
a not
:
# x and y platí, když jsou oba výroky x a y pravdivé
True and True
True and False
# x or y platí, když je aspoň jeden z výroků x a y pravdivý
True or True
True or False
False or False
# not invertuje pravdivostní hodnotu výroku
not True
not False
V praxi použijeme třeba následovně:
řetězec = "kočka"
len(řetězec) > 3 and "č" in řetězec
Jak jsme psali výše, pravdivostní hodnotu mají všechny objekty v Pythonu, můžeme tedy klidně napsat:
not "abc"
True or 0
Díky tomu můžeme trochu upřesnit chování operátorů and
a or
. and
začne vyhodnocovat výrazy (zleva doprava) a vrátí buď první nepravdivý, nebo poslední zbývající:
# poslední zbývající (pravdivý)
True and 1 and "abc" and sorted
# první nepravdivý
True and 1 and "abc" and sorted and [] and 0
or
taky začne vyhodnocovat výrazy (zleva doprava) a vrátí buď první pravdivý, nebo poslední zbývající:
# první pravdivý
0 or [] or False or 42 or "abc"
# poslední zbývající (nepravdivý)
0 or [] or False or {}
Jakmile and
nebo or
narazí na výraz, podle kterého se může rozhodnout, další už nevyhodnocuje. Můžeme si to ukázat, když jeden z výrazu bude volání funkce, jejímž vedlejším efektem je, že něco vytiskne na obrazovku.
def funkce():
print("volám funkci...")
return "výsledek funkce"
# zde může and vrátit hned první výraz, takže se funkce nezavolá
0 and funkce()
# zde je potřeba po prvním výrazu pokračovat v ověřování pravdivostních
# hodnot, takže se funkce zavolá
1 and funkce()
Pomocí or
tedy můžeme např. nahradit metodu setdefault
(viz odd. Kolekce -- Slovníky) -- úpravu slovníku provedeme pouze v případě, že daný klíč ještě neobsahuje:
slovník = dict(a=1, b=2)
# "a" už ve slovníku je, první výraz tedy platí a druhý se ani
# nevyhodnotí
"a" in slovník or slovník.update(a=42)
slovník
# "c" ve slovníku není, první výraz tedy neplatí a druhý se
# vyhodnotí
"c" in slovník or slovník.update(c=42)
slovník
Máme-li kolekci objektů a chceme ověřit, zda je aspoň jeden z nich pravdivý, můžeme použít zabudovanou funkci any
:
any([True, False, False])
Chceme-li ověřit, zda jsou všechny pravdivé, použijeme zabudovanou funkci all
:
all([True, True, True])
any
a all
jsou velmi užitečné v kombinaci s konvertory kolekcí (viz odd. Konvertory kolekcí).
Varování na závěr -- složitější logické výroky jsou zrádné, jejich úpravy jsou netriviální a řídí se přesně danými pravidly. Kdo si je podobně jako já ze střední školy pamatuje spíš matně, měl by si dát extra pozor ;) Např. výrok not (x and y)
je ekvivalentní výroku not x or not y
, ne výroku not x and not y
, jak bychom možná mohli mít tendenci naivně "roznásobit".
Složitější případy si naštěstí vždy lze pro jistotu ověřit pomocí pravdivostní tabulky, ať už si ji nakreslíme ručně nebo si rovnost výroků pro všechny možné kombinace hodnot x
a y
zkontrolujeme pomocí Pythonu:
for x in [True, False]:
for y in [True, False]:
# ↓ výroky, jejichž rovnost ověřujeme ↓
is_equal = (not (x and y)) == (not x or not y)
print(f"x is {x}, y is {y}, the equality is {is_equal}")
Aby rovnost dvou výrazů platila obecně, musí platit pro každou variantu dosazení konkrétních hodnot za proměnné (zde x
a y
). O tom se můžeme přesvědčit buď ověřením výstupů funkce print
(viz předchozí buňka), nebo můžeme kód taky upravit jako konvertor kolekce v kombinaci s funkcí all
a dostaneme tak celkový výsledek rovnou:
all(
(not (x and y)) == (not x or not y)
for x in [True, False]
for y in [True, False]
)
A ještě pro srovnání, jak by vypadaly výstupy v případě, že si porovnávané výrazy obecně vzato rovné nejsou (všimněte si, že pro některé kombinace hodnot x
a y
rovnost platí, ale ne pro všechny)...
for x in [True, False]:
for y in [True, False]:
is_equal = (not (x and y)) == (not x and not y)
print(f"x is {x}, y is {y}, the equality is {is_equal}")
... takže celkový verdikt je...
all(
(not (x and y)) == (not x and not y)
for x in [True, False]
for y in [True, False]
)
Podmínky: if, elif, else (a while-cyklus)¶
Na logických výrocích lze dál stavět větvení programu pomocí podmínek:
if 2 + 2 == 4: # hlavička
print("matematika funguje") # tělo
Syntax podmínek je znovu postavená na principu hlavičky a odsazeného těla, které se vykoná, pokud podmínka uvedená v hlavičce platí.
if
je jako výhybka: platí-li podmínka, vydá se program trochu jinou cestou, než když neplatí. Alternativních podmíněných cest může být více, napojíme je pomocí klíčového slovíčka elif
:
num = 2
if num == 0:
print("nula")
elif num == 1:
print("jedna")
elif num == 2:
print("dva")
Na závěr můžeme přidat ještě jednu nepodmíněnou alternativní cestu pomocí klíčového slovíčka else
. Tou se programu vydá, když se ukáže, že ani jedna z předchozích podmínek neplatí:
num = 3
if num == 0:
print("nula")
elif num == 1:
print("jedna")
elif num == 2:
print("dva")
else:
print("něco jiného")
Je důležité si uvědomit, že větvení skutečně funguje jako výhybka: program se vydá první cestou, kde narazí na pravdivou podmínku, a zbývající ignoruje, i kdyby byly nakrásně pravdivé taky:
if True:
print("podmínka u téhle větve je vždy pravdivá")
elif True:
print("u téhle taky, ale není to nic platné, je až druhá")
Vizuálně si to můžeme představit takto:
.---> if
/
/-----> elif
/
--------> elif
\
\-----{ ...
\
`---> else
Důležitým důsledkem je, že záleží na pořadí podmínek. Pravidlo pravé ruky zní, že specifičtější podmínky by měly pro jistotu být na začátku, jinak se k nim program nemusí dostat, protože ho odchytí dřívější méně specifické podmínky.
Např. podmínka x >= 0
je méně specifická než podmínka x == 2
, takže když bude před ní, sebere jí všechny potenciální zákazníky:
for x in range(4):
if x >= 0:
print(f"{x} je větší než 0")
elif x == 2:
print("HA! DVOJKA.")
Srovnejte s výstupem, když pořadí podmínek prohodíme:
for x in range(4):
if x == 2:
print("HA! DVOJKA.")
elif x >= 0:
print(f"{x} je větší než 0")
Každé klíčové slovíčko if
odstartuje nové větvení, pod něž spadají případná následující elif
a else
na stejné úrovni odsazení:
num = 2
if num > 1:
print("1. větvení: num je větší než jedna")
elif num < 1:
print("1. větvení: num je menší než jedna")
else:
print("1. větvení: num je rovno jedné")
if num == 2:
print("2. větvení: num je rovno dvěma")
Vizuálně si to můžeme představit takto:
.---> if
/
/-----> elif
/
--------> elif
\
\-----{ ...
\
`---> else
--------> if
Existuje též speciální dvoučlenný operátor if ... else
, který jako výsledek vrátí jednu ze dvou možností podle toho, jak dopadne pravdivostní test:
výsledek_je_li_test_pravdivý if test else výsledek_je_li_test_nepravdivý
Konkrétní příklad bude asi srozumitelnější:
"A" if True else "B"
"A" if False else "B"
Díky tomuto operátoru máme k dispozici kompaktnější zápis, když chceme hodnotu nějaké proměnné stanovit na základě pravdivostního testu. Bez operátoru if ... else
bychom např. museli psát:
lednička = {"puding", "sýr", "salát"}
if "puding" in lednička:
reakce = "hurá, puding!"
else:
reakce = "není puding :("
reakce
Ale s pomocí operátoru if ... else
stačí napsat:
reakce = "hurá, puding!" if "puding" in lednička else "není puding :("
reakce
S podmínkami souvisí i jiná podoba cyklu, tzv. while-cyklus. While-cyklus se opakuje tak dlouho, dokud je podmínka v hlavičce pravdivá:
i = 0
while i < 3:
print("Číslo", i, "je menší než 3.")
i += 1
Konvertory kolekcí¶
Mnoho funkcí pracuje s kolekcemi, např. zabudovaná funkce sorted
nebo konstruktor FreqDist
z knihovny nltk
. Někdy je užitečné mít možnost kolekci trochu upravit ve chvíli, kdy ji takové funkci předáváme. K tomu přesně slouží konvertory kolekce.
Kamkoli můžeme dát normální kolekci...
věta = "Bylo nás pět .".split()
věta
sorted(věta)
... můžeme propašovat místo ní i konvertor:
sorted(slovo for slovo in věta if slovo.islower())
Jejich syntax připomíná syntax for-cyklu, jen jsou jednotlivé prvky přeskládané a možnosti jsou omezenější než v plném for-cyklu. Abychom se v jejich zápisu lépe zorientovali, využijeme toho, že uvnitř jakýchkoli závorek můžeme kód v Pythonu libovolně nasekat na řádky a přidat odsazení, aby se nám lépe četl:
sorted(
slovo
for slovo in věta
if slovo.islower()
)
Teď už je lépe vidět, že konvertor kolekce má tři části. Čtou se odprostředka:
convert(item) # 3.
for item in collection # 1.
if test(item) # 2.
Popis jednotlivých fází vypadá následovně:
- Procházíme kolekci prvek po prvku.
- Nepovinně můžeme provést nějaký test; pokud ho aktuální prvek nesplní, bude z výsledku vyřazen. Tím můžeme zdrojovou kolekci profiltrovat.
- Nakonec spočítáme hodnotu, kterou za daný prvek ze zdrojové kolekce zařadíme do výsledku. Tato hodnota může být původní prvek samotný, může být vypočítaná na základě prvku, nebo s ním taky vůbec nemusí souviset.
Nejjednodušší konvertor kolekce, kterým kolekce projede nezměněná, vypadá takto:
item # 3.
for item in collection # 1.
# 2. nic
sorted(
slovo # 3.
for slovo in věta # 1.
# 2.
)
Chceme-li místo slov samotných do výsledku zařadit n-tici (slovo, délka_slova)
, musíme upravit fázi 3:
sorted(
(slovo, len(slovo)) # 3. úprava zde
for slovo in věta # 1.
# 2.
)
Chceme-li navíc zahodit všechna slova, která nesestávají z malých písmen, musíme doplnit fázi 2:
sorted(
(slovo, len(slovo)) # 3.
for slovo in věta # 1.
if slovo.islower() # 2. úprava zde
)
Konvertor kolekce lze vždy přeskládat na normální for-cyklus, např. ten bezprostředně předcházející:
pomocný_seznam = []
for slovo in věta:
if slovo.islower():
pomocný_seznam.append((slovo, len(slovo)))
sorted(pomocný_seznam)
Naopak přeskládat for-cyklus na konvertor kolekce pokaždé nejde, protože for-cyklus poskytuje mnohem větší volnost. Výměnou za toto omezení poskytují konvertory kolekce oproti for-cyklům několik výhod:
- úspornější syntax (nesnaží se pokrýt plnou flexibilitu for-cyklů)
- při běhu programu zabírají méně času a paměti (např. není potřeba vytvářet žádné pomocné kolekce typu
pomocný_seznam
) - ve for-cyklu se kvůli jeho flexibilitě snadno může schovat mnohem větší množství chyb
Vztah konvertorů kolekcí k for-cyklům je tedy podobný jako vztah (nemodifikovatelných) n-tic k (modifikovatelným) seznamům: konvertory (a n-tice) jsou mnohem méně flexibilní, ale ve chvíli, kdy tu flexibilitu nepotřebujete, je dobré mít možnost se jí explicitně vzdát a uchránit se tak možných chyb, které by z ní mohly plynout.
Když není konvertor kolekce jediným argumentem funkce, je potřeba ho uzávorkovat...
sorted((slovo for slovo in věta if slovo.islower()), reverse=True)
... jinak nás Python vyplísní:
sorted(slovo for slovo in věta if slovo.islower(), reverse=True)
Existuje speciální syntax na to, když chceme výsledky z konvertoru kolekce nasypat do seznamu (angl. se tomuto zápisu říká list comprehension)...
[slovo for slovo in věta if slovo.islower()]
... do množiny (angl. set comprehension)...
{len(slovo) for slovo in věta}
... nebo do slovníku (angl. dict comprehension):
{slovo: len(slovo) for slovo in věta}
U ostatních kolekcí speciální syntax neexistuje, ale není proč si zoufat, jejich konstruktory většinou podporují inicializaci na základě zdrojového iterovatelného objektu, takže stačí vepsat konvertor kolekce do konstruktoru:
tuple(slovo.lower() for slovo in věta)
from nltk import FreqDist
FreqDist(slovo.lower() for slovo in věta)
Konvertory kolekcí jsou velmi užitečné v kombinaci s logickými funkcemi all
a any
(viz výše odd. Logika):
all(x < 10 for x in range(5))
any(x == 2 for x in range(5))
all(x == 2 for x in range(5))
Konvertorům kolekce též říkáme generátorové výrazy (angl. generator expression), podle toho, že jejich výsledkem je generátor -- objekt, který generuje další objekty (na základě prvků původní kolekce). Když ho vytvoříme samostatně, můžeme si ho i prohlédnout:
gen = (slovo for slovo in věta)
gen
type(gen)
Generátory poslouží kdekoli, kde je potřeba iterovatelný objekt (podobně jako kolekce nebo funkce range
). Navíc z nich lze prvky vytahovat po jednom pomocí zabudované funkce next
:
next(gen)
Existují dvě široké kategorie iterovatelných objektů:
- kolekce
- a něco, co bychom mohli souhrnně nazvat potenciální či virtuální kolekce (sem patří generátory, výstupy funkcí
range
,reversed
,enumerate
apod.)
Rozdíl mezi reálnou kolekcí o milionu prvků a potenciální kolekcí o milionu prvků je v tom, že v případě reálné kolekce musí celý milion prvků zároveň fyzicky existovat v paměti počítače. U potenciální kolekce stačí mít recept, jak ten milion prvků vytvořit... až budou potřeba.
Často nepotřebujeme všechny prvky kolekce najednou, stačí nám je zpracovávat jeden po druhém. Pak jsou potenciální kolekce ideální -- zabírají mnohem méně paměti.
Jindy ale všechny prvky najednou potřebujeme, typicky když si je chceme prohlédnout. Pak nám potenciální kolekce moc neposlouží:
(slovo for slovo in věta)
enumerate(slovo for slovo in věta)
reversed([1, 2, 3])
range(-2, 2)
Řešení je naštěstí jednoduché -- stačí potenciální kolekci donutit, aby vygenerovala všechny prvky, které v ní dřímají, a uložit je do reálné kolekce, např. do seznamu:
list(slovo for slovo in věta)
list(enumerate(slovo for slovo in věta))
list(reversed([1, 2, 3]))
list(range(-2, 2))
Drobnosti¶
Práce se soubory¶
Nejjednodušeji se v Pythonu pracuje se soubory v podobě tzv. čistého textu (angl. plain text; často mívají příponu .txt
).
K otevření souboru slouží zabudovaná funkce open
. Chceme-li do souboru zapisovat, je potřeba specifikovat mód w
(jako write):
text = "kočka leze dírou\npes oknem"
with open("kočka.txt", "w", encoding="utf-8") as file: # hlavička
file.write(text) # tělo
Argument encoding
je nepovinný, Python ho automaticky stanoví na základě nastavení vašeho operačního systému (v podstatě všude kromě Windows to bude UTF-8). Vzhledem k tomu, že vaše první volba kódování by vždy měla být UTF-8 (viz notebook unicode.ipynb
), je dobré si zvyknout ho vypisovat explicitně a nenechat operační systém rozhodovat za vás.
Syntaxi s klíčovým slovíčkem with
se říká context manager. V hlavičce zavoláme funkci open
a její výsledek uložíme do proměnné file
, která reprezentuje otevřený soubor. Tělo je pak kontext, v jehož rámci s tímto otevřeným souborem můžeme pracovat (v našem případě do něj zapisovat). Jakmile kontext skončí (= zrušíme odsazení), Python za nás soubor automaticky zase zavře, aniž bychom museli ručně volat metodu file.close()
. Co víc, pokud v rámci těla nastane nějaký problém (chyba), tak Python soubor taky zavře, ještě než zkolabuje, abychom po sobě nenechali nepořádek.
Když chceme soubor znovu načíst, můžeme při jeho otevírání specifikovat mód r
(jako read), ale nemusíme, protože je to default. Při načítání máme tři možnosti -- buď načteme celý soubor najednou jako jeden dlouhý řetězec...
with open("kočka.txt", encoding="utf-8") as file:
text = file.read()
text
... nebo celý soubor najednou jako seznam řádků...
with open("kočka.txt", encoding="utf-8") as file:
lines = file.readlines()
lines
... nebo ho můžeme zpracovávat řádek po řádku pomocí for-cyklu:
with open("kočka.txt", encoding="utf-8") as file:
for line in file:
print(line, end="")
Poslední varianta se hodí zejména v případě, že máme velký textový soubor, který nepotřebujeme (a tím pádem ani nechceme) načítat celý najednou do paměti.
Někdy jsou plaintextové soubory strukturované, takže načítat je řádek po řádku není úplně nejlepší způsob, jak s nimi pracovat. Např. jupyterovské notebooky jsou ve formátu JSON, který se snaží reprezentovat základní datové typy (čísla, řetězce, seznamy a slovníky) tak, aby šly vyměňovat mezi různými programovacími jazyky.
Když načteme tento notebook jedním ze standardních způsobů popsaných výše, tahle struktura se nám nevyjeví:
with open("python_crash_course.ipynb", encoding="utf-8") as file:
nb = file.readlines()
# prvních deset řádků souboru s notebookem
nb[:10]
Vypadne na nás jen seznam řetězců (= řádků). Podle jejich obsahu se zdá, že tam nějaká struktura bude (vidíme odsazení, uvozovky, hranaté a složené závorky), ale nemůžeme s ní nijak pracovat, jediné, co máme k dispozici, jsou řádky textu.
Na rozpoznání struktury většiny běžných plaintextových formátů (tzv. parsování) naštěstí existují knihovny:
import json
with open("python_crash_course.ipynb", encoding="utf-8") as file:
nb = json.load(file)
Pomocí knihovny json
za nás Python naparsuje strukturu textového souboru a převede ji do podoby, s níž umíme pracovat (vnořené kolekce):
type(nb)
nb.keys()
nb["cells"][0]["source"]
A tak podobně.
Pokud budete někdy potřebovat v Pythonu pracovat s jinými typy souborů než s čistým textem (např. Excel, Word atp.), na 99 % půjde vygooglit nějakou knihovnu, která vám s tím pomůže. Třeba s excelovými tabulkami se dobře pracuje pomocí knihovny pandas.
Výrazy vs. příkazy¶
Někdy je užitečné mít jemnější terminologické rozlišení pro různé části kódu.
Výraz (angl. expression) je jakýkoli kousek pythonovského kódu, který lze vyhodnotit a jeho výsledek uložit do proměnné. Jinými slovy, výraz je cokoli, co můžeme napsat napravo od operátoru =
. Všechno ostatní v Pythonu jsou příkazy (angl. statement).
Příklady:
Hlavička for-cyklu (for x in y:
) je příkaz -- sama o sobě nemá žádný výsledek, který by šel uložit do proměnné. Klíčové slovíčko for
ale může být i součástí výrazu, konkrétně generátorového výrazu, jehož výsledkem je generátor:
(x for x in range(3))
Podobný rozdíl existuje mezi hlavičkou podmínkového větvení (if x:
, elif y:
i else:
jsou všechno příkazy) a operátorem if ... else
, který je součástí výrazu:
42 if False else 0
A do třetice všeho dobrého: přiřazování proměnné (např. x = 3
) je příkaz tvořený:
- jménem proměnné (zde
x
) - přiřazovacím operátorem
=
- a libovolným výrazem (zde číselný literál
3
)
Dokumentace¶
Dokumentace programu slouží k tomu, aby byl čitelný nejen pro Python, ale i pro nás a další programátory. To je důležité, protože když je program nesrozumitelný, těžko se upravuje / opravuje.
Složitější místa v kódu si zaslouží komentář. Je ale zbytečné komentovat každou operaci, zejména když je její záměr i výsledek naprosto jasný už z kódu:
# sečíst dvě jedničky a výsledek uložit do proměnná dva
dva = 1 + 1
Funkce, které nejsou jen na jedno použití, by měly mít dokumentační řetězec (angl. docstring).
def funkce():
"""Toto je dokumentační řetězec.
První řádek by měl obsahovat stručný popis, k čemu funkce
slouží. Další mohou obsahovat detaily, charakteristiku
parametrů funkce atp.
"""
pass
Dokumentační řetězec k libovolné funkci si lze zobrazit pomocí zabudované funkce help
...
help(funkce)
... nebo v prostředí Jupyter pomocí ?
:
funkce?
Inspiraci, jak psát užitečné docstringy, doporučuju hledat u funkcí, které sami používáte :)
sorted?
Comments
comments powered by Disqus