Programování je v současné češtině (resp. coding v současné angličtině) relativně běžné slovo. Stejně jako slova integrál nebo kvantum je však spojováno se subkulturou ne zcela normálních lidí, pro něž má angličtina slovo nerd (české mimoň není zcela přesným ekvivalentem).
Je to osoba vnímaná jako nadprůměrně inteligentní až obsesivní, většinou s nedostatkem sociálních dovedností. Poznávacím znamením je jejich neschopnost chápat a navazovat sociální vztahy s většinou populace.
...
Typickou pracovní náplní nerdů je programování a věda.
...
Česká Wikipedia, heslo nerd [https://cs.wikipedia.org/w/index.php?title=Nerd&oldid=15961484].
Klasická představa nerdů (tzv. "Párty na matfyzu" resp. "Brutální pařba informatiků"), skutečný kontext viz např. forum24.cz </center>
Hlavním cílem programování však není identifikace s touto subkulturou, ale možnost lépe využívat pomoci počítačů pro řešení jednoduchých i složitějších úkolů z běžného ale především odborného života (spojení odborný život doufám není jen synonymum pro život nerda).
Běžní uživatelé s počítači komunikují pomocí grafických rozhraní, která obsahují různé vstupní a výstupní vizuální. To je sice možné, ale podobá se to komunikaci dospělé osoby s dítětem pomocí obrázkové knížky. Dospělé osoby navzájem komunikují pomocí velmi složitého lidského jazyka, který je v případě přírodovědně orientovaných jedinců doplněn o abstraktní terminologii a o mnohdy ještě abstraktnější matematické konstrukce (funkce, rovnice).
Pokud chcete s počítačem komunikovat na této úrovni nezbývá Vám nic jiného, než se naučit programovat tj. přepisovat svoje myšlenky do některého ze speciálních jazyků pro komunikaci s počítačem tzv. programovacího jazyka.
Komunikace pomocí programovacího jazyka se může na začátku jevit jako obtížná, neboť se programovací jazyky (prozatím?) výrazně odlišují od jazyků lidských. Důvodem je jiný výchozí model digitálních počítačů (aritmetické operace mezi pamětí složené z jednotlivých paměťových buněk) a jejich omezené (především) paměťové prostředky (nelze tak prozatím simulovat lidský mozek ani lidskou komunikaci). Většina programovacích jazyků je proto založena na elementárních matematických konstrukcích (jako jsou aritmetické operace a kopírování hodnot mezi paměťovými místy). Programátoři tak museli (a do značné míry stále musejí) dobře znát některé části matematiky (naštěstí dosti elementární) a především chápat interní representaci digitálních počítačů (procesory, strojový jazyk apod.).
Modernější programovací jazyky (dále jen jazyky) však nabízejí konstrukce, které jsou mnohem abstraktnější a v mnoha případech bližší lidskému myšlení (na druhé straně však v nových programovacích jazycích existují i složité koncepce, jejichž pochopení není pro člověka úplně snadné). Komunikace s počítačem tak stále ještě není komunikací mezi dvěma rovnocennými partnery, je však dostupná (a užitečná) i nenerdům.
Otázka: Čím se principiálně liší programovací jazyk od matematického zápisu (notace)?
Stejně jako v případě přirozených jazyků (tj. jazyků určených pro komunikaci člověk ↔ člověk) existují i v případě jazyků programovacích (komunikace člověk ↔ počítač) tisíce různých jazyků a jejich dialektů.
Přirozené jazyky jsou sice běžně navzájem zcela nesrozumitelné většinou však sdílejí mnoho společných koncepcí (i přes zjevnou rozdílnost angličtiny a češtiny oba jazyky sdílejí například protiklad podstatné jméno versus sloveso, koncepce předložek, gramatických osob a slovesných časů). Přirozené jazyky navíc běžně sdílejí tzv. sémantické pole (čeština a angličtina má řádově shodný počet slov, které v zásadě označují podobné entity a ideje). I když může být překlad mezi jazyky přirozenými jazyky obtížný, je v zásadě možný a délky obou jazykových verzí se řádově neliší (po přeložení textu z angličtiny do češtiny běžně nezískáme desetkrát delší text).
U programovacích jazyků je situace poněkud odlišná. Za prvé mohou vycházet ze zcela odlišných koncepcí, která se většinou označují jako tzv. programovací paradigmata. Některá paradigmata jsou široce rozšířena (např. tzv. strukturované nebo procedurální) jiná jsou podporovány jediným programovacím jazykem. Existují však i rysy sdílené většinou programovacích jazyků (podpora operací nad celými čísly nebo koncept označování hodnot dočasnými symboly).
Ještě větší rozdíly existují v podpoře různých druhů informací, objektů nabízených informačními technologiemi a operací nad nimi. Existují jazyky silně specializované (podporující často jen zcela okrajové přístupy ke zpracování informací), jazyky teoreticky univerzální (schopné, alespoň teoreticky zpracovat libovolné informace a přistupovat ke všem prostředků, i když mnohdy za cenu extrémně dlouhého a složitého programu), a jazyky téměř universální (snadno použitelné pro téměř libovolné úkoly).
Buhužel (nebo bohudík) neexistuje žádný skutečně univerzální jazyk, který by plně uspokojoval majoritu resp. alespoň výraznější minoritu programátorů (neexistuje žádná počítačová "angličtina"). Nezbývá tak nic jiného než si nějaký jazyk zvolit (na základě jeho popularity nebo maximální kompatibility s Vaším myšlením) resp. naučit se hned několik programovacích jazyků (pro profesionální programátory je to nutnost).
Pro účely tohoto studijního materiálu jsem provedl volbu za Vás a zvolil programovací jazyk Python. I když ani Python dokonalý má hned několik konkurenčních výhod:
je tzv. multiparadigmatický, tj. kromě hlavního paradigmatu (je jím v současnosti nejpoužívanějšé objektové) popdporuje i další
má velkou a aktivní komunitu uživatelů a to i mezi vědci a inženýry
je užíván i podporován i v komerční sféře (a to i velkými firmami)
má jednoduchou syntaxi (která je sice jedinečná, ale v zásadě se neliší od syntaxe programovacích jazyků hlavního proudu)
pro celý dokumentace a výukové materiály pro začátečníky i pokročilé (především v angličtině, ale ani podpora v českém jazyce není špatná)
Otázka: Podle čeho je pojmenován jazyk Python?
- podle kobry (anglicky python)
- podle Monty Pythonů
- podle draka (hada) střežícího Delfy
Nápověda: Logo Pythonu
Pro programování v Pythonu v zásadě potřebuje jen jedinou aplikaci: interpret jazyka Python, který vykonává program v Pythonu, tj. provádí jednotlivé kroky programu na procesoru a s využitím dalších prostředků počítače nebo počítačové sítě.
Interpret buď vykonává jednotlivé příkazy programu zadávané z konzole (režim interaktivní) nebo vykonává příkazy uložené v textovém souboru (tzv. skriptu) v režimu dávkovém. Skripty jsou vytvářeny a editovány pomocí textového editoru (může to být libovolný editor nevkládající formátovací značky jako např. notepad, ale častěji to bývají tzv. programátorské editory).
Instalace pythonského interpretu není triviální, neboť kromě spustitelného souboru interpret vyžaduje stovky dalších souborů – knihoven, dokumentace, pomocných skriptů a dokonce i pomocných spustitelných souborů. Proto je vhodnější nainstalovat interpret i s celou jeho podporou tzv. distribuci Pythonu.
Základní možností je standardní distribuce dostupná na domovských stránkách Pythonu: Python download. Aktuální hlavní větev Pythonu je nyní Python 3 a proto stahujte nejnovější verzi v této větvi (v době psaní tohoto textu to byla verze 3.6.5).
Výhodou této distribuce je vždy nejnovější verze Pythonu a podpora velkého množství platforem (operačních systémů).
V mnoha případech je však pohodlnější distribuce tzv. Intel Pythonu. Ta kromě optimalizací pro procesory Intel obsahuje i velké množství dodatečných knihoven, z nichž mnohé budeme dále využívat (lze je snadno doinstalovat i do standardní distribuce, ale je to pamalejší a občas se mohou vyskytnout problémy).
Distribuci Intel Pythonu najdete na stránce Intel Distribution for Python. Je opět k dispozici pro Linux i Windows a v obou větvích (opět zvolte novější Python 3!). Pozor: tato distribuce má již při stažení velikost přes jeden gigabyte (po instalaci je to ještě více).
Kromě výše uvedených existují i další zajímavé distribuce: Active Python resp. Anaconda.
V případě Linuxu je Python běžně k dispozici v balíčcích v repositářích dané linuxovské distribuce. I zde zvole balíčky pro verzi Python 3 (nejběžnější jméno je python3). Balíčky nemusí podporovat nejnovější verzi Pythonu (u Ubuntu 16.04, který používám je to Python 3.5).
Navíc zde existuje možnost, jak se (lokální) instalaci Pythonu zcela vyhnout. Python je běžně nabízen jako cloudová služba, nejčastěji prostřednictvím rozhraní notebooků Jupyter (čtěte dále)
Jak bylo řečeno výše může být interpret jazyka Python využit i v interaktivní režimu. Je to však velmi nepohodlné, neboť standardní interpret jazyka Python neumožňuje ani řádkovou editace, tím spíše celoobrazovkový režim.
Z tohoto důvodu vznikl vyspělejší textově orientovaný interaktivní interpret ipython, který kromě řídkové editace podporuje i historii či doplňování syntaxe. Dalším vývojem ipythonu vznikl projekt jupyter (skutečně s ypsilonem, spojení py je jakýmsi poznávacím znamením pythonských nástrojů). Jupyter nabízí interaktivní dokumenty tzv. notebooky. Notebooky mohou kromě kódu, textového vstupu a výstupu zobrazovat i grafiku resp. strukturované texty.
Pomocí Jupyterovského notebooku (dále jen notebook) je vytvořen i tento dokument a budeme jej využívat i nadále pro komentované ukázky kratších pythonských kódů. Můžete jej sa mozřejmě využívat jako jakousi vylepšenou pythonského kalkulačku pro řešení konkrétních úkolů.
Pokud máte nainstalován Intel Python, pak již nemusíte níc instalovat. Stačí spustit příkaz:
jupyter notebook
Příkaz najdete v adresáři bin uvnitř adresáře, který vznikne po instalaci Pythonu (v Linuxu je to adresář intelpython3.
Po spuštění se vytvoří lokální webový server a vyvolá se webový prohlížeč, který zobrazí tzv.dashboard. V něm lze otvírat již existující notebooky (dole prostřednictvím výpisu obsahu aktuálního adresáře resp. vytvářet notebooky nové (tlačítko New vpravo nahoře).
Pokud není popdpora Jupyteru obsažena v instalaci Pythonu, lze ji doinstalovat pomocí nástrojů pro instalaci pythonských modulů (knihoven).
Obecně lze využít napřiklad nástroj pip, který obsahují všechny moderní instalace Pythonu. Na příkazovém řádku stačí uvést:
pip install jupyter
(program pip musí být v aktuálním adresáři resp. v tzv. cestě).
Nejjednodušší možností použití notebooků je využití cloudové podpory. V tomto případě není nutno nic instalovat (dokonce ani Python).
Nejjednodušší použití nabízejí dva projekty:
Použití je většinou zcela intuitivním vyžaduje však registraci a Google resp. Microsoft (internetový) účet.
Protože nevlastním účet Microsoftu (nepoužívám MS Windows), popíši zde použití Google Collaboratory.
1) přihlašte se do Googlu 2) přejděte na URL https://colab.research.google.com 3) vytvořte nový (Python 3) notebook (resp. otevřte existující se seznamu)
Notebooky jsou v případě této služby uloženy na Google Drive (alternativně lze využít i GitHub). Google Collaboratory používá mírně odlišný grafický vzhled (styl) notebooků, základní rozvržení (je však stejné).
Potevření notebooku lze psát kód přímo tzv. vstupních buněk (jsou označeny výzvou In vpravo od buňky, viz také obrázek).
Po zadání kódu jej lze vyhodnotit současným stiskem Shift+Enter. Výsledek se objeví v tzv. výstupní buňce (vlevo s označení Out). Pokud kód žádný výstup neprodukuje (viz vstupy číslo 8 a 9 na obrázku), pak se výstupní buňka nevytvoří.
Pod ní se objeví nová vstupní buňka (na rozdíl od již vyhodnocených není označena číslem).
Kromě lineárního vkládání vstupů (a následného zobrazování výstupů) lze notebook využívat jako skutečný poznámkový blok. Můžete se vracet do již existujících (a vykonaných) vstupních buněk, editovat je a následně opakovaně vykonávat. Lze vkládat nové (vstupní) buňky resp. buňky naopak mazat.
Editace notebooku je relativně komplexní činnost, kterou můžete provádět pomocí myši (a hlavního menu resp. nástrojové lišty) nebo pomocí četných klávesových zkratek (to je rychlejší a většinou i pohodlnější).
Základní pravidla jsou tato:
editor se může nacházet ve dvou režimech:
editační režim – přímá editace vstupní buňky (dostupné jsou jen základní editační klávesy podobně jako např. v notepadu)
příkazový režim – klávesnice slouží k zadávání příkazů pracujícími s celými buňkami (příkazy jsou běžné znaky, není třeba využívat přepínače Ctrl nebo Alt). V tomto režimu nelze přímo zadávat text!
Aktuální režim lze zjistit pomocí ikonky režimu vpravo od toolbar. Pokud tam je stylizovaná tužka pak jste v editačním režimu, tj. můžete začít přímo vkládat kód nebo texty. V příkazovém režimu se ikonka nezobrazuje. Režim se navíc projevuje i v dalších příznacích, např. zobrazováním textového kurzoru v editačním režimu (v příkazovém se nezobrazuje) nebo barvou okraje aktuálního vstupního poo le (v příkazovém je okraj zelený).
Při práci s notebookem jste běžně v editačním režimu (po startu, po vyhodnocení kódu a vložení nové vstupní buňky). Do příkazového režimu se dostanete stiskem klávesy Esc. Nazpět do příkazového je nejjednodušší použít klávesu Enter (lze použít i kliknutí myší do příslušné buňky).
V příkazovém režimu (bez ikony, s modrým okrajem) lze zadávat velké množství příkazů, z nichž si na začátku stačí pamatovat jen následující:
| klávesa | funkce v příkazovém režimu |
|---|---|
| kurzurové klávesy | posun mezi vstupními buňkami (nahoru a dolů) |
| a | přidat vstupní pole nad aktuální buňku ([a]bove) |
| b | přidat vstupní pole pod aktuální buňku ([b]elow) |
| dd (=2×d) | smazání aktuální buňky |
| y | nastaví aktuální buňku jako výpočetní (= obsahuje kód) |
| m | nastaví aktuální buňku jako popisnou (= obsahuje značkování) |
Poslední dvě klávesové zkratky souvisejí s dalším charakteristickým rysem notebooků. Kromě kódu lze do buněk vkládat i formátovaný text ve funkci anotací, doprovodného textu apod. Tento text je zapisován pomocí jednoduchého značkovacího jazyka Markdown.
I když je tato možnost velmi praktická a je plně využita při tvorbě této opory (veškerý text je vložen pomocí popisných buněk) není popis Markdown součástí této opory. Pokud máte o popisné texty zájem doporučuji například úvod na stránce http://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Working%20With%20Markdown%20Cells.html.
Úkoly: Otevřete nový notebook (v lokální instalaci Pythonu resp. v Google Collaboratory) a vložte následující postupně buňky (až po buňku označenou jako konec příkladu vyjma):
from math import *
log(10)
log10(10)
[konec příkladu]
Jen za pomoci klávesnice se pohybujte mezi výše vloženými buňkami (v příkazovém režimu). Do první buňky doplňte programovou poznámku (v editačním režimu), tak aby vypadala takto (vyhodnoťte a popřípadě odstraňte novou prázdnou buňku, která se po vyhodnocení vloží):
from math import * # vložení modulu matematických funkcí nad reálnými čísly
log(10)
smažte buňku s výpočtem log10 (tvoři jí vstup i výstup). A místo ní vložte (nad) novou kódovou buňku s následujícím kódem a vyhodnoťte ji (a opět popřípadě odstraňte novou prázdnou buňku, která se po vyhodnocení vloží)
log(100)/log(10)
I když je Jupyter notebook velmi užitečným nástrojem pro interakci s Pythonem a v mnoha případech i plně dostačuje, existují i činnosti, na které se příliš nehodí. Problém nastává hlavně v případě, že potřebujete editovat rozsáhlejší programy (aplikace) resp. vytvářet vlastní moduly.
V těchto případech je vhodnější editace pomocí externího editoru (v podobě lokální aplikace). Použít lze sice jakýkoliv editor nevkládající formátovací znaky (např. notepad), existuje však i komfortnější alternativa: integrovaná vývojová prostředí (angl. zkratka IDE).
Pro Python existuje hned několik integrovavaných vývojových prostředí (Komodo, LiClipse, Wing, přehled dalších viz https://wiki.python.org/moin/IntegratedDevelopmentEnvironments). V rámci této výukové opory bude využívána aplikace PyCharm od firmy JetBrains.
Toto vývojové prostředí podporuje (mimo jiné):
Určitou nevýhodou je skutečnost, že PyCharm není zcela open-source. Otevřená (a zcela bezplatná) je pouze verze Community. Ta má sice určitá omezení avšak pro naše účely bohatě stačí.
PyCharm je k dispozici pro všechny hlavní platformy (Linux, MS Windows, Mac OS X) a jeho instalace je snadná. Instalační aplikaci (včetně detilního návodu) lze nalézt na stránkách
Upozornění: Před instalací PyCharmu je vhodné provést instalaci vlastního Pythonu (viz výše). Vývojová prostředí běžně neobsahují interprety a překladače příslušných programovacích jazyků.
Při prvním spuštění PyCharmu provedete základní konfiguraci (zásadnější je pouze rozhodnutí o baravném tématu, u ostatních je možné nechat defaultní hodnoty).
Poté se objeví okno startovní menu, v němž zvolte vytvoření nového projektu (levá část okna). Poté se zobrazí konfigurační dialog.
Nejdříve zadejte jméno projektu, které tvoří jméno adresáře na konci cesty, jež je zobrazena v prvním vstupním poli (jméno projektu je na obrázku zvýrazněno červeným obdélníkem). Můžete samozřejmě měnit i celous cestu, u zkušebních projektů to nicméně není nutné (pro volbu cesty k projektovému adresáři lze samozřejmě využít i běžný výběr souboru/adresáře dostupný tlačítkem se třemi tečkami vpravo od vstupního pole).
Při vytváření prvního projektu je nutno zvolit i cestu k interpretu Pythonu, který chcete použít pro spuštěnní programů (to je kritické především u uživatelů s více interprety tohoto jazyka).
Nejdříve zvolte přepínač Existing interpreter. Pokud je požadovaný interpret v nabídce rozbalovacího vstupního pole (rozbalí se po stisku tlačítka se trojúhelníkem) máte vyhráno. Pokud tam není pak je nutno vyvolat dialogový box dostupný prostřednictvím tlačítka s třemi tečkami zcela vpravo od vstupního pole.
Poté se zobrazí další dialogový box, v němž je nutno nastavit vstupní pole "Interpreter" (opět nejlépe s využitím tlačítka s třemi tečkami v pravo). Poté se již zobrazí běžný výběr souborů. Pomocí něho musíte najít cestu k interpretu, tj. ke spustitelnému souboru s názvem python3 nebo python3.X kde X je číslo podverze (6,7,…). Pokud nemůžete tento soubor najít, použijte běžný souborový manažer a jeho nástroje pro hledání (nalezenou cestu pak stačí jen nakopírovat do hlavního konfiguračního dialogu). Po volbě se musí v pravé části vstupního pole objevit ikonka Pythonu s číslem verze (na obrázku je to Python 3.6).
Nastavení interpretu je relativně nepohodlné a zdlouhavé, ale provádí se naštěstí jen u prvního projektu (můžete ho však samozřejmě změnit).
Po nastavení projektu se zobrazí projektové okno, podobné tomu na následujícím obrázku (skutečné okno se může poněkud lišit stylem i uspořádáním).
Hlavní okno aplikace se skládá ze dvou základních panelů. Vlevo je panel projektu, který zobrazuje jednotlivé části projektu (adresáře, soubory, externí knihovny, apod.). Vpravo je (výrazně větší) vlastní editační okno, které umožňuje zobrazovat jednotlivé textové soubory projektu. V jednom okamžiku lze vidět a editovat jediný soubor, ostatní soubory jsou dostupné pomocí bežného systému záložek (oušek) v horní části editoru (na obrázku je pouze jedna záložka).
Ostatní panely jsou nepovinné a zobrazují se jen tehdy, pokud jsou potřeba. Nejčastěji se setkáte s výstupním panelem, který zobrazuje textový výstup programů (na obrázku v dolní části).
Po otevření nového projektu je editační okno prázdné, neboť projekt neobsahuje žádný zdrojový soubor. Proto je nutné nejdřívě tento soubor vytvořit. V projektovém panelu klikněte na jméno projektu, které representuje projektový adresář (prozatím prázdný) a z hlavního menu zvolte volbu File | New …. Zobrazí se malé menu, v němž zvolte Python File. V minidialogovém okně zadejte jeho jméno (stačí bez přípony py ta se doplní automaticky).
Nově vytvořený zdrojový soubor se automaticky zobrazí v editačním okně (i s příslušným ouškem). Jméno nového souboru se zobrazí i uvnitř projektového adresáře v projektovém panelu a v tzv. projektové liště pod hlavím menu.
Po napsání programu lze program spustit pomocí menu Run | Run … (objeví se seznam zdrojových souborů v daném projektu, z něhož jeden vybereme). Pokud program neobsahuje chybu, pak se objeví výstupní okno, v němž se (mimo jiné) zobrazí i výstup programu (na obrázku v dolním červěném čtverci).
Opakované spuštění programu je již jednodušší. Stačí zvolit tlačítko se zeleným trojúhelníkem vpravo v projektové liště nebo vlevo ve výstupním panelu. Lze samozřejmě využít i hlavní menu (Run | Run xxx.py, kde xxx je jméno skriptu bez přípony) resp. klávesovou zkratku Shift + F10).
Úkol: Pomocí vývojového prostředí a vytvořte zdrojový soubor (= skript) s následujícím obsahem:
import os
print(os.uname().sysname + " " + os.uname().version)
Poté program spustťe. Po spuštění interpretujte jeho výstup (může se lišit od výše uvedeného).
Termínem syntaxe se označují pravidla určující přípustný zápis (skript, program) v Pythonu. Pokud je program syntakticky správný, pak jej lze spustit (program však nutně nemusí vykonávat užitečnou činnost).
Syntaxe odpovídá pravopisu, který známe z češtiny a dalších jazyků se standardizovaným textovým zápisem. Mezi syntaxí a pravopisem však existují jeden zásadní rozdíly:
Syntaxe programovacích jazyků je striktní. F přýpaďe přyrozenýh jazikú lzepřětšýzd y zápyz porušujýcý fšechna prawopysnáprawy dla. U programů v programovacím jazyce stačí jediný chybný znak a program nemůže být spuštěn
Se syntaxí Pythonu se budeme seznamovat v celé první polovině kurzu (a v omezené míře i ve druhé). Na začátku si uveďme jen ta nejzásadnější pravidla.
Mezery lze v Pythonu psát kdekoliv mezi jednotlivými tzv. tokeny (čísly, identifikátory, operátory apod.). Ve většině případů jsou však nepovinné a slouží pouze ke zvýšení přehlednosti kódu (lépe vizuálně oddělují tokeny).
2 + 3*6
2+3 * 6
Oba zápisy jsou ekvivalentní. V obou případech se nejdříve provede násobení a až poté sčítání (bez ohledu na mezery). Z důvodů přehlednosti a tradic jsou však některé zápisy více preferovány více než jiné (zde je výrazně přehlednější první zápis). Mnoho uživazelů Pythonu však píše mezery kolem všech operátorů (= symbolů aritmetických operací).
2 + 3 * 6
Formálně jsou povoleny i vícenásobné mezery:
2 + 3 *6
V praxi se však vícenásobné mezery téměř nikdy nevyužívají (rozhodně ne ve stylu předchozího příkladu).
V Pythonu však existuje i velmi důležitá výjimka z volného zápisu mezer. Mezery na začátku (neprázdného) řádku slouží k odsazení, které má v Pythonu klíčovou roli pro organizaci kódu. Mezery na začátku lze využívat jen u vnořených konstrukcí a roli hraje i (relativní) počet použitých mezer!
for i in range(3):
print(i)
for i in reversed(range(3)):
print(i)
Mezery na počátku druhého a čtvrtého řádku jsou povinné. Formálně by stačila i jedna, ale čtyři jsou doporučovány. V každém případě se musí jednat o stejný počet na obou odsazených řádcích. Naopak stranu na prvním a třetím řádku (začínají slovem for) nesmí být na začátku žádná mezera!.
Na rozdíl od mezer je odřádkování v Pythonu téměř vždy syntakticky významné. Odřádkování (neviditelný znak vkládaný klávesou Enter) odděluje příkazy. V předchozím příkladě jsou čtyři příkazy na čtyřech řádcích.
Rozložení příkazů na více řádků je sice možné, ale nelze je provádět kdekoliv. V našem případě je to navíc zbytečné (řádky jsou krátké a rozdělením přehlednost nezvýšíte).
U dlouhých příkazů (typicky > 80 znaků) je rozdělení běžné, lze jej však provést pouze na místě, kde jsou přípustné mezery a zároveň leží mezi dvěma párovými závorkami.
(1 + 1/2 + 1/3 + 1/4 + 1/5 + 1/6 + 1/7 + 1/8 + 1/9 + 1/10
+ 1/11 + 1/12)
I když výše uvedený zápis využívá dva řádky, jedná se stále o jediný příkaz. Odřádkování je zde využito jen pro přehlednost (dlouhé řádky se nemusí vejít na displej). Rozdělení je přípustné, neboť je umístěno mezi dva tokeny (číslo 10 a operátor +) a leží uvnitř párových závorek (ty jsou zde použity jen z důvodů podpory rozdělení, u jednořádkové verze by byly nadbytečné). Zajímavé je i odsazení pokračovacího řádku. To je v tomto případě nepovinné (není to odsazení na začátku příkazu) a je zde pouze z důvodů přehlednosti (na první pohled je zřejmém, že se jedná jen o pokračování předchozího řádku). Velikost odsazení pokračovacího řádku (pokud je vůbec použito) může být libovolné (v našem případě bylo dosaženo hezkého zarovnání operátoru +).
Spojení více příkazů do jednoho řádku je sice možné, ale velmi omezené. Navíc se důrazně nedoporučuje.
Přípustné jsou i prázdné řádky (a to kdekoliv). Prázdné řádky nemají pro překladač žádný význam a používají se jen pro (vertikální) vizuální oddělení některýchh sekcí kódu.
Na konci řádků (po všech nemezerových znacích) lze uvádět tzv. poznámky. Poznámky nejsou překladačem interpretován, a slouží tak jen k orientaci programátorů. Poznámky začínají znakem # a končí na konci řádku (tj. jsou vždy jednořádkové). Znak odřádkování k dané poznámce nepatří (slouží k oddělení příkazů tj. je interpretován překladačem)
print("Hello Middle-earth") # toto je poznámka
# a toto je jiná (překladač místo ní vidí nevýznamný prázdný řádek)
Shrňme si základní syntaktická pravidla Pythonu:
#.Základní funkcí programovacích jazyků je manipulace (vytváření, transformace, interakce, apod.) s tzv. objekty.
Objektem může být například:
Základními objekty univerzálních jazyků včetně těch vysokoúrovňových jsou číselné hodnoty. Ostatní objekty jsou svou podstatou jen jinou interpretací čísel nebo jejich posloupností (výjimkou jsou objekty spojené s externími prostředky).
Celočíselné hodnoty se v Pythonu zapisují pomocí běžných desítkových číslovek. Python navíc neomezuje rozsah representovatelných čísel (jediným omezením je tak pouze operační paměť počítače a trpělivost jeho uživatelů). Zcela standardní je i zápis základních operací: sčítání(+), odečítání (-) a násobení (*).
Od verze Python 3.6 lze větší číslice psát s oddělovačem v podobě podtržítka (typicky jako oddělovač tisíců, Python však pozici podtržítka nekontroluje).
2 + 99 - 100
111 * 333 * -555
101 * 1_001 * 10_001 * 100_001 * 1_000_001
Mírně komplikovanější je dělení, neboť výsledkem běžného dělení dvou celých čísel nemusí být celé číslo. Programovací jazyky (a obecněji počítače) tuto situaci řeší zavedením dvou typů dělení, z nichž každé vrací odlišný výsledek:
běžné (reálné) dělení vždy vrací tzv. číslo s pohyblivou řádovou čárkou, což je (potenciálně nepřesná) representace racionálního čísla (zlomku). V Pythonu se zapisuje znakem (operátorem) /.
celočíselné dělení, které vždy vrací celé číslo, zanedbává však tzv. zbytek (zjednodušeně řečeno vrací celou část podílu). V Pythonu se tato operace zapisuje dvojznakem //.
4 / 3 # reálné dělení (vrací dekadický zlomek, který s určitou přesností representuje výsledek)
4 // 3 # celočíselné dělení (vrací celou část výsledku)
Ve výše uvedeném případě je rozdíl výsledků zřejmý (1 ≠ 1.33333333…). Rozdíl mezi oběma verzemi dělení však může být mnohem skrytější:
4 / 2
4 // 2
Zde je výsledek z pohledu matematika stejný, neboť obě čísla lze dělit celočíselně beze zbytku. Různé formátování výsledku (výsledek reálného dělení je zapsán s desetinnou tečkou) však odráží jemný avšak potenciálně důležitý rozdíl z pohledu počítačů.
Výsledkem reálného dělení je hodnota representovaná ve tvaru $m \times 2^e$, kde $m$ je mantisa v rozsahu 0 (včetně) až 1 (vyjma) s omezeným počtem za řádovou tečkou a $e$ binární exponent. Tato hodnota je svou podstatou nepřesná a s omezeným (i když obrovským) rozsahem. Na druhou stranu hodnota zaujímá vždy jen 8 bytů v operační paměti (bez ohledu na řád čísla, tj. např. čísla 2 a $10^{100}$ zaujímají stejný prostor v paměti) a stejně tak na velikosti čísla nezávisí rychlost provádění operací (ta je navíc rychlá, pokud počítač obsahuje speciální podporu této representace na procesoru tzv. FPU). Čísla s touto representací patří do třídy float (třída je množina objektů se stejnou representací a operacemi). Označení float pochází z anglického termínu number with floating point representation (číslo v pohyblivé/plovoucí čárce/tečce).
Výsledkem celočíselného dělení celých čísel je číslo v běžné celočíslené representaci (patří do třídy označované int což je zkratka anglického slova integer). Tato representace má právě opačnou charakteristiku — čísla jsou svou podstatou přesná a neomezená, mohou však v případě velkých čísel (řádu $10^18$ a vyšších) zaujímat více paměti a vést k výrazně pomalejším operacím. Celočíselná aritmetika nevyžaduje speciální podporu na procesoru.
float (v pohyblivé řádové čárce)¶Čísla třídy float lze přímo zapisovat pomocí běžné notace (jediným rozdílem je použití desetinné tečky na místě desetinné tečky). Navíc lze využít zápis s explictním uvedením (desítkového) exponentu, který pravděpodobně znáte např. z Excelu:
3.0 # obsahuje desetinnou tečku tj. vyhodnotí se na číslo třídy 'float'
3.25e6
Zápis 3.25e6 odpovídá matematické notaci $3{,}25 \times 10^6$ = 3 250 000. Všimněte si, že standardní výstupní formát se liší od vstupního, avšak i výstup signalizuje číslo v pohyblivé řádové čárce (explicitní tečka a jedna číslice destinné části).
Základní operace lze samozřejmě aplikovat i na čísla třídy float. Počítají se však v omezené přesnosti a výsledkem je opět číslo třídy float.
2.0 * 5.6 + 1.0
1e11 + 1e-6 - 1e11 # pozor přijde překvapení
Omezená přesnost může vést k nepřesným, či dokonce zcela matoucím výsledkům (tj. k výsledkům v nichž žádná číslice není platná). Správným výsledkem výše uvedeného výrazu je $10^{-6}$ (hodnoty $10^{11}$ se vzájemně vyruší). I když je rozdíl zdánlivě velmi malý a zanedbatelný (pokud výše čísla representují vzdálenosti v metrech pak nepřesnost odpovídá chybě v měření vzdálenosti Země od Slunce ($\approx 1{,}5 \times 10^{11}$ km) v řádu mikrometrů, nelze jej zcela přehlížet. Nepřesnosti se totiž mohou hromadit (a dosáhnut tak řádu, který již není zanedbatelný) resp. ovlivnit různé testy (například testy na nulovou hodnotu).
3.0 * 0.1 - 0.3 # výsledek by měl být 0
Číslo $5{,}551115123125783 \times 10^{-17}$ je sice extrémně malé, ale pro počítače je různé od nuly!
3.0 * 0.1 == 0.3 # operace '==' porovnává dvě hodnoty
Hodnota False representuje nepravdu, tj.levá strana porovnáná se nerovná pravé! Počítače jsou v tomto případě hloupější než absolventi 5.třídy ZŠ.
Úkol: Ověřte přesnost representace čísel 0.1, 0.2, 0.3 až 0.5 pomocí jejich trojnásobku. Zkuste zdůvodnit výsledek (podívejte se výše, jak jsou tato čísla interně representovaná).
Nyní se vraťme k operacím nad čísly. U čísel třídy float je přirozenou operací dělení běžné (reálné) dělení.
42.0 / 4.0
V Pythonu však lze celočíselné dělení využít i pro neceločíselná čísla (přesněji čísla třídy float). Výsledkem dělení a//b je v tomto případě float representace čísla $\left\lfloor a/b\right\rfloor$, kde závorky s příčkou dole representují nejbližší menší celé číslo (tato definice je zobecněním celočíselného dělení mezi čísly).
42.0 // 4.0
Kde 10.0 je $\lfloor 10{,}5\rfloor$ Vztah platí i pro výrazy se zápornými operandy (kde je však méně názorná):
-10.0 // 3.0
Operace celočísleného dělení mezi desetinnými čísly má i praktické využití. Například lze snadno zjistit kolik úplných otáček provedlo těleso, které provedlo rotaci o 50 radiánů:
50 // (2 * 3.1416)
Kromě čtvera (přesněji patera) klasických operací Python podporuje i dvě operace, které na jednoduché kalkulačce nenajdeme: zbytek po celočíselném dělení (operátor %) a mocnění (operátor **). Obě operace jsou použitelné pro obě číselné třídy.
10 % 3 # zbytek po dělení 10 // 3
10.0 % 3.0 # totéž ale s výsledkem třídy `float` (zde je výsledek přesný, ale obecně to není zaručeno)
Pro záporná a necelá čísla (representovaná třídou float) lze využít obecnější vztah a % b = a - (a//b) * b, pro nějž (stejně jako v případě zbytku podílu přirozených čísel platí že absolutní hodnota zbytku je menší než absolutní hodnota dělitele) a znaménko je rovné znaménku dělitele (výsledek je viditelně trochu nepřesný):
10.3 % 3.0 # = 10.3 - 3.0 * 3.0 (výsledek je viditelně nepřesný)
10 % -3
-10 % 3
V případě mocnin je nejjednodušší mocnění celého čísla číslem přirozeným. Pokud jsou obě čísla třídy int pak je této třídy i přesný výsledek (i když může být značně velké!)
2 ** 256
Výpočet pomocí čísel třídy float vrací podobný výsledek, který je však o poznání méně přesný:
2.0 ** 256.0
Navíc lze snadno získat číslo, které se do objektů třídy float nevejde (rozsah je omezen na cca $\pm 10^{300}$). To vede ke vzniku tzv. výjimky, což je signalizace výjimečného a v daném kontextu neřešitelného problému. Výjimka vede k přerušení výpočtu (bez dosažení výsledku) a výpisu chybového hlášení (v případě komplexnějších aplikací k jejjich předčasnému ukončení).
2.0 ** 1024.0
Výpis ukazuje, že na vyznačeném řádku (textová šipka zprava) došlo k výjimce třídy OverflowError tj. k tzv. přetečení (výsledek je tak velký, že se nemůže vejít do objektu dané třídy).
Stejně jako v matematice je operace umocnění zobecněná pro libovolná reálná čísla a to jak v mocněnci tak mocniteli. Výsledek je (pokud leží v oboru reálných čísel) representován číslem třídy float. Pokud je komplexní je výsledkem třídy complex (Python umí pracovat i s komplexními čísly).
1.5 ** -1 # převrácená hodnota
1.5 ** 0.25 # čtvrtá odmocnina
(-1.0) ** 0.5 # odmocnina záporného čísla -1 (závorky kolem záporného čísla jsou nutné!)
Formátování výsledku (= i) je poněkud překvapivé. Reálná část není nulová, ale jen velmi blízká nule (rozdíl je dán nepřesnou representací, žádné z číslic reálné část není platné), imaginární část využívá namísto symbolu i symbol j (ten je užíván například v elektrotechnice). Navíc násobitel 1 (v zápise 1j), který v matematice zbytečný, je v Pythonu povinný (jinak by se pletl s proměnnou j).
Úkol: Vypočtěte hodnotu $\sqrt[3]{2-\sqrt{2}}$.
Python (a většina ostatních programovacích jazyků) umožňuje ve výrazech libovolně míchat čísla třídy int s čísly třídy float.
2 * 3.0 + 1 # int * float + int
Jak lze vidět z příkladu (výsledkem je hodnota třídy float) dochází v tomto případě k tzv. sjednocení typů (typ je jiné označení pro třídu hodnot). Při sjednocení se hodnoty striktnějšího typu přizpůsobí typu obecnějšímu, a to tím že se provede jejich konverze na obecnější typ/třídu. V případě čísel se tedy hodnoty třídy int konvertují na hodnoty třídy float. Ve výše uvedeném případě se tedy hodnota 2 (třídy int) převede na hodnotu 2.0 (třídy float) a až poté se vyhodnotí celý výraz.
Při této konverzi se ve většině případů neztrácí žádná informace, neboť čísla typu float jsou schopna representovat všechna celá čísla v intervalu cca $(-9 \times 10^{15}, 9 \times 10^{15})$. U větších čísel však může dojít k ztrátě přesnosti, přetečení či zbytečné alokaci paměti.
Dvanácté Mersennovo prvočíslo (objeveno Eduardem Lucasem v roce 1876) lze vypočítat vztahem:
2 ** 127 - 1
Smíšené použití float a int čísel vede k přibližnému výsledku (odečtení jedničky nemá v tomto případě žádný efekt, omezená přesnost float nedovoluje representovat u tak velkého čísla jednotky).
2.0 ** 127 - 1
Výsledek je nepřesný, je však relativně rychle spočítán a nevyžaduje příliš mnoho paměti. Mocnina se totiž vypočte ve float aritmetice pomocí exponecionální a logaritmické funkce (127 se předtím konvertuje na 127.0). Na závěr se k výslednému float číslu připočte float hodnota 1.0 (po přetypování). To sice nemá žádný efekt je to však alespoň rychlé.
To však zdaleka není ten nejhorší možný zápis. Ještě o řád horší je:
2 ** 127 - 1.0
Výsledek je stejný, vyžaduje však celočíselný (a tudíž přesný) výpočet $2^{127}$ (výsledkem je číslo s 38 číslicemi, zaujímající 16 bytů paměti a desítky instrukcí, neboť i ty nejmodernější počítače přímo provádějí jen 64 bitové operace = 8 bytů). Až poté je toto číslo převedeno na float representaci (přizpůsobí se číslu 1.0 v operaci odečítání, která se vždy provádí až po mocnění) přičemž se vyhodí 8 bytů dat a cca 22 dekadických číslic výsledku) a to zcela zbytečně (ani zde odečtení 1.0 nic nezmění).
Obecně platí, že je lepší se automatickým konverzím vyhnout (tj.používat jen celá čísla s celočíselnou aritmetikou nebo čísla s desetinnou částí s aritmetikou čísel s pohyblivou řádovou čárkou). Pokud už jsou potřeba lze je explicitně vynutit pomocí následujících zápisů (ve skutečnosti se jedná o volání konverzních funkcí):
float(2) # funkce float převádí číslo libovolného typu na typ float
int(3.1415) # funkce int převádí čísla na typ `int` (u necelých čísel odsekává desetinnou část)
Úkol: Co je výsledkem následujícího výpočtu:
int((1.0/49.0) * 49). Vyzkoušejte a zdůvodněte.
U složitějších výrazů s více operátory hraje roli tzv. priorita operátorů. Operátory s vyšší prioritou se vyhodnocují dříve než operátory s prioritou nižší. To není nic překvapivého, neboť stejný přístup používá i běžný matematický zápis. Už od základní školy například víte, že násobení má přednost před sčítáním:
2 + 3 * 3
Pořadí vyhodnocování lze stejně jako v matematickém zápise ovlivnit pomocí závorek.
(2 + 3) * 3
Pokud mají operace stejnou prioritu, pak se vyhodnocují zleva doprava (tj. ve směru ve kterém je čteme).
2 * 3 // 5 # nejdříve se násobí, pak se dělí (obě operace mají stejnou prioritu)
2 * (3 // 5) # pokud by se dělilo dříve pak získáme jiný výsledek
Pořadí priorit v Pythonu odpovídá očekávání většiny uživatelů (vychází důsledně z běžného matematického úzu). Ve většině případů se stačí řídit citem a jen v případě, kdy si nejste jisti využívat závorky (příliš mnoho závorek, programátorova smrt). V případě nejistoty lze navíc konzultovat dokumentaci např. na https://docs.python.org/3/reference/expressions.html#operator-precedence (tabulka je řazena od nejnižší k nejvyšší prioritě).
Zde bych zmínil jen dvě situace, kdy může Python nepříjemně překvapit.
Prvním nepříjemným překvapením je priorita mocniny.
-1 ** 2
Mocnina má vyšší priorotu než unární mínus (operátor opačné hodnoty), zápis je interpretován jako -(1 ** 2)! Mezery nehrají při vyhodnocování žádnou roli!
(-1) ** 2
Druhým a snad ještě nepříjemnějším překvapením je skutečnost, že kvůli ztrátám přesnosti, přetypování a přetečení neplatí v počítačové aritmetice ani elementární matematické zákony.
V Pythonu například v některých (relativně řídkých) situacích neplatí zákon asociativnosti u násobení čísel $(a \times b) \times c = a \times (b \times c)$
(1e200 * 1e200) * 1e-100
V tomto případě je výsledkem přetečení, které však nevede k výjimce ale ke speciální hodnotě inf representující nekonečno.
1e200 * (1e200 * 1e-100)
Pokud změníme pořadí vyhodnocení je výsledek správný (nevznikne mezivýsledek, který nelze representovat).
Objekty vzniklé při vyhodnocení pythonských výrazů existují jen krátce. Mezivýsledky zanikají ihned poté, co jsou použity pro další výpočet, konečné výsledky výrazů použitých v Jupyter notebooku ihned poté, co jsou zobrazeny ve výstupním řádku.
Při zániku objektu je uvolněna paměť, kterou zaujímaly a ztrácí se jakákoliv informace o jejich existenci. Pokud stejný výraz vyhodnotíme podruhé, vzniká nový objekt.
Python však (stejně jako mnohé další jazyky) nabízí prostředek pro dlouhodobější uchovávání objektů — proměnné.
Proměnná je jakýsi štítek nebo nálepka, kterým lze dočasně označit libovolný objekt. To má dva (vzájemně provázané) důsledky:
Pro označení objektu proměnnou/štítkem lze využít tzv. přiřazení.
a = 2 ** 4_423 - 1
Nalevo od znaku "=" se uvádí jméno proměnné (popisek štítku) napravo výraz, jehož vyhodnocením vznikne objekt, který je danou proměnnou označen.
Tato proměnná vznikla v kontextu aktuálního notebooku a existuje od okamžiku vyhodnocení přiřazení (Ctrl + Shift) až do uzavření notebooku. Lze ji tak využít pro všechny následující výpočty.
a # proměnná se vyhodnotí na objekt, který odkazuje (označuje)
Proměnnou lze využít i ve složitějších výrazech. Vždy se vyhodnotí na objekt, který je danou proměnnou oštítkován resp. odkazován (a stále je to tentýž objekt, není potřeba jej počítat znovu).
a % 10
Proměnné mohou být přirozeně použity i na pravé straně přiřazení.
b = a
c = a % 3
Proměnná b nyní odkazuje na stejný objekt jako proměnná a (tj. objekt má zároveň štítek a i a). Proměnná c odkazuje na zbytek po dělení tohoto čísla třemi (což může být jen číslo jedna nebo dva). Tento objekt je odkazován jedinou proměnnou (má jediný štítek).
c
Jak je vidno, jeden objekt může být v určitém okamžiku odkazován z více proměnných (tj. objekt může být opatřen více štítky). Je proto nutné striktně odlišovat proměnou (štítek) a objekt, na nějž proměnná odkazuje (proměnná není objekt a objekt není proměnnou!). Pro pochopení vztahů mezi proměnnými je dobrá vizuální představa podobná následujícímu obrázku (proměnné jsou na něm znázorněny obdélníky, objekty elipsami). Zápis int:1 vyjadřuje ve zkratce, že je to objekt representující číslo 1 třídy int.
Proměnná sice může sice v jednom okamžiku odkazovat jen jeden objekt (štítek nemůže být nalepen najednou na dvou objektech), odkaz však může být bez problémů přesměrován (ve štítkovém modelu to odpovídá odlepení z jednoho objektu a přilepení na jiný).
b = c # b nově odkazuje na objekt odkazovaný proměnnou c
c = 0 # c je přesměrováno na nový objekt (0 třídy 'int')
b # podíváme se co označuje proměnná b
c # a proměnná c
Novou situaci lze graficky znázornit následujícím obrázkem.
Zajímavá situace nastane po následujícím přiřazení:
c = c + 1
Nejdříve vznikne nový objekt třídy int, který je výsledkem sečtení objektů int:0 (odkazovaný proměnnou c) a int:1 (dočasně vytvořený uvedením konstanty). Tento objekt (opět int:1) je označen proměnnou (štítkem) c. Výsledek lze vidět na dalším obrázku.
Všimněte si, že objekt int:0 už není odkazován žádnou proměnnou (tj. je beze štítku). Proto není viditelný a může být destruován. Ve skutečnosti je to jen detail; objekt, který není po dokončení vyhodnocení nějakého výrazu označován nějakou proměnnou, není de facto dostupný a tudíž jako by nebyl. Programovací jazyky přinášejí téměř dokonalý solipsismus, co nevidíte nebo neznáte, neexistuje (a jakoby ani nikdy neexistovalo).
Důležité upozornění:
Pro popis proměnných se občas místo štítkového modelu (proměnná je dočasný štítek nalepený na objekt) používá model schránkový (proměnná je pojmenovaná schránka, do níž se vkládá objekt).
Tento model není pro Python vhodný,neboť nedokáže popsat sdílení objektů (jeden objekt nemůže být zároveň ve dvou schránkách může však mít dva různé štítky). Důvodem je skutečnost, že proměnné obsahují ve skutečnosti odkaz na objekt nikoliv objekt samotný. Problémy jsou i s definicí doby života objektu (objekt nezaniká tím, že je přepsán, ale tím že není odkazován žádnou proměnnou).
Na druhé straně je popis schránkového modelu o něco jednodušší a v případě čísel i dostatečný. Navíc i názvy proměnná (ang. variable) a přiřazení (assignment) vycházejí spíše ze schránkového modelu (z historických důvodů).
Podívejme se například na následující přiřazení:
x = x + 1
Ve štítkovém modelu lze toto přiřazení popsat takto:
Objekt opatřený proměnnou (štítkem) x se sečte s objektem int:1, čímž vznikne nový objekt, který je následně označen touže proměnou (štítkem) tj. myšlený štítek je odlepen z původního objektu a nalepen na nový (tím původní objekt štítek ztrácí a pokud žádný jiný nemá pak zaniká).
Což lze ekvivalentně vyjádřit pomocí odkazů (což může být pro někoho přehlednější viz také obrázky výše):
Objekt odkazovaný proměnnou xje sečten s objektem int:1, čímž vznikne nový objekt, na který je odkaz v proměnné x přesměrován (tj. odkazuje nový objekt). Pokud není původní objekt odkazován jinou proměnnou pak zaniká.
Ve schránkové sémantice lze toto přiřazení popsat takto: k objektu uloženému v proměnné 'x' je přičtena 1 a výsledek je uložen zpět do stejné proměnné (tj. proměnná se změní).
Pokud navíc přijmeme předpoklad, že číselný objekt lze změnit (což není ve skutečnosti pravda), pak lze použít ještě stručnější popis – hodnota/objekt v proměnné x je zvýšena o jedničku (proměnná je proměnná protože se mění její hodnota).
Z důvodů stručnosti se často v praxi používají stručnější i když nepřesné popisy schránkového modelu. Je to téměř nutné především v případě složitějších programů s mnoha přiřazeními. Proto budu tento přístup používat i já (i když ve velmi omezené míře). Chápejte to však jen jako užitečnou, i když nepřesnou zkratku.
Proměnné hrají v programovacích jazycích klíčovou roli. Nejjednodušším způsobem využití je symbolické označení hodnot ve výrazech:
%precision %.5g
kappa = 6.67408e-11 # gravitační konstanta
m1 = 60 # hmotnost prvního objektu (kg)
m2 = 80 # hmotnost druhého objektu (kg)
r = 1 # vzdálenost (m)
kappa * (m1*m2) / r**2
Je zřejmé, že kód počítá vzájemné gravitační sílu, která působí mezi dvěma objekty o hmotnostech $m_1$ = 60 kg a $m_2$ = 80 ve vzdálenosti 1 metru (charakter objektů nechám na Vaší představivosti).
První řádek předchozí vstupní buňky není kód v Pythonu, ale tzv. magický kód Jupyteru. Magický kód vždy začíná znakem procento a slouží pro různá nastavení a akce v notebooku. V tomto případě je použit magický kód precesion, který nastavuje formátování číselných výstupů (nastavení platí od daného místa až do konce notebooku).
Výstup běžně obsahuje všech 15 platných číslic, z nichž bohužel ne všechny bývají platné (viz například nepřesný výstup výše). Formát %.5g omezuje výstup na prvních pět číslic, přičemž inteligentně využívá zápis s exponentem pro malá a velká čísla. Výstupním formátům se budeme věnovat později detailněji.
Všimněte si také názvů proměnných. Pythonské identifikátory (mezi něž názvy proměnné patří) mohou obsahovat libovolné písmenné znaky (nejen latinkové) a číslice (nikoliv jen arabské). Číslice se však nesmí vyskytovat na první pozici identifikátoru. Zakázány jsou také některá anglická slova, která mají v Pythonu speciální význam (typicky jsou to jména příkazů). Symboly a speciální (nepísmenné a nečíselné) jsou zakázány s jedinou výjimkou, jíž je znak podtržítko (_). Nepřípustná je i mezera resp. libovolbý mezerový znak (tabulátor, odřádkování)
Kromě povinných pravidel se uplaňuje i určitý úzus. Jeho narušením neučiníte kód nesprávným, můžete ho však učinit méně čitelným pro ostatní programátory (kteří dodržení úzu mnohdy podvědomě očekávají).
Mezi základní doporučení patří:
# vhodné identifikátory (přiřazení)
prumerny_plat = 42_000
prumernyPlat = 42_000 # pro Python méně typická tzv. velbloudí notace
alfa0 = 2e-7
alfa_0 = 2e-7 # explictnější oddělení spodního indexu
pocet_mesicu = 12
dph = 1.22 # standardní zkratka
# využití
pocet_mesicu * prumerny_plat # zkuste doplnění po klávese Tab
# méně vhodné identifikátory
κ = 6.67408e-11 # řecké kappa
ℵ = 1 # hebrejské aleph
průměrný_plat = 42_000 # znaky mimo anglickou abecedu
ppvp = 80_000 # nečitelná zkratka (průměrný plat vedoucího pracovníka)
# zcela nevhodné idenrfifikátory
R1887211 = 0.0 # velké počáteční písmeno + nejasné (a velké) číslo
_x = R1887211 # počáteční podtržítko
_ = 0 # opět počáteční podtžítko
Poznámka: písmena řecké abecedy a matematické speciální znaky lze v Jupyter notebooku (pouze v kódu) vkládat pomocí sekvence \ + TeX_jméno + Tab. Znak κ tak lze vložit sekvencí \kappa, jenž je následovaný stiskem tabulátoru (stačí zadat jen začátek TeXovského jména).
Kromě symbolického označení objektů mají proměnné i další funkce. Jednou z nich je i eliminace (zbytečně) opakovaných výpočtů. Ukažme si to na praktickém příkladě.
Pro výpočet hodnoty výrazu $\frac{1}{x^2} + \frac{1}{x^2 + x/2} + \frac{1}{x^2-x/2}$ pro $x = 2^{10}$ lze použít přímočaré řešení (dosazením hodnoty $2^{10}$ za x). Výraz $x^2$ je z důvodů efektivity počítán jako x * x.
1/(2**10 * 2**10) + 1/(2**10 * 2**10 + 2**10/2) + 1/(2**10 * 2**10 - 2**10/2)
Již na první pohled to není nejefektivnější řešení: výraz je dlouhý a při zápise je snadné udělat chybu. Navíc se podvýraz 2**10 počítá osmkrát! Pokud navíc chceme změnit vstupní hodnotu $x$ musíme provést záměnu výrazu x**10 v každém z jeho osmi výskytů, což je nejen otravné, ale i náchylné chybám.
Řešení je nasnadě (doufám, že jste ani jiné neuvažovali). Do proměnné x vložíme hodnotu $2^{10}$ a pak výraz zapíšeme pomocí x.
x = 2.0 ** 10.0
1.0/(x * x) + 1.0/(x * x + x/2.0) + 1.0/(x * x - x/2.0)
To je mnohem přehlednější a méně náchylné k chybám. Změna vstupní hodnoty je triviální. Navíc výpočet mocniny se provádí jen jednou. Jen tak mimochodem jsme výpočet optimalizovali důsledným uváděním konstant třídy float. Výpočet se tak celý a důsledně provádí ve float arimetice bez zbytečného (a mnohdy zbytečně pozdního) přetypování.
Můžeme však jít ještě dál. Ve výrazu se zbytečně třikrát počítá hodnota x * x a dvakrát hodnota x/2.0. I ty můžeme vypočítat předem a umístit do proměnných.
x = 2.0 ** 10.0
x2 = x * x # x**2 nebo x^2 není bohužel platný identifikátor proměnné
xpul = x / 2.0 # xpul namísto x_půl
1.0 / x2 + 1.0 / (x2 + xpul) + 1.0 / (x2 - xpul)
Tato úprava výraz výrazně nezjednoduší a nezpřehlední (není např. na první pohled jasné, co je x2). Očekávatelné je však určité zrychlení (eliminujeme 3× operaci násobení a 2× operaci dělení).
Očekávání lze navíc snadno potvrdit. Jupyter podporuje jednoduchý benchmarking tj. měření času běhu programů a jejich fragmentů. Stačí uvést magický příkaz %%timeit na začátku vstupní buňky (benchmarking je založen na standardní knihovně timeit).
%%timeit
x = 2.0 ** 10.0
1.0/(x * x) + 1.0/(x * x + x/2.0) + 1.0/(x * x - x/2.0)
Po vyhodnocení (které chvíli trvá) se nevypíše výsledek posledního výrazu, ale údaje o době provedení celé buňky. Provedení trvalo (u mne) průměrně cca 235 nanosekund (se směrodatnou odchylkou 3-8 ns), který byl získán ze sedmi opakování, z nichž každé provedlo daný fragment programu milionkrát (měření jednotlivých provedení není možné neboť režie měření času by byla větší než vlastní výpočet, navíc přesnost měření v PC je v řádu nejvýše mikrosekund). Výsledek je při každém vyhodnocení trochu jiný a ještě výrazněji se může lišit podle počítače na němž program běží.
%%timeit
x = 2.0 ** 10.0
x2 = x * x # x**2 nebo x^2 není bohužel platný identifikátor proměnné
xpul = x / 2.0 # xpul namísto x_půl
1.0 / x2 + 1.0 / (x2 + xpul) + 1.0 / (x2 - xpul)
Optimalizovaná verze se u mne provádí cca za 180 nanosekund (s menší směrodatnou odchylkou, neboť se vykonávalo 10 miliónů výpočtů v každém běhu). Absolutní údaj je však nezajímavý (až na to, že si uvědomíme jak rychlé jsou dnešní počítače). Důležitější je podíl obou hodnot:
235 / 186
Malou úpravou programu jsme získali zrychlení o cca 26% (tj. asi 4tvrtinu).
Úkol: Ověřte rychlost tří základních implementací druhé mocniny:
- násobení celých čísel (
int)- násobení čísel v pohyblivé řádové čárce ('float')
- použití operátoru mocnění pro
floatVýsledky se pokuste zdůvodnit.
print¶Při použití Jupyter notebooku je výpis výsledných hodnot snadný, neboť do výstupní části buňky se vždy vypíše hodnota posledního výrazu. Pouze pokud je na posledním řádku přiřazení či jiný příkaz (seznámíme se s nimi později) není vypsáno nic.
a = 3
a + 1 # poslední řádek, je to výraz tj. vypíše se jeho hodnota
a = 3
b = 4 # přiřazení (není výrazem) na posledním řádku nic se nevypíše
Tento přístup má základní omezení. Vypsat lze jen jeden údaj a to vždy podle stavu na posledním řádku. Toto omezení lze obejít použitím vestavěné funkce print. Tato funkce nic nevrací, vypisuje však do výstupní buňky všechny své parametry.
from math import sqrt # importování funkce pro odmocninu
a = 1
b = -3
c = 2 # vstup (hodnoty mají díky proměnným symbolická jména a přežijí do další části programu)
diskriminant = b*b - 4.0*a*c
print(diskriminant) # vypíše do výstupní buňky hodnotu diskriminantu (např. pro kontrolu)
x1 = (-b + sqrt(diskriminant)) / 2.0
x2 = (-b - sqrt(diskriminant)) / 2.0
print(x1, x2) # vypíšeme najednou obě řešení (funkce má dva parametry)
Úkol: Vyzkoušejte předchozí program pro výpočet kvadratické funkce i pro jiné vstupní hodnoty. Jak se zachová, pokud neexistuje řešení v množině reálných čísel (diskriminant < 0).
Kromě základního přiřazení podporuje Python i některé syntaktické zkratky. Velmi častá jsou například přiřazení následujícího druhu:
x = 1
x = x + 1 # do x se vkládá původní hodnota zvýšená a jedničku
x = x * 2 # do x se vkládá zdvojnásobená původní hodnota
x # vyhodnotí se na výslednou hodnotu
Tento typ přiřazení (na původní hodnotu je aplikován operátor a výsledná hodnota je opatřena stejnou proměnou) lze zapsat zkráceně:
x = 1
x += 1
x *= 2
print(x)
Použít lze i další operátory se dvěma operandy (-, /, //, **, apod.)
a = 10
a **= 2 # umocni na druhou a výsledek opět vlož do proměnné a
a %= 3 # do proměnné 'a' vlož zbytek po dělení původní hodnoty proměnné 'a' a hodnoty 3
a # zbytek po dělení sta třemi
Další možnou zkratkou je tzv. paralelní přiřazení. To umožňuje v jednom zápise provést zároveň přiřazení většího počtu hodnot do většího počtu proměnných:
# původní tvar
x = 0
y = 1
# lze napsat jako
x,y = 0,1
print(x,y) # vypíše zároveň x i y
Tanto zápis se používá především tehdy, pokud dané proměnné spolu souvisejí, resp. souvisejí hodnoty, které do nich přiřazujeme.
x = 10
y = 3 # příprava
podil, zbytek = x // y, x % y
print(podil, zbytek)
Zápis je to velmi stručný, avšak trochu nepřehledný. Někteří programátoři jej proto používají jen výjimečně (na rozdíl od složeného přiřazení, které se využívá běžně).
Existuje však speciální tvar paralelního přiřazení, které se naopak využívá velmi často:
x = 0
y = 1 # příprava
print(x,y) # první výpis
x,y = y,x # výměna hodnot x <-> y
print(x,y) # druhý výpis
Jak lze vidět pomocí paralelního přiřazení je možné na jednom řádku vyměnit obsah proměnných (přesněji se vyměňují štítky či přesměrovávají odkazy). Bez použití paralelního přiřazení by bylo nutno využít pomocnou proměnnou (jinak bychom při prvním přiřazení přišli navždy o hodnotu první proměnné):
x = 0
y = 1 # příprava
print(x, y)
p = x # do pomocné proměnné vložíme hodnotu proměnné x
x = y # přesuneme hodnotu z y do x (nyní mají obě základní proměnné stejnou hodnotu)
y = p # a pak přesuneme původní hodnotu x (dočasně uloženou v proměnné 'p') do 'y'
print(x, y)
Python byl navržen jako univerzální programovací jazyk. Použití Pythonu pro matematické výpočty je jen jedním z mnoha možností využití Pythonu (a nikoliv tou hlavní).
Toto východisko se projevuje i v návrhu jazyka. Python například přímo podporuje jen cca dvacítku tzv. vestavěných funkcí, z nichž jen funkce abs se přímo týká matematiky. Vestavěné funkce můžete použít, kdekoliv bez jakékoliv přípravy.
abs(-3)
Ostatní funkce (resp. další objekty jako jsou symbolické proměnné) jsou v Pythonu dostupné pomocí tzv. modulů.
Pokud chceme použít funkce z modulů musíme modul resp. jeho obsah tzv. importovat, tj. učinit dostupné v našem programu. Podívejme se jak lze například tímto způsobem využít modul math, který nabízí běžné matematické funkce a konstanty.
Nejdříve zkusíme importovat modul jako celek:
import math
Po importování se modul chová jako objekt, jehož atributy jsou příslušné funkce (resp. konstanty). Jinak řečeno identifikátor funkce musí v tomto případě obsahovat i jméno příslušného modulu (uvádí se na začátku a odděluje se tečkou).
math.log10(100) # volání funkce 'log10' z modulu 'math'
math.pi # konstanta (ve skutečnosti je to proměnná, kterou nelze změnit)
Využití jména modulu jako předpony (prefixu) může být u delších výpočtů nepohodlné resp. nepřehledné. Proto lze přímo importovat jen určitou funkci nebo i několik funkcí z modulu (zde hrozí učité nebezpečí, že funkce stejného jména již existuje resp. byla importována z jiného modulu, pravděpodobnost však není velká).
from math import sin, cos, pi # importujeme pouze funkce sin a cos a hodnotu pi
r = 2
alfa = pi / 6.0 # úhel 30°
# převod polárních souřadnic na kartézské
x = r * cos(alfa)
y = r * sin(alfa)
print(x,y)
U výsledku si nelze nevšimnout důsledků nepřesného výpočtu. Hodnota y nenabývá hodnoty 1, ale je o pár trilióntin menší. I přes tak malý rozdíl je výsledek vizuálně poněkud matoucí (jsou to takové baťovské ceny dovedené do dokonalosti). U funkce print se neuplatňuje nastavení přesnosti, kterou jsme provedli výše pomocí magického kódu tj.%precesion "%.5g". Řešení existuje, ale ještě musíte chvíli počkat (do té doby se musíte smířit se skutečností, že 0.9999999999999999 je toliko jiný zápis hodnoty 1).
Pokud potřebujeme importovat větší počet funkcí a proměnných, může být jejich explicitní uvádění v importu únavné (navíc by se celý příkaz měl vejít na jedinou řádku). V těchto případech lze jednoduše importovat všechny funkce či proměnné nabízené modulem (bez ohledu na to zda je použijeme či nikoliv):
from math import *
Nyní můžeme přímp volat jakoukoliv funkci z modulu math (přímo bez prefixu):
x = 2
1/log(x) + exp(x) # log a exp jsou funkce z modulu math
Nevýhodou tohoto "masového" importu je skutečnost, že získáte přístup i k funkcím, které nepotřebujete a dokonce je ani nemusíte znát. To není problém, dokud se nepokusíte importovat stejnojmennou funkci z jiného modulu nebo se pokusíte takovou funkci vytvořit sami. Poté dojde ke kolizi jmen, kterou nemůže překladač vyřešit a proto program skončí s chybou dříve než se začne vykonávat. Byli/y jste varováni/y!
Modul math nabízí všechny základní goniometrické funkce a jejich inverze (tzv. cyklometrické funkce). Všechny tyto funkce pracují s radiány nikoliv stupně. Pokud potřebujete pracovat s úhly ve stupních musíte zajistit převod ze stupňů na radiány a popřípadě i ve směru opačném (u cyklometrických funkcí).
V dalším kódu není potřeba modul math importovat, neboť v případě prostředí Jupyteru importy platí od svého uvedení až do konce notebooku. Další pokusy o importování jsou jednoduše ignorovány (občas je budu uvádět, neboť importování může být v místě použití funkce už dosti vzdálené).
Řešený příklad:
Mějme trojúhelník se stranami délky $a = 1, b = 3, c = 4$. Za úkol máme zjistit všechny vnitřní úhly v trojúhelníku.
Pro výpočet úhlu $\alpha$ použijeme kosinovou větu $a^2 = b^2 + c^2 - 2 b c \cdot \cos \alpha$. Poté využijeme stejnou větu i pro úhel $\beta$ (pro proměnné úhlů jsou použita řecká písmena, což sice narušuje úzus, ale je to přehlednější). Převod z radiánů na stupně zajišťuje funkce degree.
a = 1
b = 3
c = 2.5
α = degrees(acos((a*a - b*b - c*c)/(-2*b*c)))
print(α)
β = degrees(acos((b*b - a*a - c*c)/(-2*a*c)))
print(β)
γ = 180 - α - β
print(γ)
Pro jistotu provedeme kontrolu pomocí sinové věty $\frac{a}{b} = \frac{\sin \alpha}{\sin \beta}$.
a /b
sin(radians(α)) / sin(radians(β))
Převod stupňů na radiány zajišťuje funkce radians.
Z oblasti trigonometrických funkcí zaslouží zmínku klíčová funkce atan2(y,x) která vrací úhel mezi osou x a průvodičem bodu (x,y). Tento úhel může nabývat hodnot $-\pi$ až $\pi$. Výpočet pomocí výrazu atan(y/x) vrací hodnoty z intervalu 0 až $\pi$ (to jest jen z I a IV kvadrantu) a nefunguje pro x = 0 (dělení nulou, vyzkoušejte).
degrees(atan2(1,-1)) # výsledek bude ve stupních
Modul math samozřejmě podporuje exponenciální funkci a také několik funkcí logaritmických:
exp(-1) # e^-1
log(16) # přirozený logaritmus
print(log10(16)) # dekadický logaritmus
print(log2(16)) # binární logaritmus (o základu 2)
Úkol: Vypočtěte logaritmus o základu 3 čísla 81 ($\log_3 81$). Nápověda: je to podíl dvou čísel.
Řešený příklad:
Zkouška z nejmenovaného předmětu skončila s těmito výsledky: 10% studentů bylo ohodnoceno výborně, 20% velmi dobře, 40% dobře a zbytek (30% neuspěl). Kolik bitů informace získal student, tím že mu vyučující sdělil výsledek zkoušky?
Pokud by byly výsledky zkoušky rozloženy rovnoměrně (25% pravděpodobnost dané známky), je odpověď triviální: čtyři možnosti výsledků lze kódovat dvěmi binárními číslicemi (například takto 00=1, 01=2, 10=3, 11=4), tj. student obdrží přesně dva bity informace (doufám, že informační přínos zkoušky není omezen na tyto dva bity).
Vyšší pravděpodobnost horšího výsledku (pravděpodobnost 3 a hůře je 70%) informační přínos snižuje. Pro výpočet konkrétní hodnoty lze využít Shannonův vztah (kde $p_i$ jsou pravděpodobnosti výskytu i-té možnosti výstupu, zde pravděpodobnosti i-tého výsledků zkoušky):
$H = - \sum_{i=1}^n p_i \cdot \log_2 p_i$
p1 = 0.1
p2 = 0.2
p3 = 0.4
p4 = 0.3
H = -p1*log2(p1) - p2*log2(p2) - p3*log2(p3) - p4*log2(p4)
print(H)
Výsledek: Student získal průměrně 1,85 bitů informací.
Zaokrouhlování je jednou z typických operací prováděných při výpočtech na počítačích. Ve skutečnosti jsem již poznali dva typy zaokrouhlení, které se aplikují automaticky:
V praxi však často potřebujeme zaokrouhlování řídit přesněji a explicitněji. Základem zaokrouhlovací aritmetiky jsou funkce floor a ceil, které zaokrouhlují na nejbližší menší resp. větší celé číslo (a vrací objekt třídy int).
floor(pi)
ceil(2*pi)
Funkce floor se někdy chybně zaměňuje s funkcí int (= převod na typ int). Funkce int však jednoduše odsekává destinnou část, což se u záporných čísel liší od funkce floor (ta vrací nejbližší nižší celé číslo).
print(int(-2.5))
print(floor(-2.5)) # nejbližší nižší číslo je -3!
Běžné (finanční) zaokrouhlení k nejbližšímu celému číslu provádí funkce round (ta je přímo vestavěná takže pro její použití není nutno importovat modul math) Pokud je funkce použita v základním tvaru (s jedním parametrem), pak vrací objekt třídy int. U čísel s desetinnou částí 0.5 se zaokrouhluje k nejbližšímu sudému číslu.
round(2.5)
round(3.5)
Funkce kromě zaokrouhlení na nejbližší celé číslo umí i zaokrouhlení k nejbližšímu násobku mocniny desítky (tj. nejen na jednotky, ale i na desítky, stovky, resp. desetiny, setiny, apod). Stačí přidat druhý parametr (označovaný jako digits). Zaukrohlení se provede na hodnotu $10^{-digits}$. Výsledek je stejné třídy jako vstupní číslo.
round(2042.0, -3) # zaokrouhlení na tisíce = 10^3 (výsledek je 'double')
round(2**32, -9) # zaokrouhlení na miliardy (výsledek je 'int')
round(pi, 3) # zaokrouhlení na tisíciny (výsledek je 'double')
round(3.5, 0) # zaokrouhlení na jednotky (výsledek však není `int` jak je tomu v round(3.5))
Úkol: Vypište hodnotu funkce $sin(x) + cos(x)$ pro $x = \frac{\pi}{4}$ zaokrouhlenou na setiny.
Další užitečné funkce z modulu math si ukážeme jen v jednoduchých příkladech.
factorial(69) # funguje jen pro kladná celá čísla
Úkol: Vypočtěte hodnotu kombinačního čísla $n\choose{k}$ pro $n=100$ a $k=50$. Použijte vztah ${{n}\choose{k}}=\frac{n!}{k!(n-k)!}$. Výpočet podle tohoto vztahu není ideální, dokážete říci proč?
float(factorial(100))
Další užitečnou funkcí je druhá odmocnina.
sqrt(16) # odmocnina, funguje jen pro kladná čísla (ověřte co dělá pro záporná)
Odmocnění lze samozřejmě zapsat i pomocí operátoru mocniny, ale může to být méně efektivní (závisí na procesoru a oprimalizaci Pythonu).
16 ** 0.5 # méně přehledné a pravděpodobně i o něco pomalejší
Nepřesnost čísel v pohyblivé řádové čárce (float) se může jevit jako drobná kosmetická vada, kterou lze navíc odstranit vhodným zaokrouhlení při výpise. I když tomu tak v mnoha případech opravdu je, existují výjimky, kdy i použití elementárních funkcí vede k překvapivě velkým chybám. V těch nejextrémnějších případech nemusí výsledek obsahovat ani jedinou platnou číslici (jinak řečeno je to hausnumero). Problémem je i zbytečné přetečení v mezivýsledku.
Z tohoto důvodu modul math podporuje i zdánlivě nadbytečné funkce, které jsou však navrženy tak, aby tyto případy alespoň částečně eliminovaly.
Ďábel zbytečného přetečení je ukryt i v běžných výpočtech. Vezměme například výpočet délky přepony ze známých délek odvěsen (tento výpočet se využívá i pro výpočet (eukleidovských) vzdáleností v rovině).
a = 5e200
b = 6e200
c = sqrt(a*a + b*b)
print(c)
Trojúhelník s odvěsnami s délkou v řádu $10^{200}$ je obtížně představitelný (a to i v případě, že by délkovou jednotkou byla Planckova délka a trojúhelníkem mince ningy = 1/8 triganského pu). Přesto je však výpočet pro dané float hodnoty možný i v omezené počítačové aritmetice. Stačí využít specializovanou funkci hypot (není to žádná magie, podívejte se na Wikipedii na článek https://en.wikipedia.org/wiki/Hypot).
a = 5e200
b = 6e200
c = hypot(a, b)
print(c)
Kromě číselných objektů podporuje Python i další třídy jednoduchých (tj. nesložených) objektů. Klíčovou roli hrají především tzv. logické resp. pravdivostní hodnoty (logical, truth).
Logické hodnoty representují výsledek vyhodnocení tzv. výroku, tj. tvrzení, které je buď pravdivé nebo nepravdivé (v jednoduché logice může nastat právě jedna z těchto situací a žádná jiná). Z tohoto důvodu existují jen dvě pravdivostní hodnoty -- True, representující pravdivost a False representující nepravdu (lež). Tyto hodnoty patří v Pythonu do jediné třídy označované bool (zkratka z označení boolovská (boolean) algebra, což je algebraický model dvouhodnotové logiky pojmenovaný podle anglického matematika George Boola).
Pravdivostní hodnoty jsou nejčastěji výsledkem vyhodnocení tzv. relačních operátorů, které umožňují zapisovat výroky o rovnosti, či identitě objektů resp. o jejich upořádání.
Základním relačním operátorem je operátor rovnosti zapisovaný pomocí dvou rovnítek (jedno rovnítko má v Pythonu jiný význam, používá se pro přiřazení).
2 == 3
Tento výrok je triviální, neboť jakékoliv celé číslo se rovná pouze samo sobě. Výroky porovnávající celá čísla mohou být i složitější, vlastní porovnání je však triviální (a zcela v souhlase s matematikou).
a = 3
b = 10
b % a == b - 3 * a # výsledek ověřte
Úkol: Pro několik čísel
nověřte, že platí $a^2 - b^2 = (a-b)\cdot(a+b)$
U necelých čísel rovnost závisí na interní representaci třídy float. Dvě float čísla jsou shodná, pokud mají stejnou representaci. To je ve většině případů ve shodě s matematickou definicí.
from math import *
print( pi == 22/7 ) # pi se skutečně nerovná tomuto zlomku
print( 1/3 == 2/6 ) # to by se rovnat mělo
Dozajista však už pro vás není překvapením, že výsledek může být občas z hlediska matematiky mírně řečeno překvapující:
3 * 0.1 == 0.3 # to v matematice platí (ve světě čísel `float` nikoliv)
x = 1e16
x + 1 == x # to je naopak v matematice nepravda ( 0 ≠ 1)
Z tohoto důvodu se pro testování čísel třídy float často používá nasledující obrat:
x = sin(pi/4)
y = sqrt(2)/2
epsilon = 1e-15
(abs(x-y) < epsilon) # x a y je přibližně rovno (s chybou menší než zvolené epsilonT)
Přibližná nerovnost řeší většinu problémů s porovnáním čísla třídy float. V běžném světě stačí vědět, že
$\sin\frac{\pi}{4}$ se od $\frac{\sqrt{2}}{2}$ liší méně než o zanedbatelných $10^{-15}$ (to že jsou zcela shodné by měl sice vědět každý středoškolák, ale i ti co si to pamatují to ve valné většině nedovedou dokázat). Úplně dokonalé řešení to však není, neboť:
epsilon. Lze ho sice zvolit i větší, ale pak naopak můžeme označit za rovná i čísla, která by měla být rozdílná. Např. zvolíme-li epsilon = 1e-10, pak dokážeme, že gravitační konstanta je nulová a gravitační síla neexistuje!x + 1 == x. I když se pokusíte k číslo 1e16 přičíst jedničku opakovaně (potenciálně i nekonečněkrát) stále jeho hodnotu nezvýšíte!Python nově obsahuje i funkci, která porovnání čísel typu float usnadňuje (je součástí modulu math).
isclose(sin(pi/4), sqrt(2)/2)
Tato funkce je o něco inteligentnější než náš návrh.
Úkol: podívejte se na dokumentaci funkce na https://docs.python.org/3/library/math.html Proč je knihovní funkce lepší, než náš návrh.
Rovnost je základním relačním operátorem. Existují však samozřejmě i další (z nichž ten s operátorem < jsme již použili). Shrňme si je v tabulce:
| operátor | matem. zápis | význam |
|---|---|---|
| == | = | rovnost (equality) |
| != | $\neq$ | nerovnost |
| > | < | větší než |
| >= | $\geq$ | větší nebo rovno |
| < | > | menší než |
| <= | $\leq$ | menší nebo rovno |
Jejich použití je obdobné rovnosti. Pro celá čísla využivá přirozené uspořádání čísel. U čísel typu float je pouze rozumným přiblížením matematickým relacím a výsledky tak mohou být překvapivé resp. je nelze vůbec rozumně využít (u skutečně malých a skutečněvelkých čísel).
pi < 4
V Pythonu lze navíc nerovnosti řetězit, tj. lze psát podmínky typu $x < y \leq z$.
2 < 3 < 4 <= 4 < 6
A o trochu praktičtější příklad:
a = 0.0
b = 10.0
x = 10.0
# x leží v otevřeném intervalu (a,b)
a < x < b
# x leží v uzavřeném intervalu [a,b]
a <= x <= b
Upozornění: Z důvodů nepřesné representace je rozdíl mezi otevřenými a uzavřenými intervaly spíše kosmetický.
Oba zápisy (s operátorem < resp. <=) lze číst jako, dolní (resp. horní) mez je někde v (těsné) blízkosti a resp. b.
Testování přítomnosti bodu v intervalu lze alternativně zapsat i pomocí logoické spojky and (česky a zároveň). Zápis je o něco delší a pro začátečníky i méně čitelný, je však použitelný téměř ve všech programovacích jazycích.
# x leží v uzavřeném intervalu [a,b]
a <= x and x <= b
# čteme a je menší než x a zároveň x je menší nebo rovno b (obě dílčí podmínky musí být splněny)
Při složitějších podmínkách je použití logických spojek nezbytností. Pokud chceme testovat zda číslo leží vně intervalu [a,b] (tj. výraz je pravdivý, pokud leží mimo), pak se logické spojce nevyhneme. Máme přitom hned několik možností.
# negace zřetězených relací
not (a <= x <= b)
# čteme: neplatí tvrzení, že a je menší nebo rovno než x, které je menší nebo rovno než b
# negace výrazu s logickou spojkou AND
not (a <= x and x <= b)
Nejjednodušší je však zápis využívající spojky OR (česky nebo):
x < a or x > b
Tuto podmínku lze vymyslet přímo. Číslo leží vně intervalu [a,b] pokud je menší než dolní mez nebo pokud je větší než horní mez (stačí splnění jedné dílčí podmínky). Podívejte se na obrázek.
Lze ji však odvodit z podmínky $\neg (a\leq x \vee x \leq b)$ aplikací několika jednoduchých pravidel
De Morganův zákon: $\neg(a \vee b)$ je ekvivalentní $\neg a \wedge \neg b$
Porovnejme tvrzení: není pravda, že krade a zároveň lže s ekvivalentním tvrzení nekrade nebo nelže (tj. jedno alespoň jednu věc nečiní).
V našem případě dostaneme podmínku not(a <= x) or not(x <= b).
Využijeme skutečnosti, že negací relace <= je > (a opačně). Pozor skutečně neplatí, že negací ostré nerovnosti (např. menší než) je opačná ostrá nerovnost (např. větší než)! Negací ostré nerovnosti ($<$) je neostrá ($ \geq$) a vice versa.
V našem případě dostaneme podmínku a > x or x > b. První porovnání lze samozřejmě otočit, čímž dostaneme požadovaný tvar x < a or x > b.
Řešený příklad: Napište podmínku, která ověří, že celé číslo (int) označené identifikátorem i je kladné sudé číslo.
Testování kladnosti je snadné. Číslo je kladné pokud je (ostře) větší než 0. Na druhou stranu Python nemá vestavěnou funkci na testování sudosti, můžeme však vyjít z jednoduché definice, že číslo je sudé pokud je dělitelné dvěma beze zbytku (tj. zbytek pod celočíselném dělení dvěma je nula).
Obě podmínky spojíme logickou spojkou AND (číslo musí zároveň kladné i sudé).
i = 42 # náhodně zvolené číslo (zkuste jej změnit)
i > 0 and i % 2 == 0
Všimněte si priority operací. Nejvyšší prioritu mají aritmetické operace (zde je to zbytek po dělení), pak se provedou relační operátory a nakonec se provede operátor s nejnižší prioritou – logická spojka AND.
[konec příkladu]
Úkol: Napište výraz, který ověří, že číslo označené proměnou
xje nezáporné ($\ge 0$) jen za použití operátoru rovnosti (rada: použijte elementární funkci).
Kromě běžných hodnot (objektů), které representují reálné objekty dané třídy existují v Pythonu hodnoty, které representují neexistenci hodnoty resp. její nedostupnost.
None¶Základní nehodnotou je None. (Ne)hodnota None může být interpretována jako neexistující hodnota libovolné třídy (resp. dokonce jako hodnota bez určení třídy či mimo jakoukoliv rozumnou třídu).
i = None
Proměnná i nyní sice existuje, označuje však hodnotu, která representuje "nic". Méně kostrbatě: existuje, ale nic neoznačuje.
None nejčastěji vyjadřuje dva významy:
S hodnotou None lze provádět jen jedinou operaci, porovnávat ji samu se sebou nebo s ostatními hodnotami (libovolné třídy). None je nicméně rovna jen sama sobě. Proto je efektivnější testovat None na identitu a nikoliv na rovnost (existuje jen jeden objekt None).
Identita je nejstriktnějším typem rovnosti, neboť dva objekty jsou identické, pokud zaujímají stejné místo v paměti (resp. abstraktněji v časoprostoru).
Rozdíl mezi identitou a rovností lze vysvětlit i na příkladu z běžného světa. Každá bankovka 100 Kč je rovna jakékoliv bankovce 100 Kč (z hlediska hlavního použití, jímž je placení). Můžeme navrhnout i striktnější definici rovnosti (shody) bankovek. Bankovky jsou rovné, pokud nesou stejný identifikátor (tj. číslo bankovky). Nocměné nelze zcela vyloučit situaci, kdy uvidíte najednou dvě bankovky se stejným číslem, které leží vedle sebe (tj. zaujímají různé místo v prostoru). Je zřejmé, že i když jsou podle všech kritérii shodné, nejsou identické.
i == None # testování rovnosti (funguje, ale je pomalé)
i is None # doporučené testování, zda je hodnota None
Operátor is testuje zde je levý operand identický s pravým. V Pythonu se příliš nepoužívá, neboť ve většině případů je důležitější běžná rovnost. Výjimkou jsou jen některé speciální objekty (včetně None).
Pokud například použijeme test identity na celá čísla, můžeme získat dosti překvapivé a na zdánlivě rozporné výsledky.
2 is (1 + 1)
(2 ** 100) is (2 ** 100)
V případě malých čísel platí, že jsou-li výsledky operací rovné pak jsou i identické. Důvodem je skutečnost, že Python šetří paměť tím, že v případě malých čísel uchovává jen jednu kopii celého čísla (často existuje již od okamžiku spuštění programu a nikdy není uvolněno). U velkých čísel je však dlouhodobé uchovávání čísel neefektivní a tak se tyto objekty chovají podle základního modelu. Při výpočtu vznikají vždy nové a poté co je nikdo neodkazuje dochází k destrukci.
Úkol: Oveřte co jsou ve Vašem Pythonu malá a velká čísla (vzhledem k identitě nově vzniklých objektů).
O tom jaký přístup ke sdílení čísel bude zvolen rozhoduje překladač a může záviset i na vnějších parametrech (např. velikost operační paměti). Nelze jej proto predikovat a využívat v programu (např. pro urychlení). Důsledek je proto zřejmý: nikdy nepoužívejte operátor is pro testování rovnosti čísel.
Porovnání (resp. testování identity) je jedinou operací, kterou lze s hodnotou None rozumně provádět. Lze ji sice vypsat funkcí print, avšak výstupy se mohou lišit podle použitého Pythonu.
print(None)
Ostatní operace vyvolají výjimku (tj. výpočet či program se ani nedokončí).
x = None
x + x
V případě čísel v pohyblivé řádové čárce existuje ještě jedna speciální hodnota primárně representující neplatný výsledek některých matematických operací. Tato hodnota je označovabná jako NaN tj. Not A Number.
import math
x = math.nan
Hodnota NaN se chová jako numerická hodnota, která není číslem (to není kontradikce). Patří do třídy float a lze na ní aplikovat běžné aritmetické operace.
x + x * x
Výsledkem těchto operací je však vždy opět NaN (tj. NaN se šíří jako epidemie). Tp platí i pro funkce z modulu math.
math.sin(x)
S NaN se proto v případě číslených hodnot pracuje povětšinou lépe než s hodnotou None (ta se nešíří, způsobuje rovnou výjimku a tím i potenciální nebezpečí ukončení porogramu). I ona však má své mouchy. Lze ji sice porovnat s libovolným číslem třídy float (resp. i int po přizpůsobení typu). Platí však, že se nerovná žádnému objektu a dokonce ani sama sobě! Je to jediná Pythonská hodnota s tímto podivným chováním.
math.nan == math.nan
Zde nepomůže ani testování identity. To sice může vrátit True, to je však jen implementační detail, který navíc narušuje základní elementární tvrzení: pokud nejsou objekty shodné (rovné) pak nemohou být ani identické. Jediným košer způsobem testování, zda není numerická hodnota rovna NaN je funkce math.isnan.
math.isnan(x)
Výrazy vracející pravdivostní hodnoty (podmínky) jsou nejčastěji využívány pro tzv. větvení programu, tj. k vykonání určitého kódu jen za určité situace.
Podmíněné vykonání určité činnosti je samozřejmě známé i z běžného života. Naobědvám se, jen tehdy pokud mám hlad (peníze, čas). Ožením/vdám se, pokud někoho miluji (chci ušetřit na daních, potřebuji získat občanství).
Podmínky v případě počítačového kódu jsou běžně výrazně jednodušší, avšak stejně jako v reálném životě tvoří podmíněné konstrukce podstatnou část programu.
Základní větvící konstrukcí je příkaz if, který umožňuje podmíněně vykonávat příkazy (typicky přiřazení, vstupy a výstupy).
Příkaz if si ukážeme na jednoduchém příkladě. Nejdříve však v si rámci přípravy vypíšeme aktuální verzi Pythonu. Využijeme proměnnou version_info, která je poskytována modulem sys (zkratka za běhový systém).
import sys # iportování modulu
sys.version_info
Výsledkem je složitý objekt, který obsahuje detailní informaci o verzi aktuálního překladače. Kromě hlavní verze (major) obsahuje i číslo dílčí verze (minor) a další ještě detailnější údaje. Pokud chceme získat jen hlavní verzi, stačí získat tzv. atribut objektu, uvedením jeho jména za tečkou.
Zápis čteme takto: z modulu sys (první tečka) použij proměnnou version_info, která odkazuje na objekt, který má atribut se jménem major. Atribut odkazuje číselný int objekt (jehož hodnota bude vypsána).
sys.version_info.major
Nyní již k příkazu if, který na základě hodnoty výše uvedeného atributu větví program do dvou větví, z nichž jedna vypíše text "Používáte správný Python!" a druhá text "Používáte zastaralou verzi Pythonu".
if sys.version_info.major >= 3:
print("Používáte správný Python!")
else:
print("Používáte zastaralou verzi Pythonu")
Nejdříve k syntaxi (tj. zápisu) příkazu if.
Příkaz if začíná klíčovým slovem if, za nímž následuje podmínka (tj. výraz, který se vyhodnotí na pravdivostní hodnotu, buď True nebo False). Poté následuje znak dvojtečky. Tento znak v Pythonu jednoznačně signalizuje, že bude následovat tzv. vnořený blok příkazů.
Blok příkazů obsahuje jeden nebo několik příkazů, které tvoří určitý celek. Bloky se v Pythonu definují pomocí odsazení (tj. určitého počtu mezer na začátku řádku). První příkaz v bloku má alespoň o jednu mezeru větší odsazení než předchozí řádek (s dvojtečkou na konci patřící do nadřazeného bloku) tj. je vizuálně více vpravo. Ostatní příkazy v bloku mají stejné odsazení (tj. jsou zarovnány pod sebou). Uvnitř bloku mohou být vnořené bloky (ještě více odsazené). Konec bloku je signalizován návratem k odsazení vnějšího bloku (tj.text je opět méně odsazen tj, začíná víc vlevo). Blok je samozřejmě ukončen i dosažením konce programu.
Zní to složitě, ale je to zcela přirozené. Každým dalším odsazením se dostáváte do hlouběji zanořených bloků, z nichž se pak můžete vracet na vyšší úrovně do nadřazených bloků.
V našem případě je vnořený (osazený) blok tvořen jediným příkazem, který tvoří jednu z větví příkazu if.
Poté následuje klíčové slovo else, které patří do stejného bloku jako if, není tudíž odsazeno. I tento řádek je ukončen dvojtečkou takže lze očekávat, že bude následovat nový vnořený blok, odsazený více vpravo (většina specializovaných editorů včetně Jupyteru proto odsazení vloží automaticky). Tento blok tvoří druhou větev příkazu if.
Obecnou syntaxi příkazu if tak lze zapsat například takto:
if podmínka:
blok-then
else:
blok-else
Po vyčerpávajícím popisu syntaxe se nyní přesuneme k sémantice, tj. k popisu chování konstrukce if za běhu aplikace.
Nejdříve dojde k vyhodnocení podmínky. V nejjednodušším případě se podmínka vyhodnotí na objekt třídy bool. Tak je tomu i v našem případě, kdy se testuje, zda je hlavní číslo verze větší, nebo rovno třem.
Pokud je výsledkem True (což by mělo platit), pak se vykonají příkazy pouze v prvním vnořeném bloku blok-then (následuje za řádkem s if). V našem případě se vypíše text "Používáte správný Python!" (to by mělo skutečně nastat).
V opačném případě (podmínka je vyhodnocena na false) se vykonají příkazy druhého vnořeného bloku tj. blok-else. V našem příkladě se vykoná příkaz print vypisující text "Používáte zastaralou verzi Pythonu".
Všimněte si, že bez ohledu na pravdivost podmínky se provede právě jedna z větví (vnořených bloků) příkazu if. Nikdy se neprovedou obě dvě, resp. nehrozí, že by se neprovedla žádná z nich.
Úkol:
Upravte podmínku předchozího příkazu
iftak, aby testovala zda je využita verze 3.6 resp. novější (správný Python) a nikoliv verze 3.5 a nižší (zastaralý Python).Rada: Je nutné, aby to fungovalo i pro případné nové hlavní verze v budoucnosti např. 4.1 nebo 5.0. Zkuste využít sumární verzi Pythonu například podle vzorce
major * 100 + minor(vedlejší verze jsou vždy menší než 100).
Příkaz if je jedním z klíčových konstrukcí jazyka Python, takže si uveďmě ještě několik jeho příkladů. Nejdříve si vypočteme trochu divný dvojnásobek celého čísla. Je-li vstupní číslo $x$ sudé, je vypsán skutečný dvojnásobek $2x$, u lichého čísla je vypsána hodnota $2x - 1$ (výsledek tedy zachovává sudost a lichost).
x = int(input("Zadej celé číslo: "))
if x % 2 == 0: # je-li x sudé
x *= 2
else: # je-li liché
x = x*2 - 1
print(x)
Ukázkový program začíná přečtením textu z textového (konzolového) vstupu pomocí vestavěné metody input. V případě, že je tato funkce použita v Jupyter notebooku, pak se po každém vyhodnocení vstupní buňky (Ctrl+Shift) objeví vstupní pole, do něhož je možno zadat vstup do programu. Před vstupní pole se napíše text, který je parametrem funkce input tzv. výzva (angl. prompt).
Výsledkem vyhodnocení funkce input je textový objekt, nikoliv číslo (obecně lze totiž zadat zcela libovolný text). Proto je na výsledek funkce input zavolána (vestavěná) funkce int, která se jakýkoliv rozumný objekt pokusí převést na celé číslo (už jsme se s ní setkali dříve, kdy jsme ji používaly na převod z čísla třídy float).
V případě volání na textový objekt (v programátorském slangu na řetězec) je text interpretován jako desítkový zápis nějakého čísla. Výsledkem je interní representace tohoto čísla tj. objekt třídy int. Pokud text obsahuje nepřípustné znaky (písmena či např. mezery), je vyvolána výjimka (a výpočet se tudíž nedokončí).
Po načtení následuje větvení programu. Pokud je hodnota označená proměnnou x sudá (tj. zbytek po dělení dvěma je roven nula) pak je tato hodnota zdvojnásobena a výsledek je odkazován toutéž proměnnou (složené přiřazení).V opačném případě (větev else) je nová hodnota proměnné x rovna 2*x - 1 .
Po skončení konstrukce if (končí druhý odsazený blok, a následně již není žádné odsazení) je (nepodmíněně) vypsána nová hodnota proměnné x.
Vyzkoušejte různé vstupní hodnoty (malá i obrovská celá čísla), neplatný vstup (texty s písmeny, apod.)
Poznámka: Předchozí kód je prvním "skutečným" počítačovým programem v tomto textu. Veškerý předchozí kód vracel vždy stejné výsledky při každém spuštění (tj. vyhodnocení). Takový kód lze vyhodnotit i na jednoduché (tj. neprogramovatelné) kalkulačce. Zahrnutím vstupu jsme toto základní omezení překonali a vytvořili, něco co je vícekrát použitelné. Zatím je to spíše potenciál (po ověření funkce pro různé vstupy, pravděpodobně již nikdy daný kód znovu nespustíte), ale je to ten příslovečný první krok.
Řešený příklad
Náledující program je o malinko složitější, i když stále nepříliš použitelný. Vyžádá si vstup dvou čísel, a nakonec vypíše, které z nich je větší (tj, je to tzv. maximum). Vstupem mohou být tentokráte čísla i desetinnou části (i když na druhé straně omezená na rozsah cca $-10^{308}$ až $10^{308}$.
x = float(input("x: "))
y = float(input("y: "))
if x > y:
m = x
else:
m = y
print(f"Maximum je", m)
Pro vstup textu je použita opět funkce input (dvakrát). Vložený text je však tentokrát převeden na číslo třídy float pomocí stejnojmenné funkce. Vstupní hodnoty jsou označeny proměnnými x a y (v tomto pořadí).
Následně je vyhodnocena podmínka zda je první číslo (označené proměnnou x) větší než číslo druhé (proměnná y). Pokud tomu tak je, je číslo se štítkem x označeno i štítkem m (tj.přiřazeno do proměnné m).
V opačném případě (druhé číslo je větší nebo rovno [!] prvnímu) je štítkem m označeno druhé číslo. Pokud je větší je zřejmé proč (je to určitě maximum). Pokud jsou obě čísla rovna, pak je jedno, které z nich bude označeno za maximum.
Výsledek (tj. číslo označené proměnnou m) je vypsán pomocí funkce print. Ta je volána se dvěma parametry, které postupně vypisuje. Prvním je text (řetězec) a druhým číslo (objekt označený proměnnou m). Všimněte si, že mezi oba výstupy se automaticky vkládá mezera.
[konec řešeného příkladu]
Nyní však na chvíli opusťme řídící konstrukce a zaměřme se právě na základní možnosti zpracování textů v Pythonu.
Pro vstup textu je použita opět funkce input (dvakrát). Vložený text je však tentokrát převeden na číslo třídy float pomocí stejnojmenné funkce. Vstupní hodnoty jsou označeny proměnnými x a y (v tomto pořadí).
Následně je vyhodnocena podmínka zda je první číslo (označené proměnnou x) větší než číslo druhé (proměnná y). Pokud tomu tak je, je číslo se štítkem x označeno i štítkem m (tj.přiřazeno do proměnné m).
V opačném případě (druhé číslo je větší nebo rovno [!] prvnímu) je štítkem m označeno druhé číslo. Pokud je větší je zřejmé proč (je to určitě maximum). Pokud jsou obě čísla rovna, pak je jedno, které z nich bude označeno za maximum.
Výsledek (tj. číslo označené proměnnou m) je vypsán pomocí funkce print. Ta je volána se dvěma parametry, které postupně vypisuje. Prvním je text (řetězec) a druhým číslo (objekt označený proměnnou m). Všimněte si, že mezi oba výstupy se automaticky vkládá mezera.
[konec řešeného příkladu]
if s jednou větví¶V příkazu if lze vynechat větev else, pokud se v této větvi nemusí nic provádět. Typicky se této konstrukce využívá v případě podmíněného výstupu (výstup se provede jen v případě jisté podmínky) nebo při podmíněném ukončení programu (resp. i jiných konstrukcí).
cislo = int(input())
if cislo == 42: # pokud je to hledané číslo
print("Bingo") # vypíše se text `Bingo`
# v opačném případě se nevypíše nic (větev `else` chybí)
Při programování někdy nastanou situace, které nelze jednoduše vyřešit. Uživatel zadá neplatnou hodnotu, chybí prostředky pro provedení dané činnosti (například neexistuje vstupní soubor, chybí připojení k Internetu, dojde k abolutní ztrátě přesnosti, apod.).
V tomto případě existuje jen jedna správná reakce — vyvolání výjimky. Program je přerušen, a pokud kód vyšší úrovně na výjimku nezareaguje, je nakonec i ukončen (s chybovým hlášením). S výjimkami jsme se již setkali, prozatím však vznikaly automaticky v knihovním kódu (tj. kódu nižší úrovně, který jste nenapsali jen ho pasivně využíváte). Výjimky však můžete vyvolávat (spouštět) i sami.
Vyvoláním výjimky se v zásadě zbavujete odpovědnosti za příslušný problém (a přenášíte jej na někoho dalšího, v případě ukončení programu je to uživatel programu). Pokud je problém způsoben vnějšími okolnostmi (chybný vstup, nedostatek prostředků, apod.) je to to nejlepší řešení. Pokud byste problém řešili, pak by se program zbytečně komplikoval a stal se závislým na okolnostech, které nemůžete ovlivnit (například na mechanismu interakce s uživatelem). Platí prosté: pokud máte problém, který nelze v daném místě vyřešit, pak vyvolejte výjimku.
PS: Je zřejmé, že pokud je problém ve Vás (například nevíte, jak něco naprogramovat) a děje se to při každém spuštění programu, pak vyvolání výjimky nic nevyřeší (hlavně pokud program vytváříte sami).
A nyní prakticky. Výjimka se vyvolá příkazem raise za nímž následuje konstrukce objektu, který nese informaci o tom co se stalo (resp. co se nestalo). Prozatím budeme využívat jen objekty třídy Exception (exception je angl. výjimka). Vyvolání výjimky je často uvedeno v jednovětvém příkazu if, kde podmínka testuje, zda nastal daný problém.
hmotnost = float(input("Zadejte hmotnost předmětu: "))
if hmotnost < 0: # váš program neumí stejně jako fyzika pracovat se zápornými hmotnostmi
raise Exception("Záporná hmotnost") # a pokud to nastane, pak to dále neřešíte (někdo to musí vyřešit za Vás)
Nyní však na chvíli opusťme řídící konstrukce a zaměřme se na základní možnosti zpracování textů v Pythonu.
Jako řetezce v Pythonu označujeme objekty, které representují texty (což nění nic jiného než libovolná posloupnost znaků). Řetězce jsou instancemi třídy str (zkratka za anglické string pro než se již od počátků programování v Čechách vžil překlad řetězec).
Nejjednoduším způsobem jak vytvořit nový řetězec je napsat ho přímo do programu. Aby se však odlišil od ostatního kódu musí být z obou stran opatřen uvozovkami nebo apostrofy (obě možnosti jsou plně ekvivalentní). Vymezující znaky do vlastního řetězce nepatří (proto jsou i při použití uvozovek na výstup vidět apostrofy, i zde hrají jen pomocnou roli)
"Toto je řetězec, který vznikne, je vypsán a poté zanikne
'Toto je jiný řetězec, který je uzavřen v apostrofech'
Záleží jen na Vás, zda dáte přednost apostrofům nebo uvozovkám (já jsa odkojen programovacím jazykem Pascal preferuji uvozovky). V každém případě doporučuji jednotnost s výjimkou speciálních případů. Například, pokud řetězec obsahuje uvozovky pak se snadněji píše omezený apostrofy (a vice versa).
'A bůh řekl: "Budiž světlo!"'
V běžném pythoním řetězci mohou být v zásadě jakékoliv znaky, kromě omezovačů (tj.znaků, které je omezují) a znaku odřádkování. Speciální význam má i znak zpětného lomítka \, které tak nelze uvést přímo.
Znak lomítko se používá pro zápis tzv. escape sekvencí, což je skupina několika znaků, která však realizuje znak jediný. V praxi stačí znát jen několik těchto escape sekvencí:
\n : znak nového řádku (odřádkování)
\t : znak tabulátoru
\ : znak zpětného lomítka
\" : znak uvozovky
\' : znak apostrofu
print("Řetězec s odřádkováním\n toto je už druhý řádek a tabulátorem \t ...")
print('Tento řetězec obsahuje "nebezpečné" znaky jako je \' (apostrof) a \\ (zpětné lomítko)')
Python podporuje kromě běžného zápisu řetězců i několik dalších. Ty se liší uvedením klíčového znaku před počáteční uvozovkou resp. apostrofem.
Nejužitěčnější jsou tzv. formátované řetězce, které v zápise řetězců umožňují tzv. interpolace výrazů. Pokud do těchto řetězců zapíšeme výraz ve složených závorkách, pak je tento výraz nahrazen svým výsledkem v textové podobě. Formátované řetězce musí začínat znakem f:
f"1 + 1 = {1+1}"
Všimněte si, jak byl výraz {1+1} nahrazen svou hodnotou. Formátované řetezce se hodí pro formátování textového výstupu ve funkci print (obecně při jakémkoliv typu výstupu):
from math import *
r = float(input("Zadej poloměr koule v metrech: "))
v = 4/3*r**3 # spočítáme objem podle známého vzorečku
print(f"Objem koule je {v} m^3, což je {1000*v} litrů")
Výsledek je přehledný, až na zbytečný počet desetinných míst (je málo pravděpodobné, že poloměr koule mám změřenu s přesností femtometrů). Interpolace naštěstí umožňují specifikovat i výstupní formát interpolovaných číselm pomocí speciálního formátovacího jazyka. Pro Vás jako začátečníky stačí znát jen dva základní formáty (formáty jsou určeny jediným tzv, formátovacím znakem):
Univerzální formát g: vypisuje desetinné číslo se zadaným počtem desetinných místm a to preferovaně v běžném tvaru (bez exponentu). Pouze čísla, pro něž je exponenciální tvar stručnější resp. lépe vyjadřuje počet platných číslic, jsou zobrazována s exponentem. Hodí se pro výstupy určené pro lidi (nejlépe přírodovědce):
V tomto formátu lze specifikovat i počet platných číslic míst (nikoliv desetinných!).
c = 299_792_458.0
au = 149_597_871e3 # vzdálenost Slunce-Země
print(f"Rychlost světla je {c:.3g} m/s, tj. doba letu světla od Slunce k Zemi je cca {au/c:.3g} sekund")
Jak lze vidět, formát se zadává uvnitř interpolačních složených závorek a to za příslušný výraz a je oddělený dvojtečkou. Vlastní formát začíná nepovinnou specifikací počtu platných číslic (jenž je sám tvořený znakem tečka a číslem udávajícím daný počet). Na konci formátu je formátovací znak tj. zde g.
Velká číslo (rychlost světla) je zobrazeno v exponenciální tvaru zaokrouhlené na tři platné číslice (jsou to číslice 3,0,0, koncové nuly po desetinné tečce se v tomto formátu nezobrazují). Čas letu světla je malé číslo, a tak se zobrazuje bez exponentu právě na 3 platné číslice.
Úkol: Zkuste zvýšit resp. snížit počet platných číslic na 4 resp. 2. Proč se v případě dvou platných čísel čas zobrazuje jako 5e2 (přestože je to malé číslo)?.
Fixní formát f: vždy vypisuje číslo v neexponenciálním tvaru (i když je velké či blízké nule). Hodí se například pro finanční částky.
Číslo za tečkou ve formátu specifikuje v tomto případě počet (povinných) číslic za desetinnou tečkou.
gdp = 193.5e9
print(f"HDP ČR je {gdp:.2f}")
I když je HDP české republiky velké číslo je zobrazeno bez použití exponenciálního zápisu a s dvěma desetinnými místy (je v zásadě vyjádřitelný v haléřích).
Pokud chceme v tomto formátu použít oddělovač tisíců, pak stačí použít znak "," na začátku formátu (oddělovač čárka se využívá v angličtině, nastavení pro češtinu je složitější).
gdp = 193.5e9
print(f"HDP ČR je {gdp:,.2f}")
Úkol: Vypište v základním tvaru (tj. bez použití indexu) největší číslo representovatelné typem
float. Použijte atributmaxobjektusys.float_info, kdesysje jméno modulu (pro přehlednost použijte oddělovače tisíců). Pak číslo přečtete :)
Řešený příklad: Vytvořte skript, který pro místo na zadaných geografických souřadnicí vrátí jeho vzdálenost od centra kampusus UJEP. Pozornost soustřeďte na formátování výstupu (pro geo-souřadnice zvolte formát DD°MM.M', pro vzdálenost přesnost na desetiny kilometrů).
Pro výpočet vzdálenosti použijte tento vztah:
$d=\arccos\bigl(\sin\phi_1\cdot\sin\phi_2+\cos\phi_1\cdot\cos\phi_2\cdot\cos(\Delta\lambda)\bigr)$.
# vstupy
r = 6371 # poloměr Země v km viz
f1 = 50.6654175 # zeměpisná šířka UJEP
l1 = 14.024250 # zeměpisná délka UJEP
f2 = float(input("Zem. šířka místa: "))
l2 = float(input("Zem. délka místa: "))
f2stupne = int(f2) # celá část (stupně) úhlu
f2minuty = abs((f2 - f2stupne) * 60.0) # minuty
l2stupne = int(l2)
l2minuty = abs((l2 - l2stupne) * 60.0)
d = r * acos(sin(radians(f1)) * sin(radians(f2))
+ cos(radians(f1)) * cos(radians(f2)) * cos(radians(l2 - l1)))
print(f"Místo se souřadnicemi {f2stupne}°{f2minuty:.1f}' z.š. a {l2stupne}°{l2minuty:.1f}' z.d." +
f" je vzdáleno {d:.1f} km od UJEP")
Nejjednodušší operací nad řetězcem je zjištění jeho délky, tj. počtu znaků v řetězci. Python obsahuje vestavěnou funkci len (zkratka za length), která vrací délku u všech objektů, u nichž to má smysl (tj. skládají se z nějakého počtu elementárnějších částí)
len("Geralt")
len("") # délka prázdného řetězce
Téměř veškeré další manipulace s řetězci lze provádět pomocí tří základních operací: skládání (= spojování) řetězců, indexace (získávání) podřetězců (= souvislých fragmentů řetězů) a hledání podřetězců (i když ne vždy je to nejjednodušší a nejefektivnější).
Řetězce jsou v Pythonu striktně neměnnými objekty, tj. žádnou operací nelze změnit obsah řetězce. To se týká i dvou základních operací,
Skládání řetězců se zapisuje pomocí operace +.
"Frodo" + "Pytlík"
Výsledkem operace je nový objekt: řetězec obsahující všechny znaky levého operandu (řetězce vlevo od operátoru +) následované znaky pravého operandu (bez jakéhokoliv oddělení).
Skádání dvou přímo zadaných řetězců není příliš užitečné (spojit dva řetězce není na rozdíl od číslené aritmetiky nevyžaduje kalkulačku v hlavě nebo na papíře). Praktičtější využití nabízí následující miniprogram (zadejte si prosím Vaše skutečná jména).
jmeno = input("Vaše křestní jméno: ")
prijmeni = input("Vaše příjmení: ")
celeJmeno = jmeno + " " + prijmeni # spojení tří řetězců
print(f"Vaše celé jméno je {celeJmeno}")
Pro získání podřetězce se v Pythonu používá tzv. indexace. Na objekt řetězce se aplikuje index v hranatých závorkách. Nejjednodušším indexem je kladné celé číslo.
s = "Samvěd"
s[1]
Jak lze vidět tak v tomto případě se vrací podřetězec tvořený jediným znakem a to znak na pozici určené indexem. První znak má pozici 0, druhý pozici 1, atd. V našem případě (index = 1) je tedy vrácen řetězec obsahující znak a.
Indexování od nuly může být na první pohled překvapivé a poněkud ztěžuje komunikaci programátorů, neboť lidé preferují počítání od jedné. Třetí znak tak má index 2, a desátý index 9 (obecně n-tý znak má index $n-1$). Je však pro počítače přirozenější a má i další výhody a proto se v programovacích jazycích běžně využívá (jsou však i jazyky, ve kterých zvítězil lidský resp. matematický pohled).
Python je zde však důsledný: ve všech kontextech (nikoliv jen při počítání znaků) se používají indexy počínající nulou.
Poslední použitelný index je proto roven délce řetězce bez jedné.
s[5] # poslední znak šestiznakového řetezce
Pokud použijeme (omylem) větší index je vyvolána výjimka (a program předčasně končí), Všimněte si, že výjimka informuje o příčině potíží.
s[6]
Co se však stane, pokud použijeme záporný index (celé číslo může být i záporné).
s[-1]
Kupodivu, výjimka nevznikla. Python totiž využívá záporné indexy pro indexování od konce (u řetezců od posledního znaku). Poslední znak má index -1 (bez ohledu na délku řetězce), předposlední -2 atd.
s[-6] # šestý od konce (což je u šestiznakového řetězce první znak)
U získávání jednotlivých znaků však indexace v Pythonu nekončí. Lze získat i delší podřetězce pomocí tzv. výřezů (angl. slice).
s[1:3]
Výřez se skládá ze dvou celých čísel oddělených dvojtečkou. První určuje index prvního znaku podřetězce (opět se indexuje od nuly!). Druhé číslo nicméně neurčuje index posledního znaku, ale index prvního znaku, který už v podřetězci neleží (tj. chápe se jako hraniční pozice vyjma
Zápis 1:3 tedy vyjadřuje pozice od indexu 1 (včetně) do indexu 3 (vyjma) tj. dvě pozice (= dva znaky po indexaci). Délka výsledného řetězce je vždy horní_mez - dolní_mez.
s[1:4] # 3 znaky od druhého (index 0) do pátého (index 4) vyjma (= druhý, třetí, čtvrtý)
Výřezem můžeme získat i celý původní řetězec (přesněji jeho kopii, neboť při indexaci vždy vzniká nový objekt)
s[0:6] # šest znaků (od indexu 0 do indexu 5)
To však ještě není vše. Ve výřezech můžete používat i záporné indexy (počítané od konce) a dokonce jednotlivé meze vynechávat. Pokud vynecháte první pak se začíná indexem 0 (první znak) a končí indexem rovným délce seznamu (pozice za posledním znakem).
print (s[0:-1]) # řetězec bez posledního znaku (-1 je index posledníjo znaku, který již do výřezu nepatří)
print (s[2:-2]) # od třetího (včetně) k předpředposlednímu (vyjma) = ke znaku s indexem 6-2 (vyjma) = 4
print(s[:2]) # první dva znaky (od indexu nula včetně do dva vyjma)
print(s[:-2]) # bez posledních dvou znaků (od indexu nula včetně do indexu 6-2 vyjma)
print(s[3:]) # od indexu 3 (čtvrtý znak) do konce (= bez prvních tří)
print(s[-1:]) # od posledního do konce (= poslední jeden)
A nakonec typický Pythonský ideom (zápis, který se v Pythonu relativně často používá, ale nikdo z nezasvěcených mu nerozumí).
print(s[:]) # vrací kopii řetězce
Poznámka: Pokud však rozumíte Pythonu opravdu dobře, pak zjistíte, že tento ideom nemusí na řetězce fungovat, neboť je zbytečné vytvářet kopii neměnných objektů. Kopii od originálu totiž u neměnných objektů nikdy nerozlišíte (pro rozlišení by stačilo jeden z objektů změnit, ale to se u neměnných objektů z principu nelze).
Představte si, že máte rozlišit mezi dvěma identickými jednovaječnými dvojčaty (bez možnosti je vidět pohromadě). Pokud jsou slepí, hluší a nemůžete se jich dotknout (zanechat např. znaménko) tj. jsou neměnní, pak se vám to nikdy nepodaří.
Jedinou šancí jak tento trik rozpoznat je testování identity objektů, to však praktickou nerozlišitelnost neovlivní (stejně jako, když uvidíte obě dvojčata zároveň).
s[:] is s # Python je skutečně inteligentní (kopírováním se neobtěžoval, objekt s je identický s objektem [:])
Úkol: Napiště výraz, který z daného řetězce vytvoří nový, jenž nebude obsahovat předposlední znak (rada: nový řetězec není souvislým podřetězcem původního)
Pro ověření, zda je nějaký (pod)řetězec obsažen v jiném řetězci lze využít operátor in:
"alf" in "Gandalf"
Vlevo je hledaný (podřetězec), vpravo řetězec v němž hledáme. Výsledkem je pravdivostní hodnota ("alf" je obsažen v řetezci "Gandalf" a proto je výsledkem našeho příkladu hodnota True).
Řešený příklad
Vytvořte jednoduchý program který zjistí, zda je v zadané genomové sekvenci (tvořený posloupností bází, jež jsou representovány znaky ACGU) obsažen zadaný kodon (trojice znaků tvořená kombinací znaků ACGU).
genom = input("Genom (kombinace znaků ACGU):")
kodon = input("Kodon (trojznaková kombinace znaků ACGU):")
if kodon in genom:
print(f"Kodon {kodon} je obsažen v genomu {genom}")
else:
print(f"Kodon {kodon} není obsažen v genomu {genom}")
Tento program má ještě k dokonalosti daleko. Hlavním problémem je testování správnosti vstupu, neboť do vstupních polí můžete zadat libovolný řetězec (nekontroluje se zda obsahují pouze znagy ACGU).
[dočasné přerušení příkladu]
Kontrola, zda řetězec splňuje určitou podmínku ohledně obsahu, není za použití elementárních nástrojů (výřezy, operátor in) ve vždy jednoduchá.
Mezi ty jednoduché operace testování patří:
a) řetězec je obsažen v jiném řetězci
"gard" in "Asgard"
b) řetězec není obsažen v jiném řetězci (negace bodu a)
"gard" not in "Asgard"
Tento zápis s použitím negace operátoru not in je přehlednější (a bližší přirozenému anglickému jazyku) než zápis s běžnou negací (závorky jsou zde nutné, negace má vyší prioritu než operátor in):
not ("gard" in "Asgard")
c) řetězec začíná nějakým podřetězcem (prefixem):
"Asgard"[:2] == "As" # výřez vrací první dva znaky (s indexy 0 a 1)
Hledání prefixu je tak běžnou operací, že pro ni existuje i přehlednější zápis využívající metody startswith. Metoda je v zásadě speciálním případem volání funkce, v němž je nejdříve uveden objekt, který je hlavním parametrem a teprve poté (po tečce) jméno funkce a ostatní parametry (v závorkách).
"Asgard".startswith("As")
Řetězec "Asgard" je hlavním parametrem metody (tj. objektem, s nímž se snažíme komunikovat), metoda se jmenuje startswith a dalším (vedlejším) parametrem je hledaný prefix (počátek řetězce). Výsledkem je i zde logická hodnota.
Poznámka: Metody (včetně zápisu jejich volání) pocházejí ze světa tzv. objektově orientovaného programování. To vychází z představy, že různé objekty spolu interagují tím, že si navzájem volají své metody (které mohou být navíc doplněny dalšími parametry tj. interakce nemusí být typu jeden s jedním). To, který objekt je hlavní parametr (v OOP terminologii adresát) a které jsou dodatečné je dáno pouze zvoleným pohledem. V případě metody
startswithje adresátem prohledávaný řetězec a dodatečným parametrem podřetězec (hledaný prefix). Lze si však představit i opačný model např. volání typu"As".isprefixof("Asgard"), který se však v Pythonu neuplatnil.
d) řetězec končí nějakým podřetězcem (sufixem)
I když i zde můžete použít výřez (a porovnání řetězců), tak i pro testování sufixu existuje specializovaná metoda (jejíž jméno určitě odhadnete)
"Asgard"[-2:] == "rd"
"Asgard".endswith("rd")
e) všechny znaky řetězce patří do určité třídy znaků
Znaky lze podle jejich funkce klasifikovat do několika kategorií. Mezi hlavní kategorie patří písmena (znaky použitelné pro zápis přirozeného jazyka), číslice (znaky sloužící pro zápis čísel) a mzerové znaky (bílé tj. netištěné oddělovače). U těchto klíčových kategorií (a pár dalších) lze využít metod, které testují, zda všechny znaky řetězce patří do příslušné skupiny.
print("Dobříň".isalpha()) # všechny znaky jsou písmena
print("42".isdigit()) # všechny znaky jsou číslice
print(" \t \n".isspace()) # všechny znaky jsou mezerové znaky (patří tam i tabulátor a odřádkování)
Úkol: Napište program, který pro daný řetězcový vstup ověří, že jeho první i poslední znak je číslo (ostatní znaky mohou být jakékoliv). Pokud řetězec odpovídá, je vypsán text "Platný vstup" (v opačném případě se nic nevypíše).
I když jsou tyto metody užitečné, mají několik nevýhod. Za prvé je lze využít jen pro pár všeobecně používaných kategorií znaků (mezi tyto tyto kategorie rozhodně nepatří napříkald znaky genetického kódu). Navíc jsou tyto kategorie výrazně širší než byste čekali. Moderní počítače podporují znaky všech jazyků a písmených soustav (včetně např. emotikonů). Tj. písmena nejsou omezena na latinku a číslice na euroarabské dekadické číslice.
print("Ζεύς".isalpha()) # řecké písmo
print("٤٢".isdigit()) # arabsko indické číslice
I když je užitečné znát všechny výše uvedené metody a operaci in existuje v Pythonu nástroj, který je všechny nejen nahradí, ale vyřeší i jejich nedostatky — regulární výrazy.
Regulární výrazy (název pochází z matematické teorie tzv. gramatik) jsou více než užitečným nástrojem pro práci s řetězci. Používány jsou již od sedmdesátých let, ale jejich současná podoba se vyprofilovala v jazyce Perl v letech osmdesátých. Python stejně jako většina ostatních programovacích jazyků a aplikací převzala perlovskou syntaxi (s malými omezeními i rozšířeními, tj. různé implementace regulárních jazyků se v detailech mohou lišit, společný základ je však naštěstí dosti rozsáhlý).
Regulární výrazy jsou velmi komplexním jazykem pro popis struktury řetězců (o regulárních výrazech vycházejí celé knihy). My se omezíme jen na nezbytné základy se kterými se navíc seznámíme jen postupně. Vyhneme se navíc také jejich teorii (i když ta je zajímavá a může výrazně pomoci při tvorbě rozsáhlejších výrazů).
Regulární výraz je v zásadě řetězec, který jednoznačně definuje určitou množinu řetězců s podobnou strukturou. Tato množina může být i jednoprvková, ale typicky je potenciálně nekonečná (tj. s neomezeným opakováním nějakého vzoru).
Základní operací nad regulárními výrazy je test shody (angl. match). Tato operace testuje zda zadaný řetězec
odpovídá regulárnímu výrazu či nikoliv. V Pythonu je tato operace implementována pomocí metody fullmatch modulu re (zkratka ze Regular Expression). Funkce má dva parametry: nejprve regulární výraz (vzor, jak by měl řetězec vypadat), a poté testovaný řetězec.
import re
s = input() # načteme řetězec
if re.fullmatch("[ab]+", s): # otestujeme, zda řetězec odpovídá regulárnímu výrazu
print("shoda!") # pokud ano
else:
print("špatný řetězec") # pokud ne
Znaky v řetězci regulárního výrazu mají dvě zcela odlišné funkce. Písmena, čísla a bílé znaky (mezery) jsou tzv. terminály, tj. znaky, které budou (resp. potenciálně mohou být) obsaženy v popisovaném řetězci (tj. v regulárním označují sebe sama). V našem příkladě jsou to znaky a a b (výsledný řetězec tak může obsahovat jen tyto znaky a žádné jiné). Ostatní znaky mohou mít speciální význam, který závisí na jejich použití: mohou popisovat výběr z z nějaké skupiny znaků, pozice mezi znaky, operátory nad vnořenými regulárními jazyky, apod.
Hranaté závorky použité v regulárním výrazu [ab]+, popisují právě jeden znak, kterým může být buď znak a nebo znak b (nikoliv oba najednou). Na tento regulární (pod)výraz je aplikován operátor +, který v případě regulárních výrazů znamená, že tento řetězec popsaný tímto podvýrazem se může libovolněkrát opakovat (ale musí tam být vždy alespoň jednou). V každém opakování se vždy můžeme rozhodnout mezi a nebo b, tj. regulární výraz popisuje všechny řetězce libovolné délky (avšak neprázdné!), tvořené znaky a a b, například a, b, ab, ba, aaa, aab, aba, … abba, baba, atd.
Výsledkem aplikace regulárního výrazu je v případě shody vzoru a řetězce tzv. match objekt, který nese dodatečné informace o shodě. Vyzkoušejme například následující příklad:
pattern = "a+"
s = "aaa"
re.fullmatch(pattern, s)
Regulární výraz popisuje řetězce tvořené jen znaky a (znak a je terminál, který se může libovolně opakovat). Výsledný match objekt nemá standardní textovou representaci (jako např. čísla) a tak je vypsána jeho pomocná textová representace: obsahující třídu objektu a jeho klíčové atributy (pozice shody, apod.). Pomocná textová representace je navíc v tzv. lomených závorkách (znaky menší a větší než).
Pomocná textová representace ve většině případů ukazuje, že se snažíme vypsat příliš složitý objekt. Řešením je vypsání některého z jeho atributů nebo přetypování na jednodušší objekt. V našem případě nás zajímá jen logická hodnota (splňuje/nesplňuje), takže zkusíme tento objekt převést na pravdivostní hodnotu:
bool(re.fullmatch(pattern,s))
Toto přetypování se provádí u každé podmínky konstrukce if a tak lze na místě podmínky použít i složitější objekty, pokud mají rozumné přetypování na logické hodnoty.
Co se však stane pokud, řetězec vzoru neodpovídá.
re.fullmatch(pattern, "aaaaaaaaac")
Zdá se, že v tomto případě metoda nic nevrací. V Pythonu však každá funkce či metoda musí něco vracet. V nejhorším případě vrací hodnotu None (to není kontradikce; None není nic, jen něco co není dostupné nebo není aplikovatelné). Možná už tušíte co se stane, pokud se pokusíme None přetypovat na pravdivostní hodnotu:
bool(None)
Ano. 'None' je vždy nepravdivé. Nyní už by mělo být jasné, jak funguje větvení programu v prvním příkladě s regulárními výrazy. Pokud řetězec odpovídá regulárnímu výrazu, pak se jde větví if (výsledný match objekt je vždy pravdivý), jinak se pokračuje větví else (výsledná hodnota None je nepravdivá).
Poznámka: Pro ty, co nechtějí využívat magického rozdělené světa Pythonských objektů na pravdivé a nepravdivé (proč je, probůh, například úspěch vždy pravdivý) existuje i explicitnější podmínka:
re.fullmatch(pattern,string) is not None. Python podporuje jak pohodlnost implicitnosti tak jistotu explicitnosti (i když Zen of Python tvrdí: 'Explicit is better than implicit.'
Úkol: Jaké řetězce popisuje regulární výraz
(ab)+? Vyzkoušejte svoji teorii pomocí několika vhodně zvolených příkladů řetězců (všechny vyzkoušet nemůžete, tento regulární výraz popisuje potenciálně nekonečné množství řetězců).
pattern = "(ab)+"
testString = "aabb" # zde můžete zadat zkušební řetězce
bool(re.fullmatch(pattern, testString))
Nyní se můžeme vrátit k řešenému příkladu s genetickým kódem. Pomocí regulárních výrazů je snadné otestovat zda je zadaný řetězec tvořen jen znaky využívanými pro zápis genetického kódu (representují jednotlivé nukleotidy). Nejdříve navrhneme regulární výraz popisující genetický kód libovolné délky tj. řetězec obsahující libovolně dlouhou posloupnost znaků A,C,G a T.
import re
re.fullmatch("[ACGT]+", "ACCGGCCTTTC") # otestujte i pro jiné (i nepřípustné) řetězce
Jen o málo složitější je regulární výraz pro trojpísmenné kodony:
re.fullmatch("[ACGT][ACGT][ACGT]", "AAA")
Všimněte si, že každá znaková množina popisuje právě jeden znak tj. daným regulárním výrazem jsou popsány jen tříznakové řetězce. Každý znak je navíc z omezené (a velmi malé množiny). Lze tedy snadno spočítat, že existuje právě $4^3$ = 64, které jsou tímto regulárním výrazem definovány.
Úkol: Doplňte program pro nalezení kodonu v genomové sekvenci o testování přípustnosti vstupů pomocí výše uvedených regulárních výrazů. V případě, že vstup nevyhovuje vyvolejte výjimku pomocí příkazu
raise Exception("popis výjimečné situace"). Nezapomeňte podmínku negovat (problém je když řetězec neodpovídá vzoru!).
Miniúvod do regulárních výrazů ukončíme popisem asi nejužitečnějšího zástupného symbolu. Znak . (tečka) má v regulárním výrazu speciální význam — nahrazuje libovolný znak (avšak vždy jen jeden). Ukažme si několik příkladů:
print( re.fullmatch("...", "abc") ) # tři jakékoliv znaky
print( re.fullmatch("a.+", "ahoj" )) # řetězec začínající a mající alespoň dva znaky
# ("a" + 1 až nekonečně mnoho dalších jednotlivých znaků)
print( re.fullmatch("(.a)+", "ratata")) # řetězec s sudým počtem znaků, kde na každé druhé pozici je 'a'
Pokud chceme v regulárním výrazu využít tečku jako terminální znak (tj. tečku, která stojí sama za sebe), je nutno použít zápis \\. (zpětné lomítko je zdvojeno proto, že jej lze v pythonském řetězci zapsat jen pomocí tzv. escape sekvence). Podobně lze jako terminály vkládat i další speciální znaky např. ( nebo ).
re.fullmatch("\\(\\..\\.\\)", "(.a.)")
Tento regulární výraz obsahuje terminály pro vnější dvojici párových závorek, uvnitř nichž je libovolný znak mezi dvojicí teček (první a třetí tečka je předcházena dvěma lomítky, zatímco druhá nikoliv). Všimněte si, že regulární výraz s větším počtem dvojic zpětných lomítek není příliš přehledný. Mírné zpřehlednění může přinést použití tzv. surových řetězců, v nichž nemá lomítko speciální význam (takže se nemusí zdvojovat). Surové řetězce se zapisují pomocí znaku r (angl. raw) bezprostředně před uvozovkou resp. apostrofem.
re.fullmatch(r"\(\..\.\)", "(.!.)") # surový řetězec využitý pro zápis regulárních výrazů
Úkol: Napište program, který pro daný řetězcový vstup ověří, že je tvořen číslem s povinnou desetinou tečkou a desetinnou části.
V některých situacích se hodí modifikovat některé znaky řetězce (nahradit je jinými, odstranit, či vložit jiné). To však zcela přirozeně nejde, neboť řetězce jsou neměnné objekty.
Náhradním řešením je nahradit (nepřípustnou) modifikaci vytvořením nového řetězce, u něhož jsou dané modifikace provedeny (tj. vytvoří se už v modifikované podobě). Python tento přístup využívá hned u několika metod objektů třídy string.
s = "Mis nenasitných sislů"
s.replace("i", "y") # řetězec označený proměnnou `s` je nezměnen
# nově vytvořený není po provedení řádku dostupný (zaniká)
print(s) # vypíše se nezměněný původní řetězec
s = s.replace("i", "y") # nový řetězec je přesměrována proměnná 's', původní objekt zaniká
print(s)
Jak lze vidět na příkladě metody replace, výsledek je nutno přiřadit do proměnné (jinak volání nemá žádný efekt). Často to bývá proměnná, která označovala původné řetězec (ten se sice stane nedostupným, avšak to v mnoha případech nevadí).
Volání metody replace ukazuje základní rys (objektové) programování — spolupráce několika objektů, jejímž výsledkem je nový objekt. Metoda je volána nad objektem (třídy řetězec) a předány jsou jí další dva (dočasně vytvořené) objekty: prvním je nahrazovaný (pod)řetězec, druhým nahrazující. Výsledkem je vytvoření nového řetězce, v němž jsou všechny výskyty prvního podřetězce nahrazeny druhým podřetězcem.
Podívejme se na obrázek.
V první fázi (černá barva) jsou postupně vytvořeny vstupní objekty a jeden z nich (původní řetezec) je opatřen proměnnou s. Ve druhé fázi (modré) se vykoná volání metody, jehož výsledkem je nový objekt se zaměněnými znaky. Třetí (červená) fáze je přiřazení: odkaz v proměnné je přesměrován na nový objekt (štítek je odlepen z původného objektu a přilepen na nový). Po skončení je dostupný pouze jediný objekt (změněný řetězec). Ostatní objekty už vlastně neexistují.
Zaměňovat lze samozřejmě i delší podřetězce:
s = "Frodo & Sam"
s = s.replace("Frodo", "Bilbo")
print(s)
Pokud máte ještě složitější požadavky, lze využít funkci re.sub, která umožňuje definovat nahrazované řetězce pomocí regulárního výrazu. Jako příklad uveďme nahrazení všech celých čísel v řetězci otazníkem (celé číslo je nahrazeno jediným otazníkem).
s = "333 stříbrných stříkaček stříkalo přes 333 stříbrných střech"
s = re.sub("[0123456789]+", "?", s) # i zde se původní řetězec nemění
print(s)
Všimněte si, že funkce re.sub (není to metoda, re není objekt, na nějž je volána, ale modul) má tři parametry v pořadí regulární výraz určující nahrazovanou část (zde libovolná posloupnost číslic), nahrazující řetězec (znak otazníku) a teprve třetím je řetězec, v němž se náhrady provedou (sub je zkrácené anglické substitute).
Úkol: Vytvořte program, který přečte řetězec a nahradí vícenásobné mezery (tj. mezery tvořené dvěma a více znaky mezer) za mezery jednoduché. To je užitečné, neboť v vložení více mezer za sebou je typickou chybou při zadávání textů. Opravený text následně vypište.
S úpravou víceznakových mezer souvisí i další požadavek. Při zadávání formalizovaných řetězců je nutno preventivně odstraňovat mezery na počátku řetězce a především mezery na jeho konci (ty jsou ve většině případů zcela neviditelné, přesto však mohou negativně ovlivňovat porovnání (i když se řetězce na první pohled neliší, strojově se jedná o dva odlišné řetězce, které se liší už v počtu znaků).
Požadavek na odstranění počátečních a koncových mezer se vyskytuje tak často, že pro něj existuje speciální metida (resp. hned několik speciálních metod).
jmeno = " Jiří Fišer " # řetězec s nadbytečnými mezerami (na začátku, uprostřed i na konci)
print( jmeno.lstrip()) # vrací nový řetězec s odstraněnými mezerami na začátku (vlevo = left)
print( jmeno.rstrip()) # vrací nový řetězec s odstraněnými mezerami na konci (vpravo = right)
print( jmeno.strip()) # vrací nových řetezec bez počátečnách i koncových mezer
Všiměnte si, že odstranění mezer na konci se vizuálně nijak neprojevilo. Navíc se neodstranili vícenásobné mezery uvnitř (resp. přesněji nenahradily za jednoduchou mezeru). Pro tento účel je nutno spojit metodu strip se substitucí pomocí regulárního výrazu:
re.sub(" +", " ", jmeno.strip())
Nejdříve se odstraní mezery na počátku a na konci (strip) a pak se nahradí vícenásobné mezery za jednoduché. Lze to udělat i obráceně:
re.sub(" +", " ", jmeno).strip()
Toto řešení může být o něco méně efektivní, neboť zjednodušujeme i ty vícenádobné mezery, které nakonec stejně odstraníme metodou strip (což je pravděpodobně o něco rychlejší než substituce). Tuto teorii v Jupyteru snadno ověříme.
%timeit re.sub(" +", " ", jmeno.strip())
%timeit re.sub(" +", " ", jmeno).strip()
Rozdíl opravdu existuje, není však příliš velký. První postup je cca 20% rychlejší.
Poslední užitečnou dvojicí jsou funkce, které nahrazují velká písmena (verzálky) za malá (minusky) a vice versa (samozřejmě opět vytvořením nového řetězce).
s = "Minas tirith"
print(s.lower()) # velká na malá (ostatní beze změny)
print(s.upper()) # malá na velká (ostatní beze změny)
print(s.title()) # malá na velká na začátku slov
Tyto funkce se opět hodí při porovnání, V tomto případě tehdy, pokud se nemá brát zřetel na velikost písmen.
s = input("Město: ")
if s.lower() == "ústí nad labem": # porovnáváme v malých písmenech bez ohledu na vstup
print("Sídlo UJEP")
Python má relativně omezený počet složených programových konstrukcí (tj. konstrukcí spojujících více příkazů). Kromě větvení (příkaz if) jsou to i cykly. Cykly umožňují vícenásobné provádění bloku příkazů.
Hlavním cyklem jazyka Python je cyklus for. Cyklus for prochází (konečnou) posloupnost objektů, na každý z nich postupně přesměruje tzv. řídící proměnou a následně vykoná (odsazený) blok příkazů tzv. tělo cyklu.
range¶for i in range(5): # hlavička cyklu
print(i) # tělo cyklu
Tento cyklus postupně prochází posloupnost čísel, jež je generována vestavěnou funkcí range. Je to posloupnost čísel 0, 1, 2 až 4 (tj.$n-1$ kde $n$ je jediným parametrem funkce). To že, posloupnost začíná nulou a končí hodnotou $n-1$ souvisí s indexací (předaná hodnota je horní mez a ta v Pythonu není nikdy zahrnuta do generovaného rozsahu).
Při prvním průchodu řídící proměnná i odkazuje první hodnotu posloupnosti tj. hodnotu nula. S touto hodnotou se poprvé provede tělo cyklu tj. do výstupu se vypíše hodnota 0 (a výstup se odřádkuje). Tím první průchod končí.
Při druhém průchodu řídící proměnná i odkazuje druhou hodnotu posloupnosti tj. hodnotu jedna. S touto hodnotou se podruhé provede tělo cyklu tj. do výstupu se vypíše hodnota 1 (a výstup se odřádkuje). Tím druhý průchod končí.
To se opakuje i pro třetí (i=2), čtvrtý (i=3) a pátý již poslední průchod (i=4). Výsledkem provedené je tak výpis čísel 0,1,2,3,4 do výstupu (přičemž každé číslo je na zvláštním řádku).
Poznámka: Řídící proměnná cyklu může mít zcela libovolné (přípustné) jméno. V případě cyklů přes celočíselné rozsahy se však téměř vždy používá identifikátor i (resp. pokud je již užíván pak j nebo k). Tento úzus vychází z podobného úzu v matematice.
Předchozí cyklus jednoduchý a názorný, ale v praxi nepříliš užitečný. Stejně tak neužitečný je příklad následující příklad, který ukazuje, že tělo cyklu nemusí řídící proměnou (zde pojmenovanou j):
for j in range(10):
print("Python je prostě boží")
Tento cyklus 10× vypíše řetězec "Python je prostě boží". Při větším počtu výstupních řádků Jupyter naštěstí pozná, že výstup bude extrémně dlouhý a vytvoří tak skrolovatelný výstup, který lze posuvat pomocí posuvníku vpravo (vyzkoušejte)
Nyní přejdeme ke (zdánlivě) užitečnějšímu příkladu.
n = int(input("Celé číslo:"))
suma = 0
for i in range(1, n+1):
suma += i
print(suma)
Tento program počítá součet všech celých čísel od 1 do $n$ (včetně), kde $n$ je čteno ze standardního vstupu. Implementace je přímočará. Nejdříve si připravíme proměnou do níž budeme ukládat jednotlivé dílčí součty. Tato proměnná má na začátku hodnotu 0 (ještě se nic nepřičetlo).
Metoda range se dvěma parametry vrací posloupnost celých čísel od minima (první parametru) včetně do maxima (druhý parametr) vyjma. V našem případě je minumum rovno 1 a maximum je rovno $n+1$ (poslední přičtené číslo je tudíž $n$).
Tato čísla se postupně prochází a každé z nich je v těle cyklu přičteno k proměnné suma (zápis suma += i je zkratka za suma = suma + i). Toto přičtení se provede ($max - min$)krát tj. v našem případě ($n+1 - 1 = n$)krát.
Tělo cyklu tvoří odsazený (vnořený) blok tvořený jediným příkazem. Příkaz pro výpis (s funkcí print) již odsazen není, nepatří tak již do cyklu. Provede s proto až po dokončení cyklu a to pouze jedenkrát.
Úkol: Použití cyklu je v tomto případě zdánlivě přirozené, avšak ve skutečnosti zcela zbytečné. Vytvořte program se stejnou funkčností ale bez použití cyklu (rada: aritmetická posloupnost).
Řešený příklad:
Implementace zjednodušeného modelu pro popis vývoje populací dravců a kořisti (např. lišek a zajíců). Tento model vychází ze zjednodušeného předpokladu, že kořist (zající) mají neomezený zdroj potravy a neumírají stářím (jejich počet by tak za nepřítomnosti dravců rostl exponencionálně). Naopak dravci se živí jen danou kořistí a umírají jen stářím. Tj. za nepřítomnosti kořisti jejich počet exponenciálně klesá. Změna počtu obou druhů popisuje soustava dvou diferenciálních rovnic (Lotka-Volterra):
$\frac{\mathrm {d} x}{\mathrm {d} t}=Ax-Bxy$,
$\frac {\mathrm {d} y}{\mathrm {d} t}=Dxy-Cy$.
kde $x$ je počet kořisti a $y$ je počet dravců. Konstanta $A$ odráží rychlost rozmnožování kořisti, $B$ její úbytek daný lovem (závisí na počtu obou druhů). $C$ určuje přírůstek dravců daný úspěšnosti lovu a $D$ přirozený úbytek dravců (stářím).
Při implementaci budeme opakovaně počítat průběžnou změnu počtu počtu kořisti ($\frac{\mathrm{d} x}{\mathrm{d} t}$) a dravců ($\frac{\mathrm{d} y}{\mathrm{d} t}$) za nějaký malý časový okamžik (kladný, blížící se nule). Tuto změnu vždy přičteme k příslušnému počtu.
x = float(input("Počáteční počet zajíců: ")) # zkuste jak se program chová pro různé počáateční počty
y = float(input("Počáteční počet lišek: "))
a = 2
b = 0.003
c = 3
d = 0.0001
dt = 1e-2
for i in range(500): # opakovaně (pětsetkrát) vypočítáme
dx_dt = a*x - b*x*y # průběžnou změnu (derivaci) počtu zajíců
dy_dt = d*x*y - c*y # průběžnou změnu (derivaci) počtu dravců
x += dx_dt * dt # změnu dx přičteme k počtu zajíců (je-li záporná tak odečteme)
y += dy_dt * dt # změnu dy přičteme k počtu lišek (opět může být zaporna)
if i%10 == 0: # pro stručnost vypíšeme jen každý páty výsledek
print(f"{i}.\t zajíci: {x:.0f}\t lišky {y:.0f}") # vypíšeme příslušné počty
Pro vhodně zvolené zvolené počty zajíců a lišek (doporučuji cca 1000 zajíců a 500 lišek) získáte typické řešení, v němž se občas výrazně zvýší počet zajíců, který je se zpožděním následován zvýšeným počtem lišek (což ovšem vede k snížení počtu zajíců a následně i lišek). Model pracuje s počty representovanými s neceločíselnými čísly (typu float, je to dáno malou hodnotou dt). Ty jsou ve výpise (pouze ve výpise) zaokrouhleny na celá čísla (pomocí formátování nikoliv pomocí funkce round).
Pomocí cyklu for lze procházet i řetězce.
for znak in "Geralt":
print(znak)
Řětězec je v tomto případě chápán jako posloupnost jednoznakových řetězců. V každém opakování (iteraci) vzniká nový jednoznakový řetězec dočasně označený řídící proměnou.
Poznámka: To, že v každé iteraci musí vzniknout nový objekt (který navíc po dokončení iterace zaniká) ukazuje, že iterace přes všechny znaky není příliš efektivní. Je tudíž lepší, pokud se této konstrukci můžete vyhnout (zcela eliminovat ji však nemůžete). Nejčastějším alternativním řešením je využití regulárních výrazů.
Řešený příklad:
Počet souhlásek v textu bez diakritiky (důvodem tohoto omezení je snížení počtu možných souhlásek) včetně výpisu procentuální podíl mezi všemi hláskami.
Návrh řešení: budeme procházet jednotlivé znaky seznamu a za každý výskyt samohlásky přičteme jedničku k proměnné (ta musí být na začátku nulová). Abychom nemuseli rozlišovat malá a velká písmena (to zvýší počet možných samohlásek dvakrát) převedeme řetezec na malá písmena (převedení na velká by bylo ekvivalentní, ale malá písmena jsou ta běžnější).
s = input("Řetězec: ")
pocet = 0
for znak in s.lower():
if znak in "aeiouy":
pocet += 1
print(f'Počet samohlásek v řetězci "{s}" je {pocet}, což je {100*pocet/len(s):.2f}%.')
Řešený příklad:
Výpis prvního nemezerového znaku v řetězci (načteném ze standardního vstupu). Pro testování, zda je znak nemezerový, se použije metoda isspace objektů třídy string (kromě mezery do této skupiny znaků patří tabulátor a odřádkování).
Pro nalezení prvního znaku splňující jistou podmínku stačí postupně procházet jednotlivé znaky řetězce (typicky pomocí cyklu for) a testovat tuto podmínku.
Co se však uděláme, pokud znak najdeme? Po výpisu prvního nalezeného znaku je již zbytečné procházet ostatní znaky, což je však v rozporu s chováním cyklu for (ten prochází všechny znaky). Řešením je předčasné ukončení cyklu příkazem break:
s = input()
for znak in s: # procházej všechny znaky (jednoprvkové podřetězce) řetězce
if not znak.isspace(): # a pokud to není! mezera
print(znak) # vypiš ho
break # a předčasně ukonči cyklus
Co se však stane, pokud řetězec žádný mezerový znak neobsahuje (tj. je buď prázdný nebo obsahuje jen mezerové znaky). Pokud nevíte, pak to vyzkoušejte (zadejte třeba tři mezery). Pokud to víte, pak to to vyzkoušejte pro jistotu také :)
Odpověď je samozřejmě jednoduchá, neboť podmínka není nikdy splněna a cyklus proběhne celý (speciálně u prázdného řetězce ani jednou). Po skončení cyklu už není uveden žádný příkaz a program tak skončí bez jakékoliv další interakce (program tedy zdánlivě nic nedělá).
Otázkou však zůstáva, jak by reagovat měl. Jsou zde tří základní možnosti:
První řešení je pravděpodobně tím nejhorším. Pokud program nic nedělá (tj. ani minimálně nekomunikuje s uživatelem), pak uživatel (v tomto případě je to přímo programátor) neví co se děje. Spustilo se to vůbec? Je chyba u mne nebo v programu?
Druhé řešení je sice dostatečně komunikativní, avšak v obecném případě nelze vždy rozlišit, co je skutečný výstup a co je upozornění o nestandardním výsledku. V našem případě to možné je, neboť standardní výstup je vždy jednoznakový, nám však jde o obecně použitelný přístup.
Jako optimální se tak jeví vyvolání výjimky, které signalizuje neočekávaný stav, a zároveň odkládá skutečné řešení (v rámci komplexnějšího programu, lze výjimku ignorovat resp. vypsat chybové hlášení).Zůstává tak otázka, kde výjimku vyvolat. Může to být až po provedení celého cyklu (tj. až po kontrole všech znaků). Bohužel po ukončení cyklu nelze jednoduše zjistit, zda cyklus proběhl celý či, zda byl předčasně ukončen (po nalezení nemezerového znaku). V Pythonu lze sice i po ukončení cyklu přistupovat k řídící proměnné, obecně je však hodnota této proměnné nedefinovaná (tj. může obsahovat poslední položku, ale nemusí). Navíc v mnoha dalších programovacích jazycích řídící proměnná není vně cyklu přístupná vůbec.
Jediným bezpečným řešením je použití nové logické proměnné, která reflektuje stav hledání. Na začátku (před vstupem do cyklu) je False, neboť prozatím nebylo nic nalezeno. Na hodnotu True je nastavena jen v případě, že byl příslušný znak nalezen. Po skončení cyklu tak snadno zjistíme, zda byl hledaný znak nalezen (proměnná byla změněna na True) či nikoliv (proměnná má původní hodnotu False).
s = input()
nalez = False # pesimisticky předpokládáme, že nic nenalezneme
for znak in s:
if not znak.isspace(): # a pokud přesto nalezneme
nalez = True # zmměníme proměnou na `True`
print(znak)
break # a nezapomeneme předčasně ukončit cyklus
if not nalez: # až po ukončené cyklu ošetříme případ, že jsme nic nenalezli
raise Exception("Nemezerový znak nenalezen")
Úkol: Ověřte, zda je zadaný řetězec monotónní tj. je tvořen opakováním jediného znaku. Jednoznakové řetězce jsou z definice monotónní. Výsledek vypiště do standardního výstupu (text "Řetězec je/není monotónní). Vyzkoušejte, jak se Váš program chová v případě prázdného řetězce (je toto chování rozumné). Rada: řešení se lépe chápe, pokud program chápeme jako hledání nemonotónnosti.
I když je procházení řetězců pomocí přímé aplikace cyklu for pohodlné, nelze jím bohužel řešit všechny problémy. Ukážeme si to na následujícím řešeném příkladě.
Řešený příklad:
Ověřte, zda je zadaný řetězec palindrom tj. obsahuje stejné znaky čtený zleva doprava i obráceně (zprava doleva).
Postup (algoritmus) je v tomto případě zřejmý, stačí si představit souběžně čtení řetězce zleva doprava a zprava doleva. Je pak zřejmé že první znak musí být shodný s posledním, následně druhý s předposledním, třetí s předpředposldním atd. Toto porovnání stačí ukončit v polovině řetězce (další porovnání je již zbytečné) viz obrázek.
Pokud bychom i v případě tohoto algoritmu uvažovali použití běžného cyklu for přes vstupní řetězec, narazíme již na začátku na závažný problém: v každé iteraci máme přímý přístup jen k jedinému znaku řetězce (je odkazován řídící proměnnou). I když si můžeme zapamatovat i některé ostatní znaky (jako tomu bylo v předchozím úkolu, v němž jsme si zapamatovali první znak), je počet těchto znaků omezen (vždy existuje jen konečný a v praxi velmi malý počet proměnných) a především nelze odkazovat znaky, které jsme ještě neprošli. To je však v případě palindromu nezbytné, neboť v první iteraci porovnáváme první znak s posledním (i když jsme jej ještě neprošli), ve druhé druhý znak s předposledním, atd.
Obecně lze říci, že pomocí cyklu for přes řetězec lze řešit pouze lokální vlastnosti jednotlivých znaků resp. vzory omezené délky.
V ostatních případech je nutné použít cykly přes rozsahy (range), které jsou následně využívány pro indexaci (tj. neiterujeme přes jednotlivé znaky, ale přes jejich indexy).
Nechť $n$ je délka řetězce (= počet znaků), pak nejdříve porovnáváme znak na indexu 0 (první) s prvkem na indexu $n-1$ (poslední). Pak se porovnává prvek s indexem 1 (druhý) s prvkem na indexu $n-2$. Obecně se porovnává prvek na indexu i s prvkem na indexu n-i-1. Ověřte na obrázku výše, kde $n=9$ (tyto obrázky jsou pro ověření správné indexace nezastupitelné). Poslední porovnání se děje mezi prvky s indexy n//2 - 1 a n - n//2 (opět ověřte na obrázku). Všimněte si, že u řetezců s lichou délkou prostřední prvek do porovnání nevstupuje. V případě řetězců sudých délek tento prostřední prvek neexistuje, indexy poslední dvojice prvků jsou nicméně stejné (opět ověřte, tentokrát si ilustrativní schéma vytvořte sami, požijte např. slovo 'anna').
Jádrem řešení je tedy cyklus přes rozsah 0 (včetně) do n//2 (vyjma) (dělení je celočíselné!), v jehož těle se porovnává i-tý znak (získaný pomocí indexace) se znakem na indexu n-i-1 (znaky čtené zprava).
s = input("Potenciální palindrom: ")
n = len(s)
palindrom = True # prázdný řetězec je vždy palindrom
for i in range(0, n//2): # horní mez je vyjma (poslední je tedy prvek s indexem n//2-1)
if s[i] != s[n-i-1]:
palindrom = False # řetězec již nemůže být palindromem
break # předčasně ukončujeme cyklus (výpis necháváme )
if palindrom:
print("Řetězec je palindrom")
else:
print("Řetězec není palindrom")
Úkol: Vytvořte program, který provede proložení (angl. interlacing) dvou řetězců). Výsledný řetězec začíná prvním znakem prvního řetězce, pokračuje prvním znakem druhého, následuje druhý znak prvního řetězce, druhý druhého, třetí prvního, třetí druhého atd. Pokud je jeden z řetězců delší, pak je jeho nadbytečná část připojena na konec výsledku bez proložení.
Příklad: první řetězec: "123", druhý řetězec: "Frodo". Výsledek by měl být "1F2r3odo"
Rady: Předpokládejte, že první řetězec není delší než druhý (není-li tomu tak, pak lze proměnné prohodit). Použijte cyklus přes indexy prvního řetězce (které lze využít i pro indexaci řetězce druhého). Výsledný řetězec lze konstruovat postupným připojováním jednotlivých jednoznakových řetězců.
while¶Cyklus for je základní konstrukcí zajišťující opakování kódu v Pythonu. Lze ji však použít jen v případě, kdy je počet opakování znám předem resp. pokud se prochází konečná posloupnost objektů.
Pokud tyto podmínky nejsou splněny, pak je nutno využít obecnější cyklus while. Cyklus while (opakovaně) vykonává své tělo, dokud je splněna určitá podmínka.
Typickým případem užití je opakovaný vstup, jehož ukončení je signalizováno zadáním nějakého specialního textu (zarážky). Typickou zarážkou je prázdný text.
vstup = input()
suma = 0
while vstup: # dokud je vstupní řetězec neprázdný
suma += float(vstup)
vstup = input()
print(f"Součet je {suma}")
V této ukázce jsou ze standardního vstupu opakovaně čtena čísla a je počítán jejich celkový součet (suma). Počet čísel není omezen, na konci zadání je však nutno zadat prázdný řádek (vstup se bez zadání jakéhokoliv znaku ihned ukončí stiskem klávesy Enter).
Popišme si krok za krokem průběh programu:
input). Vstupní text je jako řetězec označen proměnnou novou vstup.suma, která označuje číslo 0while. Podmínkou je přímo objekt řetězce, který tak musí být převeden na hodnotu třídy bool. Řetězec je v Pythonu interpretován jako pravdivý, pokud je neprázdný (tj. obsahuje alespoň jeden znak). Je-li podmínka pravdivá (neprázdný tj. neukončovací řetězec) je vykonáno tělo cyklu (dále krok 4), jinak je cyklus ukončen (řízení přechází na první příkaz za tělem cyklu), dále krok 6float (explicitní přetypování na float)Všimněte si několika charakteristických rysů:
vstup, která je v každé iteraci přesměrována na jiný řetězec). Pokud by k žádné změně nedošlo, pak by byla podmínka stále pravdivá a cyklus by byl nekonečný.Úkol: Vypište nejvyšší číslo (maximum) ze zadané posloupnosti čísel (čtených ze standardního vstupu, vždy jedno na řádce). Koncovým vstupem je opět prázdný řádek. Pro jedboduchost předpokládejme, že zadáno bude alespoň jedno číslo (maximum z nula čísel není definováno).
Řešený příklad
Collatzova (nedokázaná) domněnka tvrdí, že posloupnost čísel určená počáteční hodnotou $a_0$, v níž je každá následující hodnota vypočtena podle vztahu: $a_{i+1}={\begin{cases}3a_i+1{\text{,}}&{\text{ je-li }}n{\text{ liché,}}\\\frac{a_i}{2}{\text{,}}&{\text{ je-li }}n{\text{ sudé.}}\end{cases}}$
dosáhne po konečném počtu kroků hodnoty 1, pro libovolnou počáteční hodnotu $a_0$ z $\mathbb{N}$.
I když je tato hypotéza navzdory velmi triviálnímu zadání prozatím nedokazatelná, lze za pomoci počítačů relativně snadno prokázat, že platí pro všechna relativně malá čísla ($<2^{20}$, https://en.wikipedia.org/wiki/Collatz_conjecture).
Program, který počítá pro dané $a_0$ příslušnou Collatzovu posloupnost (zakončenou 1) je velmi snadné vytvořit. Jádrem je samozřejmě while cyklus, neboť výpočet následující hodnoty se stále opakuje, přičemž není známo kolik těchto opakování bude.
vstup = int(input("Celé číslo: "))
delka = 0 # tato proměnná bude počítat délku posloupnosti
a = vstup # počátkem posloupnosti je vložené číslo
while a != 1: # dokud nejsme na konci posloupnosti
delka += 1 # zvýšíme délku posloupnosti o jedna
print(a, end=", ") # výpis s explicitně předaným zakončením
# a vypočteme nové a
if a % 2 == 1: # liché
a = 3 * a + 1
else: # sudé
a //= 2 # celočíselné dělení dvěma
print(1) # do výstupu doplníme poslední člen poslopupnosti
print(f"Délka Collatzovy posloupnosti čísla {vstup} je {delka}")
Implementace je zřejmá, po načtení čísla do uložíme do proměnné a aktuální (zde tedy počáteční) člen posloupnosti a připravíme proměnnou pro postupné zvyšování počtu členů. V těle cyklu pak počítáme další člen posloupnosti (sudost a lichost je určena zbytkem po dělení dvěma), který nahradí předchozí člen v proměnné a (stane se novým aktuálním). Navíc se zvýší délka zatím prozkoumané části posloupnosti o jedničku a vypíše se (pro kontrolu) aktuální člen. Tělo cyklu se opakuje dokud je aktuální člen různý od jedné (tj. nebylo dosaženo konce posloupnosti).
Všimněte si volání funkce print, které kromě svého základního parametru (co se má vypsat) obsahuje i parametr ve tvaru end=", ". Pokud parametry ve volání začínají identifikátorem následovaným rovnítkem, jsou to tzv. pojmenované parametry. Běžné parametry jsou tzv. poziční, tj, jejich funkce závisí na pořadí při volání (první, druhý, … parametr), Naopak pojmenované parametry mohou být uváděny v libovolném pořadí (jejich funkce se pozná podle identifikátoru). U pojmenovaných parametrů si tak nemusíte pamatovat pořadí (na druhou stranu si musíte pamatovat jejich jméno).
Podívejme se na například na volání funkce re.sub. Ta má tři parametry, které musíte uvádět v pořadí hledaný vzor (regulární výraz), náhrada (řetězec nahrazující všechny výskyty) a nakonec řetězec, ve kterém se provádí substituce.
import re
re.sub("n+", "?", "anna") # nahradí libovolnou posloupnost znaků `n` za otazník
Pokud máte problém za zapamatováním pořadí, lze parametry volat jako pojmenované (jména najdete v dokumentaci)
re.sub(string="anna", pattern="n+", repl="?")
Abě možnosti lze i kombinovat, musí však být splněny nasledující podmínky:
Úkol: V případě volání funkce
re.subexistuje jen tři smíšené možnosti volání, zkuste je najít a vyzkoušet (žádný z parametrů nelze z volání zcela vyjmout)
I když Python nabízí relativně velké množství různých kombinací pozičních a pojmenovaných parametrů při volání, v praxi se běžně používají jen dva základní:
print, kde end je povinně pojmenovaný parametr). Poziční parametry jsou typicky klíčovými parametry, které musí být vždy uvedeny, pojmenované jsou doplňkové a tudíž nepovinné.A abychom nezapomněli, parametr end u vestavěné funkce print umožňuje změnit znaky, který se automaticky vypisují na konci výpisu. Standardně je to znak odřádkování (proto se každý výpis vypisuje na nový řádek). Změnou na jiný znak lze zajistit postupný výpis do jediného řádku s příslušným oddělovačem.
Úkol: (složitější) Pomocí rozšířeného programu, jaké číslo v rozsahu 2-99 má nejdelší Collatzovu posloupnost.
Rada: vnější
forcyklus přes kód uvedený v předchozí ukázce (s malými změnami v oblasti vstupu a výstupu) a nalezení maxima podle vzoru předchozího úkolu.
Úkol: Vypište prvních
nčlenů Fibonnaciovy posloupnosti (pokud ji neznáte, najdete ji určitě na tetě Wikipedii). Hodnotanby měla být přečtena ze standardního vstupu:
I když jsou elementární numerické výpočty typu Collatzovy či Fibonnaciovy posloupnosti zajímavé, praktické použití příliš nemají. I když tvoří čísla a řetězce základní objekty Pythonu, v praxi většina uživatelů používá komplexnější objekty nabízené externími moduly a balíčky třetích stran (tj. nikoliv od tvůrců Pythonu nebo od Vás resp. Vašeho týmu). Pythonské balíčky mohou kromě modulů (což je pythonský program primárně určený pro volání z jiných programů a modulů) obahovat i další soubory (konfigurační soubory, data, spustitelné soubory, dokumentaci, apod.).
Python nabízí desetitisíce externích prostřednictvím portálu Pypi (Python Package Index), z nichž několik využijeme v rámci této opory. Hlavním cílem je ilustrace obecných rysů jazyka nikoliv zevrubný popis těchto balíčků. Pokud Vás zaujmou, pak další informace naleznete v jejich dokumentaci.
Prvním externím balíčkem, s nímž se seznámíme je PyEphem, který obsahuje modul ephem nabízející výpočty poloh hlavních objektů naší sluneční soustavy spolu s dalšími astronomickými charakteristikami (fáze, jasnosti, apod.).
Modul ephem je vysoce specializovaný a tak nebývá předinstalován ani ve vědeckotechnických distribucích Pythonu (Anaconda, Intel Python). Naštěstí instalace většiny balíčků z Pypi je velmi snadná. Stačí pokud v příkazovém řádku (shellu) zavoláte příkaz pip s následujícími parametry.
pip install pyephem
V případě, že máte více instalací Pythonu (což je pravděpodobné, pokud používáte Linux) resp. využíváte-li virtualizované překladače (virtual environment), je nutné zajistit, že se volá příkaz pip odpovídající danému interpretru. V tomto případě je vhodnější použít celou cestu k příkazu pip a/nebo spustitelný soubor obsahující číslo verze Pythonu (příklad je z mé instalace Linuxu a Intel Pythonu, cesta ve Vašem systému bude zcela jistě jiná).
/home/fiser/apps/intelpython3/bin/pip3.6 install pyephem
Výrazně jednodušší je situace pokud používáte Jupyter notebook. Zde stačí zadat v kódové buňce tzv. externí příkaz (začíná znakem vykřičník), jenž se vykoná automaticky vykoná v shellu a výsledek se vloží do výstupní buňky (navíc příkaz pip by měl být ten správný pip)
!pip install pyephem
Pokud je vše v pořádku, pak výpis obsahuje text installation succeed. Je-li balíček již nainstalován, pak se provede aktualizace resp. se vypíše zpráva, že požadavek je již zplněn. Pokud je vše v pořádku můžeme přejít k využití balíčku.
Řešený příklad
Jako superměsíc se označuje měsíční úplněk, který nastává blízko maximálního přiblížení Měsíce k Zemi (tzv. přízemí), viz https://en.wikipedia.org/wiki/Supermoon. Měsíc má v tomto případě cca o 20% větší jas a o trochu větší velikost než když je v odzemí. Rozdíl je ve skutečnosti velmi malý a většina pozorovatelů ho ani nezaznamená (tím spíše nedívá-li se na Měsíc pravidelně). Navzdory tomu je to velmi populární úkaz, který se objevuje i mainstraimových médiích (http://tn.nova.cz/clanek/nebe-ozari-novorocni-supermesic-pak-nas-ceka-modry-uplnek.html)
Pomocí modulu ephem lze snadno vypsat několik nejbližších superměsíců tj. úplňků, v nichž je (geocentrická) vzdálenost středu Měsíce od středu Země menší než 36000 km (viz článek na anglické Wikipedii).
V programu budou využity následující funkce a metody:
Funkce ephem.new() vrací aktuální čas v representaci používané v modulu ephem, což je objekt třídy ephem.Date.
Tato representace se liší od běžné representace kalendářních dat v Pythonu (je optimalizována pro astronomické výpočty). Lze ji nicméně převádět na běžnou textovou representaci pomocí přetypování na string (vestavěná funkce str). Funnguje i opačné přetypování.
str(ephem.now())
Všimněte si, že kalendářní údaj se uvádí v pořadí rok/měsíc/den (tento formát by měl být srozumitelný většině astronomů). Čas není v lokálním časovém pásmu, ale v univerzálním (dříve greenwichském) čase. Ten je o hodinu (v době platnosti středoevropského času) resp. dvě hodiny (při platnosti středoevropského letního času) posunut oproti času v ČR. I tento čas je pro astronomy přirozenější, než čas diktovaný (a často i posouvaný) lokálními politiky.
Přetypování na string se využívá i při interpolaci řetězců.
f"Aktuální datum a čas je {ephem.now()}"
Druhou (a pro nás klíčovou) funkcí je funkce ephem.next_full_moon(). Tato funkce najde čas nejbližšího úplňku počínaje časem, který je uveden jako jediný parametr funkce.
str(ephem.next_full_moon(ephem.now()))
Počáteční čas lze zadat i jako řetězec (funkce jej přetypuje na objekt třídy ephem.Date)
str(ephem.next_full_moon("2001/1/1")) # první úplněk nového tisíciletí
Pro zjištění aktuální polohy Měsíce, slouží objekt třídy ephem.Moon (existují i třídy ephem.Mercury, ephem.Mars, apod.). Po vytvoření pomocí tzv. konstruktoru (je to funkce, která má stejné jménom jako třída) lze využívat pouze atributy, které nejsou závislé na čase.
m = ephem.Moon() # konstruktor objektu
print(m.name) # jméno objektu se nemění časem
m.earth_distance # tohle už nejde (vzdálenost Země – Měsíc na čase závisí )
Řešení je jednoduché (a doporučuje ho i zpráva ve výjimce), nejdříve je nutné zavolat metodu compute (volá se nad objektem měsíce)
m.compute() # počítá polohy pro aktuální čas
m.earth_distance
Vrácena je vzdálenost v astronomických jednotkách (= průměrná vzdálenost Slunce — Země). Pro převod na metry lze využít konstantu ephem.meters_per_au.
m.compute("935/9/28 6:00") # změníme čas, pro nějž je počítána poloha na šestou hodinu 28.9 935 AD
print(m.earth_distance) # vzdálenost v astronomických jednotkách
print(m.earth_distance * ephem.meters_per_au) # a v metrech
Nyní máme vše připraveno a můžeme se tak podívat na program pro nalezení nejbližsích super úplňků.
import ephem # importuje nainstalovaná modul
uplnek = ephem.next_full_moon(ephem.now()) # nalezení nejbližšího úplňku (od aktuálního okamžiku)
mesic = ephem.Moon() # vytvoření objektu representujícího Měsíc (jako nebeské těleso)
for i in range(12): # otestujeme dvanáct nejbližších úplňků
mesic.compute(uplnek) # spočítáme polohu Měsíce v okamžiku úplňku
vzdalenost = mesic.earth_distance * ephem.meters_per_au / 1000.0 # zjistíme vzdálenost v kilometrech
if vzdalenost < 360_000: # je-li menší než 360 000
print(f"{uplnek}\t{vzdalenost:.0f} km") # vypíšeme čas a vzdálenost (zaokrouhlenou na kilometry)
uplnek=ephem.next_full_moon(uplnek) # a nalezneme čas dalšího úplňku
Vzdálenosti Měsíce jsme zaokrouhlili na kilometry i když výpočet vrací mnohem přesnější údaje. Důvod je skutečnost, že přesnost výsledků počítačových výpočtů závisí na přesnosti vstupu a použitém matematickém modelu (a jak víme v určité míře i na representaci čísel v počítači). Nemá smysl udávat zdánlivě super-přesné údaje, u nichž je většina číslic jen šum. Do publikačních výstupů uvádějte příslušně zaokrouhlené údaje.
Jak však můžeme příslušnou chybu a tím i řád zaokrouhlení odhadnout? Odpověď není jednoduchá, neboť problematika šíření chyb je opravdu věda. Pro základní orientaci stačí několik úvah:
zdroje chyb ve výpočtu vzdálenosti:
chyba modelu = chyba výpočtů poloh v ephem. Tu je nutné najít v dokumentaci programu XEphem (http://www.clearskyinstitute.com/xephem, sekce Accuracy). Z ní lze zjistit, že chyba v poloze by měla být menší než 0.5 úhlové minuty, kterou Měsíc překoná za cca 2 vteřiny (pohybuje se průměrně 33 úhlových minut za hodinu).
chyba daná representací čísel je v řádu $10^{-15}$, což je na úrovní mikrometru (řád výsledku je $10^{8}$ * $10^{-15}$ = $10^{-7}$). To je přirozeně zanedbatelné (za 2 vteřiny Měsíc urazí výrazně větší vzdálenost)
chyba daná přesností vstupu. Vstupem do výpočtu vzdáleností je čas, který je vypočítán s přesností minimálně jednotek vteřin (stropem je zde opět chyba výpočtů)
Jak lze vidět největší chybu vnáší měření času a to v řádu větších desítek minut. Spočítáme tedy polohu měsíce v časech $\pm 5s$ (chyby se mohou sčítat).
m.compute("2019/1/21 05:16:00") # o půl hodiny dříve
print(m.earth_distance * ephem.meters_per_au)
m.compute("2019/1/21 05:16:10") # o půl hodiny dříve
print(m.earth_distance * ephem.meters_per_au)
Z výsledků je zřejmé, že chyba je v řádu stovek metrů, tj. zaokrouhlení na kilometry je rozumné. Je to navíc jen orientační údaj, neboť skutečná jasnost je ovlivněna i dalšími skutečnostmi, které jsme zanedbali (úhlová odrazivost Měsíčního povrchu, poloměr Země, neboť Měsíc nepozorujeme z jeho středu)
Úkol: Upravte program tak, aby našel superúplněk s minimální vzdáleností Měsíce v tomto tisíciletí.
Seznam (angl. list) je základní druh tzv. kolekce v Pythonu. Kolekce jsou objekty, které primárně slouží k uchovávání jiných objektů (jinak řečeno jsou to schránky na objekty).
V Pythonu existuje větší počet různých typů kolekcí (a mnoho dalších dodávají externí balíčky). Jednotlivé typy kolekcí se liší uspořádáním prvků, optimalizací základních operací (vkládání, vyhledávání, vyjímání apod.) a omezeními kladenými na jejich položky.
Seznamy mají tyto základní charakteristiky:
položky jsou v seznamu uspořádány sekvenčně (jedna za druhou) a jsou dostupné pomocí pozičních indexů
počet položek je neomezený (resp. omezený pouze dostupnou operační pamětí) a může se měnit (tj. do seznamu lze přidávat i odebírat položky)
položky v seznamu mohou být různých typů a to i v rámci jediného seznamu
Nejjednodušším způsobem vytvoření seznamu je specifikace všech jeho položek výčtem uzavřeným v hranatých závorkách:
seznam = [1, 2, 3, 4 ,5] # seznam šesti čísel (`int`)
Počet prvků seznamu lze získat nám již známou vestavěnou funkcí len (stejně jako délku řetězce)
len(seznam)
Pro přístup k položkám lze (opět podobně jako u řetězců) využít indexaci (indexuje se opět od nuly, fungují i záporné indexy od konce). Výsledkem indexace však není jednoprvkový seznam, ale přímo daná položka (tj. v našem případě číslo).
seznam[0] + seznam[-1] # součet první a poslední položky
Fungují i některé další operace se kterými jsme se seznámily u řetězců (není to překvapivé neboť i řetězec je sekvenční kolkce, která je však specializována na ukládání znaků)
seznam[1:-1] # výřez (od druhé do poslední vyjma)
6 in seznam # zjištuje zda se hodnota vyskytuje v seznamu
Operátor in lze v případě seznamů použít jen pro hledání jednotlivého prvku (nikoliv např. podseznamu)
seznam + [10, 11, 12] # spojení dvou seznamů (výsledkem je nový seznam)
Seznamy lze vytvářet i z dalších kolekcí resp. objektů vracejících posloupnost prvků (například rozsahů). V tomto případě se použije funkce (konstruktor), který má stejné jméno jako třída. Konstruktor projde originální posloupnost (stejně jako by se využil cyklus for a získané prvky uloží do nově vytvářeného seznamu).
list("ahoj") # získáme seznam jednoprvkových řetězců
list(range(20)) # získáme seznam od nuly včetně do dvacet vyjma
Seznamy nejsou omezeny na číselné položky. Položkami mohou být objekty libovolných typů např. řetězce či jiné objekty.
Python se kromě jiných oblastí používá i pro skriptování na úrovni souborového systému. Pomocí Pythonu lze například snadno získat seznam souborů v domovské adresáři:
from pathlib import Path
soubory = list(Path.home().glob("*")) # vrací seznam všech souborů v domovském adresáři
# soubor je representován jako objekt třídy `pathlib.PosixPath` bebo `pathlib.WindowsPath`
print(len(soubory)) # počet nalezených souborů
print(soubory[:3]) # pro ukázku vypíšeme první tři položky seznamub
Výraz pro získání absolutních cest k souborům v domovském adresáři je dosti složitý a proto si ukážemě jeho vyhodnocení krok za krokem:
1) volání metody home nad třídou pathlib.Path (zde se metoda nevolá nad objektem, ale nad třídou, což je v Pythonu možné). Metoda vrací cestu k domovskému adresáři aktuálního užiavatele (jako objekt třídy PosixPath nebo WindowsPath podle hostitelského operačního systému, označení POSIX zahrnuje všechny Unixy, Linux, Mas OS X a další).
Path.home() # vrací cestu k domovskému adresáři
2) nad objektem representujícím cestu k adresáři je volána metoda glob, jejímž parametrem je vzor názvu souboru (vzor typicky obsahuje tzv. žolíky). Metoda nalezně všechny soubory v daném adresáři odpovídající vzoru.
Vzoru * odpovídají všechny soubory. Pokud bychom například chtěli hledat jen soubory s příponou png použili bychom vzor *.png.
Výsledkem volání není seznam nalezených souborů, ale objekt, který je vrací po jednom na požádání (stejně jako range vrací na požádání čísla). Proto je nutné provést ještě jeden krok.
3) voláme vestavenou funkci pro konstrukci seznamu list nad objektem vráceným metodou glob. Ta si postupně vyžádá všechny nalezené cesty a vytvoří z nich seznam (tj. od této chvíle existují objekty cest pro všechny nalezené soubory).
Všimněte se také výpisu (fragmentu) seznamu. Seznam je vypisován jako výčet řetězcových representací třídy PosixPath (pracuji v Linuxu).
Seznam jednotlivých cest k souborům lze procházet pomocí cyklu for. Lze tak například vypsat všechny běžné soubory s velikostí větší než 5MiB (5 binarních megabitů tj. $5\times 2^{20}$).
for soubor in soubory:
if soubor.is_file() and soubor.stat().st_size > 5*2**20:
print(soubor.relative_to(path.home()))
Uvnitř cyklu proměnná soubor postupně odkazuje jednotlivé objekty PosixPath, na něž je možno volat různé metody. V ukázce je použita metoda is_file, která vrací hodnotu True, pokud daná cesta přísluší běžnému souboru (tj. nikoliv adresáři, v případě Unixu to nesmí ani symbolický odkaz či speciální soubor). Metoda stat vrací metainformace souboru (čas vytvoření, vlastník, apod.) Zde je použit atribut st_size, který vrací velikost souboru v bytech (předpona st je ve jméně atributu z historických důvodů).
Poslední použitou metodou je relative_to, která vrací relativní cestu k souboru počínaje adresářem, jehož cesta je předána jako parametr. Pokud předáme adresář, který jsme pomocí globu procházeli získáme vlastní jméno souboru (relativní cesta vztažená k adresáři v němž se soubor nachází obsahuje pouze vlastní jméno souboru).
Řešený příklad:
Spočítejte počet souborů s příponou *.png ve Vašem adresáři/složce pro úschovu obrázků včetně jeho podadresářů.
Python bohužel nenabízí metodu pro získání cesty ke standardnímu uživatelskému adresáři/složce pro úschovu obrázků. V Ubuntu, který používám (české jazykové nastavení) je to podadresář Obrázky domovského adresáře. Objekt cesty k tomuto adresáří lze získat zápisem Path.home() / "Obrázky", kde operátor / slouží v případě objektů cest k řetězení (skládání) částí cesty.
Ukázka mechanismu řetězení cest:
from pathlib import Path
print( Path("/") / "home" / "fiser" / "bin" )
Pro získání globu (objektu pro procházení souborů), který prochází i soubory v podadresářích daného adresáře stačí využít rozšířený vzor začínající znaky **/, který se shoduje s libovolnou posloupností vnořených adresářů (včetně nulové posloupnosti, tj. jsou procházeny i soubory umístěné přímo v daném adresáři).
Zde konkrétně použijeme vzor **/*.jpg, kterému odpovídají všechny cesty začínající v daném adresáři a končící příponou jpg (tj. i soubory v podadresářích, podadresářích podadresářů, atd.)
from pathlib import Path
adresar = Path.home() / "Obrázky"
cesty = list(adresar.glob("**/*.jpg")) # vytvoříme seznam všech souborů s příponou png
print(f"Počet PNG obrázku v adresáři {adresar} je {len(cesty)}")
Úkol: Vytvořte program, který nalezne a vypíše největší soubor ve Vašem domovském adresáři a jeho poadresářích libovolné úrovně.
Seznamy jsou na rozdíl od řetězů (a mnoha dalších objektů) měnitelnými objekty, tj. lze je modifikovat i po vytvoření, Důvodem je efektivita, neboť seznamy jsou optimalizovány pro operace typu změna položky, resp. její přidání či odebírání. Pokud by byl seznam neměnný pak by například přidání jedné jediné položky do seznamu s milionem položek znamenal vytvoření nového seznamu a zkopírovaní všech milion položek.
Podívejme se na základní modifiční operace:
seznam = list(range(1,10)) # ukázkový seznam
print(seznam)
Jednotlivé položky lze změnit pomocí přiřazení na jehož levé straně je indexovaný výraz:
seznam[0] = -1 # mění se přímo seznam nikoliv jeho kopie!
print(seznam)
for i in range(1, len(seznam)): # nyní obrátíme znaménka u všech dalších položek
seznam[i] = -seznam[i]
print(seznam)
Pozor!: předchozí buňku s kódem vyhodnoťte jen jednou. Pokud jej vyhodnotíte dvakrát dojde k dvojité změně znaménka (a nic se tak nezmění). Toto upozornění se samozřejmě týká i téměř všech následujicích příkladů.
Pro přidání položky na konec lze použít metodu append:
seznam.append(-1)
print(seznam)
Všimněte si, že nelze psát seznam = seznam.append(10), neboť se mění přímo objekt tj. proměnná nemusí být přesměrována (ukazuje stále na stejný objekt). Zápis je dokonce nepřípustný, neboť metoda vrací objekt None (zjednodušeně nic nevrací). Přiřazením byste naopak přišli o odkaz na seznam (= odstranili bychom štítek) a seznam by tak de iure přestal existovat.
obet = [0, 0, 0, 0] # tento seznam bude obětován, abychom ukázali jak to nedělat
obet = obet.append(10) # chyba!!!! Tak to nikdy nedělejte
print(obet) # nic nevypise a seznam `obet` je navždy ztracen
seznam.remove(-1) # smaže první výskyt shodného objektu v seznamu
print(seznam)
Výmaz položky na zadané pozici se provádí příkazem del (zkratka za delete), jehož argumentem je indexovaný výraz (index určuje pozici odstraňované položky).
del seznam[-1] # výmaz poslední položky
print(seznam)
del seznam[:3] # mazat lze i několik položek najednou (použije se výřez)
print(seznam)
Pro odebrání posledního prvku slouží metoda pop. Ta navíc odebraný prvek vrátí (a může tak být uložen jinam).
posledni = seznam.pop()
print(posledni)
print(seznam)
Někdy se hodí seznam obrátit. K tomu slouží metoda reverse.
seznam.reverse()
print(seznam)
Prvky lze vkládat i na jiné pozice než na poslední. Po vložení se všechny původní prvky posunou (tj. nic se nepřepisuje)
seznam.insert(0, 10) # vložení prvku na pozici (prvním parametrem je pozice, druhým přidávaný prvek)
print(seznam)
Prvky v poli je možno i setřídit. K tomu slouží metoda sort. Ta prohází prvky seznamu tak, že jsou uspořádány od nejmenšího k největšímu.
seznam.sort()
print(seznam)
A nakonec je možno smazat celý obsah seznamu. Vymaže se pouze obsah, seznam samotný bude stále existovat, jen bude prázdný.
seznam.clear() # a vymažeme dočista do čista
print(seznam)
Řešený příklad
Naším úkolem bude vytvoření seznamu 10 čísel typu float v rozmezí 0 až 100. Poté tento seznam rozdělíme na dva podseznamy. V prvním budou hodnoty menší než průměr a v druhém ty vyšší (včetně těch, které jsou rovné průměru, ale to je spíše teoretické, neboť pravděpodobnost dvou stejných náhodných čísel typu float je téměř nulová)
Vytváření náhodných čísel patří mezi základní mechanismy nabízené počítačem. Pomocí náhodných čísel mohou počítačové programy modelovat náhodu, (informační) šum nebo nejistotu (Přijede ten vlak? Nebo si mám už raději zajistit náhradní odvoz).
Generátor náhodných čísel je harwarové nebo softwarové zařízení, které produkuje posloupnost čísel na které je kladeno hned několik požadavků:
Klasickým příkladem mechanického generátoru náhoedného čísla je hrací kostka, která:
Jediným skutečně dokonalým generátorem náhodných čísel jsou kvantové jevy, o něco méně dokonalé jsou různé šumy vzniklé jako důsledek některých komplexních procesů, příkladem je tepelný šum (detaili viz https://en.wikipedia.org/wiki/Hardware_random_number_generator).
V praxi však převažují softwarové generátory využívající aplikaci elementárních aritmetických posloupností na pomocná data uložená v paměti. Ty sice mají mnohem horší kvalitu (např. jen částečně splňují druhou podmínku), ale dokáží ryhle produkovat velká množství náhodných dat bez investic do drahých hardwarových generátorů. Navíc pro mnoho účelů stačí (fyzikální modelování, počítačové hry). Nejmodernější trendem jsou smíšené generátory integrované do CPU (viz např.instrukce RDRAND https://en.wikipedia.org/wiki/RdRand).
Standardní knihovny Pythonu podporuje kvalitní softwarový generátor prostřednictvím modulu random (existují však i lepší alternativy se kterými se ještě seznámíme). Modul nabízí několik funkcí, z nichž ta pro nás nejdůležitější je random.uniform:
from random import uniform
print( uniform(0,100)) # vrací náhodné číslo s rovnoměrným rozdělením v intervalu 0 a 100 (včetně)
Při rovnoměrném rozdělení je zajištěno, že limita pravděpodobnosti (pro počet pokusů blížící se nekonečnu) vygenerování čísla z podintervalu $[\alpha,\beta] \in [a,b]$ (kde $[a,b]$ je interval z něhož se generují čísla) je dána pouze jeho velikostí (tj. rozdílu $\beta-\alpha$). Tato limitní pravděpodobnost je rovna $\frac{\beta-\alpha}{b-a}$
Dílčí úkol:
Ověřte, že rozdělení generátoru se blíží rovnoměrnému rozdělení. V programu generujte čísla v rozsahu $[0,1]$ a zjistěte s jakou pravděpodobností leží v (náhodně zvoleném) intervalu $[0.42, 0.52]$. Zjištěná pravděpodobnost pro dostatečně velký počet generovaných čísel by se měla blížit $\frac{0.52-0.42}{1-0} = 0.1$
Rada počet pokusů (= počet iterací cyklu) volte v řádu stovek tisíc.
Nyní se vraťme k anšemu hlavnímu úkolu. Nejdříve musíme vygenerovat seznam 100 (různých) náhodných čísel. Prozatím však umíme generovat jen jednotlivá náhodná čísla. Jak z nich vytvoříme seznam?
Prozatím známe čtyři způsoby jak vkládat položky do seznamů:
1) uvedení všech položek v explicitním zápise seznamu
2) přidávaní položek metodou append
3) vkládání položek metodou insert
4) přiřazením hodnot do existujicích položek (seznam[i] = ....)
I když je možnost (1) nejjednodušší má zásadní nedostatek. Vytvoření seznamu dvou náhodných čísel je snadné:
cisla = [uniform(0,100), uniform(0,100)]
print(cisla)
Zápis 100-prvkového seznamu pomocí tohoto zápisu je však dosti nepohodlný (a co když si vymyslím seznam 1000 čísel).
Možnost (4) je již rozumná, neboť ji lze provést v cyklu (v každé iteraci nastavíme i-tý prvek). Má to však malý háček, nastavit (změnit) lze jen existující položky (přístup k neexistující položce vyvolá výjimky). Proto nejdříve musíme vytvořit libovolné stoprvkovy seznam.
Nejjednodušší možností je operátor *, který je-li použit na seznam vytváří nový seznam tvořené n-násobným opakováním původního seznamu.
[0, 1, 2] * 3 # třikrát se opakuje
Nyní již víme, jak tímto způsobem vytvořit seznam 100 náhodných čísel v daném rozdělení (pro porovnání časové efektivity necháme provést benchmarking pomocí makockého příkazu %%timeit)
%%timeit
p = [0.0] * 10 # vytvoříme seznam 10 hodnot 0.0
for i in range(10): # a stokrát (řídící proměnná není dále využita)
p[i] = uniform(0,100) # a přepíšeme je náhodným obsahem
Toto řešení není špatné, ale může se jevit neefektivní. Seznam je totiž plněn dvakrát (přičemž první nastavené hodnoty tj. nuly nejsou nikde použity).
Zkusíme tedy i druhý základní. Na začátku vytvoříme prázdný seznam, do něhož budeme postupně přidávat náhodná čísla. Nejjefektivnějším způsobem přidávání je přidávání na konec (metoda append), neboť vkládáná na jakoukoliv jinou pozici vyžaduje posun položky před níž provádíme vkládání a všech následujících. Vyzkoušíme tedy nejdříve řešení s přidáváním na konci.
%%timeit
p = [] # seznam je na začátku prázdný
for i in range(10): # a stokrát
p.append(uniform(0,100)) # přidám na nové náhodné číslo na konec
Kupodivu je to ještě o pár mikrosekund horší. Důvodem je skutečnost, že při zvětšování seznam musí systém alokovat další paměť pro položku a to přináší dodatečnou režii (Python není tak hloupý, aby to dělal pro každou přidanou položku, paměť alokuje po větších úsecích).
A pouze pro kontrolu řešení s přidáváním na začátku (což je nejhorší možnost, neboť při každém vkládání je potřeba o jednu pozici posunout všechny dříve vložené položky).
%%timeit
p = [] # seznam je na začátku prázdný
for i in range(10): # a stokrát
p.insert(0, uniform(0,100)) # přidám náhodné číslo na začátek
Zde už je rozdíl zřejmější (je to o cca 50% pomalejší). U delších seznamů bude rozdíl ještě propastnější.
Nyní je potřeba spočítat aritmetický průměr ze všech hodnot seznamu. Něco podobného jsme už programovali, stačí v cyklu přičítat položky do sumační proměnné a následně provést podíl součtu a počtu položek. V Pythonu však lze běžné operace provádět i elegantněji. Pro sumaci seznamů (a dalších podobných kolekcí) slouží vestavěná funkce sum, průměr lze tedy vypočíst na jediném řádku. Nejdříve však musíme vytvořit znovu pole náhodných čísel, neboť proměnné vytvořené v sekci za %%timeit nejsou globální tj, viditelné v celém notebooku.
n = 10 # požadovaný počet položek
p = [0.0] * n # volíme nejrychlejší přístup
for i in range(n):
p[i] = uniform(0,100)
# a nyní spočítáme průměr
prumer = sum(p)/n
print(prumer) # měl by být blízký 50
Rozdělení seznamu na dva podseznamy se podobá známému úkolu pro Popelku, neboť čísla nejsou v seznamu nijak uspořádána a tak se malá čísla zcela náhodně prolínají s velkými. Podívejme se na seznam.
p
I v tomto malém vzorku naáhodně prolínají jak čísla menší než průměr tak čísla větší. Podobají se tak misce čočky a hrachu, s nímž Popelce museli pomoci holoubci. Naše situace je jednodušší, neboť máme Python.
Doufám, že Vás napadlo alespoň jedno řešení. Mně napadla nejdříve tato dvě:
for. Každé číslo porovnat s průměrem a podle výsledku porovnání číslo přidat do seznamu menších nebo větších čísel (tyto seznamy jsou vytvořeny ještě před vstupem do cyklu jako prázdné)Zkusíme nejdřívě naprogramovat první řešení, neboť je přímočařejší (viz čočka a hrách):
mensi = []
vetsi = []
for x in p: # projdeme seznam nájodných čísel
if x < prumer:
mensi.append(x) # je-li menší průměru přidáme do prvního seznamu
else:
vetsi.append(x) # jinak (je větší nebo rovno průměru) do druhého
print(mensi)
print(vetsi)
Druhé řešení se jeví jako elegantnější (především pro ty, kteří mají rádi pořádek). Hlavním problémem je najít hraniční položku, což pro nás bude první položka v uspořádaném seznamu, která je větší než průměr (může to samozřejmě být opačně i poslední menší průměru, ale první přístup zjednoduší indexování).
Jak tuto položku najdeme? Jak bylo řečeno výše nachází se někde kolem poloviny seznamu (viz pravděpodobnosti podintervalů v rovnoměrném rozdělení). Zkusíme tedy nejdříve položku uprostřed (s indexem len(p)//2) jako první odhad (nástřel) hraniční položky. Pokud je menší než průměr, pak postupně procházíme položky vpravo (s vyšším indexem), dokud nenajdeme první, která je větší (což je hraniční). Pokud je střední položka větší než průměr jdeme naopak doleva (k menším indexům) a hledáme první, která je menší (což je položka s indexem o jedna menší než je hraniční).
p.sort() # setřídíme položky od nejmenší k největším
hranice = len(p)//2 # první odhad
if p[hranice] < prumer:
while p[hranice] < prumer:
hranice += 1
else:
while p[hranice] >= prumer:
hranice -= 1
hranice += 1 # jsme na posledním menším, musíme se posunout na první větší
mensi = p[:hranice] # hranice je index vyjma, což je správně (hranice je první větší)
vetsi = p[hranice:] # tentokrát je položka s indexem hranice zahrnut
print(f"pro kontrolu průměr je {prumer}")
print(mensi)
print(vetsi)
Obě řešení mají své výhody a nedostatky: přímočarý přístup je efektivnější, neboť seřazení celého pole není zadarmo (ve skutečnosti může být u velkých polí výrazně pomalejší). Toto řešení je i pochopitelnější. Řešení využívající seřazeného pole je oproti tomu mnohem snadněji kontrolovatelné (díky seřazení, je na první pohled zřejmé, že se rozdělení provedlo správně).
Ve skutečnosti však existuje ještě jedno řešení, které je ještě efektivnější než přímočarý přístup a navíc relativně snadno pochopitelné. Navíc stejně jako řešení založené na seřazení vychází z přeuspořádání čísel, tak aby ty menší (něž průměr) byly vlevo a ty větší vpravo. Navíc vychází z postřehu, že ještě před rozřazením se řádově polovina čísel nachází na správné straně původního seznamu, takže by bylo nejlepší s nimi vůbec nehýbat.
Z tohoto pozorování lze již relativně snadno odvodit algoritmus. Jádrem je prohození prvků mezi levou a pravou částí seznamu, pokud jsou tyto prvky v nesprávné části (tj. velká čísla v levé a malá v pravé). Navíc musíme být důslední, takže špatně umístěné prvky hledáme nejdříve co nejvíce vlevo (větší než průměr) resp. vpravo (menší než průměr).
Výsledkem je algoritmus pracující se dvěma indexy — levým (na začátku ma hodnotu 0 tj. odkazuje první prvek) a pravým (na začátku odkazuje poslední prvek s indexem $n-1$).
Nejdříve najdeme první špatně umístěný prvek zleva (první, který je větší průměr), a to tím, že postupně posouváme levý index (přičítáním jedničky). Podobně najdeme i první špatně umístěný prvek zprava (poslední, který je menší než průměr) tentokrát posouváme pravý index opačným směrem (odečítáním jedničky). Po skončení této fáze může nastat situace, že levý index odkazuje na prvek vpravo od prvku, na nějž odkazuje pravý index (tj. levý je vpravo a pravý vlevo tzv. inverze indexů). V tomto případě máme vyhráno, neboť seznam je rozdělen tak jak potřebujeme (a levý index je hraniční).
V opačném případě ještě nemáme hotovo. Musíme prohodit prvky indexované levým a pravým indexem (oba indexy určitě odkazují různé prvky). Následně oba indexy posuneme o jednu položku vpravo (levý index) resp. vpravo (pravý index), abychom zbytečně v dalším kroku nekontrolovali již prohozené prvky.
Nyní znovu zkontrolujeme, zda nedošlo k inverzi indexů (pokud ano můžeme skončit) a pokračujeme hledáním dalších špatně umístěných prvků.
Celý algoritmus pro malý ukázkový seznam ilustruje následující obrázek.
Implemenrace v Pythonu je přímočará. Jádrem je využití cyklů while a to dokonce ve dvou úrovních (použité cyklu while je zřejmé, neboť neznáme počty výměn ani vzdálenosti mezi špatně umístěnými prvky).
n = 10
p = [0.0] * n # inicializace pole náhodných hodnot
for i in range(n):
p[i] = uniform(0,100)
print(p)
# nyní spočítáme průměr
prumer = sum(p)/len(p)
print(prumer)
levy = 0 # počáteční nastavení levého indexu (první položka)
pravy = n - 1 # počáteční nastavení pravého indexu (poslední položka)
while(levy<= pravy): # dokud nedojde k inverzi indexů
while(p[levy] < prumer): # posun levého indexu na první (prozatím neprohozený) špatně umístěný prvek
levy += 1
while(p[pravy] >= prumer): # posun pravého indexu na poslední (prozatím neprohozený) špatně umístěný prvek
pravy -= 1
if levy < pravy: # opět kontrolujeme zda nedošlo k inverzi
p[levy], p[pravy] = p[pravy], p[levy] # pokud ne, prohodíme prvky
levy += 1 # posuneme se na následující položku u levého
pravy -= 1 # a předcházející
print(p[:levy]) # vypíšeme seznam menších než průměr
print(p[pravy+1:]) # a větších než průměr
Úkol: Ve výše uvedené implementaci se na dvou místech kontroluje, zda bylo dosaženo konce (tj. k inverzi indexů). Navíc podmínka vnějšího cyklu explicitně zahrnuje i rovnost (tj. rovnost, ještě není inverze). Argumentujte proč? Svouji argumentaci podpořte příkladem, v němž je tento rozdíl kritický.
Úkol: Zkuste napsat alternativní implementaci, která eleminuje dvojí testování inverze indexů.
Rada: Nadbytečná je podmínka vnějšího cyklu
while, tento cyklus může být formálně nekonečný (podmínka je stále pravdivá), zakončení zajistí výskok z cyklu (break)
V předchozí části jsme se seznámili s třemi základními metodami vytváření rozsáhlejších seznamů:
rangeposloupnost = list(range(1,10,2)) # plnění z rozsahu (od 1 do 10 vyjma s krokem 2)
print(posloupnost)
ctverce = [] # pole druhých mocnim
for i in range(10):
ctverce.append(i*i)
print(ctverce)
sameNuly = [0] * 10
print(sameNuly)
Z těchto tří způsobů je nejobecnější použití append v cyklu. Pomocí cyklu lze snadno vytvářet seznamy tvořené posloupnostmi čísel resp. seznamy tvořené opakovaným vzorem. Bohužel je také nejméně přehledné, neboť vyžaduje minimálně tří příkazy (= řádky) kódu: inicializaci, hlavičku for-cyklu a jeho tělo (s volaním metody append).
Python proto podporuje i zápis, který vychází z přidávajího cyklu je však výrazně přehlednější --- seznamovou komprehenzi (český název prozatím neexistuje). Syntaxe má tento tvar:
[výraz for proměnná in zdroj]
kde, proměnná je řídicí proměnná komprehenze (obdoba řídicí proměnné cyklu), zdroj je objekt poskytující posloupnost hodnot (rozsah, jiný seznam nebo sekvenční kolekce). Klíčovou částí je pak počáteční výraz, který je vyhodnocen nad každým objektem získaným nad zdrojem. Výsledky tohoto vyhodnocení postupně tvoří prvky nového seznamu.
Ukažme si několik jednoduchých příkladů:
[i*i for i in range(10)] #
Výsledkem je seznam hodnot i*i (druhých mocnin), kde i postupně nabývá hodnot od nuly (včetně) do 10 (vyjma). Všimněte si i základního rozdílu mezi komprehenzí a for-cyklem (navzdory výrazné syntaktické podobnosti). Cyklus nevrací žádnou hodnotu (není to výraz), pouze uvnitř modifikuje proměnné nebo objekty. Komprehenze žádné proměnné (kromě řídicí) ani objekty nemění pouze vytváří nové objekty, které umisťuje do nově vytvářeného seznamu)
[0 for i in range(10)] # pole deseti nul
Stejně jako u cyklů nemusíte řídicí proměnnou vůbec využívat (důležité je pouze to, že nula se přidá do pole desetkrát).
from random import uniform
[uniform(0,100) for i in range(10)]
Vytvoření seznamu náhodných hodnot je pomocí komprehenze snadné.
Další možnosti přináší rozšíření komprehenze o sekci if pomocí níž lze filtrovat jen některé položky získané ze zdroje.
[i*i for i in range(1,30) if "2" in str(i*i)]
Tato komprehenze prochází všechny čísla od jedné do 30 a vytváří seznam některách jejich druhých mocnin, a to jen je těch jejichž druhá mocnina obsahuje číslici 2 (testování číslic se děje pomocí hledání znaků v řetězcové representaci čísla).
p = [uniform(1,100) for i in range(10)] # deset náhodných čísel
prumer = sum(p)/len(p)
mensi = [x for x in p if x < prumer]
vetsi = [x for x in p if x >= prumer]
print(mensi)
print(vetsi)
Další řešení příkladu, který jsme diskutovali v předchozí kapitole (podseznam čísel menších resp. větších než průměr). Toto řešení je rozhodně nejjednodušší a tím i nejlépe čitelné. Na druhou stranu je nejpomalejší (v zásadě se jedná o variantu postupného procházení seznamu a průběžného vytváření nového seznamu). Pro seznamy běžné velikosti (tisíce položek) je však rozdíl daný nižší efektivností zanedbatelný a jasně vyhrává elegance a jednoduchost zápisu (strojový čas je mnohem levnější než práce programátora).
Úkol: Pomocí komprehenze vytvořte seznam obsahující čísla [0, 0.1, 0.2, … 1.9] (krok 0.1)
Úkol: Vytvořte pomocí komprehenze seznam řetězců ["a", "b", "c", … "z"].
Rada: Využijte vetavěnou funkci
chrkterá vrací řetězec tvořený znakem, jehož interní kód je předán jako parametr (znaky jsou v počítači representovány číslem, které identifikuje znak v rozsáhlé tabulce všech znaků). Běžné (anglické) znaky tvoří v této tabulce souvislou posloupnost:
print(chr(97))
print(chr(98))
print(chr(122))
Úkol: Vytvořte 16-prvkový seznam tvořený střídajícími se hodnotami
TrueaFalse(první je optimistickyTrue)
Iterátory jsou objekty, které na požádání vrací prvky určité posloupnosti. Vyskytují se téměř ve všech objektově orientovaných jazycích, ale v Pythonu mají zvlášť klíčovou roli.
Iterátory lze v Pythonu vytvářet mnoha způsoby. Jedním z nich je vytvoření iterátoru nad kolekcí tj. například nad seznamem.
Iterátor nad seznamem lze získat voláním vestavěné funkce iter.
seznam = [1,2,3]
iterator = iter(seznam)
Nad iterátorem se poté volá vestavěná funkce next, která vrací následující hodnotu iterátoru (při prvním volání je to první hodnota). V případě seznamového iterátoru jsou to postupně hodnoty položek seznamu od první do poslední.
next(iterator)
next(iterator)
next(iterator)
Tím jsme, ale vyčerpaly položky a iterátor nemá, co vrátit. Nezbývá mu nic jiného, než vyhodit výjimku.
next(iterator)
Tato výjimka se poněkud liší od ostatních výjimek v Pythonu, neboť nesignalizuje výjimečnou, či dokonce chybovou hodnotu. Rozhodně by nikdy neměla vést k ukončení.
Podíváme-li se na původní seznam, tak zjistíme, že se nezměnil:
seznam
Naopak iterátor už je tzv. vyčerpaný (angl. exhausted). Už nikdy nevrátí žádný další prvek ani nelze nějak (magicky) obnovit.
next(iterator)
Snadno však můžeme, získat nový iterátor nad seznamem, či dokonce celou řadu iterátorů, které jsou na sobě zcela nezávislé (tj. každý vrací postupně položky seznamu bez ohledu na stav ostatních iterátorů).
seznam = [1,2,3]
i2 = iter(seznam)
i3 = iter(seznam)
print(next(i2))
print(next(i3))
print(next(i2))
Co se však stane, pokud, změníme podkladový seznam, například přidáním prvku, či jeho výmazem?
seznam = [1,2,3]
i4 = iter(seznam) # získáme iterátor nad původním seznamem
seznam.insert(0,0) # přidáme na začátek 0
print(next(i4)) # a iterátor vypíše přidanou nulu (aktuálně první položka seznamu)
seznam.insert(0,0) # znovu přidáme nulu (v seznamu už jsou aktuálně dvě nuly na začátku)
print(next(i4)) # vypíše 0 (= aktuálně druhá položka)
print(next(i4)) # vypíše 1 (= aktuálně třetí poéložka)
seznam.clear() # vyprázdní seznam
print(next(i4)) # na čtvrté pozici nic není (= výjimka `StopIteration` je důsledkem jeho vyčerpání)
Python sice zvládne situaci, kdy je průběžně modifikován podkladový seznam (využívaný živým iterátorem), ale za obtížně pochopitelného chování. Navíc v některých jiných programovacích jazycích je modifikování kolekce, nad níž existuje iterátor nepřípustné.
Nyní již proto můžeme uvést tří zásady práce se seznamovými iterátory (resp. obecně s iterátory nad kolekcemi):
StopIteration je vyčerpaný a nelze ho žádným způsobem obnovit.První zásada je relativně snadno pochopitelná, neboť odpovídající mechanismy známe i ze svého života. Předpokládejme, že cílem bulvárních novinářů je pomlouvat celebrity. Jednu celebritu (obecně pomlouvatelnou osobu) může samozřejmě (nezávisle na sobě) pomlouvat více novinářů (pomlouvačů).
Nyní však přicházíme ke skutečnému zenu jazyka Python:
Každý iterátor je zároveň iterovatelným objektem. Pokud na něj zavoláme metodu iter, tak vrátí sám sebe.
Tento přístup je pro začátečníky dosti matoucí, neboť zdánlivě stírá rozdíly mezi iterovatelnými objekty a iterátory. Ve skutečnosti je to však jen užitečná sémantická zkratka (finta), která umožňuje použití iterátorů na místě, kde je očekáván iterovatelný objekt. Stále však musíme rozlišovat mezi voláním funkce iter nad kolekcí (vrací se nový čerstvý izerátor) a voláním téže funkce nad iterátorem (žádný nový iterátor nevzniká, vrací se původní iterátor, tj. funkce je prostou identitou).
Použití si ukážeme na konstrukci, která je pro iterátory klíčová.
I když lze iterátory využívat přímo, většinou se využívají prostřednictvím cyklu for, který již známe. Tento cyklus ve skutečnosti slouží k procházení libovolných iterovatelných objektů.
Při použití cyklu for se v zásadě provádějí následující kroky:
iter). Pokud je iterovatelným StopIteration), je volána funkce next a její výsledek je přesměrován odkaz hlavní proměnné cyklu (jinak řečeno proměnná je vždy přelepena na nový objekt)Předpokládejme například následující cyklus.
for i in [1,2]:
print(i)
Tento cyklus postupně vykonává tyto činnosti:
iterator = iter([1,2])next, ta vrátí hodnotu 1, která je označena proměnnou i (tj. provede se přiřazení iterator = next(it)).i.next, ta vrátí hodnotu 2, která je označena proměnnou i (tj. provede se přiřazení iiterator = next(it)).i.next. Tentokrát vznikne výjimka, která je zachycena a cyklus je ukončen.Úkol: Vysvětlete roli iterátorů v následujícím cyklu, který vypíše všechny možné kombinace znaků "abc" (tj, jejich kartézský součin). Kolik itertátorů vznikne?
znaky = "abc"
for i in znaky:
for j in znaky:
print(i + j)
Kromě funkce iter existuje i funkce reversed, která vrací iterátor procházející položky určité kolekce v opačném pořadí. Je nejčastěji využívána pro zpětné procházení kolekcí.
for c in reversed("abc"):
print(c)
Za klíčovým slovem v tomto případě není uvedena (iterovatelná) kolekce, ale iterátor. To však nevadí, neboť jak už víme, že díky fintě je v Pythonu iterovatelný i každý iterátor.
Na počátku cyklu se postupně zavolají tři funkce. Nejdříve se volá metoda reversed, která vrací reverzní iterátor (původní řetezec se samozřejmě nemění, tím spíše že je neměnný). Poté se stejně jako u všech objektů předaných cyklu for volá funkce iter (tj. získání iterátoru nad předaným iterovatelným objektem). Tato funkce vrátí původní (revezní) iterátor. Nad ním se pak volá funkce next, aby se získala první hodnota iterátorů (= poslední položka).
Je to trochu komplikované, ale navenek zcela přirozené a především efektivní (v průběhu se nevytváří žádné pomocné pole, a využívá se jen jediný objekt iterátoru).
Úkol: Jaký je rozdíl mezi následujícími zápisy?
for i in seznam
for i in iter(seznam)
Jako odlehčené iterátory lze označit iterátory, které nejsou spojeny s podkladovou kolekcí. Označují se jako odlehčené (ligtweight), neboť poskytují posloupnosti hodnot bez toho, že by vyžadovaly jejich souhrné uložení uložení v paměti (tj. v nějaké kolekci).
S jedním odlehčeným iterátorem jsme se už nepřímo seznámili při seznámení s cyklem for. Je to iterátor, který získáme aplikací funkce iter na objekt vracený funkcí range (ten není sám iterátorem, je jen iterovatelný).
next(iter(range(10,20)))
# ověříme, že je to iterátor (vypíše vždy dolní mez = první člen posloupnosti)
Iterátor přes rozsahy (ranges) je typickým příkladem odlehčeného iterátoru, neboť v paměti kromě mezí a kroku uchovává jen poslední vrácenou hodnotu (= na začatku dolní mezi). Další hodnotu iterátor získá jednoduše přičtením kroku (implicitně 1) k této uložené hodnotě. Že je tomu skutečně tak, lze ověřit vytvořením a použitím rozsahu s enormní horní mezí.
next(iter(range(100,10_000_000_000_000)))
Pokud by se skutečně vytvořil seznam (či jiná kolekce) vyžadovalo by to minimálně 400 TB (dekadických terabytů = 364 TiB binárních terabytů) paměti, neboť by bylo nutno uložit $10^{13}$ 4 bytových čísel. Kolekce by se tak nevešla ani na vnější paměťová zařízení (disk). Navíc jen inicializace kolekce by trvala řádově hodiny.
Ve skutečnosti zaujímá iterátor jen pár desítek bytů, a provede se jen dvě operace, vrácení dolní meze a přičení jedničky jako příprava pro další volání next (které už nikdy nenastane, protože iterátor je po skončení nedostupný).
Tento přístup se běžně označuje jako lenivý (lazy) přístup a je typický nejen pro lidi, ale i pro programy. Základní pravidlo je, že se vždy vykoná jen to, co je v dané chvíli nezbytně nutné (tj. zde vrácení první hodnoty). Jak jistě víte, je tento přístup vhodný v případech, kdy lze očekávat, že nakonec na provedení nikdy nedojde. Tak je tomu i v našem případě, neboť iterace přes tak velký cyklus by v reálném čase nemohla skončit.
Poznámka: Zajímavou otázkou je, proč tvůrci Pythonu neimplementovali
rangepřímo jako iterátor.
Důvodů je hned několik:
1) přes objekt rozsahu lze iterovat i vícenásobně. Lze tak například z jednoho objektu range inicializovat více seznamů (iterátor by se vyčerpal hned při prvním plnění).
r = range(1,3)
print(list(r))
print(list(r))
2) objekt rozsahu může poskytovat více informací o sobě. Kromě mezí je to například i počet hodnot v rozsahu. V případě iterátoru lze počet položek zjistit jen tím, že projdeme všechny jeho prvky (čímž ho však vyčerpáme, takže už není k použití). Hodnoty si samozřejmě můžeme i uložit např. v seznamu, což však může být paměťově neefektivní. Počet hodnot v rozsahu lze zjistit snadno (i bez procházení).
len(r)
3) nad rozsahem lze efektivně implementovat funkci reversed. U iterátoru by bylo nutné uložit všechny položky do seznamu a až ten zpětně iterovat. U rozsahu jen stačí upravit meze a obrátit znaménko u kroku.
for i in reversed(range(5)):
print(i)
Vytvoření reverzního iterátoru je rychlá operace, která zachovává lenivý přístup.
next(reversed(range(100,10_000_000_000_000)))
Iterátory získané z rozsahu mohou potenciálně poskytovat téměř neomezené množství hodnot. Existují však i iterátory, které jdou ještě dál, neboť potenciálně poskytují nekonečné množství hodnot (důležité je zde slovo 'potenciální') tj. jinak řečeno jsou nevyčerpatelné.
Některé z nich najdete v modulu itertools (obsahuje funkce užitečné pro práci s iterátory).
from itertools import count
infinity_iter = count(0)
print(next(infinity_iter))
print(next(infinity_iter))
print(next(infinity_iter))
Iterátor, který získáme voláním funkce itertools.count se podobá iterátoru nad jednoduchým rozsahem. Jako první vrací hodnotu, která byla předána jeho konstruktoru (zde je to 0), následně pak hodnotu o jednu vyšší. Na první pohled však chybí horní mez. Ta zde nemá smysl, neboť iterátor je potenciálně nekonečný. V reálném světě však máme k dispozici jen konečný čas pro získávání hodnot z iterátoru (a také konečnou paměť pro representaci
čísel, které iterátor na požádání vrací, ale to není v tomto konkrétním čase problém, neboť nám dříve dojde trpělivost než počítačová paměť).
Je zřejmé, že nekonečné iterátory jsou extrémním případem lenivého přístupu a nemůžeme je tudíž používat v kódu, který předpokládá úplné vyčerpání iterátoru.
list(infinity_iter) # potenciálně vyžaduje nekonečný čas a nekonečnou paměť
for i in infinity_iter: # nekonečný výpis
print(u)
Nekonečné iterátory jsou však i přesto užitečné, je však nutno iterace předčasně ukončit Nedoporučuji však metodu uvedenou na následujícím obrázku, může být neučinná (iterátor může přežít, například pokud program běží v cloudu) a má nepřijemné vedlejší efekty.
Praktičtejší je použití metody itertools.islice, pomocí níž lze získat jen omezený výřez iterátoru (typicky prvních $n$ prvků).
from itertools import islice
for i in islice(count(0), 10): # vypíše prvních deset členů iterátoru `count`
print(i)
Předchozí příklad není příliš praktický, neboť stejného efektu lze dosáhnout použitím rozsahu range. Existují však i praktičtější příklady:
Například »občas« potřebujete iterátor, který vrací tisíckrát opakovanou posloupnost znaků Morseova kódu ".--. -.-- - .... --- -." (vždy vrací jen jeden znak tj. tečku, čárku nebo mezeru).
Triviálním řešení je vytvořit příslušný seznam pomocí operátoru opakování (*) a na něj pak získat iterátor.
morse_iter1 = iter([".--. -.-- - .... --- -."] * 1000)
Toto řešení však vyžaduje uložení celé zprávy do pole (o mnoha tisících položkách). Elegantnější je využití iterátorů.
Úkol: Navrhněte řešení využívající nekonečný iterátor poskytovaný funkcí
itertools.cycle.
Iterátor, přes opakovaný Morseův kód, který jsme získali můžeme využít k různým účelům. Můžeme například spočítat počet čárek a teček v kódu poskytovaném iterátorem.
Využijeme k tomu objekt třídy Counter, který se nachází ve standardním modulu collections.
from collections import Counter
c = Counter(morse_iter2)
# v konstruktoru předáme iterátor, jehož položky se budou počítat
print(c.most_common())
Není překvapením, že počty jsou dělitelné 1000.
Poznámka: Zpracování signálu v Morseově kódu pomocí iterátoru zcela přirozeně modeluje charakter tohoto kódu v jeho počátcích (a běžně i později). Stejně jako v realitě se jednotlivé znaky nikam nezaznamenávají (jen se po jednom přenášejí komunikačním médiem), namísto toho se ukládá informace vzniklá jejich transformací (v realitě to není počet teček a čárek, ale dekódovaný text).
Obecně lze říci, že rozdíl mezi kolekcí a iterátorem je stejný jako mezi vytištěnou knihou (kolekce znaků) a textem přenášeným komunikačním kanálem (v každý okamžik fyzicky existuje jen pár znaků této komunikace, ale víme, že potenciálně nakonec dorazí všechny).
V předchozích kapitolách jsme se již seznámili s mnoha funkcemi, zmínit lze například matematické funkce v modulu math, vestavěné funkce jako je len nebo funkce-konstruktory (vytvářejí nové objekty standardních tříd) jako list, int nebo float. Speciálními případy funkcí jsou pak metody, které lze volat nad objekty některých tříd, například třídy string (lower, startswith, apod.) nebo list (append, remove, apod.)
Mezi funkce v zásadě patří i tzv. operátory (známe zatím aritmetické, relační, logické a některé speciální jako operátor is nebo in).
Co však mají všechny tyto různé reinkarnace funkce společné. Co dělá funkce funkcemi?
Nejlépe si funkce představíte jako magické objekty (jakési malé a silně specializované počítače), které získávají na vstupu různé objekty/hodnoty (čísla, řetězce, seznamy) a vrací jako výsledek nové objekty/hodnoty. Tento mentální model ilustruje (na příkladě vestavěné funkce round) následující obrázek.
Na obrázku si můžete všimnout i dvou základních rysů funkcí (nejen) v Pythonu:
funkce nemá stejně jako jakýkoliv objekt v Pythonu vlastní (interní) jméno. Typicky je však stejně jako ostatní déletrvající objekty opatřena proměnnou (jako štítkem). Existují však i anonymní funkce či funkce opatřené více štítky (více proměnnými)
funkce je v podstatě černá skříňka, tj. nemusí nás zajímat, jak způsobem nalezne ten správný výstupní objekt (to mimo jiné znamená, že mnohdy musíte funkcím důvěřovat)
Bohužel s tímto jednoduchým modelem funkcí si v Pythonu nevystačíte. Funkce v Pythonu totiž mohou měnit i své parametry (vstupní objekty). Výstupní hodnota často hraje v tomto případě jen vedlejší roli a proto mnohé z těchto funkcí vrací pouze hodnotu None (de facto tedy nevracejí nic). Příkladem (jak již bylo dříve zmíněno) jsou některé metody modifikovatelných objektů.
s = []
navratovaHodnota = s.append(0)
print(navratovaHodnota) # funkce metoda nic nevrací (= vrací None)
print(s) # změnil se však parametr funkce (adresát metody odkazovaný proměnnou)
Funkce nevracející hodnotu se v některých jazycích označují jako procedury. Často se používá i termín rutina resp. podprogram, který nejčastěji zahrnuje jak (vlastní) funkce tak procedury.
Ve skutečnosti je však terminologie v této oblasti dosti rozkolísaná, neboť programovací jazyky používají z historických důvodů svou vlastní terminologii. V Pythonu se pro veškeré podprogramy (vlastní funkce i procedury) používá termín funkce. Proto tak budeme činit i my.
Funkce mají v programovacích jazycích dvě základní role:
rozdělují program na menší snadněji spravovatelné části (s vlastní proměnnými)
umožňují stejný kód volat vícenásobně z různých míst programů (každé volání může být navíc parametrizováno, tj. vykonávat stejný kód nad různými daty)
Těchto možností však můžete plně využívat jen v případě, že kromě funkcí vestavěných a knihovních budete vytvářet i funkce vlastní. To je naštěstí v Pythonu velmi snadné. Ukážeme si to na příkladu.
def zerolist(n): # hlavička funkce
return [0] * n # tělo funkce
Tímto stručným zápisem jsme vytvořili novou funkci se jménem zerolist, která očekává jediný parametr. To vše prozrazuje hlavička funkce začínající klíčovým slovem def (zkratka za define). Následuje jméno funkce. Může to být v zásadě libovolný platný identifikátor, který je v daném kontexu jednoznačný (tj. není použit pro jinou funkci nebo pro proměnou). Úzus navíc doporučuje využívat jen malá písmena resp. podržítka pro případné oddělení slov.
Za jménem funkce jsou závorky, které obsahují seznam identifikátorů proměnných, jež jsou využity pro předávání parametrů. Naši funkci lze volat s právě jedním parametrem, jehož hodnota bude v těle funkce odkazována proměnou n. Tato proměnná je tzv.lokální tj. existuje jen po dobu vykonávaní dané funkce (pak zaniká a nikdy nemůže být obnovena).
Po seznamu parametr následuje dvojtečka, takže je zřejmé, že musí následovat odsazený blok příkazů. Ten tvoří tzv. tělo funkce. Tělo funkce je vykonáno při každém volání funkce, přičemž každé volání pracuje nad novými proměnnými parametrů (které mohou obecně označovat jiné hodnoty). V našem příkladě je tělo tvořeno příkazem return. Tento příkaz ukončí volání funkce, která následně vrátí hodnotu, jež vznikne vyhodnocením výrazu následujícím za klíčovým slovem return (v našem případě je to seznam tvořený n nulami).
Nyní zkusíme funkci zavolat:
zerolist(10)
V rámci tohoto volání vznikla lokální proměnná n odkazující hodnotu 10. Byla využita v těle funkce pro vytvoření nového seznamu (v příkazu return) a poté zanikla. Návratovou hodnotou je seznam deseti položek 0.
Funkci můžeme zavolat znovu (tentokrát s jinou hodnotou parametru):
zerolist(3)
I při tomto volání vznikla nová proměnná. I když měla stejný název jako v prvním volání, jednalo se o jinou proměnnou (existovala v jiný čas a pravděpodobně i na jiném místě operační paměti. I tato proměnná nakonec zanikla.
Napotřetí tentokrát s využitím volání pojmenovaného parametru (namísto pozičního volání):
zerolist(n=8)
Už z tohoto jednoduchého příkladu jsou vidět hlavní výhody použití funkce. Vytváření nulových seznamů je jednodušší, a především se nám podařilo ukrýt detaily implementace (funkce jako černá skříňka). Uživatel nemusí přemýšlet jak se nám podařilo vytvořit požadovaný seznam. Navíc pokud se rozhodneme implementaci změnit (například použít seznamovou komprehenzi) navenek se nic nezmění (stále se bude používat stejné volání). Změna se tak projeví jen na jednom místě programu, což usnadní udržení konzistence (při opakovaném využití kopie stejného kódu hrozí nebezpečí, že při změně implementace zapomeneme některý výskyt opravit, nebo jej opravíme špatně).
Funkce může mít samozřejmě i více parametrů:
def between(x, a, b): # funkce testující zda x leží v intervalu [a, b]
return a <= x <= b
between(2, 1, 8)
Rozsáhlejší může být i tělo funkce:
def numberOfWords(text): # funkce vracející počet slov v řetězci
words = text.split(" ") # rozdělení řetezce podle oddělovače
return len(words)
numberOfWords("Three Rings for the Elven-kings under the sky")
Tato funkce má (resp. by měla) zjišťovat počet slov v řetězci (ten je předán jako jediný parametr funkce a je dostupný v lokální proměnné text), Funkce využívá metodu split, kterou lze volat na řetězec obsahující části oddělené určitým znakem nebo víceznakovou skupinou tzv. oddělovačem. Metoda vrací seznam těchto částí.
"a,b,c,d".split(",")
"kniha::Pán prstenů::Tolkien".split("::")
Všimněte si také proměnné words,do níž je pole podřetězců přiřazeno. Tato proměnná je stejně jako proměnné určené pro ukládání parametrů tzv. lokální tj. existuje jen po dobu provádění dané funkce. Navíc je viditelná jen z těla funkce (tj. není viditelná ani z globální úrovně ani z jiných funkcí.
V Pythonu jsou lokálními proměnnými všechny proměnné vzniklé v těle funkce (prvním přiřazením) a proměnné uchovávající parametry (kterým se ne zcela přesně říká formální parametry).
# lokální proměnné nelze používat mimo těla své funkce (tj. ani na globální úrovni)
words
V okamžiku kdy navrhnete funkci tak jste odpovědni za její správnost (což je klíčové v okamžiku, kdy jsi spolu s jejím modulem nabídnete ostatním). Naše funkce pro počítání slov však má k dokonalosti daleko. Zkuste najít texty pro něž vrací chybný počet slov.
text = input() # text je globální proměnná, která nemá nic společného se stejnojmenným lokálním parametrem
print(numberOfWords(text))
Pokud jste na žádný problém nenarazili, pak zkoušíte příliš jednoduché kombinace mezer (tj. oddělovačů). Problémem jsou mezery na začátku, mezery na konci a vícenásobné mezery. Řešení je myslím zřejmé a již jsme se jím zabývali — před rozdělení zjednodošíte mezery pomoci metody string. a funkce re.sub.
Úkol: Vytvořte robustnější verzi funkce pro počítání počtu slov.
Řešený příklad:
I když Python a jeho knihovny nabízejí přehršel funkcí a metod (a o několik řádů menší počet operátorů), stále některé užitečné chybí. Jako příklad si uveďme generování náhodných řetězců. Zatímco Python v případě čísel Python podporuje desítky různých funkcí pro náhodné generování, u řetězců je situace tristní. Nic nám však nebrání vytvořit si funkci vlastní. Nejjednodušší možností jsou řetězce tvořené zcela nahodilým seskupením písmen jako např. "týívepvepfwá" nebo "jgýfěšýfěcpgp". Ty se však jen obtížně vyslovují a pravděpodobnost, že tvoří rozumné slovo či sousloví je pro delší řetězce de facto nulová (https://en.wikipedia.org/wiki/Infinite_monkey_theorem).
Generování všech foneticky či gramaticky realizovatelných posloupností znaků je na druhé straně příliš složitý úkol. Zvolíme tudíž střední cestu. Za prvé budeme generovat rozumné slabiky tvaru CV, CCV nebo CVC (kde C je souhláska a V samohláska). Navíc použijeme jen ty nejčastější spojeni tohoto druhu (vybrány ze seznamu tzv. bigramů resp. trigramů). Z nich pak budeme vytvářet náhodná slova.
Podívejme se na generování náhodných slabik:
from random import choice
def randomSyllable(): # funkce nemá žádný parametr
bigramy = ["ní","po","ro","na","je","te","le","ko","ne","ra",
"to","no","la","li","ho","do","se","ta","ce","va",
"ře","ti","lo","ně"]
trigramy = ["pro","sta","pře","ter","pod","pra","sti","řed",
"kon","nos","při","sou","při","tel","pol",
"spo","jak","val","sto","tak","lov"]
slabiky = list(bigramy) # vytvoříme nový seznam a naplníme jej slabikami z pole bigramy
slabiky.extend(bigramy) # které přidáme ještě jednou
slabiky.extend(trigramy) # a nakonec (jednou) slabiky ze seznamu trigramy
return choice(slabiky) # a náhodně vybereme jednu slabiku ze seznamu
print(randomSyllable())
Funkce pro generování je relativně přímočará. Nejdříve vytvoříme seznam bigramů (slabik o dvou písmenech) a trigramů (o třech písmenech). Pak vytvoříme spojený seznam, který obsahuje položky z obou seznamů, přičemž bigramy přidáme dvakrát (tím dvakrát zvýšíme pravděpodobnost jejich výběru, v češtině jsou slabiky typu CV tzv. otevřené častější).
Výběr je pak již snadný neboť v modulu random je funkce choice, která provádí náhodný výběr ze seznamu.
Poznámka: Navržená funkce je sice funkční, ale není příliš efektivní, neboť všechny tři seznamy (bigramy, trigramy, slabiky) vznikají při každém volání funkce
randomSyllable, což je zbytečné, neboť jsou stále stejné a neměnné (stačilo by je vytvořit jen jednou a pak je využívat). Řešení tohoto problému však vyžaduje znát základy tvorby OOP tříd (na což si musíte ještě trochu počkat).
Druhým krokem je funkce pro vytváření slov, jejímž parametrem je délka slova ve slabikách.
from random import randint
def randomWord(length=3):
return "".join([randomSyllable() for i in range(length)])
print(randomWord(2))
print(randomWord())
Definice funkce obsahuje konstrukci, kterou zatím neznáme tzv. implicitní hodnotu parametru (v našem případě má parametr length implicitní hodnotu 3). Díky použití implicitní hodnoty lze funkci volat buď s jedním parametrem (proměnná length má pak hodnotu tohoto parametru) nebo bez parametru (proměnná lengthmá pak implicitní hodnotu 3).
Pro implicitní hodnoty platí následující omezení:
Poslední dvě omezení jsou důsledkem skutečnosti, že k vyhodnocení implicitních hodnot dochází jen jednou v místě definice funkce, nikoliv při každém jejím vyvolání. V tomto okamžiku nejsou dostupné žádné další parametry (funkce ještě nebyla volána) a vzniklý objekt je sdílen všemi voláními (což může u měnitelných objektů vést k podivnému a neočekávanémi chování).
Uvnitř těla funkce je použita metoda string.join, která spojuje seznam řetězců do jediného velkého řetězce. Navíc vkládá mezi spojované řetězce oddělovač (což je fixní znak nebo skupina znaků). Metoda join je tedy opakem metody split (ta naopak podle oddělovače řetězec rozděluje). Adresátem metody je řetězec oddělovače (zde je to prázdný řetězec takže řetězce budou spojeny bez oddělovače). Parametrem je seznam náhodných slabik (seznam je vytvořen komprehenzí pomocí length volání naší funkce randomSyllable).
Úkol: Vytvořte funkci generující náhodné věty (posloupnost náhodných slov oddělených mezerou). Slova mají náhodnou slabičnou délku s Poissonovým rozdělením s parametrem $\lambda = 2$ posunutý o 1 (https://en.wikipedia.org/wiki/Poisson_distribution). Počet slov ve větě je dán parametrem (implicitně 5).
Rada: Náhodné číslo s požadovaným rozdělením získáte voláním funkce poisson z modulu
numpy.randoma přičtením jedničky.
from numpy.random import poisson
poisson(2) + 1
Jak již bylo zmíněno v Pythonu se jako funkce označují i procedury, které nic nevrací (= vrací formálně None), namísto toho mění objekty předané jako parametry.
Parametrem musí být v tomto případě měnitelný objekt, tj. nikoliv čísla nebo řetězce. Prozatím známe jen jednu třídu (typ) měnitelných objektů — seznamy.
Řešený příklad:
Jako první příklad uveďme funkci, která se seznamu čísel odstraní všechny záporné a nulové hodnoty (jinak řečeno nechá jen kladné).
Elemenární algoritmus je zřejmý. Procházíme jednotlivé prvky, a pokud nalezneme záporné číslo resp. nulu tak jej se seznamu smažeme. Vlastní provedení však naráží na nečekané obtíže.
Za prvé nemůžeme využít cyklus for pro přímé procházení prvků, neboť během procházení seznamu tímto způsobem nelze seznam žádným způsobem modifikovat (v opačném případě by totiž nebylo možno jednoduše určit, co vlastně znamená projít všechny prvky jeden za druhým).
Pro procházení je tak nutno využít indexaci (tj. cyklus přes všechny indexy). Ani tak však nemáme vyhráno, neboť pokud bychom zvyšovali index od nuly po len()-1 skončíme s výjimkou IndexError
def onlyPositive(vector): # (chybná) definice metody
for i in range(len(vector)): # projdeme všechny indexy
if vector[i] <= 0: # je-li položka na indexu `i` záporná nebo nulová
del vector[i] # pak ji smažeme
p = [0, 1, 2, -2, -3, 4, 0] # testovací seznam
onlyPositive(p)
print(p)
Vše se jeví v pořádku, neboť iterujeme přes správný rozsah od 0 do len(vector) vyjma (tj. do len(vector)-1 včetně). Nesmíme však zapomínat, že smazáním prvku se délka seznamu zmenší o jedničku. Tj. i když má ukázkový seznam na začátku 7 prvků, po smazání posledního (zde nuly na konci) má velikost 3. Funkce range se však vyhodnocuje jen jednou na začátku cyklu (včetně zjištění délky seznamu pomocí funkce len). Cyklus tak přistupuje i k prvkům, které už v seznamu nejsou.
Jedním z možných řešení je obrátit směr procházení, neboť v tomto případě se změna indexů týká jen té části seznamu, který již byl projit.
pnoyt
Pro iteraci přes čísla v opačném směru lze využít vetavěnou funkci reversed. Tato funkce očekává jako parametr libovolný objekt poskytující posloupnost objektů tzv. iterátor (rozsah range, seznam, apod.) a vrací objekt, který vrací tuto posloupnost v opačném směru.
for i in reversed(range(5)):
print(i)
Řešení pomocí procházení seznamu v opačném směru je již funkční:
def onlyPositive(vector): # (chybná) definice metody
for i in reversed(range(len(vector))): # projdeme všechny indexy (od nejvyššího k nulovému)
if vector[i] <= 0: # je-li položka na indexu `i` záporná nebo nulová
del vector[i] # pak ji smažeme
p = [0, 1, 2, -2, -3, 4, 0] # testovací seznam
onlyPositive(p)
print(p)
Ve skutečnosti není to řešení příliš efektivní, neboť každý výmaz vyžaduje přesunutí všech následujích prvků. Proto je ve většině případů efektivnější namísto mazání vytvořit nový seznam jen s požadovanými prvky. Navíc je jednodušší i zápis neboť lze využít seznamovou komprehenzi.
p = [0, 1, 2, -2, -3, 4, 0]
[x for x in p if x > 0]
Řešený příklad:
Užitečnou operací (kterou navíc není ve standardní knihovně) je rotace řetězců. Při rotace (doleva) se všechny položky seznamu posunou o jednu pozici vpravo (tj. na index o jedničku menší). První položka se pak přesune na konec. Rotace lze navíc provádět i o více pozic najednou (roatce o dvě pozice je ve skutečnosti shodná s dvěma rotacemi o jednu pozici).
Zákaldní implementace je snadná:
def rotate(lst, shift=1): # shift = o kolik pozic se seznam posune
for i in range(shift): # opakuj (implementace rotací o více pozic)
item = lst.pop(0) # vyjmeme první položku a uložíme ji do lokální proměnné
lst.append(item) # a pak ji vložíme na konec
return lst # vrátíme odkaz na původní seznam (avšak posunutý)
testList = [1, 2, 3, 4, 5]
print(rotate(testList)) # rotace o 1 vlevo
print(rotate(testList, 2)) # rotace o 2 vlevo (rotace se sčítají, takže seznam bude nakonec )
Funkce mění svůj první parametr (seznam) avšak zároveň vrací seznam jako návratovou hodnotu (tj. je to skutečná funkce). Nevrací však nový seznam ale originální seznam (resp. přesněji odkaz na něj). Tento styl může zjednodušit další zpracování seznamu, neboť tak lze jednoduše řetězit volání (i v našem případě můžeme modifikovaný seznam zobrazit voláním funkce print na výsledek volání funkce rotate).
Na druhou stranu může být tento přístup matoucí, neboť uživatel funkce může snadno nabýt dojmu, že funkce vytváří nový seznam (který pak vrací). Proto se tento přístup v Pythonu příliš neužívá. Stačí se podívat na modifikující metody seznamu:
p = []
print(p.append(0)) # nic nevrací (vypíše se None)
Výše uvedená implementace rotace má hned několik nedostatků. Hlavním nedostatkem je značně neefektivní implementace rotace pro velké hodnoty shift (kdy se provede několik úplných rotací seznamu)
testList = [1, 2, 3]
print(rotate(testList, 334)) # provede se 111 úplných rotací + rotace o jedno číslo vlevo
Někteří uživatelé by navíc ocenili, kdyby bylo lze zadat záporný posun pro rotaci opačným směrem (doprava). Jak se při záporném posunu bud chovat naše funkce:
testList = [1, 2, 3]
print(rotate(testList, -1)) # cyklus se neprovede ani jednou (nedojde k žádnému posunu)
Úkol: Upravte funkci pro rotaci tak, aby efektivně fungovala i pro velké posuny a záporné posuny (záporný posun znamená rotaci opačným směrem tj. doprava).
Rada: Řešení vychází z úpravy hodnoty předané v parametru
shift, tak aby ležel v rozsahu $[0, n-1]$, kde $n$ je délka seznamu (posunutí 0 je speciálním případem, k žádnému posunu nedojde). Platí například, že-8 % 5 = 2
def rotate(lst, shift=1): # shift = o kolik pozic se seznam posune
shift %= len(lst)
if shift == 0:
return lst # vracíme nezměněný originální seznam
lst.extend(lst[:shift]) # přidáme na konec kopii prvků s indexem 0 až shift-1 (včetně)
del lst[:shift] # a originální prvky smažeme (mažeme `shift` prvků)
return lst
testList = [1, 2, 3, 4, 5]
print(rotate(testList)) # rotace o 1 vlevo
print(rotate(testList, 22)) # rotace o 22 vlevo = 2 vlevo
print(rotate(testList, -1)) # posun o 1 vpravo
print(rotate(testList, -202)) # posun o 202 vpravo = 2 vpravo
# nyní máme původní seznam
# zkontrolujeme i triviální případ (rotace o 0 pozic)
print(rotate(testList, 0))
Vytváření vlastních funkcí si ukážeme ještě na jednom trochu rozsáhlejším příkladu (v němž se navíc ještě naučíme pár nových pythonských grifů).
Řešený příklad:
Jedním z oblíbených příčin konce světa u apokalyptiků je situace, kdy jsou všechny planety sluneční soustavy na jedné polopřímce (s počátkem na Slunci). To by podle předpovědí mělo vést k různým negativním jevům jako je změna rotační osy Země, smrtící výtrysk hmoty ze Slunce apod (je zbytečné jít příliš do detailů, neboť toto není opora pro psychiatry).
Naším úkolem bude vytvořit program, který zjistí, nejbližší termín potenciálního konce světa způsobený tímto fenoménem. Nějdříve si však musíme zpřesnit zadání, neboť k absolutnímu zarovnání nemůže prakticky dojít (resp. k němu nedojde v tézo geologické epoše). Je tak nutno stanovit nějaký nenulový limitní úhel, určující výseč do něhož se planety musí vejít. Apokalyptici běžně pracují s úhly 10 a mnohdy i 30 stupňů (což by asi jen málokdo označil za dokonalou přímku).
Navíc vnější planety Uran a Neptun se pohybují tak pomalu, že k jejich zarovnání dochází jen velmi zřídka. Synodickou periodu Uranu a Neptunu, což je (mimo jiné) perioda mezi dvěma konjunkcemi obou planet (konjunkce je zjednodušeně okamžik nejtěsnějšího přiblížení dvou planet na obloze) lze vypočítat z jejich orbitálních (siderických) period (= čas za jakou dobu oběhnou Slunce) podle vztahu ($P_1$ a $P_2$ jsou oběžné periody jednotlivých těles zde tedy Urana a Neptunu):
${\displaystyle {\frac {1}{P_{\mathrm {syn} }}}={\frac {1}{P_{1}}}-{\frac {1}{P_{2}}}}$
def synodic_period(orbital_period_1, orbital_period_2):
return abs(1/(1/orbital_period_1 - 1/orbital_period_2)) # abs je přidáno, aby nezáleželo na pořadí vstupů
synodic_period(165, 84) # orbitální periody Neptunu a Uranu
K zarovnání planet na přímce může docházet jen v relativně krátkém období v synodické periodě Uranu a Neptunu (když jsou obě planety blízko sebe) prokládané mnohem delšími obdobími, kdy těsná přiblížení nehrozí. Z tohoto důvodu umožníme některé planety z výpočtů vyloučit (typicky vnější). Tím můžeme zkrátit čekaní na apokalypsu na rozumný časový interval (apokalypsou za 150 let nikoho nevystrašíte :).
Nyní můžeme přejít k výpočtu, jehož jádrem je určení úhlové vzdálenosti mezi dvěma planetami. Tu lze zjistit z rozdílu tzv. ekliptikálních délek planet (pro jednoduchost předpokládáme, že se planety pohybují v jediné rovině). Ekliptikální délka je obdobou zeměpisné délky. Je to úhel průvodiče planety v polárních souřadnicích s centrem ve Slunci a základní polopřímkou ve směru jarního bodu.
Obrázek ukazuje Slunce (ve středu) a dvě planety. Hlavní osa (od níž se počítá úhel ekliptikální délky) směřuje vpravo a je označena symbolem jarního bodu (♈). Ekliptikální délka první (červené) planety $\ell_1$ je znázorněna červenou výsečí (je to cca 50°), ekliptikální délka druhé (modré) planety je znázorněna modrou výsečí a je cca 280°. Úhová vzdálenost mezi planetami (označená jako $\delta$) je konvexní úhel sevřený průvodiči obou planet. Absolutní hodnota rozdílu $\ell_1-\ell_2$ je buď úhlová vzdálenost (konvexní úhel) nebo jeho doplňový (konkávní úhel). V našem případě je rozdíl 230°(konkavní úhel) tj. úhlová vzdálenost je 360 - 230 = 130°.
Pro návrh funkce pro výpočet úhlové vzdálenosti dvou planet pomocí modulu ephem musíme získat heliocentrickou ekliptikální délku objektu representujícího planetu. K tomu slouží atribut hlong (heliocentric longitude). Výsledný úhel je speciální objekt, který se při použití aritmetiky chová jako float číslo vyjadřující úhel v radiánech. Při výpisu (kdy se převádí na řetězec) je formátován jako úhel ve stupních, minutách a vteřinách.
import ephem
mars = ephem.Mars()
mars.compute(ephem.now())
print( float(mars.hlong) ) # převádí se na číslo `float` v jednotkách radián
print( mars.hlong ) # formátuje se jako lidmi čitelný řetězec
earth = ephem.Sun() # Země = Slunce?
earth.compute(ephem.now())
print (earth.hlong)
Výše uvedený testovací kód vytváří dva objekty planet. Zatímco první z nich (Mars) nepřináší nic zajímavého, je v případě Země využit konstruktor ephem.Sun (nikoliv ephem.Earth). Důvodem této podivnosti, je skutečnost, že modul je napsán primárně pro (téměř) geocentrického pozorovatele (pozorování ze Slunce je to dost nepohodlné). Ten samozřejmě Zemi na obloze nevidí, tj. nemá smysl uvažovat o poloze Země na obloze, jejím západu a východu. Konstruktor ephem.Earth tak v modulu vůbec není.
Objekt ephem.Sun popisuje Slunce z geocentrického pohledu (má polohu na obloze, zapadá a vychází, apod.). Jedinou výjimkou jsou heliocentrické ekliptikální souřadnice, které nemají u objektu Slunce smysl (byla by to poloha Slunce vzhledem k Slunci). Autor tak využil tyto atributy pro helicontrickou polohu Země: "For a Sun body, they give the Earth’s heliocentric longitude and latitude". Je to poněkud matoucí, ale je to v souladu s tradicí (pozorovatelská astronomie je stále ze své podstaty geoecentrická).
Nyní už můžeme napsat (a otestovat) funkci počítající uhlovou vzdálenost ($delta$):
import math
def angle_between(planet1, planet2):
delta = abs(planet1.hlong - planet2.hlong)
return delta if delta < math.pi else 2*math.pi - delta # úprava na konvexní úhel
angle_between(mars, earth) # vrací float číslo (úhel v radiánech)
Ve výrazu, který je argumentem příkazu return je použita nová konstrukce: podmíněný výraz. Jedná se o obdobu příkazu if který však lze použít na místě výrazu (tj. vyhodnotí se na nějakou hodnotu/objekt). Je tak méně obecnější než příkaz if (v něm lze provádět i jiné činnosti, než je získávání nějaké výsledné hodnoty), může však výrazně zkrátit některé běžně používané zápisy.
Základní syntaxe podmíněného výrazu je:
<výraz-then> if <podmínka> else <výraz-else>
Nejdříve se vyhodnotí podmínka, pokud je pravdivá, pak se vyhodnotí výraz-then, jehož hodnota se stane hodnotou vělého podmíněného výrazu. Pokud je podmínka nepravdivá vyhodnotí se výŕaz-else, jehož hodnota se stane hodnotou celého výrazu. V následujícím příkladě se výraz vyhodnotí na jeden z řetězů (kontext viz https://en.wikipedia.org/wiki/2_%2B_2_%3D_5 (2+2=5).
"Svet je (zatim) v poradku" if 2 + 2 == 4 else "Velký bratr Vás sleduje"
Nyní již můžeme navrhnout klíčovou funkci, která pro danou množinu planet a konkrétní čas ověří, zda se všechny poaždované planety vejdou do výseče zadané velikosti (typicky malé, aby bylo možno tvrdit, že planety jsou soustředěny na jediné polopřímce).
Princip je jednoduchý: pro každou dvojici planet se zjistí jejich vzájemná úhlová vzdálenost (na to už máme funkci). Pokud je větší než limitní úhel (výseč), pak rovnou můžeme vrátit False. Pokud jsou všechny úhlové vzdálenosti menší pak (po ukončení cyklu) můžeme vrátit True.
def all_inline(planets, ephem_time, sector_angle):
"""
planets: seznam ephem objektů planet zahrnutých do výpočtu
ephem_time: čas výpočtu (v ephem representaci)
sector_angle: úhel výseče do níž se musí vějít všechny planety (ve stupních)
"""
# nejdříve přepočteme limitní úhel
sector_angle = math.radians(sector_angle)
# pak musíme všechny objekty planet aktualizovat (přepočítat) pro dané datum
for planet in planets:
planet.compute(ephem_time)
# a nyní už budeme kontrolovat všechny možné dvojice
for planet1 in planets: # vnější cyklus
for planet2 in planets: # vnitřní cyklus
if angle_between(planet1, planet2) > sector_angle: # tato dvojice se nevejde do výseče
return False # už nemusíme dál hledat
# po ukončení cyklu je zřejmé, že planety se do výseče vejdou (i ty nejvzdálenější mají mezi sebou menší úhel)
return True
# připravíme si seznam planet (Zemi zastupuje Slunce, viz výšeb)
innerPlanets = [ephem.Mercury(), ephem.Venus(), ephem.Sun(), ephem.Mars()]
all_inline(innerPlanets, ephem.now(), 30)
Řetězec umístění v trojitých uvozovkách na začátku těla funkce slouží jako tzv. dokumentační řetězec. Není určen pro překladač Pythonu (ten je ignoruje), ale pro programátory využívající příslušný kód. Dokumentační řetězec využitý u funkce typicky obsahuje popisy parametrů, návratové hodnoty apod. Typicky se zobrazuje v různých nápovědách (zkuste v notebooku zadat ?all_inline).
Upozornění: Řetězec je interpretován dokumentační, protože je umístěn přímo na začátku těla funkce (nikam se nepřiřazuje ani není použit jako parametr). Trojité uvozovky (resp. trojité apostrofy) lze použít u jakéhokoliv řetězce. Jediný rozdíl od běžného řetězcového literálu (v jednoduchých uvozovkách) je možnost využití normálního odřádkování uvnitř literálu (proto se tento zápis běžně označuje jako víceřádkový řetězcový literál). V běžném řetězci musí být odřádkování representováno escape sekvencí \n (což není právě přehledné).
Klíčovou částí funkce je dvojice cyklů přes objekty planet, z nichž jeden je vnořen do druhého. Program nejdříve vstoupí do první iterace vnějšího cyklu, v němž proměnná planet1 označuje Merkur. V rámci této iterace vnějšího cyklu se provedou všechny iterace vnitřního tj. planet2 označuje postupně Merkur, Venuši, Zemi a Mars. Volání funkce angle_between tak kontroluje úhlovou vzdálenost dvojic Merkur–Merkur, Merkur–Venuše, Merkur–Země a Merkur–Mars. Tím je dokončena první iterace vnějšího cyklu. V druhé iteraci vnějšího cyklu planet1 označuje Venuši. Nyní se tedy kontrolují vzdálenosti od Venuše ke všem planetám (Venuše–Merkur, Venuše–Venuše, Venuše–Země a Venuše–Mars). Ve třetí iteraci vnějšího cyklu se kontrolují vzdálenosti od Země (dvojice si doplníte již sami). V poslední iteraci vbějšího cyklu první planetou Mars a v pozici druhé se unovu vystřídají všechny (poslední dvojicí je Mars–Mars).
Výsledkem použití vnořeného cyklu je tudíž iterace přes všechny dvojice planet včetně dvojic v nichž se dvakrát opakuje stejná planeta. To výsledek neovlivní neboť úhlová vzdálenost je v případě těchto homogenních dvojic vždy nulová. Mírně se tím snižuje efektivita (výpočet je zcela zbytečný). Zjednodušený algoritmus však přináší i mnohem výraznější neefektivitu, neboť každá vzdálenost mezi dvěma různými planetami je ve skutečnosti počítána dvakrát (stačí si uvědomit, že např. vzdálenost Merkur–Venuše je stejná jako Venuše–Merkur). Naštěstí zpomalení není dvojnásobné, neboť ve většině případů dojde k brzkému předčasnému ukončení.
Samotný výsledek není příliš zajímavý. S vysokou pravděpodobností je výsledkem False (planety jsou nejsou ve stejném sektoru z pohledu od Země). Navíc to rozhodně nestačí na ověření správnosti.
Správnost kódu můžeme ověřit až v cílovém skriptu, který se pokusí nalézt nejbližší těsné zarovnání planet (alespoň těch vnitřních, kde k zarovnání dochází relativně často).
Kód tohoto skriptu je přímočarý. Stačí použít cyklus v němž budeme postupně procházet jednotlivé dny a ověřovat, zda nedošlo k požadovanému jevu (planety ve výseči). Jednodenní krok v zásadě stačí pro výseče o velikosti 20° a více, neboť i ta nejrychlejší z planet se v průběhu jednoho dne posune (vzhledem k Slunci) o maximálně jednotky stupňů (jaká je nejrychlejší?).
Pro posun v kalendáři využijeme interní (avšak veřejnou!) implementaci času v modulu ephem. Čas je representován jako počet dnů od poledne 31.12 1899 (denní čas je representován jako desetinná část, tj. číslo je třídy float).
now = ephem.now()
print(now) # přetypování na str, lidský formát
print(float(now)) # přetypování na float, interní formát
print(ephem.Date(now + 1)) # přičtením jedničky získáme počet dnů representujících časový okamžik za 24 hodin
# výsledkem je `float` které musíme převézt zpět na datum (= objekt třídy `ephem.date`)
now = ephem.Date("2000/1/1") # začneme na začátku století
for day in range(36525): # pro všechny dny v celém století
checkedTime = ephem.Date(now + day) # polohu testujeme v čase: nyní + `day` dnů
if all_inline(innerPlanets, checkedTime, 18): # pokud jsou všechny ve výseči 18°
print(checkedTime)
Pro kontrolu jsem si zobrazil polohy vnitřních planet ve středu nejbližšího nalezeného intervalu (prosinec 2037) [zdroj program Skychart, https://www.ap-i.net/skychart/en/start].
Úkol: Upravte program tak, aby vypisoval úhel nejmenší výseče, v níž se nacházejí vnitřní planety.
Jedním z mechanismů programování, které úzce souvisejí s funkcemi (obecně podprogramy) je rekurze.
Princip rekurze se používá i v matematice, tam je to však relativně okrajové téma.
Podívejme se například na jednu z možných definicí faktoriálu:
$n!=n\cdot (n-1)!$
$1! = 1$
Všimněte si, že v této definici je faktoriál čísla $n$ definován pomocí jiného faktoriálu (čísla $n-1$). Například faktoriál 3! je podle této definice roven $3\cdot 2!$. Pokud použijeme definici znovu získáme hodnotu $3\cdot 2\cdot 1!$, což po dosazení podle druhého vztahu vede na $3\cdot 2\cdot 1 = 6$.
Tento tzv. rekurzivní vztah vede k vnořené aplikaci rekurentního pravidla, které však skončí po konečném počtu kroků použitím tzv. koncového již nerekurentního pravidla.
Tuto definici lze velmi snadno realizovat i v Pythonu:
def factorial(n):
return 1 if n== 1 else n*factorial(n-1) # pro procvičení podmíněný výraz
factorial(3)
Rekurzi snadno poznáte podle toho, že se v těle funkce volá tatáž funkce (s jinou hodnotou parametru), tj. funkce volá sebe samu.
Použití takto jednoduchých rekurzí v Pythonu však vede ve většině případů k značně neefektivnímu kódu. Například faktoriál lze mnohem efektivněji naprogramovat pomocí cyklu.
Někdy může vést použití rekurze k extrémně pomalému kódu. Klasický (proti)příklad je rekurzivná výpočet Fibonnaciho posloupnosti:
$F(n)=\left\{{\begin{matrix}1&&{\mbox{pro }}n\in{0,1};\ \ \,\\F(n-1)+F(n-2)&&{\mbox{jinak.}}\end{matrix}}\right.$
Jinak řečeno n-té Fibonnacciho číslo je součtem dvou předchozích členů posloupnosti, přičemž posloupnost začíná dvěmi jedničkami. Dalšími členy posloupnosti je 2, 3, 5, 8, 13, atd.
I tuto rekurzivní definici lze přímo zapsat v Pythonu:
def slow_fibbo(n):
if n <= 1:
return 1
else:
return slow_fibbo(n-1) + slow_fibbo(n-2)
slow_fibbo(5)
Pro malá čísla je doba výpočtu okamžitá. Ale již pro malé desítky, je doba výpočtu neakceptovatelná (pro hodnoty kolem 50 můžete čekat na výsledek celý den.
%timeit slow_fibbo(38)
Důvodem enormní pomalosti je skutečnost, že pro výpočet n-čísla je nutno volat dvakrát funkci slow_fibbo na první úrovni vnoření, poté čtyři volání na druhé (dvě volají dvě), na třetí osm (dvě volají dvě, z nichž každá pak volá dvě). I když některé větve relativně rychle skončí, je celkový počet volání roven číslu $k^2$, kde $k$ je koeficient ležící v intervalu $(1,2)$.
Počet operací tak roste ecponenciálně tj, již pro relativně malé hodnoty $n$ je doba výpočtu neexceptovatelná. Pro $n$ v řádu malých stovek by nemusela stačit ani geologický eon (řádově miliarda let, viz https://cs.wikipedia.org/wiki/Eon).
Úkol: Odhadněte konstantu (základ) $k$ pro rekurzivní algoritmus výpočtu Fibonnaciho čísel. Odhadněte dobu trvání výpočtu pro $n$ = 120 (v rocích).
Nepříjemnou skutečností je, že existují i relativně elementární problémy, které nelze řešit jinak než v exponenciálním čase. Typickým příkladem je problém obchodního cestujícího (https://cs.wikipedia.org/wiki/Probl%C3%A9m_obchodn%C3%ADho_cestuj%C3%ADc%C3%ADho).
Fibonnacciho čísla však nejsou tento příklad. Je totiž zřejmé, že pokud začneme s počátečními hodnotami 1,1, pak výpočet každé následující hodnoty vyžaduje pouze jednu operaci sčítání: 1+1 = 2, 1+2 = 3, 2+3 = 5, 3+5 = 8, atd. Stačí tedy napsat cyklus který mění hodnoty dvou proměnných (pro výpočet stačí znát jen poslední dvě čísla).
def fibo(n):
a = 1
b = 1
for i in range(n-1): # končíme u n-2 včetně (první dvě čísla nemusíme počítat)
a, b = b, a + b # toto je jádro výpočtu, proměnná b je nové Fibonnaciho číslo (a je to předešlé)
return b
fibo(5)
A výpočet 120 čísla nebude trvat geologické věky :) Rozdíl je opravdu propastný u mne to trvá jen cca 5,3 mikrosekund.
%timeit fibo(120)
Nyní tedy již víme, kdy rekurzi nepoužít. Existují vůbec situace, v nichž je rekurze ekceluje?
Řešený příklad:
Navrhněte funkci, která najde prvek v seřazeném (setříděném) seznamu a vrátí jeho index.
Nejdříve si připravme setříděný seznam větší velikosti:
import random
# nesetříděný seznam milionu náhodných celých čísel z rozsahu nula až miliarda
biglist = [random.randint(0,10**9) for _ in range(1_000_000)]
# přidáme prvek, který budeme hledat (pravděpodobnost, že tam již je je téměř nulová)
wanted = 420_420_420
biglist.append(wanted)
# a pole setřídíme
biglist.sort()
# pro kontrolu zobrazíme prvních deset členů
print(biglist[:10])
# vyzkoušíme vestavěné řešení
print(biglist.index(wanted))
Metoda index prohledává postupně všechny prvky, dokud nenarazí na hledaný, takže pro velké seznamy není příliš efektivní (čas hledání roste lineárně s počtem prvků, takže čas je i tak stále relativně rozumný)
%timeit biglist.index(wanted)
Můžeme však být ještě efektivnější. Stačí si vzpomenout jak lze hledáte konkrétní stránku v knize. Můžeme sice postupně odlistovat (stránku za stránkou), ale v encyklopedii o 1000 stránkách může takovéto hledání např. stránky 420 trvat pár minut.
Samozřejmě existuje snadnější cesta: pokud známe celkový počet stran, pak odhadneme v jaké části je hledaní stránka (v našem případě tak ve 2/5 encyklopedie) a přibližně v této části knihu otevřeme. Pokud máme to nehorázné štěstí a otevřeme na stránce 420 pak máme hotovo. Pravděpodobněji však knihu otevřeme na některé blízké např. na straně 400. Nyní můžeme algoritmus opakovat, tentokrát na té části knihy mezi otevřenou stránkou (kam si dáme záložku) a koncem knihy (hledaná stránka má vyšší číslo). Nyní odhadneme, že knihu otevřeme cca na 1/30 tj. cca 3% od záložky. I nyní se můžeme trefit, pokud však ne, pak budeme blízko např. na straně 430. Nyní použijeme základní algoritmus potřetí tentokrát na tu část knihy mezi stránkami 400 (záložka) a 430 (aktuálně otevřená). Nyní je zřejmé, že otevření provedeme ve cca 2/3 těch 30 stránek. Nyní už je relativně velká šance, že se trefíte (pokud ne pak stačí jen pár dalších otevření).
Tento algoritmus můžeme použít i na náš seznam, i když čísla netvoří souvislou řadu. Musíme pouze počítat s tím, že hledané číslo nemusíme najít. V tomto případě stejně jako metoda index vrátíme -1. Bohužel ošetření této možnosti trochu komplikuje program.
def fastindex(s, wanted, leftindex, rightindex):
"""
s: prohledávaný řetězec
wanted: hledaný prvek
leftindex: index počáteční (od něj se hledá)
rightindex: index (až po něj včetně se hledá)
"""
# print(leftindex, rightindex) # jen pro účely ladění (zkuste odpoznámkovat)
if rightindex < leftindex: # klíčová koncová podmínka (v případě neexistence prvku)
return -1
left = s[leftindex] # položka na levém indexu (první = minimální)
right = s[rightindex] # položka na pravém indexu (poslední = maximální)
if wanted < left or wanted > right: # hledaná hodnota leží mimo rozsah
return -1 # nemůže být tedy nalezena
interindex = leftindex + (rightindex-leftindex) * (wanted - left) // (right - left)
if s[interindex] == wanted: # prvek nalezen
return interindex
if s[interindex] < wanted: # prvek je v pravém segmentu
return fastindex(s, wanted, interindex, rightindex-1)
else: # resp. v levém
return fastindex(s, wanted, leftindex+1, interindex)
fastindex(biglist, wanted, 0, len(biglist)-1)
Klíčovou částí těla funkce je výpočet odhadované pozice indexu, pomocí jednoduché lineární interpolace. Pokud známe hodnotu prvního prvku (s indexem leftindex) a hodnotu prvku posledního (index rightindex), pak při předpokladu rovnoměrného růstu prvků můžeme odhadnout pozici hledaného prvku jednoduchou interpolací (viz obrázek).
Interpolační úsečka spojuje body s pozicí (leftindex, left) a (rightindex, right). Pokud známe tuto úsečku není problém najít pro hledanou hodnotu wanted index, který jí podle interpolační úsečky přísluší. Tento index označíme jako interindex.
Všimněte si, že pokud je hledaný prvek roven prvnímu (levému) je výsledek interpolačního výrazu skutečně roven levému indexu (leftindex + 0 / (right - left)), stejně tak je roven pravému indexu při hodnotě (wanted == right).
Pokud interdidex indexuje hledaný prvek (s[interindex] == wanted) pak tento index vrátíme. Jinak se provede rekurzívní volání. V kterém však budou použit jiný levý a pravý index. Pokud je hledaná hodnota v levé části, pak bude vnořené volání hledat mezi indexy leftindex+1 a interindex (včetně), v případě pravé části mezi indexy intertindex a rightindex (opět včetně). V našem případě lze toto vnořené hledání znázornit dalším obrázkem.
Klíčovou roli má posunutí levého resp. pravého okraje na následující (leftindex+1) resp. předcházející položku (rightindex-1). Tím se prohledávání nejen o něco urychlí (je zbytečné opět kontrolovat krajní položku), ale především se zabrání vzniku nekonečné rekurze (tj.do potenciálně nekonečná vnořená volání, která v realitě vedou po krátké době k předčasnému ukončení programou výjimkou). I tento drobný posun totiž zajištuje, že nový interval je vždy menší než předchozí (a to i tehdy pokud interindex padne do některého z původních krajních). Protože v každém kroku zmenšujeme konečný interval indexů, je zřejmé, že volání musí někdy skončit (tj. je dosaženo stavu rightindex < leftindex).
Tato situace nastává i v našem případě, kde výpočet interindexu vede k hodnotě 5 (výsledek by měl být cca 5,6, ale díky použití celočísleného dělení se zaukrouhlí na 5). Interindex je tak roven původnímu levému indexu. Hodnota se ani v tomto kroku nenajde, a tak nové rekurzivní volání bude hledat v rozsahu 5 (interindex) až 6 (rightindex - 1). Na této úrovni už hledanou hodnotu najde (je na pozici rightindex).
A nyní ověříme, že se hledání urychlilo.
%timeit fastindex(biglist, wanted, 0, len(biglist)-1)
Zrychlení je úžasné, namísto původních cca 25 ms máme 1.5 µs, tj. zrychlení je v řádu desetitisíců. Důvod je zřejmý: namísto miliónů porovnání se provedlo jen pár vnořených volání (v řádu $\log\log(n)$). Tj. i pro pole rozsahu biliónů položek by hledání trvalo jen pár milisekund (taková pole se však už bohužel nevejdou do operační paměti současných stolních a přenosných počítačů).
Úkol: Interpolační hledání je pro vhodná data tím nejrychlejším exitujícím algoritmem. V praxi se však více implementuje tzv. binární vyhledávání (vyhledávání půlením intervalu). Nevyžaduje rovnoměrný nárůst prvků (stačí jen jsou-li seřazené) a má menší režii v každém rekurzivním volání.
Tento algoritmus se od interpolačního liší jiným výpočtem mezilehlého indexu (
interindexv našem programu). Vypočítá se jako (celočíselný) průměr levého a pravého indexu(rightindex + leftindex)//2(je tedy přibližně uprostřed)Vytvořte funkci pro binární vyhledávání (se stejnými parametry jako má funkce
fastindex) a ověřte na našem testovacím velkoseznamu její efektivitu.
Parametry funkcí nemusí být pouze čísla, řetězce, kolekce resp. jiné datové objekty. Parametrem funkcí mohou být i (jiné) funkce. Podobně je tomu i u návratových hodnot, tj. funkce může vracet jinou funkci. Tato možnost otvírá zcela netušené možnosti, z nichž ty nejzajímavější jsou bohužel mimo zaměření této výukové opory. Některé jednodušší případy užití si však ukázat můžeme.
Nejdříve malý příklad:
def nest(function, x, n):
for i in range(n): # n-krát
x = function(x)
return x
Tato funkce zajišťuje vícenásobné vnořené volání předané unární funkce function na hodnotu x (počet volání určuje parametr n). Jinak řečeno funkce vrací hodnotu f(f(f....f(x))), kde počet vnořených volání funkce je roven n.
import math
nest(math.cos, 10, 100)
Praktičtější příklad využití funkce jako parametru si můžeme ukázat s využitím baláčku matplotlib, který slouží pro vizualizaci dat pomocí grafů. Tato knihovna je navíc dobře integrována do Jupyter notebooku.
Ukažme si jako příklad vykreslené grafu funkce sinus. Program má v zásadě čtyři části:
magický příkaz %matplotlib (začínající znakem %), který zajistí zobrazení interaktivního výstupu v notebooku. Parametrem je jméno ovladače (zde je použit notebook zajišťující interaktivní zobrazení, využít lze i inline).
příprava zobrazovaných dat. Matplotlib nezobrazuje funkce, ale datové body, které jsou zadány jako vektor x-ových a vektor y-souřadnic (kde vektor může být například representován seznamem). Vektor x-ových hodnot je typicky posloupnost hodnot od $x_{min}$ do $x_{max}$ s určitým krokem (doporučuji řádově malé stovky bodů). Vektor y-je v našem příkladě vytvořen aplikací funkce na každou hodnotu x pomocí seznamové komprehenze.
nastavení grafu (rozsahů, dekorací). V našem případě je zapnuta mřížka.
vykreslení grafu pomocí funkce plot
%matplotlib notebook
import matplotlib.pyplot as plt
xs = [0.1 * i for i in range(0,100)] # x-ové hodnoty (od nula do 10, s krokem 0.01)
ys = [sin(x) for x in xs] # y-ové hodnoty
plt.grid(True) # zapnutí mřížky
plt.plot(xs, ys) # vytvoření grafu
Pokud chceme primárně vykreslovat funkce zadané předpisem (včetně funkcí elementárních) je rozhraní modulu pyplot poněkud nepřehledné (zobrazovaná funkce je ukryta uvnitř komprehenze). Vytvořeme se proto pomocnou funkci.
%matplotlib notebook
def fplot(f, xmin=0, xmax=10, points = 150):
step = (xmax-xmin)/points
xs = [step * i for i in range(points)] # x-ové hodnoty (od nula do 10, s krokem 0.01)
ys = [f(x) for x in xs] # y-ové hodnoty (volání předané funkce na všechny x)
plt.grid(True) # zapnutí mřížky
plt.plot(xs, ys, label=f.__name__) # vytvoření grafu
def myf(x): # lze si samozřejmě připravit i vlastní funkci jedné reálné proměnné
return 1.2*math.sin(2.0*x + math.pi/4) + 0.8*math.cos(3*x + math.pi/8)
fplot(math.cos)
fplot(math.sin)
fplot(myf) # zobrazení vlastní funkce
plt.legend()
Tělo funkce fplot (=function plot) obsahuje v zásadě stejné příkazy jako základní ukázka. Kromě zobrazované funkce lze funkci předat i meze osy x a počet bodů, v nichž se počítá funkce (u všech těchto parametrů jsou uvedeny rozumné implicitní hodnoty).
Jedinou novinkou je uvádění popisek (label) funkcí ve volání plot (předány jsou jako pojmenovaný parametr). Jako popisek se použije jméno funkce, které lze získat pomocí atributu __name__ jména funkce (dvě podtržítka na začátku a dvě na konci, atributy takto označené jsou poskytovány přímo na úrovni jazyka). Popisky funkcí jsou následně využity při tvorbě legendy (ta je přidána volání funkce plt.legend).
Úkol: Popisky získané z interních jmen funkcí nejsou v některých případech příliš užitečné. Rozšiřte funkci
fplottak, aby je bylo možno zadat explicitně (interní jméno zůstane jako implicitní volba pokud popisek nezadáte).Rada: Implicitní hodnoty nemohou odkazovat jiné parametry (implicitně jsou vyhodnocovány v místě definici funkce, nikoliv při jejím volání).
Při volání funkce není nutné, aby byly předávané objekty předem opatřeny proměnnou. Na místě parametrů lze přímo využívat konstanty nebo výrazy (nikoliv jen proměnné).
import math
math.log2(16)
V případě předávání funkcí je situace složitější, neboť zatím umíme vytvářet funkce jen prostřednictvím definice pojmenované funkce.
Podívejme se například na metodu list.sort. Tato metoda, s níž jsme se seznámili již v předchozí části opory, řadí obsah seznamu vzestupně.
s = [-1, 3, 4, -6, 5, 4]
s.sort() # řadí se přímo pole `s` (nevytváří se nové)
print(s)
Pomocí pojmenovaného prametru reverse lze zařídit setřídění v opačném směru (tj. sestupně).
s = [-1, 3, 4, -6, 5, 4]
# nový seznam (původní je již setříděný i když vzestupně)
s.sort(reverse=True)
print(s)
V praxi však máme na řazení větší požadavky. Čísla můžeme chtít setřídit na základě jejich absolutní hodnoty.
V tomto případě se nám hodí další pojmenovaný parametr metody sort s názvem key. Tento parametr očekává funkci, která určuje podle čeho se bude řadit. Zatímco v běžném případě vychází z uspořádání mezi prvky seznamu (tj. se například testuje zda $s_i < s_j$) při použití parametru key se vychází z uspořádání hodnot získaných voláním klíčové funkce na prvky tj. testuje se například $key(s_i) < key(s_j)$. Použití klíčové funkce přitom nikterak nemění hodnoty v uspořádaném poli (ty jsou stejné jako v původním poli jen v jiném pořadí).
Pokud tedy chceme čísla upořádat podle absolutních hodnot, stačí zadat funkci abs na místě klíčové funkce.
s = [-1, 3, 4, -6, 5, 4]
s.sort(key=abs) # řadí se podle aboslutní hodnoty prvků
print(s)
Problém nastane, pokud je klíčová funkce složitější. Zkusme například seznam seřadit tak, že nejdřívě budou lichá čísla a pak sudá. Klíčovou funkcí by tak mohl být výraz 1 - x % 2. Tuto funkci však nelze předat zápisem výrazu na místě parametru:
s = [-1, 3, 4, -6, 5, 4]
s.sort(key=(1-x%2)) # chyba : parametrem nemůže být výraz využívající nedef. proměnnou
print(s)
V našem případě program končí chybou typu, neboť výsledkem vyhodnocená výrazu 1 - x%2 je (celé) číslo nikoliv funkce. Problém je však hlubší. Výraz totiž využívá proměnnou x, která je v daném kontextu nedefinovaná. U nás má sice nějakou hodnotu (je to seznam), ale je to jen náhoda, neboť jsme někdy dříve definovali globální proměnou x. Její hodnotu neznáme a ani znát nemusíme. Při použití v prázdném notebooku resp. skriptu by byla nedefinovaná i de iure (tj. program by skončil s výjimkou odpovídající použití nedefinované (neznámé proměnné).
Zatím známe jediné řešení tohoto problému — vytvoření pomocné funkce a následně její předání do řadící metody:
def evenToOne(i):
return 1 - i % 2 # převádí liché na 0 sudé na 1
s = [-1, 3, 4, -6, 5, 4]
s.sort(key=evenToOne) # použití námi definované funkce jako klíčové
print(s)
Toto řešení opravdu funguje! Na začátku jsou lichá čísla (-1,3,5) na konci sudá (4,-6,4). Všimněte si také, že skupina lichých i sudých čísel zachovává původní pořadí svých členů. Je to důsledek důležitého rysu řadící metody sort, ta je tzv. stabilní tj. zachová pořadí prvků, pokud se z hlediska řazení rovnají (tj. klíčová funkce vrací stejnou hodnotu).
Vytváření takto jednoduchých pomocných funkcí (tvořených pouze příkazem return následovaným výrazem pro výpočet návratové hodnoty) je však ve většině případů zbytečné, neboť Python podporuje vytváření dočasných (a nepojmenovaných) funkcí pomocí tzv. lambda konstrukce (název je převzat z matematické teorie $\lambda$-kalkulu, která jako první přinesla matematický model univerzálních počítačů).
s = [-1, 3, 4, -6, 5, 4]
s.sort(key=lambda i: 1 - i%2)
print(s)
Konstrukce začíná klíčovým slovem lambda, pak následuje seznam jmen proměnných, které budou označovat parametry (zde je to jediná promměná označená i). Tato proměnná je viditelná jen ve výrazu, který následuje po dvojtečce a tvoří tělo vytvářené funkce). Stejně jako u běžných funkcí vzniká tato proměnná při volání funkce (pokaždé nová) a po skončení (tj. zde vyhodnocení výrazu v těle) zaniká. V našem případě tedy vznikne šest instancí této proměnné (funkce je volána na každý prvek seřazovaného seznamu).
Poznámka: Použití metody sort pro výše uvedené přeuspořádání seznamu (liché vlevo, sudé vpravo) není příliš efektivní. Nejefektivnějším způsobem je prohazování špatně umístěných čísel, podle algoritmu uvedeného v dříve (tam jsme seznam rozdělili na část menší než průměr a větší než průměr).
Přibližně stejně efektivní (ale náročnější na paměť) je algoritmus (již dříve uvedený), který extrahuje nejdříve lichá čísla (pomocí komprehenze), k nimž připojí vyextrahovaná sudá. Tento přístup je navíc pro většinu lidí a programátorů čitělnější. Nemění také původní seznam (vznikne nový přeuspořádaný), což může být výhoda.
s = [-1, 3, 4, -6, 5, 4]
news = [i for i in s if i%2 == 1] # vrací seznam všech lichých
news.extend([i for i in s if i%2 == 0]) # metoda extend přidá do svého adresáta (news) obsah svého parametru (sudá)
print(news)
> **Úkol**: Vyzkoušejte slov řazení podle (anglické) abecedy.
Úkol: Navrhněte kód, který uspořádá seznam jmen členů Společenstva prstenu podle délky jména (=počet znaků ve jméně) vzestupně. Jména o stejné délce by měla být řazena podle abecedy. Členové jsou "Frodo", "Gandalf", "Aragorn", "Sam", "Smíšek", "Pipin", "Legolas", "Gimli", "Boromir".
Rada: nejdříve je nutné třídit podle sekundárního klíče (abecední řazení), pak podle primárního (délka řetězce)
Python je jak již bylo řečeno multiparadigmatický programovací jazyk tj. podporuje více různých stylů programování. Přesto však jen jedno z těchto tzv. paradigmat ovlivňuje Python na všech úrovních programování — objektově orientované paradigma (OOP).
Ve světě tohoto paradigmatu existují tzv. objekty, které mají svou jedinečnou identitu, mění své stavy, vznikají a zanikají. Především však spolu komunikají prostřednictvím volání metod ze svých rozhraní (rozhraní je množina metod, které objekty nabízejí ostatním objektům).
Objekty se navíc seskupují do tříd. Třída obsahuje objekty se stejným rozhraním, tj. objekty, které se při komunikaci chovají podobně (resp. stejně, pokud mají identický vnitřní stav).
S objekty a třídami jsme se již setkali. Čísla jsou v Pythonu velmi jednoduché objekty, které jako své rozhraní nabízejí běžné aritmetické operace. Základními číselnými třídami jsou třídy int (representace celých čísel) a float (representace racionálních čísel pomocí tzv. pohyblivé řádové čárky). Mezi jednoduché objekty patří i objekty třídy bool (jsou jen dva True a False).
O něco složitější jsou objekty třídy string označované jako řetězce. Ty nabízejí ve svém rozhraní větší počet metod. S číselnými objekty však sdílejí dvě podstatné a vzájemně provázané vlastnosti: jsou neměnné a a z vnějšího pohledu existuje vždy jen jeden objekt se stejným stavem (např. z vnějšího pohledu existuje jen jeden objekt čísla 2). I když interně může existovat více kopií stejného objektu, jsou tyto objekty nerozlišitelné.
To již neplatí pro seznamy a mnohé další složitější objekty. Ty svůj stav v průběhu svého života mění, a jejich identita je nezávislá na jejich obsahu.
a = [1]
b = [1]
Proměnné a a b označují dva různé objekty, které mají stejný stav (u seznamů je stav určem jejich obsahem). Že tomu tak skutečně je poznáme, tím, že jeden objekt změnéme například přidáním prvku:
a.append(2)
print(a) # objekt odkazovaný proměnnou `a` se změnil
print(b) # objekt odkazovaný proměnnou `b` zůstal nezměněný (tj. oba objekty se liší už i obsahem)
Mezi další proměnné individuální objekty, které již známe patří objekty planet z modulu ephem nebo objekty representující shodu regulárního výrazu s řetězcem (tzv. match objekty).
Naši paletu tříd si ještě rozšíříme o třídy ze dvou klíčových modulů. Začneme standardní representací časových a kalendářních údajů v Pythonu.
I když se to na první pohled nezdá, je representace časových údajů jednou z nejkomplikovanějších problémů tvůrců standardních knihoven. Důvodem je relativně komplikovaný kalendářní systém a ještě komplikovanejší systém časových pásem s jejich stálými posuny a šílenými pravidly pro jejich každoroční posuny (tzv. letní čas). Proti tomu jsou problémy dané vkládáním přestupné sekundy jen detail (může to však značně zkomplikovat program očekávající milisekundovou přesnost).
Ve skutečnosti se ukazuje, že žádná univerzálně použitelná representace času neexistuje. Proto i ve standardní knihovně Pythonu jich existuje několik, a mnohé další přidávají knihovny třetích stran (jako např. pyephem).
Základní a všeobecně podporovanou representaci nabízí standardní modul datetime. Tato implementace se zaměřuje na podporu representace času v moderní době a moderním gregoriánském kalendáři (tj. cca po první světové válce) s přesností na úrovni minut či desítek vteřin. Hodí se tak pro běžné historické a průběžné databázové záznamy (datumy narození, časy prodejů apod.) a ekonomickou statistiku. Nepodporuje však správně časová pásma. Nezohledňuje totiž posuny časů v různých místech jako je změna pásmového času (v některých zemích se to děje i několikrát v desetiletí) a především letní čas.
Jednodušší rozhraní nabízí knihovna pro ty aplikace, které se nemusí starat o časová pásma a využití letního času.
import datetime
dnes = datetime.date.today() # dnešek jako kalendářní den
den_d = datetime.date(1989, 11, 17) # vytvoření kalendářního dne z roku, měsíce a dne
print(dnes) # vypsání v univerzálním textovém formátu
print(den_d.year) # vypsání atributu (rok)
Objekty jsou nejčastěji vytvářeny tzv. konstruktory. Konstruktor je funkce, která má stejné jméno jako třída (v našem případě vytváříme třídy datetime.date tj. voláme konstruktor datetime.date).
Nové objekty však často vznikají pomocí tzv. třídních metod. Tyto metody se nevolají nad objekty, ale nad třídami. Příkladem třídní metody je metoda today, která se volá nad třídou datetime.date (třída je ve volání uvedena vlevo od tečky, za níž následuje jméno metody). Bohužel vše je trochu matoucí, neboť Python používá tečku k několika různým účelům:
1) oddělení jména modulu od jména funkce nebo proměnné, pokud přistupujeme k funkci/proměnné z importovaného modulu
datetime.date(2000,1,1) # volání funkce (konstruktoru) z modulu `datetime`
import math
math.pi # proměnná umístěná v modulu `math`
2A) oddělení objektu (adresáta) a jména metody (metoda je volána nad objektem)
"test".replace("t", "r") # metoda je volána nad objektem "test"
x = [1]
x.clear() # metoda je volána nad objektem označeným proměnnou `x`
2B) oddělení objektu a jeho atributu (vlastnosti)
Atribut si lze představit viditelnou vlastnost objektu. Vlastnost je buď součástí vnitřního stavu nebo ji lze z vnitřního stavu jednoznačně odvodit. Na rozdíl od volání metody nenásledují za atributem závorky se seznamem parametrů.
import re
match = re.fullmatch("[A-z][a-z]*", "Gondor") # test shody řetězce s regulárním výrazem
match.string # atributu objektu representujícího výsledek testu (hodnotou je testovaný řetězec)
3A) oddělení jména třídy a jména metody při volání třídních metod
int.bit_length(42) # volání metody `bit_length` nad třídou `int`
Poznámka: Metoda bit_length vrací počet bitů nutných pro representaci čísla, které je předáno jako parametr.
float.is_integer(2.0)
Metoda is_integer je volána na třídu float. Vrací True, pokud je parametr metody celé číslo (o když representované jako float, pro čísla třídy int je tento test bezpředmětný).
3B) oddělení jména třídy a jejího atributu (atribut patří třídě jako takové nikoliv konkrétním objektům)
from datetime import date
print(date.max) # maximální representovatelné datum
Všechny druhy tečkové notace lze přirozeně kombinovat v jediném zápisu. Následující zápis obsahuje tečku ve všech jejích hlavních rolích.
datetime.date.today().year
Identifikátor datetime označuje jméno modulu. Ten obsahuje třídu date, na níž je možno zavolat třídní metodu today. Tato metoda vrací nový objekt této třídy (representující aktuálné den). Z tohoto objektu následně získáme atribut year (rok příslušného data). Na jednotlivé části zápisu se můžete podívat na následujícím obrázku:
Pokud Vám předchozí výraz připadá složitý, lze jej samoozřejmě rozepsat do několika jednodušších:
from datetime import date # jméno třídy není od této chvíle nutné kvalifikovat modulem
dnes = date.today() # volání třídní metody, výsledkem je objekt třídy `datetime.date
letos = dnes.year # a získáme jeho atribut
print(letos)
Úkol: Vytvořte objekt třídy
datetime.daterepresentující silvestr aktuálního roku. Program by měl využívat metodutoday, aby fungoval v libovolném roce.
Objekty třídy datatime.date nenabaízejí příliš mnoho metod. Mezi ty nejužitečnější patří:
dnes = date.today()
print(dnes.weekday()) # vrací den v týdnu jako číslo (0 = pondělí, 1=úterý atd.)
tyden = ["pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota", "neděle"] # seznam řetězců
print(f"Dnes je {tyden[dnes.weekday()]}")
dnes.strftime("%d.%m.%Y") # převede datumový objekt na řetězec podle formátu
Formát používaný v metodě strftime vychází z unixového příkazu date. Detailní popis by byl únavný, proto uvádím jen několik praktických příkladů.
print(dnes.strftime("%-d.%-m.%y")) # bez počátečních nul a bez století (všimněte si podtržítek)
print(dnes.strftime("%-d.%B %Y, %A")) # dlouhý tvar v implicitní lokalitě (nastavení jazyka) = en_US
Formátovací specifikace příkazu strftime lze využít i v běžném formátovaném řetězci.
f"Dnes je {date.today():%-d.%-m.}"
Dva objekty typu datetime.date lze odečítat.
vznikRepubliky = date(1918,10,28)
dnes = date.today()
rozdil = dnes - vznikRepubliky
print(rozdil)
Výsledkem je objekt třídy datetime.timedelta, která representuje časový interval. Ten je primárně representován v dnech a vteřinách a jejich zlomcích, neboť neexistují žádné vyšší jednotky fixní délky (kromě týdnů, ale ty jsou dost nepraktické) a ostatní hodiny a minuty lze dopočítat.
Objekty třídy datetime.timedelta lze vytvářet i přímo pomocí volání konstruktoru. V konstruktoru lze kombinovat různé standardní intervaly (týdny, dny, hodiny, minuty, sekundy) pomocí pojmenovaných parametrů (jejichž hodnotami nemusí být jen celá čísla)
from datetime import timedelta
interval = timedelta(weeks=3, days=5, hours=5.5, seconds=2.3)
print(interval)
Lze je pak přičítat k objektům datetime.date, čímž lze realizovat zadání typu "za čtrnáct dnů".
dnes + timedelta(days=14)
Úkol: Zjistěte v jakých dnech jste slavili či budete slavit tisiciny, tj. dny kdy žijete $k\times 1000$ dnů (kde $k$ je přirozené číslo).
Časové intervaly lze i dělit (výsledkem je číslo), což se hodí pokud chcete interval vyjádřit jako číslo v předem známých jednotkách.
vznikRepubliky = date(1918, 10, 28)
vyroci = date(2018,10,28)
pocetLet = (vyroci - vznikRepubliky) / timedelta(days=365.25)
print(pocetLet) # máme štěstí, že 100 je dělitelné 4 (proč?)
Úkol: I když je příčítání objektu
datetime.timedeltaužitečné, neřeší všechny požadavky praxe. Zvlášť zapeklité je přičítání měsíců (výsledkem je datum se stejným číslem dne, pokud je to možné, jinak poslední den v měsíci).Příklad: 30.ledna + 1 měsíc je 28. nebo 29. února (podle přestupného roku).
Vytvořte funkci, která přijímá objekt
datea počet měsíců, které mají být přičteny. Funkce vrací datum získané přičtením daného počtu měsíců.Rada: připravte si seznam počtu dnů v jednotlivých měsících. Doporučuji pracovat s měsící číslovanými od nuly.
Kód funkce má tři části. Nejdříve se získají jednotlivá čísla dnů, měsíců a roků. Číslo měsíce je upraveno tak, aby počítání měsíců začínalo nulou (leden) a končilo 11 (prosinec). Jedině tak lze využít operaci zbytek po dělení v další části.
V druhé (nejsložitější části) jsou spočítána nová čísla dnů, měsíců a roků. K číslu měsíce se přičte požadovaný počet měsíců. Výsledkem však může být měsíc mimo rozsah (12 a výše). Namísto měsíce 12, chceme měsíc 0, namísto měsíce 13, měsíc 2 (čísla měsíce se v následujícím roce opakují). Nové číslo měsíce proto získáme jako zbytek po dělení 12. Upravit musíme i rok. Za každých 12 měsíců v součtu (aktuální měsíc + počet přičtených) musíme zvýšit počet roků o jedna (zde využijeme celočíselné dělení). Poté upravíme maximální počet dnů v únoru přestupných roků (test, zda je rok přestupný je zjednodušen, tj. funkce bude fungovat jen do roku 2099). Nakonec upravíme denní číslo, pokud leží mimo rozsah měsíce.
Nakonec vytvoříme nový objekt date (nesmíme zapomenout na
denní čas (0:00 až 24:00) representují instance třídy datetime.time. Použití těchto objektů se příliš neliší od objektů date (rozdíl je pouze v atributech).
from datetime import time
poledne = time(12, 0)
print(poledne)
print(poledne.hour)
print(poledne.minute)
print(poledne.second)
Pro formátování se i zde používá metoda strftime samozřejmě s jinými popisovači.
poledne.strftime("%H:%M:%S")
Další operace (jako je odečítání či přičítaní intervalu) však na tento objekt nelze aplikovat (což je popravdě trochu překvapivé).
Poslední důležitou třídou modulu datetime je třída representující časové okamžiky (angl. timestamps), které nastávají v určitý denní čas v rámci jistého kalendářného dne (data). Tato třída v sobě ve skutečnosti spojuje instance třídy datatime.date a datatime.time.
Jméno třídy je poněkud matoucí, neboť je stejné jako jméno modulu — datetime. Pokud tedy hodláte používat kvalifikované jméno musíte uvádět jméno datetime.datetime.
Rozhraní třídy je v zásadě obdobou třídy datetime.date, pouze přibývají atributy (a ve strftime popisovače) denního času. Více údajů můžete předat i konstruktoru.
pristaniNaMesici = datetime.datetime(1969, 7, 20, 20, 17, 40)
print(pristaniNaMesici)
print(pristaniNaMesici.date()) # datumová část (je to metoda ne atribut)
print(pristaniNaMesici.time()) # denní čas (nápodobně)
print(pristaniNaMesici.year) # k dílčím atributům lze přistupovat i přímo
print(pristaniNaMesici.hour)
Formátování zajišťuje i zde metoda strftime, která podporuje popisovače pro datumové a časové údaje. Vše pochopitelně funguje i ve formátovaném řetězci:
f"Přistávací modul Apolla poprvé přistál na Měsíci dne {pristaniNaMesici:%d.%m.%Y ve %H:%M}"
Aktuální časový okamžik lze získat třídní metodou now. Časové okamžiky lze samozřejmě i odečítat.
rozdil = datetime.datetime.now() - pristaniNaMesici
# výsledek není přiliš přesný oba údaje jsou v různých časových pásmech (now v SELČ, přistáni v UTC)
print(rozdil)
print(rozdil/timedelta(hours=1)) # vyjádření v hodinách a jejích zlomcích
datetime¶Opakem metody strftime je u třídy datetime.datetime třídní metoda strptime. Tato metoda interpretuje řetězec jako textovou representaci data (spolu s případným denním čase) a pokusí se jej podle předaného formátui převést na ob
Je to přirozeně třídní metoda, neboť vytváří nový objekt. Pokud by byla běžnou metodou objektu, pak by tento objekt musel existovat již před volání, čímž bychom se dostali k paradoxu typu "vejce nebo slepice".
vstup = input("Zadej datum: ")
datum = datetime.datetime.strptime(vstup, "%d.%m.%Y")
print(datum)
Všechny výše uvedené příklady využívali tzv. naivní representaci časových údajů bez zohlednění časových pásem. I když se může stát, že časová pásma musí využívat jen programy podporující vstup a výstup v různých časových pásmech, není toumu tak.
zacatek = datetime.datetime(2018, 10, 28) # půlnoc 28.10.2018
konec = datetime.datetime(2018, 10, 29)
interval = konec - zacatek
print(interval) # to je ještě akceptovatelný výsledek (rozdíl je skutečně 1 den)
sekundy = interval.total_seconds()
print(sekundy) # to je určitě špatně
Je zajímavé, že i zdánlivě správný výsledek může být ve skutečnosti špatně :). Problém je v tom, že ne každý den má 24 hodin. Den 28.10. 2018 má 25 hodin, neboť v něm dochází k přechodu z letního na zimní čas. Mezi půlnocí obou dnů tak uběhne 25*3600 = 90 000 sekund.
Řešení není zcela jednoduché, neboť objekty, třídy datetime letní čas nezohledňují (o když podporují časová pásma, ale jen s fixní rozdílem od světového času). Pokud jsou časy vztaženy k časovému pásmu, jež je nastaveno v operačním systému (v Linuxu je pro ČR používáno pásmo Europe/Prague, viz https://en.wikipedia.org/wiki/Tz_database), pak je řešením převod do tzv. POSIXovské epochy (= počet sekund od 1.1.1970), které časová pásma zohledňuje. Metoda datetime.datetime.timestamp vrací tento údaj jako float číslo.
konec.timestamp() - zacatek.timestamp()
Pokud potřebujete úplnou podporu všech časových pásem, lze využít standardní, ale nízkoúrovňový modul time, nebo modul pendulum.
!pip install pendulum
import pendulum
now = pendulum.now()
print(now) # zobrazí se i údajem o časovém pásmu
pristaniNaMesici = pendulum.datetime(1969,7,20,20,17,40, tz='UTC') # časové pásmo UTC
print(pristaniNaMesici)
interval = now-pristaniNaMesici # výsledkem je třída pendulum.Period
print(interval.as_timedelta()) # pro snadnější porovnání jej převedeme na objekt `timedelta`
# pro porovnání
interval2 = (datetime.datetime.now() - datetime.datetime(1969,7,20,20,17,40))
print(interval2)
print((interval - interval2).total_seconds())
Jak je vidět modul pendulum zohlednil různá časová pásma (u data přistání je explicitně uvedeno, že jde o světový čas UTC, funkce now vrací čas se správným časovým pásmem). Komplexnost časových výpočtů ukazuje skutečnost, že ani pendulum nevrátilo správný výsledek, neboť nezohlednilo 27 přestupných sekund vložených od roku 1972 a jen obtížně odvoditelný posun daný tím, že mezi roky 1961 a 1972 se nepravidelně vkládaly skoky 100 ms, aby se čas UTC přizpůsobil rotaci Země (navíc se mírně měnila i délka sekundy). Celkově se však jedná o rozdíl cca 30 sekund.
Jakýkoliv plnohodnotný program musí pracovat s externími daty (vypsání "Hello, world" či výpis jakéhokoliv jiného fixního textu není programem). Externí data jsou typicky zadávána interaktivně uživatelem (viz nám již známou vestavěnou funkci input), čtena z datových souborů nebo jsou získávana z Internetu.
Všechny tyto externí zdroje dat lze sjednotit do abstraktního mechanismu proudu dat (angl. stream). Nejebecnější typem proudu jsou bytové proudy, které poskytují konečnou posloupnost bytů (byte = binární representace čísel 0-255 = 8 bitů).
Kromě bytových proudů se používají i proudy znakové. Znakové proudy můžeme chápat jako konečnou posloupnost znaků (písmen, číslic, symbolů, apod.). Interně se každý znak ukládá či přenáší pomocí jednoho či více bytů. které kódují pozici znaků v nějaké znakové sadě (tabulce jednotlivých znaků).
Poznámka: V dřívějších dobách se používaly znakové sady s maximálně 255 znaky, tj. pozice ve znakové sadě byla vyjádřitelná právě jedním bytem. Bytové a znakové proudy se tak lišili jen interpretací přenášených bytů (u znakových bylo navíc nutno znát použitou znakovou sadu). Počet 255 znaků však nestačí pro representaci např. činštiny tím spíše textů s více písmy. V současnosti, tak převažuje znaková sada Unicode (aktuálně cca 140 tisíc znaků). Takový počet znaků vyžaduje samozřejmě vícebytové kódování (existuje více kódování znakové sady Unicode)
Nejjednodušší je vytvoření proudů (bytových i znakových) nad soubory v rámci souborového systému (ten je tvořen typicky daty na lokálních discích, i když do něj lze připojovat i vzdálenější datová úložiště).
Vytvoření a otevření těchto proudů zajišťuje vestavěná funkce open:
stream = open("python_output.txt", "wt")
Prvním parametrem je jméno souboru. To může být buď relativní (pak se vztahuje k aktuálnímu adresáři, což je standardně adresář, v němž je pythonský skript resp. jupyter notebook), nebo absolutní (tvar se liší mezi Windows a Unixem).
Druhý parametr je režim otevření. V našem případě obsahuje znak "w" určující, že proud slouží k zápisu (write) do souboru (tj. my budeme zapisovat) a znak "t" určující, že proud bude textový (a textový tak bude i zapsaný soubor). Pokud soubor existuje pak je před zápisem zkrácen na nulovou délku (tj. náš výstup původní obsah přepíše).
Po otevření můžeme do našeho proudu zapisovat. To zajišťuje metoda se jménem write:
stream.write("One Ring to rule them all\n") # zapisuje do textového proudu celý řádek
stream.write("One Ring to find them\n")
Metoda write vloží znaky postupně do proudu. Všimněte si, že řádky musí
Metoda write vrací počet zapsaných znaků (u nás je to 22 znaků u druhého zápisu). K zápisu však prozatím pravděpodobně ještě nedošlo. Data jsou totiž většinou dočasně umisťována do vyrovnávací paměti. Jistotu budeme mít, až když proud zavřeme.
stream.close()
Každý proud by měl být po použití uzavřen! Nejenže tím zajistíme skutečný zápis na disk, ale uvolníme i prostředky, které pro správu proudu alokoval náš program i operační systém.
Nyní se pokusíme data ze souboru zpětně načíst. Nejdříve soubor znovu otevřeme, tentokrát ale zvolíme jiný režim ("r" jako read, a opět "t" pro textový proud).
stream = open("python_output.txt","rt")
Pokud chceme přečíst celý obsah souboru najednou, pak použijeme metodu read (bez parametrů).
text = stream.read()
Soubor nezapomeneme zavřít (u důvodů úspory prostředků).
stream.close()
print(text) # pro kontrolu text vypíšeme
I když je čtení celého souboru jednoduché, v praxi se příliš nepoužívá. Hlavní důvodem je skutečnost, že v některých textových souborech je členění klíčové členění na řádky (např. každý řádek obsahuje jednu položku dat). U velkých souborů může navíc načtení celého textu vést k zaplnění (či lépe přeplnění) operační paměti.
Pro čtení jednotlivých řádek slouží metoda readline. Ta vrací řádky (zakončené znakem odřádkování). Na konci souboru vrátí prázdný řetězec.
stream = open("python_output.txt","rt") # soubor musíme znovu otevřít
line = stream.readline() # (potenciální) první řádek načteme ještě před cyklem
while line != "": # dokud není konec souboru
print(line) # řádek vytiskneme
line = stream.readline() # načteme další řádek
stream.close()
Všimněte si, že na výstupu jsou prázdné řádky. Důvodem je skutečnost, že každý načtený řádek obsahuje znak odřádkování, tj. při tisku se odřádkuje dvakrát (jedno odřádkování obsahuje tištěný řetězec, druhé odřadkování přidává funkce print)
Jak lze vidět z kódu je použití metody readline komplikovanejší než by začínající programátor čekal. Je totiž nutné provádět volání metody readline na dvou místech. Jednou před cyklem while, aby bylo možno otestovat výsledek prvního čtení) a podruhé na konci těla (získání dalšího řádku).
Naštestí Python umožňuje textový proud používat jako iterátor, který postupně vrací jednotlivé řádky (bez znaku odřádkování na konci). Pro procházení tak lze využít cyklus for.
stream = open("python_output.txt","rt") # soubor musíme znovu otevřít
for line in stream: # přes všechny řádky proudu
print(line, end="") # print nemusí přidávat vlastní odřádkování
Jak je to jednoduché, když používáte Python :)
Na něco jsem ovšem zapomněl, uzavřít proud/soubor.
stream.close()
I zde Python nabízí zapomnětlivým elegantnější řešení konstrukci with. Tato konstrukce zajistí automatické uzavření souboru na konci (odsazeného] bloku příkazů. Konstrukci si ukážeme u programu, který vrací maximální délku řádku (tj. počet znaků v nejdelším řádku).
with open("python_output.txt", "rt") as stream: # otevře proud a označí jej proměnou `stream`
maxlength = max([len(line)-1 for line in stream]) # využijeme komprehenzi
# níže je už proud uzavřený
print(maxlength)
Konstrukce začíná klíčovým slovem with za nímž následuje volání funkce open. Vrácený objekt (proud) je opatřen proměnnou stream (v tomto místě se nepoužívá přiřazení, namísto toho je proměnná uvedena za klíčovým slovem as. Od této chvíli existuje jak proměnná tak proud na nějž odkazuje (a ten jen samozřejmě otevřený).
S proměnnou i s proudem lze pracovat v bloku, jenž následuje za with. Poté co skončí (a skončit může různě včetně např. příkazu return nebo vyvoláním výjimky) se automaticky proud autoamticky uzavře (tj. zavolá se jeho metoda close).
Je zajímavé, že proměnná i objekt proudu existují i poté, co skončí konstrukce with. Proud je však již nepoužitelný (nelze z něj již nic číst).
Využití seznamové komprehenze pro hledání maximálního počtu řádku výrazně zjednodušuje program, není však zcela efektivní.
Zápis [len(line) for line in stream] vytváří seznam, tím že postupně čte řádky souboru (metodou readline), zjišťuje délku získaných řádků (funkce len vrací délku řetězců, jedničku odečítáme, neboť řetězec obsahuje na konci znak odřádkování) a výsledná čísla přidává postupně do seznamu. Až po dokončení seznamu se na něj volá vestavěná funkce max, která vrátí největší prvek (= délka nejdelšího řádku).
Vytvoření seznamu je však zbytečné, neboť maximum lze získat i průběžným procházením čísel (bez toho, že bycjom si je všechny ukládali). Již v několika příkladech jsme ukázali, že si stačí pamatovat průběžné maximum a to porovnávat s postupně přicházejícími údaji (je-li větší stane se průběžným maximem).
Tento algoritmus lze ukázet i na příkladě z praktického života. Představme si, že skupina osob nastupuje v přízemí do výtahu, který si pamatuje jen jedno cílové podlaží. Je zřejmé, že nejefektivnější, když všichni oznámí patro kam jedou a na ovládacím panelu se zvolí nejbližší z nich (tj. minimum).
I když bychom měli dům s desítkami pater a do výtahu by nastupovali desítky lidí, lze minimální patro určit i bez notesu, kam bychom si jednotlivá cílová podlaží zapisovali. Stačí si prostě jen pamatovat, to průběžně nejnižší: 22, 7, 8, 11, 13, 9, 7, 3, 17, 4.
Naše předchozí řešení je tedy zbytečně složité (je to řešení s notesem). Na druhou stranu jsme se vyhnuli cyklu for a vnořené podmínce if.
I zde však existuje řešení, které je zároveň efektní (= stručné a přehledné) i efektivní (nevyžaduje téměř žádnou paměť navíc) -- generátorový výraz.
Z hlediska syntaxe se generátorový výraz liší od seznamové komprehenz pouze uzávorkováním. Na rozdíl od hranatých závorek seznamové komprehenze používá běžné oblé závorky.
(i**2 for i in range(10))
Je však vidět, že na místo seznamu vrací tzv. generátor. To je speciální případ tzv. iterátorů, objektů, které na požádání vracejí posloupnost hodnot. Generátory jsou případem tzv. lenivých iterátorů, tj. další číslo v posloupnosti vracejí až tehdy, kdy už není zbytí. Podobají se tak například objektům rozsahů (range).
Generátory se typicky volají na jiné lenivé iterátory (rozsahy, textové proudy) a transformují je na jiné opět lenivé iterátory. Funkce typu sum nebo max lze volat na libovolné iterátory vracející čísla tj. i na vhodné generátorové výrazy:
from random import randint
max(randint(0,10**9) for _ in range(10**6)) # maximum z milionu náhodných celých čísle
Zde je generátorový výraz volán na iterátor přes čísla 0 až 1000000-1. Transformuje jej do iterátoru, který poskytuje milion náhodných čísel (každé leží mezi 0 a miliardou). Funkce max vybírá postupně tato čísla z lenivého iterátoru a počítá průběžné maximum. Díky použití generátorového výrazu se nevytváří zbytečné pole o miliónu položek o velikosti desítek megabajtů.
Několik poznámek k syntaxi: Kulaté závorky kolem generátorového výrazu lze vynechat, pokud je výraz jediným parametrem funkce (jako zde). Uvnitř závorek omezujících parametr ve volání funkce tak není nutné psát další dvojici závorek. Všimněte si i použití podtržítka na místě řídící proměnné (proměnné, která postupně odkazuje prvky primárního iterátoru). Podtržítko je běžný název proměnné (stejně jako i nebo j), který se v Pythonu využívá v situacích, kdy je formálně vyžadována proměnná, která se však ve skutečnosti nikdy nepoužije (všimněte si, že zde generující výraz randint(0,10**9) na proměnné _ nijak nezávisí). Je to však jen úzus (tj. můžete použít jakékoliv jiné jméno proměnné).
Úkol: Vytvořte funkci, která vrátí počet řádků v souboru (jméno souboru je parametrem funkce). Využijte konstrukci
with.Rada: Počet řádků (stejně jako počet položek jakéhokoliv iterátoru) nejsnadněji získáte použitím generátorového výrazu poskytujícího a funkce
sum(sčítáte jedničky).
Řešený příklad:
Vygenerujte textový CSV soubor obsahující údaje o vzdálenosti mezi středem Země a středem Měsíce pro půlnoc každého dne roku 2018.
CSV (zkratka za Comma-Separated Values) je jednoduchý, ale stále široce, používaný datový formát, v němž jednotlivé řádky representují datové záznamy, v nichž jsou jednotlivé záznamy oddělené čárkou resp. jiným vhodným oddělovačem. Pokud je oddělovač obsažen i v jednotlivých hodnotách (typicky v textové hodnotě, ale viz například desetinná čárka), pak musí být hodnota uvedena v uvozovkách.
V našem příkladě by měl každý CSV řádek obsahovat dvě hodnoty, datum v rozumné textové podobě (např. 1.1.2018), a vzdálenost Země – Měsíc v kilometrech.
V případě uvádění datumu si však nemůžeme být jisti, že bude případným uživatelem špatně interpretováno (chybné časové pásmo, formát apod.) Navíc zpracování datumů v textové podobě nemusí být triviální. Z tohoto důvodu je vhodné čas uvést i v nějaké standardizované a snadno zpracovatelné podobě. Vhodný je například tzv. unixový (POSIX) čas, v němž je čas representován jako počet sekund od 1.1.1970 UTC.
Jádrem řešení je cyklus přes 365 časových okamžiků (od 1.1.2018) s krokem jednoho dne.
Pro representaci času použijeme třídu datetime.datetime, abychom si ji procvičili (lze samozřejmě použít i třídu ephem.Date).
!pip install ephem;
import datetime
import ephem
startDay = datetime.datetime(2018, 1, 1) # počáteční datum
moon = ephem.Moon() # objekt Měsíce
with open("moonDistances.csv", "wt") as stream:
for dayNumber in range(365): # dayNumber nabývá hodnot 0,1, až 364
day = startDay + datetime.timedelta(days=dayNumber) # zjistíme
moon.compute(day) # funguje i s datetime (interně se převede na `ephem.Date`)
distance = moon.earth_distance * ephem.meters_per_au / 1000.0 # převod z AU na kilometry
stream.write(f"{day:%d.%m.%Y}, {day.timestamp()}, {distance:.1f}\n")
# pozor nutný je znak odřádkování na konci řetězce
Kód po vyhodnocení nic nevypíše, neboť jediným viditelným efektem programu je vytvoření a naplnění textového souboru moonDistances.csv. Vypišme několik prvních řádků tohoto souboru (níže uvedený externí příkaz funguje jen v Linuxu resp. Unixu).
!head moonDistances.csv
A pro jistotu se podíváme i na konec souboru:
!tail moonDistances.csv
Program se jeví jako funkční (pro skutečné ověření vy však bylo záhodno výstup zkontrolovat vzhledem k nějaké autoritativní efemeridě).
Jedním z hlavních rysů Pythonu je velká nabídka modulů standardních modulů. Mezi nimi lze nalézt modul csv, který nabízí vysokoúrovňové rozhraní pro přístup k CSV souborům. Zkusíme tento model použít:
import datetime
import ephem
from csv import writer
startDay = datetime.datetime(2018, 1, 1) # počáteční datum
moon = ephem.Moon() # objekt Měsíce
with open("moonDistances2.csv", "wt") as stream:
csvwriter = writer(stream) # vytvoříme specializovaný writer
for dayNumber in range(365): # dayNumber nabývá hodnot 0,1, až 364
day = startDay + datetime.timedelta(days=dayNumber) # zjistíme
moon.compute(day) # funguje i s datetime (interně se převede na `ephem.Date`)
distance = moon.earth_distance * ephem.meters_per_au / 1000.0 # převod z AU na kilometry
csvwriter.writerow([day.strftime("%d.%m.%Y"), day.timestamp(), round(distance, 1)])
# pozor nutný je znak odřádkování na konci řetězce
!head moonDistances2.csv
Použití specializovaného CSV writeru program program zdánlivě příliš nezjednodušil. Přibyl jeden řádek (před cyklem), v němž je vytvořen objekt tzv. writeru. Writer je objekt, který využívá (textový) proud pro výpis složitějších objektů (v tomto případě řádku hodnot). Proud, do kterého writer vypisuje je parametrem konstruktoru.
Následný výpis (v těle) používá metodu writerow, který očekává seznam hodnot (před předáním se musí upravit či naformátovat).
Výhody se však ukáží v případě, kdy chceme CSV nakonfigurovat, tak aby byl použit jiný oddělovací znak včetně případného vložení uvozovek kolem hodnot obsahujících znak oddělovače. Jako příklad uveďme CSV, jehož číselné hodnoty využívají desetinnou čárku namísto tečky (nejjednodušším způsobem záměny destinné tečky za čárku je převod čísla na řetězec následovaný záměnou znaku čárky za znak tečky).
from sys import stdout # sys.stdout textový proud směrovaný na standardní výstup
from math import pi
w = writer(stdout)
w.writerow(["altitude", str(pi).replace(".",",") , 5, 'text s "uvozovkami"'])
Writer v tomto případě uvede číslo v uvozovkách (aby bylo zřejmé, že čárka není oddělovačem, ale je součást hodnoty) Ostatní hodnoty (neobsahující čárku) jsou uvedeny bez uvozovek. Navíc, pokud nějaká hodnota obsahuje uvozovky, pak je zdvojí (aby se odlišily od těch přidaných).
Writer lze přirozeně i konfigurovat, například změnit oddělovač:
from sys import stdout # sys.stdout textový proud směrovaný na standardní výstup
from math import pi
w = writer(stdout, delimiter=";")
w.writerow(["altitude", str(pi).replace(".",",") , 5, 'text s "uvozovkami"'])
Lokální soubory tvoří dnes jen menší část dostupných dat. Většina aplikací využívá primárně dat z Internetu. Ty jsou ve valné míře dostupné prostřednictvím HTTP (resp. HTTPS) protokolu.
Podporu HTTP resp. HTTPS protokolu nabízí samozřejmě i standardní knihovna Pythonu (modul urllib.request). V praxi se však používa více externí balík requests (neboli HTTP for Humans).
!pip install requests
Zkusme nejdříve jednoduchý příklad, načtení textového souboru.
import requests
response = requests.get("https://www.gutenberg.org/ebooks/28885.txt.utf-8")
print(response.text[:100])
Jak je vidět získání textového obsahu vyžaduje jen dva snadné kroky. Za prvé volání funkce requests.get, jejímž parametrem je URL příslušného souboru (zde je to textový obsah Alenčiných dobrodružství nabízený projektem Guttenberg). To může chvíli trvat, ale po skončení je vrácen objekt representující odpověď webového serveru (tzv. response).
Tento objekt nabízí velké množství atributů, z nichž nás v tuto chvíli zajímá atribut text. Ten vrací textový obsah jako jeden velký řetězec (pro úsporu místa vypisuji jen prvních 100 znaků).
Řešený příklad
Stažení tak velkého textového obsahu využijeme pro rozšíření vašich znalostí o regulárních výrazech. Naším úkolem bude zjistit kolikrát se v anglickém originálu Alenčiných dobrodružství vyskytuje Alenčino jméno.
Přímočaré řešení je jednoduché. Rozložíme řetězec na jednotliví slova a spočítáme, kolik z nich je rovno řetězci Alice (použijeme generátorový výraz a nám již známé počítání jedniček).
import requests
response = requests.get("https://www.gutenberg.org/ebooks/28885.txt.utf-8")
text = response.text
sum(1 for word in text.split(" ") if word == "Alice")
Výstup vypadá rozumně, je však chybný. Problém je v tom, že jméno Alice může být obklopeno i jinými znaky než jen mezerami (například odřádkováním, čárkou, tečkou, apod.) Řešení je to také dosti neefektivní neboť metoda str.split vrací seznam řetězců (o mnoha tisící položkách) nikoliv lenivý iterátor (použití generátorového výrazu to neřeší, ten již pracuje nad seznamem řetězců, a zabrání pouze vzniku seznamu cca 160 jedniček).
Řešení nabízí modul re (regulární výrazy), který nabízí metodu finditer, která vrací lenivý iterátor přes výskyty podřetězců, které odpovídají regulárnímu výrazu.
from re import finditer
sum(1 for _ in finditer("Alice", text))
Stručné, přehledné a správné. Jediné, co je nutno v některých situacích zohlednit, je skutečnost, že funkce nedokáže najít překrývající se výskyty vzorů.
list(finditer("aba", "ababa"))
Jak je vidět funkce finditer našla jeden výskyt (od indexu 0 do indxu 2 včetně), i když se podřetězec "aba" nachází i od indexu 2.
[konec řešeného příkladu]
Stažení a zpracování běžného textového souboru je sice užitečné, avšak v praxi se většina textového a datového obsahu na Internetu je uložena ve formě strukturovaných textových dat (a samozřejmě i multimediálních souborů).
Mezi nejdůležitější univerzální textové formáty patří HTML, XML a JSON. Nejjednodušší z nich je JSON, který je hojně využíván v rámci tzv. webových služeb. Webová služba nabízí strukturovaná data (typicky právě ve formátu JSON) jako odpověď na HTTP požadavek GET s určitým URL. Typicky se tak poskytují informace o počasí, dopravní situaci, geografické informace, rozhraní ke cloudovým službám, sociálním sítím a mnoho dalšího.
Podívejme se například na následující URL:
https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22
Tato URL adresa umožňuje využít testovací verzi webové služby nabízené portálem Open Weather Map, která obecně poskytuje meteorologické informace pro libovolné místo na Zemi (aktuální stav počasí, předpověď, klimatologická data). V tomto konkrétním případě nevrací reálná data, ale jen náhodnou ukázku formátu dat, která ve své reálné podobě popisují aktuální meteorologickou situaci. Na druhou stranu testovací ukázka nevyžaduje registraci.
URL webové služby má typicky dvě části:
endpoint: úvodní část URL, která jednoznačně identifikuje webovou službu a tím i druh poskytovaných dat. V našem případě je endpoint určen URL https://samples.openweathermap.org/data/2.5/weather.
parametry služby: parametry služby tvoří část URL po otazníku a jsou tvořeny dvojicemi klíč (=) hodnota (jednotlivé dvojice jsou odděleny znakem &). Význam jednotlivých klíčů (přesněji hodnot tímto klíčem určených je popsáno v dokumentaci wbové služby).
V našem případě jsou použity dva klíče: klíč q obsahuje identifikaci místa, pro které chceme získat meteorologická data. Jednou z možností specifikace je jméno místa doplněné zkratkou státu (v ukázkové službě nelze použít jiné než Londýn). Druhým klíčem, je tzv API key, který získáte při registraci a identifikuje žadatele o data. Lze tak provádět účtování (u placených služeb) resp. omezení přenosového pásma (u nepalcených, většinou máte stanoven maximální počet požadavků za nějakou časovou jednotku).
Ukažme si nejdříve jak specifikovat požadavek na WWW server. I když lze URL předat jako celek (jak endpoint tak parametry), výhodnější individuální specifikace parametrů. Je to jednak pružnější (snadno lze měnit jednotlivé parametry např. lokaci), jednak pohodlnější (v rámci hodnot parametrů je nutné kódovat některé znaky, které jsou v URL nepřípustné, včetně např. mezer).
import requests
response = requests.get("https://samples.openweathermap.org/data/2.5/weather",
params={"q": "London,uk",
"appid": "b6907d289e10d714a6e88b30761fae22"})
print(response.text)
Pojmenovaný parametr params definuje parametry dotazu v podobě tzv. slovníku. Slovník je kolekce, která ukládá hodnoty opatřené tzv. klíčem (klíčem je typicky řetězec). Tento konterjner je optimální pro representaci parametrů dotazu, které jsou také representovány dvojicí klíč a hodnota (Pythonské slovníky jsou však obecnější neboť klíčem i hodnotou mohou být objekty různých tříd nikoliv jen řetězce)-
Výsledkem požadavku je text tvořený textem, který se podobá zápisu pythonského slovníku, jehož hodnotami mohou být další slovníky (ve složených závorkách), seznamy (v hranatých závorkách) resp. číselné nebo řetězcové hodnoty. Není to tak docela pravda (zápis odpovídá jinému jazyku tzv. Javascriptu) lze jej však bez problémů převést do slovníku, jehož hodnotami jsou i slovníky, řetězce nebo jednoduché hodnoty. Pak už je snadné získávat jednotlivé prvky složeného objektu.
V případě modulu requests je to zvlášť snadné, stačí namísto atributu text použít metodu json, která za Vás převod na pythonský slovník provede za Vás.
data = response.json()
data
Výsledkem je slovník, který je vypsán v poněkud přehlednější podobě (navíc si můžete všimnout drobných rozdílu oproti původnímu textu ve formátu JSON jako je uspořádání klíčů a použití apostrofů namísto uvozovek, obojí však nemá žádný sémantický význam).
Hlavním důvodem převodu na pythonský složený objekt však není hezčí výpis (který navíc funguje jen v Jupyter notebooku). Nyní lze totiž jednodušeji přistupovat k dílčím položkám. Například pro přístup k teplotě lze využít následující zápis:
data["main"]["temp"]
Slovník se zde chová trochu jako seznam. Pro přístup k položkám se využívá indexace (hranaté závorky za objektem), namísto pozičního (čísleného) indexu se však použije klíč (zde tedy řetězec). Nejdříve proto získáme hodnotu odpovídající atributu "main" ve slovníku nejvyšší úrovně. To je opět slovník, takže můžeme ihned aplikovat další index (temp), čímž získáme hodnotu odpovídající příslušnému klíči ve vnořeném slovníku. Je zřejmé, že teplota není ve stupních Celsia (to bychom londýňanům nepřáli), ale v Kelvinech (OpenWeatherMap používá jen základní SI jednotky).
Pro snadnější získání si vytvoříme funkci:
def getTemperature(d):
return round(d["main"]["temp"] - 273.15, 1) # převod na stupně Celsia a zaokrouhlení na jedno desetinné
getTemperature(data)
Úkol: Vytvořte funkci, která ze slovníku vyextrahuje rychlost větru (a přepočte na km/h) a funkci vracející čas východu Slunce (jako objekt
datatime.time).
Řešený příklad
I když jsou ukázková data užitečná, jistě chcete vyzkoušet i data reálná. To je možné, neboť data o aktuální meteorologické situace a krátkodobé předpovědi jsou u OpenWeatherMap zdarma (samozřejmě s omezením počtu dotazů, které je však rozumné, maximálně 60 požadavků za minutu).
Naším úkolem bude zobrazit předpověd teploty v nejbližších pěti dnech pro místo Vašeho pobytu (nebo místo blízké).
Prvním úkolem pro Vás je registrace v OpenWeatherMap (stačí free účet) a získání API klíče (vše je jednoduché a dobře dokumentované). Získaný API klíč uložte do souboru open_weather_map v nějakém rozumném adresáři/složce (cestu k němu si zapamatujte!)
Nejdříve si přípravíme funkci pro přečtení API klíče ze souboru. Ten nebudeme z bezpečnostních důvodů uvádět přímo v programu (API klíč by měl zůstat stejný, jinak mohou ostatní čerpat z Vašeho přídělu).
def get_api_key(filename):
with open(filename, "rt") as stream:
return stream.readline().strip()
# get_api_key("/home/fiser/credentials/open_weather_map")
Nyní již můžeme načíst data. Nejdříve si připravíme URL endpointu (převzat z dokumentace https://openweathermap.org/forecast5) a parametry webové služby (lokalitu, aplikčaní klíč přečtený ze souboru, a požadovaný formát odpovědi, zde json).
Získaný JSON je v tomto případě rozsáhlejší. Na nejvyšší úrovni nás zajímá atribut list obsahující seznam předpokládaných meteorologických údajů na 5 dnů s krokem 3 hodiny tj. s 40 položkami. Výpis první položky seznamu na konci kódu ukazuje jejich strukturu (je to opět slovník s mnoha atributy)
import requests
import datetime
endpoint = "http://api.openweathermap.org/data/2.5/forecast"
params = { # slovník parametrů
"q" : "Dobříň", # jméno lokality (nahraďte za místo Vašeho pobytu)
"appid" : get_api_key("/home/fiser/credentials/open_weather_map"),
"mode" : "json" # formát výstupu
}
data= requests.get(endpoint, params).json() # načtení JSON dat a jejich převod do slovníku
print(len(data["list"])) # zkontrolujeme počet
data["list"] [0] # kontrolní výpis první položky
Každá položka obsahuje velké množství meteorologický údajů, z nichž ty nejdůležitější jsou soustředěny v rámci hodnoty s klíčem main. Důležitý je i časový údaj, který nalezneme označený klíčem dt (unixový čas). Čas je sice dostupný i v textové podobě, avšak jen ve světovém čase (UTC).
Pro extrakci těchto dat využijeme seznamovou komprehenzi, která prochází jednotlivé prvky seznamu data["list"] a vytváří seznam pro nás zajímavých údajů (tj. seznam dat a seznam teplot ve stupních Celsia).
# z každé položky vyextrahuje hodnotu s klíčem `dt` a převede na `datetime.datetime`
times = [datetime.datetime.fromtimestamp(item["dt"]) for item in data["list"]]
# z každé položky vyextrahuje teplotu, převede na stupně Celsia a zaokrouhlí na jedno desetinné místo
temperatures = [round(item["main"]["temp"] - 273.15, 1) for item in data["list"]]
print(times[:2]) # pro zkrácení vypíšeme jen první dva prvky
print(temperatures)
Nyní už data máme vyextrahována, nejsou však příliš přehledná (dokážete odpověděť jaká nejnižší teplota je předpovídána na zítra?). Nejjednoduší cestou vizualizace je graf vytvořený pomocí balíku matpolotlib (který si tak zopakujeme a doplníme pár nových ).
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as ticker
fig, axes = plt.subplots() # pro lepší nastavení potřebujeme získat odkaz na objekt třídy `Axes`
axes.xaxis.set_major_formatter(mdates.DateFormatter("%-d.%-m")) # nastavení formátu hlavních popisek
axes.yaxis.set_major_locator(ticker.MultipleLocator(5)) # nastavení intervalu hlavního kroku na ose `y`
axes.yaxis.set_minor_locator(ticker.MultipleLocator(1)) # nastavení intervalu vedlejšího kroku na ose `y`
axes.xaxis.set_minor_locator(mdates.HourLocator(interval=6)) # nastavení intervalu vedlejšího kroku `x`
axes.set_xlim(times[0].date(), times[-1].date() + datetime.timedelta(days=1)) # nastavení rozsahu osy `x`
axes.grid(True, which="major", color="k") # nastavení hlavní mřížky
axes.grid(True, which="minor", linestyle="--") # nastavení vedlejší mřížky
axes.set_ylabel("teplota (°C)") # název osy `y`
plt.plot(times, temperatures) # a nyní můžeme konečně graf vykreslit
Skript pro vykreslení grafu má jen jediný povinný řádek, a to poslední s voláním metody plot.Vše ostatní je konfigurace vzhledu. Abychom mohli vzhled grafu nastavit, potřebujeme získat objekt representující tu část obrázku, která je řízena osami (kromě vlastních os do ní patří i mřížka, různé popisky, atd.).
Voláním funkce mathplotlib.pyplot.subplots získáme dvojici hodnot: jedna representuje obrázek jako celek (třída Figure), druhá pak výše zmíněný prostor daný osami (třída Axes). První je označena proměnou fig (díle už ji nevyužíváme), druhá pak proměnou axes.
Nad objektem os lze volat velké množství metod, z nichž využíváme ty, které nastavují popisky os (angl. ticks a mřížky). U osy y nastavuje interval hlavního kroku (popisek) na 5 a vedlejšího na 1 (stupeň Celsia). Může se to zdát trochu složité, neboť parametrem těchto metod není číslo, ale objekt třídy matplotlib.ticker.MultipleLockator. Matplotlib však kromě fixního kroku podporuje i mnoho sofistikovanějších specifikací inetrvalů, které nemusí být konstantní.
U osy x nastavujeme interval vedlejších popisek na 6 hodin (interval hlavních popisek je automaticky 1 den, což nám vyhovuje). Upravíme i formát hlavních popisek (formát je shodný s metodou datetime.datetime.strftime, který již známe).
Poté nastavíme i mřížky (hlavní = major a vedlejší = minor). Specifikace vychází z frameworku Matlab a je pro začátečníky dost kryptická. Hlavní barvy jsou označeny jedním písmenem ('r' = red, 'b' = blue, černá je však k!), styl čar symbolicky pomocí běžných znaků ('--' je čárkovaní čára). Další možnosti viz např. https://python-graph-gallery.com/cheat-sheets/).
Zarovnání hlavních a vedlejších popisek na ose x vyžaduje, aby hodnoty na ose začínali v čase dělitelném šesti (0:00, 6:00, 12:00, 18:00). Zde je to dosaženo tím, že dolní limit je nastaven na půlnoc prvního dne (metoda datetime.datetime vrací objekt datetime.date representující půlnoc) a pro symetrii je nastaven i limit horní na půlnoc, která následuje za posledním časovým údajem (v případě půlnoci na následující půlnoc).
Úkol: Zobrazte do jednoho grafu očekávaný tlak (přepočtený na hladinu moře) a vlhkost. Obě veličiny se liší v řádech proto použijte sekundární osu
y.Rada: viz https://matplotlib.org/gallery/api/two_scales.html
Vložení sekundární osy se provádí voláním metody twinx na objektu hlavních os. Vrací nový objekt os, který je možné do určité míry kustomizovat nezávisle. Nastavení limitů osy x musí však být stejné (a musí se explicitně uvádět, jinak se zvolí automatické, které překryje nastavení u primárních os). Lze dokonce nastavit i vlastní mřížku (výsledek však vypada strašně, ale zkuste si to).
Python před uživateli skrývá detaily datové representace. Když vytvoříte seznam, tak vám nabídne metody resp., kterými můžete získávat informace o seznamu (např. délku seznamu, nalezení prvku v seznamu), číst a zapisovat jeho prvky (indexace) resp. se seznamem manipulovat (přidávat či odebírat prvky), apod. Vůbec vás nemusí jak je seznam representován v paměti.
To však je jen částečná pravda, neboť paměťová representace může výrazně ovlivnit efektivitu programu, tj.
Pokud jsou seznamy malé, pak si ve většině případů nemusíme s efektivotou lámat hlavu. Je v zásadě jedno pokud operace trvá 1 µs nebo 1 ms (1000× vice, obojí je ale pro člověka nepostřehnutelně malý časový okamžik). Stejně je u osobních počítačů v zásadě jedno, zda seznam zaujme 1 KiB nebo 1 MiB (1024× více).
V okamžiku, kdy se začne pracovat s velkými daty (např. abonentů u celosvětových služeb, paketů v síti, nebo měření gigantické senzorové sítě) začíná být efektivita otázkou přežití. Pokud trvá přihlášení minutu, lze to ve většině situací tolerovat, pokud trvá 1000× déle (16 minut 40 vteřin), pak už vaši službu nikdo nevyužije. Podobný rozdíl pokud seznam zaujímá 200 MiB (a vejde se tak bez problémů do operační paměti) nebo 200 GiB (a vejde se tak jen stěží na vnější paměťové úložiště)
V případě časové náročnosti výpočtu lze pro jednoduchost vycházet z toho, že doba vykonání programu je funkcí velikosti seznamu.
Podívejme se například na následující program, který vytváří seznamy seznam náhodně promíchaných hodnot 0 až $n-1$ (každá hodnota je tam obsažena jen jednou).
from random import shuffle
def shuffled_range(n): # funkce vytvářející seznam
rseq = list(range(n))
shuffle(rseq)
return rseq
# zjistíme čas a uložíme pro další zpracování
time_infos = []
ns = list(range(200_000,2_000_001, 200_000)) #seznam hodnot n, pro něž budeme měřit
for n in ns:
print(n)
t = %timeit -o shuffled_range(n) # měření rychlosti (výsledek se uloží do t)
time_infos.append(t)
Pomocí jupyterovského »magického«%timeit lze změřit čas provedení příslušného řádku (včetně relativně klíčové informace o směrodatné odchylce). Přepínač o zajistí, že se vrátí objekt, representující měření (a je pak uloženo do proměnné t).
Měření bylo provedeno pro seznamy s délkou 200 000 a 2 milióny položek (včetně, proto je uvedena horní mez + 1) s krokem 200 000.
# velikosti seznamů
import matplotlib.pyplot as plt
from matplotlib.ticker import EngFormatter
import numpy as np
def show_graph(x,y, polynomial_degree):
# nastavení popisek
fig, ax = plt.subplots()
ax.xaxis.set_major_formatter(EngFormatter())
plt.plot(x,y, "o") # graf závislosti (body)
plt.grid(True)
z = np.polyfit(x, y, polynomial_degree) # polynomiální
p = np.poly1d(z) # vytváří funkci repr. lineární polynom
plt.plot(x, p(x),"r--") # vykreslí trend
print(p)
t = [info.average for info in time_infos] # zajímá nás jen průměrný čas
show_graph(ns, t, 1)
Jak lze vidět z grafu, roste doba vykonávání lineárně a to rychlostí cca 0,9 sekundy na milion položek (hodnota závisí na rychlosti počítače). Abolutní člen, který by měl vyjadřovat konstantní režii (čas inicializace apod.) je větší seznamy zanedbatelný (v mém případě je z důvodů nepřesností dokonce záporný).
Pokud zanedbáme absolutní člen, pak je navíc zřejmé, že pokud se počet položek zdvojnásobí (např. z 500k na 1M) tak se zdvojnásobí i čas vykonávání (a zde ani nezávisí na směrnici).
Závislost mezi velikostí seznamu a dobou zpracovávání nemusí být jen lineární. Podívejme se například na elementární způsob řazení (třídění) tj. algoritmu, který seznam přeuspořádá, tak že jsou prvky umístěny od nejnižšího po nejvyšší (lze uvažovat i opačný směr). Algoritmus řazení výběrem vychází z jednoduché úvahy, že nejdříve najdeme nejmenší prvek ten vyměníme s prvním prvkem (pokud už není první), pak najdeme nejmenší prvek ve zbytkovém seznamu a prohodíme ho z druhým. Následuje nalezení nejmenšího prvku v podseznamu od třetího prvku, a tak pokračuje dál dokud se nesetříděný zbytek seznamu neomezí na jediný prvek.
def select_sort(s):
for i in range(len(s)-1): # od jakého prvku hledáme minimum
min_index = i
for j in range(i, len(s)):
if s[j] < s[min_index]:
min_index = j
s[i], s[min_index] = s[min_index], s[i]
s = shuffled_range(20) # promíchaná posloupnost čísel
print(s) # pro jistotu vypíšeme (pravděpodobnost, že je setříděná je ale malá)
select_sort(s) # setřídíme
print(s) # vypíšeme setříděné
Pravděpodobnost je rovna pravděpodobnosti, že nastane jedna konkrétní permutace čísel mezi všemi možnými, jichž je $n!$ tj. $\frac{1}{n!}$. Pro $n = 20$ je to cca $4,11\cdot 10^{-19}$.
Pro zištění doby běhu použijeme opět magický příkaz timeit. Liší se jen rozsah hodnot a název volané metody.
time_infos2 = []
ns2 = list(range(2_000,10_001, 1_000)) #seznam hodnot n, pro něž budeme měřit
for n in ns2:
print(n)
s = shuffled_range(n)
t = %timeit -o select_sort(s) # měření rychlosti (výsledek se uloží do t)
time_infos2.append(t)
Pro zobrazení použijeme naší funkcí show_graph, které přidáme nově získaná pole ns2 (počet položek řazeného pole) a t2 (průněrný čas běhu). Nejdříve zvolíme lineární trend, jako v předchozím příkladě.
t2 = [info.average for info in time_infos2]
show_graph(ns2, t2, 1)
Je zřejmé, že lineární trend, příliš neodpovídá. Body vyjadřující čas provedení pro jednotlivá $n$ mají spíše tvar paraboly. Proto zkusíme trend stupně 2 (kvadratický).
t2 = [info.average for info in time_infos2]
show_graph(ns2, t2, 2)
To už vypadá mnohem lépe. I v tomto případě je možné zenedbat absolutní člen a nyní i člen lineární. Čas provedení je pak řádově $4,75\cdot 10^{-8}n^2$. Nyní už je jasné proč jsme nezjišťovali čas pro $n = 200\,000 \ldots 2\,000\,000$. I pro nižší z těchto hodnot by to trvalo cca 1900 vteřin, je téměř 32 minut. Pro 2 milióny položek (což je desetkrát více) by to trvalo 10×10 déle tj. přes 52 hodin.
Jak lze tedy vidět, roste čas vykonávání promíchávání lineárně zatímco čas třídění kvadraticky.
Tuto skutečnost, že matematicky vyjádřit tzv. asymptotické časové složitosti a pomocí Landauovy „O“ notace. Pokud funkce růstu můžeme od jistého (typicky velkého) čísla z obou stran omezit nějakými dvěma lineárními funkcemi, pak budeme tvrdit, že má lineární asymptotickou časovou složitost a budeme to značit $O(n)$ (čteno velké „o“ „en“).
Podobně lze zavést i časovou složitost (slovo asymptotická se běžně vynechává) kvadratickou $O(n^2)$.
To lze zobecnit na polynomy libovolného řádu. Pokud čas vykonávání roste nějakou polynomiální funkcí $a_n x^n + a_{n-1} x^{n-1} + \cdots + a_0$ je odpovídající časová složitost $O(x^n)$ (typicky se setkáváme běžně jen se složitostí kubickou.
Kromě polynomiální složitosti se v praxi setkáváme i s dalšími typy asymptotických složitostí.
| složitost | označení |
|---|---|
| $O(1)$ | konstantní |
| $O(log(n))$ | logaritmická |
| $O(n)$ | lineární |
| $O(n\log(n))$ | loglineární |
| $O(n^2)$ | kvadratická |
| $O(n^3)$ | kubická |
| $O(a^x)$ | exponenciální |
Konstantní časovou složitost mají operace, jejichž doba vykonávání nezáleží na počtu prvků kolekce. V případě seznamu mají tuto časovou složitost tři klíčové operace: zjištění délky seznamu, indexace (nalezení i-tého prvku) a přidávání prvku na konec seznamu.
Úkol: Ověřte časovou složitost výše uvedených operací pomocí benchmarkingu.
Další typickou časovou složitostí u seznamů je ta lineární. Je spojena s operacemi, které přidávají či odebírají prvky uvnitř seznamu a které tudíž vyžadují posunutí prvků (musí se posunout všechny položky za vkládaným či vyjímaným, tj. průměrně $n/2$ prvků). Lineární časovou složitost má i vyhledávání, zda je nějaká hodnota obsažena v seznamu (operátor in) respektive na jaké pozici (metoda index). I zde je to jasné neboť je nutné projít buď všechny prvky (nejhorší případ), nebo alespoň půlku (průměrný případ).
Úkol: Ověřte časovou složitost operací s lineární složitostí, Vytvářejte promíchané seznamy čísel a pak:
- hledejte náhodný prvek, který se v nich nachází
- hledejte prvek, který se v nich nenachází
- vyjímejte prvek v náhodné pozici (příkaz
del)- vyjímejte náhodný prvek podle jeho hodnoty (tj. nějaké číslo, které se v seznamu určitě nachází)
- vkládejte prvek na náhodnou pozici (kamkoliv v seznamu)
Kvadratickou časovou složitost nemá žádná ze standardních operací nad seznamem (ani řazení sort viz níže), může se však vyskytnout v případě, že se provede průměrně $n$ operací s lineární složitostí.
Příkladem budiž například výmaz určitých položek seznamu, kde je počet odstraněných položek proporcionální k velikosti seznamu (tj. například je proveden výmaz cca poloviny položek).
from random import normalvariate
def random_list(n, μ, σ):
# pomocná metoda pro generování seznamu s náhodným obsahem
return [normalvariate(μ, σ) for _ in range(n)]
def remove_outliers(lst, min, max):
for i in reversed(range(len(lst))):
if not (min <= lst[i] <= max):
del lst[i]
s = random_list(10, 0, 1)
print(s)
remove_outliers(s, -1.0, 1.0) # z teorie je zřejmé že bude vymazána cca třetina položek
print(s)
print(len(s))
První pomocná funkce generuje seznam, jenž obsahuje náhodná čísla z normálního rozdělení s požadovanou střední hodnotou a směrodatnou odchylkou. Tato funkce má časovou složitost $O(n)$ neboť přidává $n$ s časovou složitostí $O(1)$.
Hlavní funkce, pak ze seznamu odebírá prvky, které neleží v intervalu $[min, max]$. což se typicky používá pro tzv. odlehlé body (outliers). Funkce prochází (pozpátku!) seznam a pomocí příkazu del vymazává prvky. Pro algoritmus je klíčové, že prvky jsou odmazávány v opačném směru, neboť dopředný algoritmus posouvá indexy ještě nezkontrolovaných prvků.
Úkol: Navrhněte algoritmus, který dané prvky odstraní s lineární časovou složitostí.
Otázka: Jaká je časová složitost spojení obou kroků tj. generování náhodného seznamu a následné odstranění prvků výmazem.
Logaritmická časová složitost je typická pro některé algoritmy vyhledávání. Ve standardním rozhraní seznamu však není zastoupena. Klasickým příkladem je binární vyhledávání (https://en.wikipedia.org/wiki/Binary_search_algorithm). V Pythonu je implementována v modulu bisect.
Binární vyhledávání funguje jen na seřazených posloupnostech. Klíčovým úkolem je proto zajistit, že je posloupnost udržována seřazená (). To lze zajistit dvěma způsoby:
Celková složitost vyhledávání je tak dána vzorem vkládání a hledání.
Úkol: Pomocí modulu
bisectimplementujte vřazování a binární vyhledávání (modul nabízí zbytečně složité i když obecně použitelné funkce, implementace vřazování a vyhledávání je však uvedena na konci dokumentace). Následně porovnejte s efektivitou množiny (setviz níže).
Loglineární časová složitost $O(n\log(n))$ je další typickou složitostí. Tuto složitost mají především všechny běžně používané univerzální řadící (třídící) algoritmy, a to včetně třídícího algoritmu užívaného v Pythonu (univerzální = použitelné na libovolný datový typ s definicí uspořádání). Python aktuálně používá nepříliš známý a dosti komplexní algoritmus Timsort (https://en.wikipedia.org/wiki/Timsort), který má v průměrném případě (tj. například pro náhodně promíchané) prvky složitost loglineární. Pokud je však vstup alespoň částečně seřazen, může dosahovat i lineární časovou složitost.
Posledním běžným typem časové složitosti nad kolekcemi je exponenciální časová složitost, kde čas roste podle křivky ($a^n$) kde $a>1$ (ostatní členy lze s klidem zanedbat). Základní charakteristikou této časové složitosti, je to že čas roste s každám dalším $a$ násobně. $a$ je v praxi relativně malé číslo (ale vždy větší než 1!), takže růst je zpočátku pomalý, ale již v řádu malých desítek rychle čas provedení rychle roste (vteřiny, minuty), pro vyšší desítky se už výpočet nedá provádět v reálném čase (tj. v řádu hodin či dnů). Pro velikosti v řádu stovek už nemusí stačit ani očekávaný věk sluneční soustavy.
Exponenciální časová složitost je typická pro algoritmy, které procházejí všechny kombinace prvků (resp. jejich $k$-procentní část). V mnoha případech existují sofistikovanější algoritmy, které úlohu řeší v polynomiálním čase (tj. $O(n^k)$). Existují však i algoritmy, kde není takové řešení známo, resp. je vysoce pravděpodobné (i když nikoliv dokázané), že neexistuje (tzv. https://cs.wikipedia.org/wiki/NP-%C3%BAplnost). Nejznámější z těchto úloh je problém obchodního cestujícího a problém batohu.
V těchto případech se musíme spokojit s optimalizací exponenciálního růstu (tj. nižším $a$, které umožňuje aplikovat je i na o něco delší seznamy). Například pokud řešíme problém obchodního cestujícího hrubou silou tak jsme omezeni skutečně jen na malé desítky, optimalizovaný algoritmus pokrývá i malé stovky). Další možností je vyhledání řešení, které není optimální a ale dostatečně s k němu blíží (viz například feromonový algoritmus pro řešení problému obchodního cestujícího).
Časovou složitost některých algoritmů lze určit aplikací dvou pravidel:
U některých algoritmů je však určení časové složitosti náročnější a u určité části je to vyšší matematika (a programátor se tak musí spokojit s výsledky matematiků).
Další komplikace přinášejí algoritmy, jejichž časová složitost závisí na uspořádání dat v kolekci (resp. na vstupu). V tomto případě se nejčastěji uvádí tzv. průměrná složitost, kterou si lze představit jako časovou složitost, která převažuje u většiny možných vstupů. Typicky je to složitost nad náhodným obsahem. Existuje však i složitost v nejlepším případě (kdy je vstup uspořádán tak, že algoritmus funguje nejefektivněji), a složitost v případě nejhorším.
Zřejmé je to například u vyhledávání. Pokud je hledaný prvek vždy na prvním místě a jen výjimečně na posledním, pak je časová složitost triviálního algoritmu konstantní. Je-li naopak vždy na konci je časová složitost shodná s průměrným případem (i když je v průměru čas hledání dvojnásobný!). U řadících algoritmů je problém podobný, jen je často mnohem obtížnější definovat vstupy vedoucí k nejlepší či nejhorší časové složitosti. U některých algoritmů s běžnou časovou složitostí $O(n\log(n))$ mohou nevhodná data vést ke složitosti $O(n^2)$. To může vést k tomu, že výpočet trvající zlomky vteřin bude trvat celé minuty či hodiny.
V praxi je nutno zohlednit skutečnost, že klíčová bývá časová složitost v nejhorším případě, neboť častým požadavkem bývá maximální doba odezvy. Navíc pravděpodobnost případů s nejhorší časovou složitostí může být více, než by vyplývalo z rovnoměrného rozložení.
Timsort), u jiných naopak ke kvadratické (Quicksort).Hlavním důvodem, proč je nutné znát časové složitosti, je odhad použitelnosti algoritmů s ohledem na velikost zpracovávaných dat. Z tohoto hlediska lze časové složitosti rozdělit do čtyř kategorií.
operace je i pro velká dat provedena téměř okamžitě (konstatní a logaritmická). hlavím problémem je tak spíše nalezení dostatečně velkého a dostatečně rychlého úložiště (tj. kritické jsou přístupové časy k paměti, případné swapování) operace jsou netolerovatelně pomalé jen pro řád stovek miliónu, či spíše ještě větší (lineární, loglineární), což lze u některých typů dat vyloučit (například počet zaměstananců jen stěží překoná hranici stovek miliónů) operace, jejichž nasazení je nutno zvažovat již na hranici tisíců či desetitisíců (kvadratické a kubické), zde už je časová složitost faktorem, který nejvíce ovlivňuje nasazení. Lze počítat exponenciální časová složitost je akceptovatelná jen pokud je počet prvků malý a striktně omezen.
Při hodnocení algoritmů jsou kromě prosté časové složitosti důležité i další apekty, které se ve formalismu časové apekty zanedbávají.
Jako případovou problémů s interpretací časové složitosti lze uvést již dříve zmíněný vestavěný algoritmus třídění.
Budeme měřit rychlost setřídění náhodně promíchaného seznamu, tj. pokusíme se odhadnbout průměrnou časovou složitost.
Nyní však nezvolíme aritmetickou posloupnost postupně rostoucích hodnot $n$ ale posloupnost geometrickou, s hodnotami $2^{15}, 2^{16}, 2^{17}, \ldots, 2^{24}$
time_infos3 = []
ns3 = [2**n for n in range(15,25)] #seznam hodnot n, pro něž budeme měřit
for n in ns3:
print(n)
s = shuffled_range(n)
t = %timeit -o s.sort() # měření rychlosti (výsledek se uloží do t)
time_infos3.append(t)
t3 = [info.average for info in time_infos3]
show_graph(ns3, t3, 1)
Závislost se na první pohled jeví jako lineární. Navíc násobitel je velmi nízký (7 miliardtin). Tj. i pole o velikosti stomiliónů položek (zaujímající paměť v řádu GiB) se setřídí za méně než sekundu. Python je obecně dost pomalý jazyk, ale třídění je implementováno v nízkoúrovňovém jazyce a je ručně optimalizováno pro třídění běžných hodnot včetně využití insertion sort pro malé seznamy (to mimo jiné znamená, že nemá prakticky smysl používat jiný algoritmus, především pak algoritmus napsaný v Pythonu a to ani pro malé seznamy).
Podle dokumentace je však složitost třídění $O(n \log(n)$ a i teorie tvrdí, že neexistuje žádný univerzální třídící algoritmus s lepší časovou složitostí. Proto se pokusíme nalézt odpovídající funkci (je třeba využít jinou funkci z balíku scipy).
import numpy as np
from scipy.optimize import curve_fit
def func(x, a): # funkce parametrizovaná parametrem `a`
return a * x * np.log(x)
par, _ = curve_fit(func, ns3, t3)
# najde takové `a`, že součet druhých mocnin odchylek je minimální
print(par)
Seznam obsahuje optimální hodnoty parametrů v pořadí jak jsou uvedeny ve funkci (v našem případě je jen jeden parametr a a je tedy roven cca 4.26e-10).
Nyní zakreslíme jak lineární tak loglineární trend a pro porovnání i trend operace pro promíchávání hodnot, jenž je také lineární.
Pro zobrazení většího rozsahu hodnot využijeme logaritmickou škálu jak na ose $x$ (se základem 2), tak na osy $y$ (se základem 10$).
x = np.fromiter((2**i for i in range(25,40)), dtype=np.float64)
fig, ax = plt.subplots() # získání objektu representujícího graf s osami
ax.set_xscale('log', basex=2) # logaritmická `x`
ax.set_yscale('log') # logaritmická `y`
plt.grid(True)
plt.plot(x, func(x, *par), "r", label="Třídění O(n log(n))")
plt.plot(x, 6.999e-9*x, "b",label="Třídění lineární přiblížení O(n)")
plt.plot(x, 8.704e-07*x, "g", label="Permutování O(n)")
plt.legend()
Graf ukazuje, že algoritmus vykazuje jisté zpomalení, ale ani v řádu $2^{40}$, kdy pole vyžaduje paměti v řádu jednotek terabytů (TiB), není rozdíl příliš patrný (obě hodnoty jsou v řádu deseti tisíců vteřin tj. cca hodin a očekávaná doba provedení je 2-3× větší). Navíc i tak je vestavěné řazení výrazně rychlejší než vestavěné promíchávání (i když to má lineární časovou složitost). I když čistě teoreticky existuje takové $n$, pro něž platí, že doba třídění překoná dobu promíchávaní, je toto $n$ tak velké, že se pro výrobu takového počítače by nestačila ani všechna hmota ve viditelném vesmíru.
Vyhledávání patří mezi nejdůležitější operace nad libovolnou kolekcí. Vyhledávání v seznamu má ale časovou složitost $O(n)$ a je tak pro větší kolekce příliš pomalé (hlavně tehdy pokud, se vyhledává často).
Z tohoto důvodu existují kolekce optimalizované na vyhledávání.
Nejjednodušší je množina (set), do níž můžeme vkládat libovolné objekty, pro něž je rozumně definováno testování shody (rovnost).
Množiny lze vytvářet dvěma zásadními způsoby:
a) přímým výčtem ve složených závorkách
m1 = {1, 2, 3}
# výjimkou je prázdná množina kterou lze vytvořit jen voláním bezparametrického konstruktoru
empty = set()
b) vyplněním prostřednictvím iterátoru (včetně přesunu z jiné kolekce)
m2 = set(range(100))
m3 = set([1,2,3])
# resp. pomocí množinové komprehenze
m4 = {i for i in range(100)}
m5 = {str(i) for i in range(100)}
Nad množinou se nejčastěji provádějí následující operace:
in) s časovou složitostí $O(1)$remove nebo discard)add a update) s časovou složitostí $O(1)$for) s časovou složitostí $O(n)$ (projití všech prvků)union resp. update), průnik (intersetion) a rozdíl (difference) vše s časovou složitostí $O(n)$, kde $n$ je je délka kratší z množin (průnik) resp. součet jejich délek.issubset resp. issuperset (časová složitost je opět $O(n)$)Zajímavé je především vyhledávání s časovou složitostí $O(1)$ . To je téměř zázračné, protože to znamená, že vyhledávání mezi miliardou objektů (například lidí) je stejně rychlé jako vyhledávání mezi desítkou objektů osob.
Úkol: Uložte do množiny aktuální seznam názvů článků anglické Wikipedie, který je dostupný na URL http://dumps.wikimedia.org/enwiki/latest/enwiki-latest-all-titles-in-ns0.gz (doporučuji nejdříve stáhnout a pak otevírat jako soubor). Zjistěte počet článků a ověřte vyhledávací čas a porovnejte s časem vyhledávání v seznamu o několika málo položkách. Rada: Pro otevření kompromovaného
gzipsouboru použijte funkciopenz modulugzip(chovás se stejně jako vestavěná funkceopenjen provádí příslušnou komprimaci či dekomprimaci.
Zázraky však nejsou v reálném světě zadarmo. Slovník využívá tzv. hashovací tabulku, která ve své základní podobě předpokládá, že předem známe počet ukládaných hodnot (označme ho $n$)
Dále zvolme číslo $m$ takové, že je řádově shodné či větší než $n$ (tj. například $\frac{m}{n} = k$ je větší než 0,5).
Dále je potřeba
Při vložení hodnoty do hashovací tabulky se na hodnotu nejprve aplikuje hashovací funkce. Výsledek se použije jako index do hashovací tabulky tj. hodnota se uloží na konec podseznamu s s příslušným indexem. Složitost je evidentně $O(1)$, protože výpočet hashovací funkce nezávisí ana $n$ ani na $m$ a konstantní čas je i čas přidání na konec seznamu.
Při vyhledávání se opět nejdříve vypočte hashovací hodnota. Pak stačí prohledat příslušný podseznam (tj. podseznam s indexem rovným výsledku hashovací funkce). Složitost je opět $O(1)$ neboť konstantní je jak výpočet hashovací funkce tak prohledání podseznamumu, který má nejvýše $\left\lceil\frac{1}{k}\right\rceil$ prvků (předpokládáme rovnoměrné rozmístění), tj. například nejvýše dva pro $k>0.5$.
Vyzkoušíme si vlastní implementovat vlastní verzi hashovací tabulky (jen pro didaktické účely, vestavěná verze je mnohem efektivnější). Navíc naše verze bude zpočátku omezena jen na ukládání čísel, protože pro ně existuje přirozená hashovací funkce, zbytek po dělení hodnoto $m$ (což je velikost hashovací tabulky).
class HashTable:
def __init__(self, m):
self.table = [[] for _ in range(m)] # m-prázdných podseznamů
self.m = m
def add(self, x):
h = x % self.m # hashovací funkce
self.table[h].append(x)
def __contains__(self, x):
h = x % self.m # stejná hashovací funkce
return x in self.table[h]
def __str__(self): # výpis pro ladění
import pprint # std. modul pro 'hezký' výpis
return pprint.pformat(self.table)
Pro otestování zkusíme vytvořit hashovací tabulku (tj. naši implementaci slovníku) a naplnit ji náhodnými čísly. Zvolíme $n=100$, $m=50$ (tj $k=0{,}5$).
import random
ht = HashTable(50)
for _ in range(100):
ht.add(random.randint(0, 1_000))
print(ht)
Úkol: Aktuální implementace neřeší příliš vvícenásobné vkládání stejního čísla. Doplňte program tak, aby při se při opakovaném vkládání číslo v daném podseznamu objevilo jen jednou. Jak se tím ovlivní časová složitost.
Jak si můžete všimnout tak čísla nejsou rozložena zcela rovnoměrně, ale i tak je délka nejdelšího podseznamu výrazně menší než $n$ (při testovacím běhu byla délka největšího podseznamu rovna 6). To je platí v případě, že jsou vkládána náhodná čísla. Pokud vložíme čísla méně náhodná, pok je situace horší:
ht = HashTable(50)
for _ in range(100):
ht.add(random.randint(0, 1_000) * 10) # náhodné násobky desítí
print(ht)
Hodnoty jsou nyní soustředěny jen do těch položek, jež jsou dělitelné deseti a jej jich tak v seznamu mnohem větší počet (průměrně 20). Formálně je to sice stále $O(1)$ (počet = $\frac{10}{k}$ což nezávisí na $n$) ale tato fixní hodnota je pro menší počet položek srovnatelná s vyhledáváním v seznamu, tj. výraznější zrychlení se projeví, až pro velká $n$.
Všimněte si, že omezení vstupu na čísla dělitelná deseti může nastat v praxi docela často, neboť čísla jsou běžně zaokrouhlována. Efekt je přitom výrazný (doba vyhledávání se prodlouží 10x)
Otázka: Jaká je časové složitost, pokud vložíme jen hodnoty dělitelné 100.
V praxi je nutno vycházet z toho, že neexistuje žádná univerzálně použitelná hashovací funkce a to ani v rámci hodnot jednoho typu (a už vůbec ne pro hodnoty různých typů). Zvláště nepříjemná je situace u řetězců, kde je obtížné najít funkci, která by rovnoměrně rozmisťovala např. slova přirozeného jazyka (včetně např. jmen).
Python definuje pro všechny porovnatelné a nemodifikovatelné objekty obecnou hashovací funkci pomocí speciální metody __hash__ (lze ji přímo volat pomocí vestavěné funkce hash), která pro libovolnou hodnotu vrací číslo v rozsahu $-2^n$ až $2^n$ (kde $n$ závisí na implementaci, typicky je to dnes 63). Konkrétní hashovací funkce má následně tvar hash(x) % m.
hash(5) # pro malá čísla je univerzální hashovací funkce identita
print(hash("Sílor"))
print(hash("Dalibor"))
Úkol: Upravte naši implementaci naší třídy
HashTabletak aby využívala vestavěnou obecnou hashovací funkci.
Výsledná rychlost vyhledávání je kromě volby hashovací funkce ovlivněna i velikostí hashovací tabulky (v našem označení $m$). Pokud zvolíme $m$ dostatečně velké (typicky větší než očekávané $n$), pak většina řádků tabulky obsahuje nanejvýše jednu položku. Jen občas dochází k tzv. kolizím, kdy jsou dvě hodnoty namapovány na stejný řádek. Zvětšení $m$ ale není zadarmo, neboť mnohé řádky jsou naopak prázdné a paměť, která je pro ně alokována zůstává ladem.
Úkol: Stáhněte si číselník obcí v ČR a zjistěte kolik kolizí nastává při uložení do hashovací tabulky (číselník najdete na adrese http://apl.czso.cz/iSMS/cisdet.jsp?kodcis=43). při volbě stabdardní heshovací funkce hodnoty $k$ rovné 0,5, 1, 1,2 a 1.5. Rada: Použijte formát CSV, který lze relativně snadno zpracovávat pomocí modulu
csv.
Upozornění: Výsledky hashovací funkce (a tudíž i statistiky) jsou rozdílné nejen pro každý počítač, ale i pro každé spuštění interpreteru. Důvodem je bezpečnost. Pokud by byly hodnoty fixní a tudíž předem známé, byly by možné DOS útoky využívající výrazné snížení efektivity (včetně časové složitosti) pro určitá vstupní data.
Pokud je předem známa cílová velikost mnižiny (hashovací tabulky), pak je možno zvolit velikost tabulky, tak aby bylo dosaženo rovnováhy mezi požadovanou rychlostí a velikostí alokované paměti (klasické trade-off).
To však není vždy možné, neboť cílová velikost je známa spíše výjimečně. V tomto případě se používá přistup, kdy se na počátku alokuje tabulka dostatečné velikosti pro počáteční prvky. Je-li množina prázdná, pak se volí rozumná fixní velikost.
V okamžiku, kdy se hashovací tabulka zaplní, je její velikost zdvojnásobena. Tím se však změní hashovací funkce pro každou položku a tak musí být všechny přehashovány, tj. ve skutečnosti přesunuty do nové hashovací tabulky. V tomto případě, je však časová složitost vkládání rovna $O(n)$. V praxi je tak nutno odlišovat konkrétní časovou složitost (závisí na pořadí vkládaného prvku) a amortizovanou časovou složitost (průměrnou časovou složitost, kterou lze očekávat při vkládání většího počtu prvkou). Python tabulku zdvojnásobuje při každém překročení kapacity, a tak je pomalé vkládání (s přeheshováním) tak řídké, že ho lze při velkém počtu zanedbat) tj. i amortizovaná časová složitost je $O(1)$, V případě, kdy je tolerovatelná doba odezvy omezena, je však nutno počítat s konkrétní časovou složitostí!
Úkol: Odvoďte strategii rehashování standardní množiny za použití metody
sys.getsizeof, která vracé velikost objektu v bytech (nepočítá se velikost vnořených objektů tj. např. položek, jern velikost odkazů na ně)-
Další datovou strukturou, která využívá hashovací tabulku je slovník. Slovník namísto jednotlivých hodnot ukládá dvojice hodnot. První u nich slouží k vyhledávání (právě na ní se volá hashovací funkce) a běžně se označuje jako klíč.
Slovník slouží pro representaci zobrazení z množiny klíčů do množiny hodnot. /říkladem je například zobrazení řetězcových identifikátorů na datové údaje.
# inicializace
slovnik = {"Sírius" : - 1.5, "Canopus": -0.7, "Alpha Cen": -0.3, "Arcturus": -0.05}
# přidání dvojice klíč hodnota
slovnik["Vega"] = 0.03 # klíč je indexem
# získání hodnoty pro daný klíč (vyhledáván je vždy klíč)
print(slovnik["Canopus"])
Alternativně lze seznam inicilizovat pomocí konstruktoru a pojmenovaných parametrů. Tento přístup však lze použít jen tehdy jsou-li klíči jen řetězce, které musí splňovat omezení kladaná na pythonské identifikátory (tj. musí to začínat písmenem nebo podtržítkem, další znaky musí být alfanumerické nebo podtržítka, nesmí tudíž mimo jiné obsahovat mezery)
slovnik2 = dict(Sírius=-1.5, Canopus=-0.7) #Alpha Cen takto nevložíme
Podobně existuje i alternativní syntaxe pro získání hodnoty pro daný klíč — metoda get. Ta se hodí v případě, pokud je vyšší pravděpodobnost, že daný klíč ve slovníku není. Indexace v tomto případě vede k pomalé a obtížně zpracovatelné výjimce, metoda get umožňuje definovat implicitní hodnotu.
slovnik.get("Vega", None) # none je implicitní hodnota
slovnik.get("Wega", None) # zkuste i jinou imlicitní hodnotu například math.nan
Pro procházení seznamů lze využít dva přístupy: procházení přes dvojice (metoda items) nebo přes klíče (metoda keys).
for key, value in slovnik.items():
print(key, value)
for key in slovnik.keys():
print(key, slovnik[key]) # méně efektivní, indexace (vyhledávání) je O(1), ale ne vždy
Pořadí, v němž se položky seznamu procházejí je v novějších verzích (>= 3.6) Pythonu shodné s pořadím, v nichž byly vkládány. Ve starších verzích však bylo náhodné (a díky náhodné komponentě používané v hashovací funkci v rámci sezení i různé při každém spuštění). Novější chování vyžaduje o něco složitější datovou strukturu).
V programu se často vyskytují situace, kdy není definováno, jak postupovat dál. Běžný běh programu není možný (např. z důvodů, že něco chybí) a v daném místě nelze zajistit ani žádné náhradní řešení.
Příklady:
a) Funkce hledající průměr dostane prázdný seznam. Pro tento seznam není definován průměr a alternativní řešení jsou nevhodná nebo matoucí:
None nelze dělat žádné aritmetické výpočtymath.NaN (hodnotu Nan nesmí vidět koncový uživatel, a obtížně se zjišťuje, kde vznikla)None, v některých jiných jazycích nemožné)max([])
b) Funkce, která komunikuje s HTTP serverem provede operaci GET s neplatným nebo neexistujícím URL. Zde sice zdánlově existuje relativně rozumná možnost, tj. vrácení chybové hodnoty (HTTP error code), ale ty jsou podle standardu generovány HTTP serverem nikoliv klientem.
from requests import get
get("xx")
c) uživatel přeruší běh programu (například během vstupu). To lze v Jupyter notebooku provést pomocí menu Kernel|Interrupt kernel (pří běhu skriptu pak klávesou Ctrl+C). Uživatel jasně naznačil, že běžné ukončení programu není možné (resp. požadované).
x = input()
Program (přesněji řečeno běhová podpora Pythonu) zareagovala ve všech těchto případech stejně. Program je přerušen a vznikne objekt označovaný jako výjimka. Ten nese dvě základní informace:
Standardní reakcí na vznik výjimky (a tím i dočasné) přerušení programu je bezprostřední dokončení programu a výpis informací o výjimce (krátký anglický text + místo vzniku výjimky).
Jednou z možných interpretací výjimky je vzdát vzdání se odpovědnosti. Když už nějaký kód neví jak dál, tak se může vzdát odpovědnosti a nechat řešení na jiné části kódu počítaje v to i kód, který program rozumně ukončí (rozumně = například s hlášením, co a kde se stalo)
Úkol: Uveďte i další případy, kdy program končí výjimkou.
Výjimka však nemusí vzniknout jen externím přičiněním (tj. přerušením programu uživatelem resp. operačním systémem) nebo uvnitř knihovních metod a funkcí (jako tomu bylo u funkce max či requests.get). Může to udělat o programátor. pokud se dostane do situace. kdy neexistuje žádné obecně použitelné řešení a musí se tak vzdát odpovědnosti.
Děje se tak mechanismem označovaným jako vyhození výjimky. Příkaz raise pozastaví program a vytvoří objket výjimky, která nese informaci o příčině.
Ukažme si příklad. Následující funkce má za úkol najít průsečík dvou přímek zadaných v obecném tvaru $ax+by+c=0$. Funkce by měla vracet jeden bod. V případě, že jsou přímky rovnoběžné, není návratová hodnota definována (protože takový bod neexistuje resp. existuje nekonečně mnoho bodů). Funkce se proto zbaví odpovědnosti tím, že vyhodí výjimku.
def intersection(p, q):
pa,pb,pc = p # dekonstrukce hodnot z n-tic resp. seznamů
qa,qb,qc = q
det = pa * qb - pb * qa
if pa * qb - pb * qa == 0: # normálové vektory jsou závislé (determinant = 0)
raise Exception("Parallel lines") # vyhození výjimky
return (pc * qb - pb * qc)/det, (pa * qc - pc * qa)/det # Crammerovo pravidlo
intersection((1,1,0), (0,1,3)) # průsečík existuje
intersection((2,3,4), (4,6,1))
Při návrhu metody by bylo lze uvažovat i o jiném řešení nedefinovaného případu. Funkce by v tomto případě mohla vrátit nějakou speciální hodnotu, v tomto případě například None. To je však méně přehledné (None v tomto případě representuje, jak neexistenci bodu tak existenci několika možných bodů). Navíc zátěž testování (které je nutné, neboť None objekt nelze využít jako běžnou representaci 2D bodu) by byla na kódu, který danou funkci využívá.
Základní pravidlo: Jakýkoliv program by měl buď fungovovat podle specifikace (tj. poskytovat správné výsledky nebo u interaktivních funčnost) nebo být předčasně ukončen výjimkou.
Objekt výjimky v našem případě instancí třídy Exception (v rámci výrazu za příkazem raise se volá konstruktor této třídy). Obecně však mohou být objekty výjimek i instancemi specializovanějších tříd.
Přehhled vestavěných tříd výjimek najdete na stránkách https://docs.python.org/3/library/exceptions.html#base-classes resp. https://docs.python.org/3/library/exceptions.html#concrete-exceptions (o něco konkrétnější výjimky).
Úkol: Pokuste se najít konkrétnější výjimku pro náš příklad s průsečíkem přímek (a použijte ji v pro gramu).
Mechanismus výjimek se využívá i v případě, tzv. asercí, které primárně kontrolují sémantické chyby v programu. Aserce je test, který ověřuje, že jsou splněny všechny předpoklady pro úspěšné pokračování programu (tj. jinak řečeno zda se program nachází v definovaném stavu), Pokud je podmínka splněna (vše je OK), program pokračuje bez jakéhokoliv ovlivnění, pokud splněna není je přerušen a je vyhozena výjimka AssertionError.
a = 2
b = 2
assert a != b, "Hodnoty a, b nesmí být stejné"
x = 2 / (a-b)
Ne zcela intuitivní je u asercí skutečnost, že podmínka definuje požadovaný (tj. bezchybný stav a nepovinná zpráva naopak popisuje stav chybový (tj. je-li splněna negace podmínky)!
Použití asercí se významně překrývá s oblastí použití výjimek. Mezi základní rozlišovací charakteristiky patří:
a, b jsou vždy různé (což může být zajištěno již ve stupní rutině např. znemožněním zadání stetjných hodnot v GUI formuláři, či testováním přípustných hodnot při čtení datových souborů), pak je namístě aserce (chyba vznikne např. špatným výpočtem, resp. použitím špatné funkce). V opačném případě je namístě vyvolání výjimkyPři běhu programu se chování asercí od běžných výjimek liší v tom, že v tzv. release nasazení (tj. ve finálním nastavení u zákazníka) neuplatňují (tj. se nevznikají výjimky a nedochází ani k testování testovací podmínky), jinak řečeno aserce v release nasazení program nezpomalují.
V Pythonu jsou aserce vypínány v případě, že vestavěná proměnná __debug__ nabývá hodnoty False, což se děje jen v případě, že překladač spustíte s přepínačem -O (tím se zapne tzv, optimalizace kódu).
Upozornění: vypnutím asercí se samozřejmě neodstraní příčina chyby. Program musí být nejdříve důkladně otestován a teprve pak se spouštěn v optimalizovaném
releaserežimu.
Aserci můžete použít i v našem případě s průsečíkem přímek. Je však nutné zajistit, že odpovědnost za zajištění nerovnoběžnosti přímek bude kódu volajícím danou funkci. To lze zajistit například tím, že tento požadavek uvedeme do dokumentace k dané funkci. Dokumentace se uvádí jako řetězec (často víceřádkový) za hlavičkou funkce.
def intersection3(p, q):
"""
Průsečík dvou přímek. Přímky přímky nesmí být rovnoběžné či identické.
Args:
p: trojice koeficientů (a,b,c) z obecného tvaru první přímky ax + by + c = 0
q: trojice koeficientů (a,b,c) z obecného tvaru druhé přímky ax + by + c = 0
"""
pa,pb,pc = p # dekonstrukce hodnot z n-tic resp. seznamů
qa,qb,qc = q
det = pa * qb - pb * qa
assert pa * qb - pb * qa != 0, "Paralllel lines are not supported"
return (pc * qb - pb * qc)/det, (pa * qc - pc * qa)/det # Crammerovo pravidlo
help(intersection3) # takto lze zobrazit dokumentaci (zobrazuje se i v růůzných IDE)
intersection3((1,0,1), (2,0,2)) # byli jste varováni
Úkol: Konkrétní formát dokumentačníjo řetězce Python nepředepisuje. V praxi existuje několik formátů podporovaných různými nástroji (generátory dokumentace, IDE). Z přehledu na stránkách datacampu (autor Aditya Sharma) určete jaký formát byl použit v příkladě (resp. proč?).
Při vzniku výjimky zanikají všechny kontexty, ve kterých došlo jejímu vyhození. Zanikají proměnné, uvolňují se objekty, apod. Jinak řečeno výjimky zajistí, že se i po předčasném skončení funkcí, či celých programů zanechá čistý stůl. To však platí jen v případě zdrojů umístěných v operační paměti. Pokud jsou v daném kontextu drženy nějaké zdroje operačního systému (otevřené soubory, dočasné soubory, síťová spojení, bitmapy v GPU, apod.) tak se neuvolní!
Podívejme se na následující příklad. Je to funkce, která otevře konfigurační soubor s názvem network.ini, která obsahuje jediný řádek, který definuje hodnotu konfigurační volby network, která může nabývat hodnoty yes nebo no.
Nejdříve vytvoříme příslušný konfigurační soubor.
%%writefile network.ini
network=no
def network_configuration():
import re
f = open("network.ini")
line = f.read()
match = re.match("network=(yes|no)", line)
if not match:
raise Exception("Invalid configuration file")
f.close() # nesmíme zapomenout zavřít soubor
return match.group(1) == "yes"
network_configuration()
Nyní vytvoříme soubor s chybnou syntaxí (hodnota false není podporována).
%%writefile network.ini
network=false
network_configuration()
Výsledek není překvapivý. Je vyolána výjimka, neboť obsah konfiguračního souboru není v našem modelu interpretovatelný.
Je zde však i jeden nepříjemný důsledek. Soubor network.ini je otevřen, ale není uzavřen! A to navzdory tomu, že je explicitně uzavírán na řádku pod vyhozením výjimky.
Důvod je zřejmý. Po vyhození výjimky se se zbytek funkce neprovede (výjimka opustí funkci) a neprovede se tedy ani volání metody close. V běžných programech se sice soubor uzavře po ukončení programu, ale k tomu nemusí dojít, neboť výjimka může být tzv. zachycena. Děje se tak i v případě vyvolání funkce v Jupyter notebooku. V tomto případě nejen, že nedošlo k uzavření souboru, ale soubor už ani nelze uzavřít (lokální proměnná totiž přestává existovat a my nemáme žádný odkaz na objekt zpřístupňující otevřený soubor).
Neuzavření souboru sice většinou nebrání dalšímu běhu, snižuje se však množství volných prostředků. Po určoté době prostředky dojdou. V případě Linuxu i dalších operačních systémů je množství simultánně otevřených souborů na proces relativně vysoké, ale i tak není nekonečné.
Úkol: Zjistěte jaké maximální simultánně běžících procesů může mít váš Jupyter notebook (v rámci vaší instance operačního systému).
Základním řešením tohoto problému je využití tzv. správců kontextů. Objekty, které fungují jako správci kontextů podporují dvě metody. Metoda __enter__ je volána při vstupu do určitého úseku kódu (tj. například do úseku kódu využívajícího otevřený soubor), metoda __exit__ při výstupu, ať už je úsek kódu opuštěn doasažením konce, výskokem (např. příkazem return) nebo vyhozením výjimky.
Příslušný úsek kódu je definován příkazem with, za nímž následuje odsazený blok. Na začátku bloku se typicky vytváří objekt funngující (mimo jiné) jako kontextový manager a je volána jeho metoda __enter__. Na konci bloku se volá metoda __exit__, která uvolní prostředky svázané s objektem (samotná objekt nicméně stále existuje).
with open("network.ini", "rt") as f:
print(f.read())
Za klíčovým slovem with je výraz, jehož vyhodnocením vznikne objekt proudu, který se zároveň stane správcem kontextu, je na něm volána metoda __enter__ a je opatřen jménem f. Uvnitř odsazeného bloku (zde je to jediný řádek) lze využívat proměnnou, objekt, který odkazuje, i otevřený soubor, který je vlastněn objektem (tj. ze souboru lze číst).
Po ukončení existuje jak proměnná (zde f, tak objekt souboru) tak objekt proudu, je však uzavřen soubor, který je s ním spjat (uvnitř volání metody exit). Ten již proto zbytečně nealokuje prostředky. Nelze jej však využít pro další práci.
f.read()
Úkol: Vyzkoušejte, že k uzavření souboru dojde i v případě, že uvnitř bloku po
withdojde ke vzniku výjimky. Implentujte správnou verzi funkcenetwork_configuration.
def network_configuration():
import re
with open("network.ini") as f:
line = f.read()
match = re.match("network=(yes|no)", line)
if not match:
raise Exception("Invalid configuration file") # opuštění with bloku výjimkou
return match.group(1) == "yes" # opuštění with bloku ukončením funkce (výskokem)
network_configuration()
Úkol: Porovnejte použití správce kontextů mezi bežnými soubory a soubory dočasnými (viz standardní modul
tempfile). Vyzkoušejte na příkladě. (můžete použít příklad z dokumentace).