From 02bb0275b36c1da43bb6f68b6a82c2956fa714ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 21 Mar 2017 14:27:18 +0100 Subject: [PATCH 001/138] Delete copyied content from MI-PYT --- lessons/intro/async/index.md | 1191 ------ lessons/intro/cython/index.md | 760 ---- lessons/intro/distribution/index.md | 418 -- lessons/intro/docs/index.md | 430 -- lessons/intro/flask/index.md | 377 -- lessons/intro/magic/index.md | 718 ---- lessons/intro/micropython/index.md | 278 -- lessons/intro/numpy/index.md | 41 - lessons/intro/pandas/index.ipynb | 5610 --------------------------- lessons/intro/pandas/index.md | 73 - lessons/intro/pyqt/index.md | 817 ---- lessons/intro/testing/index.md | 608 --- 12 files changed, 11321 deletions(-) delete mode 100644 lessons/intro/async/index.md delete mode 100644 lessons/intro/cython/index.md delete mode 100644 lessons/intro/distribution/index.md delete mode 100644 lessons/intro/docs/index.md delete mode 100644 lessons/intro/flask/index.md delete mode 100644 lessons/intro/magic/index.md delete mode 100644 lessons/intro/micropython/index.md delete mode 100644 lessons/intro/numpy/index.md delete mode 100644 lessons/intro/pandas/index.ipynb delete mode 100644 lessons/intro/pandas/index.md delete mode 100644 lessons/intro/pyqt/index.md delete mode 100644 lessons/intro/testing/index.md diff --git a/lessons/intro/async/index.md b/lessons/intro/async/index.md deleted file mode 100644 index f32c76600a..0000000000 --- a/lessons/intro/async/index.md +++ /dev/null @@ -1,1191 +0,0 @@ -Generátory a AsyncIO -==================== - -Na část toto cvičení bude opět potřeba PyQt5. -Můžete použít virtualenv z minula, nebo PyQt5 nainstalovat znovu viz minulá lekce. -(Nejde-li to, nevadí – úplně nezbytné dnes PyQt nebude.) - -Další knihovny pro dnešní den: - - python -m pip install --upgrade pip - python -m pip install notebook aiohttp quamash - -Případně pro Python 3.3: - - python -m pip install asyncio - - ---- - -Dnes se podíváme na dvě témata, která spolu souvisí čím dál tím méně, ale stále je -dobré je pochopit společně. -Napřed si ukážeme *generátory*, a poté si vysvětlíme *asynchronní programování*. - - -Generátory -========== - -Nejdříve si popíšeme, jak v Pythonu fungují *generátory*, tedy funkce s příkazem `yield`. -Předpokládám, že většina z vás už nějaký jednoduchý generátor napsala, ale pojďme si je -vysvětlit od úplného začátku: od toho, jak se v Pythonu iteruje. - - -Iterace -------- - -Když je v Pythonu potřeba iterovat přes nějakou kolekci, použije se *iterační protokol*, -který pracuje se dvěma druhy objektů: s *iterovatelnými objekty* a s *iterátory*. - -Iterovatelné objekty (*iterables*) se vyznačují tím, že je na ně možné zavolat -funkci `iter()`, která vrátí příslušný iterátor: - - >>> iter([1, 2, 3]) - - -Na iterátor pak je možné opakovaně volat funkci `next()`, čímž dostáváme jednotlivé -prvky iterace. -Po vyčerpání iterátoru způsobuje `next()` výjimku `StopIteration`: - - >>> it = iter([1, 2, 3]) - >>> next(it) - 1 - >>> next(it) - 2 - >>> next(it) - 3 - >>> next(it) - Traceback (most recent call last): - ... - StopIteration - >>> next(it) - Traceback (most recent call last): - ... - StopIteration - -Zároveň platí, že každý iterátor je iterovatelný: zavoláním `iter()` na iterátor -dostaneme ten stejný iterátor (nikoli jeho kopii) zpět. -Naopak to ale obecně neplatí: seznamy jsou iterovatelné, ale nejsou samy o sobě -iterátory. - -Iterátor je ve většině případů "malý" objekt, který si "pamatuje" jen původní iterovatelný -objekt a aktuální pozici. Příklady jsou iterátor seznamů (`iter([])`), slovníků (`iter({})`), -n-tic, nebo množin, iterátor pro `range`, a podobně. - -Iterátory ale můžou být i "větší": třeba otevřený soubor je iterátor, z něhož `next()` -načítá jednotlivé řádky. - - -Generátory ----------- - -Asi nejzajímavější druh iterátoru je tzv. *generátor*: funkce, která umí postupně -dávat k dispozici hodnoty. -Definuje se pomocí klíčového slova `yield`: každá funkce, která obsahuje `yield`, -je *generátorová funkce* (angl. *generator function*). - - def generate2(): - """generates 2 numbers""" - print('A') - yield 0 - print('B') - yield 1 - print('C') - -Zavoláním takové funkce dostáváme *generátorový iterátor* (angl. *generator iterator*): - - >>> generate2() - - -Voláním `next()` se pak stane zajímavá věc: funkce se provede až po první `yield`, -tam se *zastaví*, a hodnota `yield`-u se vrátí z `next()`. -Při dalším volání se začne provádět zbytek funkce od místa, kde byla naposled -zastavena. - - >>> it = generate2() - >>> next(it) - A - 0 - >>> next(it) - B - 1 - >>> next(it) - C - Traceback (most recent call last): - ... - StopIteration - -Tahle vlastnost přerušit provádění funkce je velice užitečná nejen pro vytváření -sekvencí, ale má celou řadu dalších užití. -Existuje třeba dekorátor, který generátorovou funkci s jedním `yield` převede na *context manager*: - - import contextlib - - @contextlib.contextmanager - def ctx_manager(): - print('Entering') - yield 123 - print('Exiting') - - - with ctx_manager() as obj: - print('Inside context, with', obj) - -Vše před `yield` se provede při vstupu do kontextu, hodnota `yield` se předá -dál, a vše po `yield` se provede na konci. -Můžeme si představit, že místo `yield` se "doplní" obsah bloku `with` – -funkce se tam na chvíli zastaví a může se tedy provádět něco jiného. - - -Vracení hodnot z generátorů ---------------------------- - -V rámci generátorové funkce můžeme použít i `return`, který funkci ukončí. -Vrácená hodnota se však při normální iteraci (např. ve `for`) nepoužije. -Objeví se pouze jako hodnota výjimky `StopIteration`, která signalizuje konec -iterace: - - def generator(a, b): - """Yield two numbers and return their sum""" - yield a - yield b - return a + b - - >>> it = generator(2, 3) - >>> next(it) - 2 - >>> next(it) - 3 - >>> next(it) - Traceback (most recent call last): - File "", line 1, in - StopIteration: 5 - - -Obousměrná komunikace ---------------------- - -Oproti normálním iterátorům, které hodnoty jen poskytují, mají generátory metodu -`send()`, kterou je možné posílat hodnoty *do* běžícího generátoru. -Klíčové slovo `yield` totiž může fungovat jako výraz, a tento výraz nabývá poslanou -hodnotu (nebo `None`, byl-li použit normální `next()`). - - def running_sum(): - total = 0 - while True: - num = (yield total) - if num: - total += num - - it = running_sum() - next(it) # pro první iteraci nelze použít send() -- nečekáme zatím na yield-u - it.send(2) - it.send(3) - assert next(it) == 5 - -Upřímě řečeno, metoda `send()` není příliš užitečná. -(Když byste něco takového potřebovali, radši si napište třídu, která si bude -stav uchovávat v atributech, a měňte ji třeba metodami. Bude to pravděpodobně -přehlednější.) -Existuje ale příbuzná metoda, která už je užitečnější: `throw()`. -Ta do generátoru "vhodí" výjimku. -Z pohledu generátorové funkce to vypadá, jako by výjimka nastala na příkazu -`yield`. - - def report_exception(): - try: - yield - except BaseException as e: - print('Death by', type(e).__name__) - yield 123 - - >>> it = report_exception() - >>> next(it) # opět – v první iteraci nelze throw() použít - >>> value = it.throw(ValueError()) - Death by ValueError - >>> value - 123 - -Podobná věc se děje, když generátorový iterátor zanikne: Python do generátoru -"vhodí" výjimku GeneratorExit. -Ta dědí z `BaseException`, ale ne `Exception`, takže klasické `except Exception:` -ji nechytí (ale např. `finally` funguje jak má). -Pokud generátor tuto výjimku chytá, měl by se co nejdřív ukončit. -(Když to neudělá a provede další `yield`, Python ho ukončí "násilně".) - - >>> import gc - >>> it = report_exception() - >>> next(it) - >>> del it; gc.collect() # zbavíme se objektu "it" (i v interpretech, které nepoužívají reference counting) - Death by GeneratorExit - Exception ignored in: - RuntimeError: generator ignored GeneratorExit - 0 - - -Kombinace generátorů --------------------- - -Máme následující generátor: - - def dance(): - yield 'putting hands forward' - yield 'putting hands down' - yield 'turning around' - yield 'jumping' - yield 'putting hands forward' - yield 'putting hands down' - - for action in dance(): - print(action) - -Opakuje se v něm jistá sekvence, kterou bychom jako správní programátoři chtěli -vyčlenit do samostatné funkce. -Pomocí samotného `yield` to ale jde celkem těžko: - - def dance_hands(): - yield 'putting hands forward' - yield 'putting hands down' - - def dance(): - for action in dance_hands(): - yield action - yield 'turning around' - yield 'jumping' - for action in dance_hands(): - yield action - - for action in dance(): - print(action) - -Tohle počtu řádků příliš nepomohlo. Existuje lepší způsob – místo: - - for action in dance_hands(): - yield action - -můžeme delegovat vytváření podsekvence na jiný generátor pomocí: - - yield from dance_hands() - -Příkaz `yield from` deleguje nejen hodnoty, které jdou z generátoru "ven" pomocí -`yield`, ale i ty, které jdou "dovnitř" pomocí `send()` či `throw()`. -A dokonce funguje jako výraz, jehož hodnota odpovídá tomu, co -daný generátor vrátil: - - def dance_hands(): - value = (yield 'putting hands forward') - yield 'putting hands down' - if value: - yield 'putting {},- in pocket'.format(value) - return value - return 0 - - def dance(): - profit = 0 - profit += (yield from dance_hands()) - yield 'turning around' - yield 'jumping' - profit += (yield from dance_hands()) - if profit: - yield 'spending {},- on sweets'.format(profit) - - def performance(): - it = dance() - print(next(it)) - print(it.send(100)) - for action in it: # pokračujeme v načaté iteraci – implicitní "iter(it)" vrací zase "it" - print(action) - - >>> performance() - putting hands forward - putting hands down - putting 100,- in pocket - turning around - jumping - putting hands forward - putting hands down - spending 100,- on sweets - - -AsyncIO -======= - -A teď něco úplně jiného: asynchronní programování. - -Jak jsme si řekli v lekci o C API, Python má globální zámek, takže pythonní kód -může běžet jen v jednom vlákně najednou. -Taky jsme si řekli, že to většinou příliš nevadí: typický síťový nebo GUI program -stráví hodně času čekáním na události (odpověď z internetu, kliknutí myší atp.), -a u tohoto čekání není potřeba držet zámek zamčený. - -Servery typicky při zpracovávání požadavku stráví *většinu* času síťovou komunikací. -Proto se často spouští několik vláken nebo přímo procesů najednou, aby se mohl vytížit -procesor. -Při velkém množství vláken ale nastanou dva problémy. -První je, že vláken nemůže být neomezeně mnoho. -Každé vlákno potřebuje vlastní stack, tj. poměrně velkou část paměti; a počet vláken -bývá omezen i jinak (na Linuxu je globální limit počtu procesů, do kterého se počítají -i jednotlivá vlákna – viz `cat /proc/sys/kernel/threads-max`). -Druhý problém je, že přepnutí z jednoho vlákna do druhého se může stát *kdykoli*. -Ověřit si, že je na to program připravený, je poměrně složité, a na zajištění -správné funkčnosti je potřeba zamykání či jiné techniky, které bývají relativně -pomalé, a tak se jim programátoři snaží vyhnout. -A chyby vzniklé nesprávným ošetřením přepínání vláken bývají složité na odhalení -a vyřešení. Pokud jste absolvovali předmět BI-OSY, jistě víte, o čem mluvíme. - -Vlákna jsou příklad *preemptivního multitaskingu*, kdy operační systém rozhoduje, -kdy přepne z jednoho vlákna do druhého, a tuto změnu si prakticky vynutí. -Jednotlivá vlákna se s tím musí vyrovnat. -Alternativou je *kooperativní multitasking*, kdy se jednotlivé úlohy umí *samy* vzdát -procesorového času, když např. čekají na síťovou komunikaci. -Programátor tak ví, že dokud takto nepředá kontrolu ostatním úlohám, žádná jiná -úloha mu pod rukama nemůže měnit stav procesu. -Na druhou stranu je ale potřeba dostatečně často kontrolu předávat, aby se všechny -úlohy dostaly ke slovu. -Tuto techniku tak nemůže používat operační systém, pod kterým můžou běžet i špatně -napsané programy. Ale v rámci jednoho procesu se to dá s úspěchem využít. - -Pojďme si to ukázat na příkladu. -Místo síťové komunikace budeme pro názornost čekat, až uplyne nějaký čas: napíšeme si -jednoduchou animaci. -V reálném serveru bychom místo čekání, než uplyne určitý počet sekund, čekali na odpověď -ze sítě, ale principy zůstávají stejné. - - import random - import time - - - def print_blinky(blinky): - print(blinky, end='\r') - - - class Blinky: - def __init__(self): - self._face = '(o.o)' - - def __str__(self): - return self._face - - def set_face(self, new): - self._face = new - print_blinky(self) - - def run(self): - while True: - self.set_face('(-.-)') - time.sleep(random.uniform(0.05, 0.1)) - self.set_face('(o.o)') - time.sleep(random.uniform(0.5, 1)) - - - Blinky().run() - -Chceme-li spustit několik takových animací, můžeme to udělat ve vláknech: - - import random - import time - import threading - - - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') - - - class Blinky: - def __init__(self): - self._face = '(o.o)' - - def __str__(self): - return self._face - - def set_face(self, new): - self._face = new - print_blinkies() - - def run(self): - while True: - self.set_face('(-.-)') - time.sleep(random.uniform(0.05, 0.1)) - self.set_face('(o.o)') - time.sleep(random.uniform(0.5, 1)) - - - blinkies = [Blinky() for i in range(10)] - - for blinky in blinkies: - threading.Thread(target=blinky.run).start() - -Ale po docela jednoduchých změnách se může stát, že se jednotlivá vlákna začnou -přepínat nevhodně, a celý program se rozsype. -Nám stačí malá změna ve funkci `print_blinkies` (podobná funkce by v reálném programu -mohla být z externí knihovny, která při přechodu na novou verzi trošku změnila -vnitřní implementaci): - - def print_blinkies(): - for blinky in blinkies: - time.sleep(0.001) - print(blinky, end=' ') - print(end='\r') - -Tohle se samozřejmě dá řešit např. zámkem kolem volání `print_blinkies`. -Chyby tohoto typu ale mají tendenci se projevovat jen zřídka: i původní -program bez `sleep` byl napsaný špatně, jen se to *většinou* neprojevilo. - -Jiný způsob, jak tohle vyřešit, je naimplementovat *smyčku událostí*. -Kdykoli je potřeba pozastavit běh některé úlohy, tak zbytek úlohy naplánujeme -na nějaký pozdější čas, a mezitím spouštíme úlohy, které byly naplánovány -na dříve. - - import random - import time - - - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') - - - blinkies = [] - - class Blinky: - def __init__(self): - self.open_eyes() - - def __str__(self): - return self._face - - def set_face(self, new): - self._face = new - print_blinkies() - - def close_eyes(self): - self.set_face('(-.-)') - schedule(random.uniform(0.05, 0.1), self.open_eyes) - - def open_eyes(self): - self.set_face('(o.o)') - schedule(random.expovariate(1/2), self.close_eyes) - - - # Scheduling via a list of [remaining time, function to run] pairs: - - task_entries = [] - def schedule(wait_time, task): - """Schedule "task" to occur "wait_time" seconds from now""" - task_entries.append([wait_time, task]) - - blinkies = [Blinky() for i in range(10)] - - - # Simple event loop - while task_entries: - # Get the entry with the least remaining time - task_entries.sort(key=lambda e: -e[0]) - wait_time, task = task_entries.pop() - - # Wait (this ignores the time needed to actually run code - time.sleep(wait_time) - - # Decrease remaining time for all tasks by the time waited - for entry in task_entries: - entry[0] -= wait_time - - # Run the actual task - task() - -V tomto řešení nefigurují vlákna: každá funkce se provede celá najednou, -a ostatní úlohy běží pouze mezi jednotlivými funkcemi jedné úlohy. -Mnohem lépe se tak ověřuje správnost programu. - -Tohle řešení je ale docela těžkopádné. -Chtěli jsme napsat *cyklus*, ale místo toho máme dvě funkce, co se "volají" -navzájem. Není z toho poznat, že jde o cyklus. -A to je jen jednoduchý příklad – složitější logika by byla ještě -nepřehlednější. -(Programy pro knihovny jako Twisted nebo Node.js se tradičně píšou tímto způsobem. -Jazyky jako JavaStript na to mají trochu pohodlnější syntaxi, přesto se -pro extrémní případy této nepřehlednosti vžilo označení *callback hell*.) - -Naštěstí ale v Pythonu umíme napsat funkce, které lze "pozastavit" – generátory! -S drobnou změnou smyčky událostí lze náš program zapsat opět téměř -procedurálně, ale s tím, že k přepínání úloh dochází jen na -vyznačených místech: tam, kde použijeme `yield`. - - import random - import time - - - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') - - - class Blinky: - def __init__(self): - self._face = '(o.o)' - - def __str__(self): - return self._face - - def set_face(self, new): - self._face = new - print_blinkies() - - def run(self): - while True: - self.set_face('(-.-)') - yield random.uniform(0.05, 0.1) - self.set_face('(o.o)') - yield random.expovariate(1/2) - - - # Scheduling via a list of [remaining time, generator] pairs: - - blinkies = [Blinky() for i in range(10)] - - task_entries = [[0, b.run()] for b in blinkies] - - - # Simple event loop - while task_entries: - # Get the entry with the least remaining time - task_entries.sort(key=lambda e: -e[0]) - wait_time, task = task_entries[-1] - - # Wait (this ignores the time needed to actually run code - time.sleep(wait_time) - - # Decrease remaining time for all tasks by the time waited - for entry in task_entries: - entry[0] -= wait_time - - # Run the actual task - new_time = next(task) - task_entries[-1][0] = new_time - -Na tomto principu je postavené moderní API, které se pro podobné úlohy používá. -Než si ho ale ukážeme, pojďme se na chvíli podívat do historie. - - -Souběžnost v Pythonu --------------------- - -V Pythonu existovala a existuje řada knihoven, které nám umožňují "dělat více -věcí zároveň". -Základ jsou `threading`, tedy podpora pro vlákna, a `multiprocessing`, tedy -způsob jak spustit nový pythonní proces, ve kterém se provede určitá funkce -(přičemž vstup a výstup se předává serializovaný přes *pipes*). - -Další knihovna, kterou lze z PyPI nainstalovat, je [greenlet]. -Ta nám dává k dispozici tzv. *mikro-vlákna*, -která se mezi sebou přepínají v rámci jednoho procesu. -Na rozdíl od systémových vláken nepotřebují tolik paměti navíc, ale -stále jde o *preemptivní* strategii: k přepnutí může dojít kdykoli, -je tedy potřeba zamykat a složitě hledat málo časté chyby. - -Byly vyvinuty i knihovny pro *kooperativní* přepínání, založené na tzv. -*futures* (které vysvětlíme vzápětí). -Nejznámější jsou [Twisted] a [Tornado]. -Obě jsou relativně staré (2002, resp. 2009), ale stále populární. - -Ačkoli byly Twisted, Tornado a podobné knihovny užitečné, jejich problém -byl v tom, že má každá jiné API. -Vznikaly tak kolem nich ekosystémy vázané na konkrétní knihovnu: -server napsaný pro Tornado se nedal použít pod Twisted, a aplikace -využívající Twisted nemohla využít knihovnu pro Tornado. - -Jak to vyřešit? - - -Jeden standard --------------- - -![xkcd 927](http://imgs.xkcd.com/comics/standards.png) - -*Komiks [xkcd](https://xkcd.com/927/), © Randall Munroe, [CC-BY-NC](http://creativecommons.org/licenses/by-nc/2.5/)* - -Podobně jako přístup k různým SQL databázím je v Pythonu standardizovaný -(knihovny pro SQLite, Postgres, MySQL atd. všechny podporují API definované -v [PEP 249]), nebo je standardizované API webových serverů (WSGI, [PEP 3333]), -tak vzniklo standardizované API pro kooperativní multitasking. -Toto API je definováno v [PEP 3156], a jeho referenční implementace, `asyncio`, -je od Pythonu 3.4 ve standardní knihovně. -(Pro Python 3.3 se dá asyncio stáhnout [z PyPI][pypi-asyncio].) -Interně je `asyncio` postavené na konceptu *futures* inspirovaných Tornado/Twisted, -ale jeho "hlavní" API je postavené na *coroutines* podobných generátorům. - -Od Pythonu verze 3.5 používá asyncio místo normálních generátorů (`yield from`) -speciální syntaxi, která "asynchronní funkce" dovoluje kombinovat s příkazy -`for` a `with` (a v budoucnu snad i se samotným `yield`). -Tuto syntaxi použijeme i tady; máte-li starší Python, podívejte se na potřebné změny uvedené níže. - -Náš příklad s animací vypadá v `asyncio` takto: - - import random - import time - import asyncio - - - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') - - - class Blinky: - def __init__(self): - self._face = '(o.o)' - asyncio.ensure_future(self.run()) - - def __str__(self): - return self._face - - def set_face(self, new): - self._face = new - print_blinkies() - - async def run(self): - while True: - self.set_face('(-.-)') - await asyncio.sleep(random.uniform(0.05, 0.1)) - self.set_face('(o.o)') - await asyncio.sleep(random.expovariate(1/2)) - - - blinkies = [Blinky() for i in range(10)] - - loop = asyncio.get_event_loop() - loop.run_forever() - loop.close() - - -V Pythonu verze 3.4 a nižší neexistují klíčová slova `async` a `await`, takže je potřeba -místo: - - async def ...: - await ... - -psát: - - @asyncio.coroutine - def ...: - yield from ... - -(Zajímavé je, že dekorátor `asyncio.coroutine` toho nedělá mnoho: označí funkci -jako *coroutine*, v *debug* módu zapne něco navíc, a pokud ve funkci není `yield`, -udělá z ní generátor. Téměř vše tak bude fungovat i bez tohoto dekorátoru – ale -doporučujeme ho použít, už jen jako dokumentaci.) - -Starý způsob zatím funguje i v novějším Pythonu, a dokonce se objevuje i v dokumentaci. - -[greenlet]: https://greenlet.readthedocs.io/en/latest/ -[Tornado]: http://www.tornadoweb.org/en/stable/ -[Twisted]: https://twistedmatrix.com/trac/ -[PEP 249]: https://www.python.org/dev/peps/pep-0249/ -[PEP 3333]: https://www.python.org/dev/peps/pep-3333/ -[PEP 3156]: https://www.python.org/dev/peps/pep-3156/ -[pypi-asyncio]: https://pypi.python.org/pypi/asyncio - - -Event Loop ----------- - -Knihovna `asyncio` nám dává k dispozici *smyčku událostí*, která se, podobně jako -`app.exec` v Qt, stará o plánování jednotlivých úloh. -Každé vlákno může mít vlastní smyčku událostí, kterou získáme pomocí -`asyncio.get_event_loop`, a pak ji můžeme spustit dvěma způsoby: - -* `loop.run_forever` spustí smyčku na tak dlouho, dokud jsou nějaké úlohy - naplánovány (to trochu odporuje názvu, ale většinou se nestává že by se - úlohy "vyčerpaly"), nebo -* `loop.run_until_complete` – tahle funkce skončí hned, jakmile je hotová - daná úloha, a vrátí její výsledek. - - - -Futures -------- - -Jak už bylo řečeno, knihovna `asyncio` je uvnitř založená na *futures*. -Copak to je? - -`Future` je objekt, který reprezentuje budoucí výsledek nějaké operace. -Poté, co tato operace skončí, se výsledek dá zjistit pomocí metody `result()`; -jestli je operace hotová se dá zjistit pomocí `done()`. -`Future` se dá popsat jako "krabička" na vrácenou hodnotu – než tam něco -tu hodnotu dá, musíme počkat, a poté je hodnota stále k dispozici. -Tohle čekání se dělá pomocí `await` (nebo `loop.run_until_complete`). - - import asyncio - - - async def set_future(fut): - """Sets the value of a Future, after a delay""" - await asyncio.sleep(1) - fut.set_result(123) - - - async def get_future(fut): - """Receives the value of a Future, once it's ready""" - await fut - result = fut.result() - return result - - - future = asyncio.Future() - - - # Schedule the "set_future" task (explained later) - asyncio.ensure_future(set_future(future)) - - - # Run the "get_future" coroutine until complete - loop = asyncio.get_event_loop() - result = loop.run_until_complete(get_future(future)) - loop.close() - - print(result) - -Do `Future` se dá vložit i výjimka: pokud proces, který by `Future` -naplnil, selže, může výjimku uložit do `Future` místo výsledku, -a `result()` potom tuto výjimku způsobí v kódu, který by výsledek zpracovával. - -Na `Future` se navíc dají navázat funkce, které se zavolají jakmile je -výsledek k dispozici. -Dá se tak implementovat *callback* styl programování, který jsme si -popsali výše – takhle, pomocí *futures* & *callbacks* se před nástupem -generátorů programovalo pro knihovny jako `Twisted`. - -A ještě jedna věc: `await` (podobně jako `yield`) je výraz, jehož -hodnota je výsledek dané `Future`. -Kód výše tak můžeme zjednodušit: - - async def get_future(fut): - """Receives the value of a Future, once it's ready""" - return (await fut) - -Další vlastnost `Future` je ta, že se dá "zrušit": pomocí `Future.cancel()` -signalizujeme úloze, která má připravit výsledek, že už ten výsledek -nepotřebujeme. -Po zrušení bude `result()` způsobovat `CancelledError`. - - -Async funkce a Task -------------------- - -Jak jsme viděli v příkladu s animací, používání *callback* funkcí je těžkopádné. -`Future` situaci trochu zlepšije, ale ne o moc. -V `asyncio` se `Future` používají hlavně proto, že je na ně jednoduché -navázat existující knihovny. -Aplikační kód je ale lepší psát pomocí `async` funkcí, tak jako v příkladu -výše. - -Asynchronní funkce se dají kombinovat pomocí `await` podobně jako generátory -pomocí `yield from`. -Nevýhoda async funkcí spočívá v tom, že na každé zavolání async funkce lze -použít jen jeden `await`: na rozdíl od `Future` se výsledek nikam neukládá; -jen se po skončení jednou předá. - - import asyncio - - async def add(a, b): - await asyncio.sleep(1) - return a + b - - async def demo(): - coroutine = add(2, 3) - print('The result is:', (await coroutine)) - print('The result is:', (await coroutine)) # chyba! - - - loop = asyncio.get_event_loop() - result = loop.run_until_complete(demo()) - loop.close() - -Tenhle problém můžeme vyřešit tak, že asynchronní funkci "zabalíme" do `Future`. -Na to ma dokonce `asyncio` speciální funkci `ensure_future`, která: - -* dostane-li asynchronní funkci, "zabalí" ji do `Future`, a -* výsledek přímo naplánuje na smyčce událostí, takže se asynchronní funkce - časem začne provádět. - - async def demo(): - coroutine = asyncio.ensure_future(add(2, 3)) - print('The result is:', (await coroutine)) - print('The result is:', (await coroutine)) # OK! - -Výsledek `ensure_future` je speciální druh `Future` zvaný `Task`. -Ten má oproti normální `Future` několik vlastností navíc, ale v podstatě -ho zmiňujieme jen proto, abyste věděli co `Task` znamená, až se vám objeví v -chybové hlášce. - - -Fan-Out a Fan-In ----------------- - -S pomocí asynchronních funkcí můžeme nad našimi programy přemýšlet tak, -jako by to byly "normální" procedurálně zapsané algoritmy: máme jedno -"vlákno", které se provádí od začátku do konce, jen na některých místech -(označených `await`) se provádění přeruší a zatímco náš kód čeká na výsledek -nějaké operace, může se spustit jiný kus kódu. -Funkce, na které je takto potřeba čekat, bývají v dokumentaci patřičně -označeny. -V síťovém programování je to většinou čtení ze socketů nebo inicializace -či ukončení serveru. - -Pomocí `ensure_future` a `await` můžeme ale dělat něco navíc: -rozdělit běh našeho programu na víc úloh, které se budou vykonávat "souběžně" – -například autor scraperu chce stáhnout několik stránek najednou, -nebo server souběžně odpovídá na několik požadavků. -Tomuto rozdělení se říká *fan-out*. - -Opačná operace je *fan-in*, kdy několik úloh opět spojíme do jedné. -Výše uvedený scraper může počkat, než jsou všechny stránky stažené – -typicky pomocí jednoho `await` pro každý `Task`, po kterém může -pokračovat zpracováním získaných dat. - -Co se týče Webového serveru, může se zdát, že tady není potřeba explicitně -počkat na výsledek každého úkolu. -Ale není to tak – i tady je poměrně důležité na každou úlohu nastartovanou -pomocí `ensure_future` "počkat" pomocí `await` – už jen proto, abychom -zachytili případnou výjimku. -Neuděláme-li to, `asyncio` bude (minimálně v *debug módu*) vypisovat -chybové hlášky. - - -Asynchronní cykly a kontexty ----------------------------- - -Až budete používat některé "asynchronní" knihovny, setkáte se pravděpodobně se dvěma -novými konstrukcemi: `async for` a `async with`. - -Fungují jako jejich "ne-`async`" varianty, jen na začátku a konci každé iterace (resp. -na začátku a konci bloku) můžou přerušit vykonávání funkce – podobně jako `await`. - -Typický příklad je u databází: začátek a konec transakce i získávání jednotlivých -řádků pravděpodobně potřebují komunikaci po síti, takže hypotetická databázová -knihovna by se mohla používat nějak takto: - - async with database.transaction(): - await database.execute('UPDATE ...') - async for row in (await database.execute('SELECT ...')): - handle(row) - - -Komunikace ----------- - -Ono `io` v `asyncio` naznačuje, že je tato knihovna dělaná především na -vstup a výstup – konkrétně na komunikaci přes síť (případně s jinými procesy). - -Ke komunikaci používá `asyncio` tři úrovně abstrakce: `Transport`, `Protocol`, -a `Stream`. -V krátkosti si je tu popíšeme; detaily pak najdete v dokumentaci (je pro nás -totiž mnohem důležitější abyste pochopili principy, než abyste uměli konkrétní -API, které lze dohledat v dokumentaci). - -Transporty a protokoly jsou postaveny na konceptech knihovny `Twisted`. - -`Transport` zajišťuje samotné posílání bajtů mezi počítači (transportní vrstvu), kdežto -`Protocol` implementuje nějaký aplikační protokol. -`Transport` většinou nepíšeme sami, použijeme existující. -V `asyncio` jsou zabudované transporty pro TCP, UDP a SSL. -`Protocol` je pak použit pro implementaci konkrétních protokolů jako -`HTTP`, `FTP`, a podobně. -V dokumentaci najdete podrobnější popis včetně [příkladů][transport-proto-examples]. - -[transport-proto-examples]: https://docs.python.org/3/library/asyncio-protocol.html#tcp-echo-server-protocol - -Kromě toho existuje i "Stream API" založené na asynchronních funkcích. -Většinou platí, že operace *otevření*, *čtení*, *flush* a *zavření* Streamu -jsou asynchronní funkce (v dokumentaci označované jako *coroutines*), a je -tedy nutné je použít s `await`; oproti tomu *zápis* asynchronní není – data -se uloží do bufferu a pošlou se, až to bude možné. - -Typicky ale místo čistého `asyncio` použijeme existující knihovnu. -Tady je příklad z knihovny `aiohttp`, která implementuje server a klienta -pro HTTP: - - import asyncio - import aiohttp - - async def main(url): - # Use a a session - session = aiohttp.ClientSession() - async with session: - - # Get the response (acts somewhat like a file; needs to be closed) - async with session.get(url) as response: - - # Fetch the whole text - html = await response.text() - print(html) - - loop = asyncio.get_event_loop() - loop.run_until_complete(main('http://python.cz')) - loop.close() - - -A další -------- - -Nakonec několik tipů, o kterých je dobré vědět. - -V `asyncio` najdeme synchronizační mechanismy známé z vláknového programování, např. -`Lock` a `Semaphore` – viz [dokumentace](https://docs.python.org/3/library/asyncio-sync.html). - -Musíme-li použít blokující funkci, která např. komunikuje po síti bez `await`, a která by -tedy zablokovala i všechny ostatní úlohy, můžeme použít -`loop.run_in_executor()`, a tím danou funkci zavolat ve vlákně nebo podprocesu, ale výsledek zpřístupnit -pomocí `asyncio.Future`. -Použití je opět popsáno v [dokumentaci](https://docs.python.org/3/library/asyncio-eventloop.html#executor). - -Občas vás při programování s `asyncio` zaskočí zrádná chyba. -V takových případech je dobré zapnout *debug* režim pomocí proměnné prostředí `PYTHONASYNCIODEBUG=1`. -V tomto režimu asyncio upozorňuje na časté chyby, do některých chybových výpisů přidává informaci o tom, -kde aktuální `Task` vznikl, apod. -Více informací je zase v [dokumentaci](https://docs.python.org/3/library/asyncio-dev.html#asyncio-dev). - - -AsyncIO a Qt ------------- - -Jak bylo zmíněno na začátku, hlavní cíl `asyncio` je definovat společné rozhraní -pro různé asynchronní knihovny, aby bylo možné např. kombinovat knihovny pro -Tornado se smyčkou událostí v Twisted. -Samotná knihovna `asyncio` je jen jedna z mnoha implementací tohoto rozhraní. -Zajímavá je například knihovna [uvloop], která je asi 2-4× rychlejší než `asyncio` -(ale má závislosti, které se pro součást standardní knihovny nehodí). - -Další zajímavá implementace je [Quamash], která pod standardním `asyncio` API používá -smyčku událostí z Qt. -Umožňuje tak efektivně zpracovávat Qt události zároveň s asynchronními funkcemi -známými z `asyncio`. - -*Event loop* z `quamash` je potřeba na začátku programu naimportovat a nastavit -jako hlavní smyčku událostí: - - from quamash import QEventLoop - - app = QtWidgets.QApplication([]) - loop = QEventLoop(app) - asyncio.set_event_loop(loop) - -a poté ji, místo Qt-ovského `app.exec()`, spustit: - - loop.run_forever() - -Jednotlivé asynchronní funkce se pak používají jako v čistém `asyncio`: -pomocí `asyncio.ensure_future`, `await`, atd. - -[uvloop]: https://pypi.python.org/pypi/uvloop/ -[Quamash]: https://pypi.python.org/pypi/Quamash - -Ukázka: - - import asyncio - - from PyQt5 import QtGui, QtWidgets - from quamash import QEventLoop - - app = QtWidgets.QApplication([]) - loop = QEventLoop(app) - asyncio.set_event_loop(loop) - - display = QtWidgets.QLCDNumber() - display.setWindowTitle('Stopwatch') - - display.show() - - async def update_time(): - value = 0 - while True: - display.display(value) - await asyncio.sleep(1) - value += 1 - - asyncio.ensure_future(update_time()) - - loop.run_forever() - -Úkol -==== - -Vaším úkolem je vytvořit asynchronní třídy reprezentující jednotlivé postavy v bludišti a rozhraní umožňující je spustit. - -Program musí stále splňovat zadání z předchozího cvičení, zejména: - - * nesmí při žádné interakci uživatele s rozhraním zhavarovat - * musí se dát nainstalovat a spustit pomocí `python -m pip install -r requirements.txt`; `python setup.py develop`; `python -m maze` - -Do programu přidejte rozhraní pro spuštění hry. -Doporučujeme přidat do menu a lišty nástrojů `QAction` s atributem `checkable=True`. -Můžete ale použít i jiný vhodný způsob, kterým lze režim hry zapnout, ale i vypnout (přechod zpět do editačního módu). - -Není možné začít hrát, pokud od některé z postav nevede žádná cesta k cíli. Této situaci vhodně zabraňte. - -Do vizualizátoru bludiště doplňte funkcionalitu hry. V režimu hry: - - * nebudou zobrazeny čáry od postav k cíli - * půjde pouze bořit či stavět zdi - * pokud máte více typů zdí, půjde bořit/stavět pouze jeden druh z nich (`-1`) - * zeď nepůjde stavět na políčko, kde je aktuálně nějaké postava - * postavit zeď půjde pouze tehdy, pokud to žádnou postavu neodřízne od cíle - * nebude vidět paleta - * úkolem hráče je měnit bludiště tak, aby co nejdéle bránil postavám dojít do cíle - * v případě že libovolná postava dojde do libovolného cíle, hra končí - * v takovém případě informujte hráče o tom, jak dlouho vydržel odolávat náporu postav - -Máte k dispozici základní třídu `Actor`, která reprezentuje aktora (postavu v bludišti): - - * [class Actor](https://github.com/cvut/MI-PYT/blob/master/tutorials/10-async/actor.py) - -Tato třída definuje rozhraní jednotlivých postav a zároveň implementuje základní chování postavy - jde nejkratší cestou k cíli rychlostí jedno políčko za sekundu. -Aby to mohlo fungovat, musí kód, který postavu používá: - - * na začátku hry udělat kopii matici bludiště (aby se bylo možné vrátit do editačního módu) a všechny postavy z ní "vyndat" a inicializovat je jako aktory - * při zavolání `update_actor(actor)` zjistit, kde se postava nachází a překreslit ji, - * při ukončení hry nebo programu všem aktorům říct, že se mají zrušit svůj task - * *tip:* na úklid po skončení hlavní smyčky událostí se hodí `loop.run_until_complete()` - -Je třeba použít smyčku událostí z `quamash`. - -Rozhraní je definováno tak, aby nezáviselo na konkrétní implementaci vizualizace, uvnitř aktorů byste tedy neměli používat nic z Qt. -Některé charaktery (viz další sekce) možná budou potřebovat použít další API vašeho gridu. - -Kromě úpravy rozhraní musíte implementovat několik aktorů, kteří dědí z třídy Actor a chovají se trochu jinak. - -Aktoři ------- - -V bludišti máte 5 typů postav. Každá by měla mít jiné chování. -Každá postava vychází svým chováním ze základního aktora, jde tedy k cíli nejkratší možnou cestou rychlostí jedno políčko za sekundu, -pokud popis neříká jinak. - -Zde je seznam různých charakterů. Každé postavě přiřaďte jeden charakter podle typu postavy (barva na obrázku, číslo v matici). - -Charakterů je více než 5. Můžete si vybrat, kterých 5 použijete, musíte jich však použít minimálně 5 různých. -Musíte implementovat alespoň dva z hvězdičkou označených charakterů. - -Poznámka o rychlostech: Když je nějaká postava rychlejší, neznamená to, že ve vašem cyklu v metodě `behavior` bude dělat delší kroky, -ale že jeden krok bude trvat kratší dobu (metoda `step` má atribut `duration`). - -Při rozhodování o chování postav berte v úvahu hratelnost hry. - -### Rychlík - -Rychlík má stabilně o 75 % vyšší rychlost než základní aktor. - -### Zrychlovač - -Zrychlovač může po každém kroku s určitou pravděpodobností trvale zvýšit svou rychlost. -Čím déle chodí, tím rychlejší může být. Pravděpodobnost nastavte tak, aby hra byla hratelná; zrychlení tak, aby bylo znatelné (např. o čtvrt políčka za sekundu). -Doporučujeme nastavit i rychlostní strop, případně zrychlovat stále o menší a menší hodnotu. - -### Skokan \* - -Pokud je za jedním políčkem zdi průchozí políčko, odkud je cesta do cíle alespoň o 5 políček kratší, než z místa, kde se Skokan nachází, -Skokan touto zdí projde (přeskočí ji). Kvůli hratelnosti doporučujeme nastavit limity na to, jak často se toto může dít, -případně zakázat projít zdí přímo na cíl apod. - -Chcete-li, můžete implementovat animaci skoku přes zeď. - -### Teleportér \* - -Teleportér se místo kroku může s určitou pravděpodobností teleportovat na náhodné průchozí a dostupné místo bludiště. -Při teleportu se postavička na malou chvíli rozechvěje, pak se přemístí a po chvilce se přestane chvět. -Celá akce by měla trvat méně než sekundu. -Doporučujeme zakázat teleport na políčka příliš blízko cíli. - -### Zmatkář - -Zmatkář s určitou pravděpodobností místo kroku směrem k cíli provede krok náhodným průchozím směrem (pokud to je možné, tak jiným, než ze kterého přišel). - -### Sprinter - -Sprinter zrychluje na rovných trasách. -Pokud půjde klikatou cestou, je stejně pomalý jako základní aktor. Pokud ale půjde déle rovně, může být velmi rychlý. -Na začátku hry a po každé změně směru má rychlost jako základní aktor. -Každý další pohyb ve stejném směru ale vykonává rychleji, než ten předchozí. -Pohyb v jiném směru je opět základní rychlostí. - -Doporučujeme nastavit rychlostní strop, případně zrychlovat stále o menší a menší hodnotu. - -Poznámka: Sprinter může chodit stejnou cestou jako základní aktor, nemusí cestu optimalizovat pro svoji schopnost. - -### Pravák \* - -Pravák se pohybuje bludištěm podle [pravidla pravé ruky](https://en.wikipedia.org/wiki/Maze_solving_algorithm#Wall_follower), místo toho, aby šel rovnou nejkratší cestou k cíli. -Pokud má kolem sebe nějakou zeď, jde podél ní. -Pokud se nachází ve volném prostoru a pravidlo nemůže aplikovat (např. na začátku, nebo pokud mu hráč jeho zeď zboří), chová se jako standardní aktor, dokud zeď nenajde nebo nedojde do cíle. - -Chcete-li, můžete detekovat, jestli se Pravák někde nezacyklil, a pokud ano, udělat nějaké rozumné kroky k tomu, aby se z prekérní situace dostal. - -### Dobíječ - -Dobíječ nejprve stojí na místě náhodnou dobu (s exponenciálním rozdělením a střední hodnotou 5 sekund), dobíjí energii. -Pak ujde tolik kroků, kolik celých sekund čekal, rychlostí 3 políčka za sekundu. A opět dobíjí/čeká... -Z dlouhodobého hlediska se tedy pohybuje pomaleji než základní aktor, ale je nevyzpytatelnější. -Při dobíjení si jednou za sekundu poskočí, aby hráč věděl, že tato postava se nezasekla. -(Dobíjení můžete signalizovat nějak sofistikovaněji.) - -### Tanečník - -Před každým krokem zkontroluje políčko před sebou (tj. ve směru cesty k cíli), -diagonálně vpravo před sebou, a vpravo od sebe. Pokud jsou všechna tři volná, projde je -a vrátí se zpět na původní políčko. -Poté to samé udělá s políčky před sebou, diagonálně vlevo před sebou, a vlevo -od sebe. -Pak teprve udělá krok dopředu. - -Pokud hráč políčko, na které by měl Tanečník vstoupit, zastaví zdí, -Tanečník poskočí, zbytek aktuálního "tance" neprovede, a pokračuje z aktuálního políčka. - -### Tlusťoch \* - -Pohybuje se o 25 % pomaleji než základní aktor. - -Nesmí vstoupit na políčko, na kterém je právě jiný aktor. -Měl-li by to udělat, stojí místo toho na místě a odpočívá. - -Ostatní aktoři nesmí vstoupit na políčko, kde se právě nachází (nebo na které -právě vstupuje) Tlusťoch. Pohybují se tak, jako by na pozici Tlusťocha byla zeď, -tj. základní aktor se Tlusťocha snaží obejít, a pokud to nejde, skáče na místě. - -(Pravák Tlusťocha nepovažuje za zeď, podél které má jít; v případě, že mu Tlusťoch překáží, skáče na místě. -Teleportér se na Tlusťocha nesmí teleportovat.) - -Tlusťoch ale nemá vliv na hráčovu schopnost stavět zdi, tj. pro tento účel se -nepočítá jako překážka. - -### Smraďoch \* - -Smraďoch všem kromě ostatních Smraďochů smrdí, a chtějí od něj pryč. -Pokud je v bezprostředním okolí (určete sami) ostatních aktorů (jiných charakterů), tito aktoři, pokud mohou, jdou směrem pryč od Smraďocha -(bez ohledu na svou plánovanou trasu), až dokud se z dosahu Smraďocha nedostanou. -Pokud nemohou pryč, se smradem se smíří a chovají se normálně. - -Při chůzi směrem pryč od Smraďocha se aktoři pohybují rychlostí dle svého charakteru. -Tanečník (pokud může) tančí, Dobíječ (pokud musí) se dobíjí atp. - -Je na vás, jestli Smraďoch smrdí i přes zdi. Chcete-li, můžete rozsah smradu vizualizovat. - -### Vlastní - -Implementujte jiné netriviální chovaní. Svůj záměr popište v README. - -Vyhněte se i triviálním variantám jiných chování, které jste již implementovali -(např. máte-li Rychlíka, nedělejte v rámci vlastního zadání "Pomalíka", který je jen o trochu pomalejší než základní aktor.) - -Odevzdání, deadliny apod. -------------------------- - -Váš úkol se skládá prakticky ze dvou částí: - - * úprava GUI pro novou funkcionalitu - * asynchronní programování aktorů - -V případě potřeby opět můžete využít naše [řešení](http://github.com/encukou/maze) minulého úkolu. diff --git a/lessons/intro/cython/index.md b/lessons/intro/cython/index.md deleted file mode 100644 index 2b9f0428e8..0000000000 --- a/lessons/intro/cython/index.md +++ /dev/null @@ -1,760 +0,0 @@ -Dnes budeme potřebovat do virtuálního prostředí nainstalovat tyto knihovny: - - python -m pip install --upgrade pip - python -m pip install notebook numpy cython pytest-profiling - -Také je potřeba nainstalovat překladač jazyka C (`gcc`) -a hlavičkové soubory Pythonu (`python3-dev`/`python3-devel`). - ---- - -C API -===== - -Mluvíme-li o „Pythonu“, máme často na mysli jak jazyk samotný, tak i interpret, -program, který programy v tomto jazyce umí spouštět. -Správně je ale „Python“ pouze jméno jazyka. -Interpretů tohoto jazyka je více, například: - -* CPython, referenční implementace napsaná v C; interpret, který spouštíme příkazem `python3` -* PyPy, implementace zaměřená na rychlost, napsaná v Pythonu -* MicroPython, implementace pro mikroprocesory a zařízení s minimem paměťi -* Jython, implementace napsaná v Javě, která umožňuje využívat javovské třídy -* IronPython, napsaný v C#, s integrací do .NET -* Batavia, Brython, pyjs – různé pokusy o integraci do JavaScriptu - -Jednotlivé interprety se liší v detailech jako jsou přesnost reálných čísel, -vypisování chybových hlášek, řazení záznamů ve slovnících nebo přístup -k interním strukturám interpretu. -Správně napsaný pythonní program by neměl na takových detailech záviset, pokud -není k nekompatibilitě mezi interprety dobrý důvod. - -Někdy to ale je potřeba, a dnešní přednáška specifická pro CPython -a přímé využití jeho API pro jazyk C. - - -Rychlost --------- - -Častý důvod proč sáhnout k C API je rychlost: CPython je celkem pomalý. -Tradiční metoda optimalizace je zjistit, které části jsou kritické, a přepsat -je do C. -Využijí se tak výhody obou jazyků: Python pro rychlý vývoj, snadné -prototypování, a přehlednost kódu, a C pro rychlost. - -Když je náš program příliš pomalý, je potřeba ho optimalizovat. -První krok k tomu je vždy zkontrolovat, co zabírá více času, než by mělo. -K tomu se dá použít nástroj `profile` ze standardní knihovny, který vypíše -tabulku počtu volání jednotlivých funkcí a času v nich stráveného: - - python -m profile -s cumtime program.py - -Profilovat běh pytest testů se dá jednoduše pomocí modulu [pytest-profiling]: - - python -m pytest --profile - -[pytest-profiling]: https://pypi.python.org/pypi/pytest-profiling - -Když máme představu o tom, co nás brzdí, můžeme začít přepisovat do C způsoby -popsanými níže. - -Jiná možnost, jak program zrychlit, je ho pustit tak jak je pod interpretem -PyPy, který obsahuje optimalizovaný překladač. To je ale jiná kapitola. - - -Externí knihovny ----------------- - -Druhý důvod proč programátoři používají C API je použití knihoven, které mají -rozhraní pro C. -Takových knihoven existuje mnoho – pokud není něco specifické pro určitý jazyk, -často se to dá volat i z C. - -Pro práci s externími knihovnami se dá použít C API nebo vestavěný modul -[ctypes], ale v dnešní době je dobré místo toho použít [CFFI], knihovnu -která funguje i s PyPy (a teoreticky jinými implementacemi). - -[ctypes]: https://docs.python.org/3/library/ctypes.html -[CFFI]: http://cffi.readthedocs.io/en/latest/ - - -CPython -------- - -Třetí důvod, proč použít C API, je práce s CPythonem samotným. -Když člověk zabředne do složitého problému, může na CPython pustit C debugger -jako [gdb] nebo [Valgrind], prozkoumat potíže na nižší úrovni, -a zjistit kde přesně se chyba nachází. - -[gdb]: https://en.wikipedia.org/wiki/GNU_Debugger] -[valgrind]: http://valgrind.org/ - - -Modul v C ---------- - -Pojďme začít příkladem. -Vytvořte si následující soubory, který implementuje rozšíření -(importovatelný modul) s jednou funkcí. - -(Nebudeme chtít, abyste podobný kód uměli napsat, ale měli byste být schopní -porozumět tomu, co dělá.) - -demo.c: - - #include - - PyDoc_STRVAR( - mod_docstring, - "Demo extension module with a Python wrapper for the system(3) function"); - - static PyObject *demo_system(PyObject *self, PyObject *args){ - const char *command; - int retval; - - /* Parse the given arguments: expect one string, convert to char* */ - if (!PyArg_ParseTuple(args, "s", &command)) { - /* Error handling: if PyArg_ParseTuple returns zero, return NULL */ - return NULL; - } - - /* Call the C function */ - retval = system(command); - - /* Return result as Python int (error handling built in) */ - return PyLong_FromLong(retval); - } - - /* List of all methods in the module */ - static PyMethodDef DemoMethods[] = { - {"system", demo_system, METH_VARARGS, - PyDoc_STR("Execute a shell command.")}, - {NULL, NULL, 0, NULL} /* Sentinel */ - }; - - /* Module specification */ - static struct PyModuleDef demo_module = { - PyModuleDef_HEAD_INIT, - "demo", /* name of module */ - mod_docstring, /* dosctring (may be NULL) */ - 0, /* size of per-interpreter state of the module */ - DemoMethods, /* list of methods */ - }; - - - /* Module entrypoint */ - PyMODINIT_FUNC - PyInit_demo(void) - { - return PyModuleDef_Init(&demo_module); - } - -Z tohoto souboru by měla být patrná struktura podobných rozšíření: -máme funkci (`demo_system`), která převádí objekty Pythonu -na datové typy C, volá samotnou funkci, a výsledek převádí zpět na pythonní -objekt. - -Dále máme pole záznamů o funkcích (`DemoMethods`), kde je ke každé funkci -přiřazeno jméno, dokumentační řetězec, a způsob volání (v našem případě -METH_VARARGS, tedy volání s proměnným počtem nepojmenovaných argumentů, -podobně jako bychom v Pythonu napsali `def system(*args)`). - -Další potřebná proměnná, `demo_module`, obsahuje informace o modulu: -jméno, dokumentační řetězec, a seznam funkcí. -Kdybychom potřebovali kromě funkcí definovat i třídy nebo konstanty, -zde bychom pomocí [slotů][PyModuleDef_Slot] definovali funkci, která modul -inicializuje, t.j. má podobnou funkci jako `__init__` u třídy v Pythonu. - -[PyModuleDef_Slot]: https://docs.python.org/3/c-api/module.html#c.PyModuleDef_Slot - -Poslední část je funkce `PyInit`, jediná která není definována jako `static`, -takže jediná, která je exportována jako API knihovny, kterou vytváříme. -Až bude Python tento modul importovat, najde tuto funkci podle jména, spustí ji, -a podle vrácené struktury typu `PyModuleDef` vytvoří pythonní objekt s modulem. - - -Překlad -------- - -Abychom mohli takovýto modul naimportovat, musíme ho nejdřív přeložit a sestavit -z něj sdílenou knihovnu – soubor .so (nebo .dll) – s názvem modulu: -buď jen `demo.so`, nebo i s identifikací architektury a verze Pythonu, -např. `demo.cpython-35m-x86_64-linux-gnu.so`. -(Výhoda delších názvů je v tom, že v jednom adresáři může být víc modulů pro -různé architektury, a že se Python nebude snažit načíst nekompatibilní moduly.) - -Překlad je nutné provést se správnými přepínači a volbami, nejlépe takovými, -s jakými byl sestaven samotný Python. - -Pro zjednodušení tohoto procesu můžeme použít setuptools: do nám už známého -souboru `setup.py` přidáme argument `ext_modules` se seznamem rozšiřovacích modulů. -Podrobný popis třídy `Extension` je v [dokumentaci][Extension]; nám bude stačit -jen jméno a seznam zdrojových souborů: - -[Extension]: https://docs.python.org/3/distutils/apiref.html#distutils.core.Extension - -setup.py: - - from setuptools import setup, Extension - - module1 = Extension( - 'demo', - sources=['demo.c'], - ) - - setup( - name = 'demo', - version = '0.1', - description = 'Demo package', - ext_modules = [module1] - ) - -Příkazy `python setup.py sdist` a `python setup.py install` budou fungovat jako normálně, -jen je na instalaci potřeba překladač jazyka C. - -Aby uživatelé překladač mít nemuseli, můžeme nainstalovat knihovnu `wheel` (`python -m pip install wheel`), a pak příkazem `python setup.py bdist_wheel` vygenerovat tzv. *wheel* archiv, -např. `dist/demo-0.1-cp35-cp35m-linux_x86_64.whl`. Tento archiv jde nahrát na PyPI a následně -nainstalovat, ovšem jen na architektuře a verzi Pythonu, pro které byl vytvořen. - -Wheels jdou vytvářet i pro moduly tvořené jen pythonním kódem. -Nejsou pak vázané na verzi a architekturu. -Jejich výhoda oproti `sdist` archivům spočívá v tom, že se rychleji instalují. - -Alternativa k instalaci, alespoň pro lokální vývoj, je rozšíření jen přeložit a dát do -aktuálního adresáře (nebo jakéhokoli jiného adresáře, odkud se importují moduly). -K tomu slouží příkaz `python setup.py build_ext --inplace`. -Pozor na to, že po každé změně zdrojového kódu je potřeba rozšíření znovu přeložit. - -Příkaz `python setup.py develop` bude fungovat jako dřív (používá `build_ext --inplace`), -jen je opět potřeba příkaz po každé změně znovu spustit. - - -PyObject --------- - -Podívejme se teď na základní mechanismy interpretu CPython. - -Základní datová struktura, která reprezentuje jakýkoli objekt Pythonu, je PyObject -([dokumentace](https://docs.python.org/3/c-api/structures.html#c.PyObject), -[definice](https://github.com/python/cpython/blob/3.5/Include/object.h#L106)). -Skládá se ze dvou prvků: - - typedef struct _object { - Py_ssize_t ob_refcnt; - struct _typeobject *ob_type; - } PyObject; - -První je počet referencí (*reference count*), který se dá popsat jako počet míst, -ze kterých je možné k tomuto objektu přistoupit. -Když objekt uložíme do proměnné nebo do seznamu, zvýší se počet referencí o 1. -Když seznam nebo proměnná zanikne (nebo náš objekt přepíšeme jiným), -počet referencí se zase sníží. -Když počet referencí dosáhne nuly, znamená to, že se k objektu už nedá dostat, a Python ho -uvolní z paměti. - -Druhý prvek struktury PyObject je ukazatel na typ. -Typ je pythonní objekt (`class`), který definuje chování třídy objektů: operátory, -atributy a metody, které ten který objekt má. - -Struktura PyObject slouží jako hlavička, za kterou pak následují data interpretovaná podle -typu daného objektu. -Například pythonní [objekt typu float][float] vypadá následovně: - - typedef struct { - PyObject ob_base; - double ob_fval; - } PyFloatObject; - -...tedy struktura PyObject, za kterou je v paměti číselná hodnota. - -[Seznamy][list] obsahují za hlavičkou např. velikost a (ukazatel na) pole ukazatelů na jednotlivé -prvky. -Podobně [objekty typu int][int] (které mají v Pythonu neomezený rozsah) mají délku a pole -jednotlivých 30bitových "číslic". -NumPy matice mají metadata (velikost, typ, popis rozložení v paměti) a ukazatel na pole hodnot. - -[float]: https://github.com/python/cpython/blob/3.5/Include/floatobject.h#L15 -[list]: https://github.com/python/cpython/blob/3.5/Include/listobject.h#L23 -[int]: https://github.com/python/cpython/blob/3.5/Include/longintrepr.h#L89 - -To základní, co potřebujeme vědět, je že na úrovni C je každý pythonní objekt reprezentován -jako struktura počtu referencí, ukazatele na typ, a dat specifických pro daný typ. - - - -Reference counting ------------------- - -Tak jako v C je důležité správně alokovat a dealokovat paměť, při tvorbě rozšíření do CPythonu -je třeba správně pracovat s referencemi: ke každému [Py_INCREF] (přičtení 1 k počtu referencí) -je potřeba později zavolat [Py_DECREF] (odečtení 1, a případné uvolnění objektu). -Jakákoli práce s objektem se smí provádět jen mezi INCREF a příslušným DECREF. - -Platí konvence, že argumenty funkcí se předávají jako tzv. *borrowed reference*: o počitadlo -se stará volající, a v průběhu volané funkce se objekt dá používat. -Pokud bychom ale argument potřebovali i po skončení volané funkce (např. si ho uložíme -do globální proměnné), je potřeba mu počitadlo zvýšit (a po skončení práce zase snížit). - -V našem modulu `demo` přebíráme jako parametr n-tici. -Zodpovědnost zavolat na tuto n-tici Py_DECREF má ale volající, ne my. -Zavoláním funkce `PyArg_ParseTuple` získáme `char*`, který ale můžeme používat jen v rámci naší -funkce: po jejím skončení může volající argumenty funkce uvolnit, a tím řetězec zrušit. - -Funkce, které vracejí pythonní objekty, předpokládají, že na vrácenou hodnotu provede DECREF volající. -V modulu `demo` voláme funkci [PyLong_FromLong], která vytvoří nové pythonní číslo. -Za vzniklou referenci naše funkce přebírá zodpovědnost, je tedy na nás, abychom se postarali -o zavolání Py_DECREF. -Vrácením výsledku tuto zodpovědnost ale předáváme na funkci, která volá tu naši. - -[Py_INCREF]: https://docs.python.org/3/c-api/refcounting.html#c.Py_INCREF -[Py_DECREF]: https://docs.python.org/3/c-api/refcounting.html#c.Py_DECREF -[PyLong_FromLong]: https://docs.python.org/3/c-api/long.html#c.PyLong_FromLong - - -Hodnoty a výjimky ------------------ - -Další konvence, kterou většina funkcí v C API dodržuje, je způsob vracení výjimek. - -Funkce, které vrací pythonní objekty, na úrovni C vrací `PyObject*`. -Nastane-li výjimka, objekt výjimky se zaznamená do globální (přesněji, *thread-local*) -proměnné, a funkce vrátí NULL. - -V našem modulu `demo` voláme funkci `PyArg_ParseTuple`, která může vyvolat výjimku: typicky -`TypeError` kvůli nesprávnému počtu nebo typu argumentů. -V takovém případě tato funkce výjimku zaznamená a vrátí NULL. -Naší funkci `system` už stačí vrátit NULL, protože víme, že výjimka už je zaznamenaná. - -Další funkce, která může neuspět, je `PyLong_FromLong`. -Vzhledem k tomu, že její výsledek rovnou vracíme, není potřeba úspěch kontrolovat – vrátíme -buď správnou hodnotu nebo NULL se zaznamenanou výjimkou. - - -GIL ---- - -Poslední omezení, kterého si autor rozšíření musí být vědom, je *Global Interpreter Lock*. -Stručně řečeno, s objekty `PyObject*` může pracovat pouze jedno vlákno. -Toto vlákno drží globální zámek, který čas od času odemkne a znovu se pokusí zamknout, -aby mohly běžet i ostatní vlákna. - -Díky GIL je vícevláknové programování v Pythonu relativně bezpečné: nemůže např. nastat souběh -(*race condition*), kdy by se nastavilo počitadlo referencí na špatnou hodnotu. -Na druhou stranu tento zámek ale omezuje paralelismus, a tedy i rychlost programu. - -Globální zámek se dá odemknout v situacích, kdy nepracujeme s `PyObject*` a nevoláme pythonní kód. -Například čtení ze souboru nebo sítě ostatní vlákna neblokuje. -Stejně tak maticové operace v NumPy typicky nedrží GIL zatímco počítají na úrovni C nebo Fortranu. - - -Cython -====== - -Teď, když víme jak to všechno funguje, se můžeme podívat na způsob, jak rozšíření psát -jednoduše. -C API se totiž dá použít nejen z C, ale z jakéhokoli jazyka, který umí volat funkce se -stejnými konvencemi, např. C++ (s pomocí `extern C`). -Další způsob, jak použít C API ale nepsat C, je použít překladač z příjemnějšího jazyka do C. - -Jeden takový jazyk je Cython (neplést s CPython). - -Cython je jazyk podobný Pythonu, který ale lze přeložit na C, a dále optimalizovat. - -Cython si nainstalujte pomocí příkazu: - - python -m pip install cython - - -Kompilace Pythonu ------------------ - -Když chceme převést modul z Pythonu do Cythonu, nejjednodušší začátek je přejmenovat soubor `.py` -na `.pyx`, aby bylo jasné, že jde o jiný jazyk, který nepůjde naimportovat přímo. - - -Jazyky Python a Cython nejsou 100% kompatibilní, ale zvláště u kódu, který pracuje hlavně s -čísly, se nekompatibilita neprojeví. -Vývojáři Cythonu považují každou odchylku od specifikace jazyka za chybu, kterou je nutno opravit. - -Jako příklad můžete použít tuto naivní implementaci celočíselného a maticového násobení: - - import numpy - - def intmul(a, b): - result = a * b - return result - - def matmul(a, b): - n = a.shape[0] - m = a.shape[1] - if b.shape[0] != m: - raise ValueError('incompatible sizes') - p = b.shape[1] - result = numpy.zeros((n, p)) - for i in range(n): - for j in range(p): - for k in range(m): - x = a[i, k] - y = b[k, j] - result[i, j] += x * y - return result - -Výsledek můžeme převést na C pomocí příkazu `cython -3 soubor.pyx`, čímž vznikne `soubor.c`, který -můžeme přeložit výše uvedeným způsobem. - -Jednodušší varianta je použít Cython v `setup.py`. -Pro naše účely bude `setup.py` s Cythonem a NumPy vypadat takto: - - from setuptools import setup - from Cython.Build import cythonize - import numpy - - setup( - name='matmul', - ext_modules=cythonize('matmul.pyx', language_level=3, include_dirs=[numpy.get_include()]), - include_dirs=[numpy.get_include()], - install_requires=[ - 'Cython', - 'NumPy', - ], - ) - -Poznámka: `include_dirs` je tady dvakrát. Ale na některých platformách (Mac OS X) -se jako parametr funkce `setup()` z nějakého důvodu neaplikuje a musí se použít -jako parametr funkce `cythonize`. To ale samo někdy nefunguje na některých Linuxech, -proto jako workaround je to zde uvedeno dvakrát, aby to fungovalo všude. - -Po zadání `python setup.py develop` nebo `python setup.py build_ext --inplace` atp. -se modul `matmul.pyx` zkompiluje s použitím nainstalovaného NumPy a bude připraven na použití. - -Nevýhoda tohoto přístupu je, že k spuštění takového `setup.py` je již potřeba -mít nainstalovaný `cython` a `numpy`. -Instalace z archivu `sdist` se tedy nemusí povést – je potřeba uživatelům říct, -že dané moduly už musí mít nainstalované. -Tento problém aktuálně řeší PyPA (správci `pip` a `setuptools`). - -Instalace z archivů `wheel` by měla být bezproblémová. - - -Anotace -------- - -Kód, který takto vznikne, není o moc rychlejší než původní Python. -Je to tím, že sekvence příkazů ve funkci je sice převedená do C a přeložená do strojového kódu, -ale každá operace pracuje s generickými pythonními objekty, takže musí pro každé číslo -číslo z matice zkonstruovat pythonní objekt, vyhledat implementaci sčítání pro dvě celá čísla, -a výsledek převést zpět na `int64` a uložit do matice. - -Na situaci se můžeme podívat pomocí přepínače `--annotate`: - - cython -3 --annotate matmul.pyx - -To vygeneruje soubor `matmul.html`, kde jsou potencionálně pomalé operace vysvíceny žlutě. -Ke každému řádku se navíc dá kliknutím ukázat odpovídající kód v C (který bývá docela složitý, -protože řeší věci jako zpětnou kompatibilitu a ošetřování chyb, a navíc používá hodně pomocných -maker). - -Obecně nebývá problém mít "žluté" řádky na úrovni funkce, kde se provádí pouze jednou. -Ale v cyklech, zvláště těch třikrát zanořených, se autor rozšíření typicky snaží žlutým řádkům -vyhnout. -Nejjednodušší způsob, jak toho docílit, je doplnění statických informací o typech. - - -Doplnění typů -------------- - -Začneme u funkce `intmul`, kde doplníme informaci o tom, že parametry `a` a `b` a proměnná -`result` jsou typu `int`. -Parametrům stačí doplnit typ podobně jako v C, ostatní lokální proměnné potřebují definici pomocí -příkazu `cdef`: - - def intmul(int a, int b): - cdef int result - result = a * b - return result - -Teď bude funkce nepatrně rychlejší, ale také méně obecná: nejde jí násobit řetězec číslem, -ale ani reálná čísla (`float`), a dokonce ani celá čísla, která se nevejdou do 64 bitů (příp. -jiné velikosti, dle systému). -Typ int v Cythonu je totiž int z C, ne ten neomezený z Pythonu. - -Další věc, kterou můžeme udělat, je změnit příkaz `def` na `cpdef`, a doplnit typ návratové -hodnoty: - - cpdef int intmul(int a, int b): - cdef int result - result = a * b - return result - -Tím se zbavíme nákladného převodu výsledku na PyObject. -Bohužel ale toto zrychlení pocíte jen když takovou funkci zavoláme z jiné funkce napsané v -Cythonu. - -Tři typy funkcí ---------------- - -Funkce jdou deklarovat třemi způsoby: - - * `def func(...):` je funkce, která jde volat z Pythonu i z Cythonu, ale volání z Cythonu je pomalé (převod na pythonní objekty a zpět) - * `cdef func(...):` je funkce, která jde volat pouze z Cythonu, ale volání je rychlé (pracuje se s C typy) - * `cpdef func(...):` je funkce, která se z Cythonu volá rychle ale jde volat i z Pythonu (ve skutečnosti Cython vytvoří dva druhy této funkce) - -Třídy ------ - - cdef class Foo: - # Tady si musím nadefinovat všechny členské proměnné - # Není možné dynamicky na selfu vytvářet nové atributy - cdef int foo - ... - - def __cinit__(self, int f): - self.foo = f - ... - - def __dealloc__(self): - ... - - cpdef int method(self): - ... - return self.foo - -Více o třídách v [dokumentaci](http://cython.readthedocs.io/en/latest/src/tutorial/cdef_classes.html). - -Používání NumPy ---------------- - -Pro funkci `matmul` můžeme nadefinovat číselné proměnné (`n`, `m`, `p`, `i`, `j`, `k`, `x`, `y`) -jako `int`, ale tím si moc nepomůžeme: většinu času program stráví vybíráním a ukládáním hodnot -z/do matic, a protože Cython nemá informace o tom, že jsou to NumPy matice, používá obecný -protokol pro pythonní kontejnery, takže se každá hodnota převede na pythonní objekt. - -Je tedy potřeba říct Cythonu, že používáme NumPy matice. -Naštěstí v NumPy existuje integrace s Cythonem, takže můžeme na úrovni C "naimportovat" -rozšíření pro NumPy: - - cimport numpy - -... a potom použít typ "dvourozměrná matice celých čísel", který se v Cythonu jmenuje -`numpy.ndarray[numpy.int64_t, ndim=2]`. -Naše funkce tedy bude začínat takto: - - cpdef numpy.ndarray[numpy.int64_t, ndim=2] matmul( - numpy.ndarray[numpy.int64_t, ndim=2] a, - numpy.ndarray[numpy.int64_t, ndim=2] b): - cdef numpy.ndarray[numpy.int64_t, ndim=2] result - ... - -Kdybychom si nebyli jistí typem matice, můžeme si ho nadefinovat pomocí `ctypedef`: - - ctypedef numpy.int64_t DATATYPE - - -...a pak používat tento alias. -Na maticové typy bohužel typedef zatím nefunguje. - -Pro práci s maticí ASCII znaků lze použít typ `numpy.int8_t`, ale je třeba při zapisování přímo na konkrétní pozice zapisovat typ `char`: - - cdef numpy.ndarray[numpy.int8_t, ndim=2] directions = numpy.full((h, w), b'#', dtype=('a', 1)) - directions[maze >= 0] = b' ' # Python level, using b' ' - directions[1, 2] == ord('x') # C level, using char - -Direktivy ---------- - -Anotací typů matic se naše demo maticového násobení dostalo skoro na úroveň -C, ale ne úplně: řádky, které pracují s maicemi, jsou ve výstupu `--annotate` -stále trochu žluté. -Cython totiž při každém přístupu k matici kontroluje, jestli nečteme nebo -nezapisujeme mimo pole, a případně vyvolá `IndexError`. - -Pokud víme – jako v našem případě – že je takové kontrola zbytečná, -můžeme Cythonu říct aby ji nedělal. -Přístupy mimo pole pak způsobí nedefinované chování (většinou program spadne, -nebo hůř, bude pracovat se špatnými daty). -Kontrola se vypíná direktivou `boundscheck`, která se dá zadat dvěma hlavními -způsoby: dekorátorem: - - @cython.boundscheck(False) - cpdef funkce(): - ... - -... nebo příkazem `with`: - - with cython.boundscheck(False): - ... - -... případně i pro celý soubor, viz [dokumnetace][set-directives]. - -Další zajímavá direktiva je `cython.wraparound(False)`, která podobným způsobem -vypíná pythonní způsob indexování zápornými čísly: místo indexování od konce -s ní dostaneme nedefinované chování. - -Seznam dalších direktiv najdete v [dokumentaci][directives]. - -Cython podporuje ještě blok `with cython.nogil:`, který je podobný direktivám, -ale dá se použít jen s `with`. -V rámci tohoto bloku je odemčený GIL (globální zámek). -Smí se použít pouze pokud nepracujeme s pythonnímmi objekty – například když -operujeme jen na obsahu už existujících maticí. -Opak je `with cython.gil:`, kterým zámek zase zamkneme – například když -potřebujeme vyhodit výjimku. - -[set-directives]: http://cython.readthedocs.io/en/latest/src/reference/compilation.html#how-to-set-directives -[directives]: http://cython.readthedocs.io/en/latest/src/reference/compilation.html#compiler-directives - - -Struktury, ukazatele, a dynamická alokace ------------------------------------------ - -Přestože v Cythonu můžete používat pythonní tuply, slovníky, seznamy a další podobné nehomogenní typy, jejich použití je pomalé, protože vždy pracují s pythoními objekty. - -Pokud máte kód, který potřebuje dočasné pole takových záznamů, -je pro časově kritické části kódu lepší k problému přistoupit spíše céčkovsky, -přes alokaci paměti a ukazatele. - -Následující příklad ukazuje, jak naplnit pole heterogenních záznamů: - - # Import funkcí pro alokaci paměti – chovají se jako malloc() apod. - from cpython.mem cimport PyMem_Malloc, PyMem_Realloc, PyMem_Free - - # Definice struktury - cdef struct coords: - int row - int column - char data - - MAXSIZE = ... - - def path(...): - # Definice ukazatele, přetypování - cdef coords * path = PyMem_Malloc(MAXSIZE*sizeof(coords)) - if path == NULL: - # no available memory left - raise MemoryError() - - cdef int used = 0 - for ...: - ... - - # - path[used] = coords(row, column, data) - used += 1 - - # pole můžeme používat - ... - - # a musíme ho před vrácením předělat na list - lpath = [] - cdef int i - for i in range(used): - lpath.append(path[i]) - - # a uvolnit - PyMem_Free(path) - return lpath - -Pro homogenní pole ale doporučujeme spíše NumPy matice. - -Následující příklad ukazuje, jak lze přiřazovat do struktur: - - cdef struct coord: - float x - float y - float z - - cdef coord a = coord(0.0, 2.0, 1.5) - - cdef coord b = coord(x=0.0, y=2.0, z=1.5) - - cdef coord c - - c.x = 42.0 - c.y = 2.0 - c.z = 4.0 - - cdef coord d = {'x':2.0, - 'y':0.0, - 'z':-0.75} - -Použití knihoven z C --------------------- - -Pro použití C knihoven z Pythonu je lepší použít [CFFI]. -Ale když už píšete kód v Cythonu -a potřebujete zavolat nějakou C funkci, můžete to udělat takto: - - cdef extern from "stdlib.h": - int rand() - void srand(long int seedval) - - cdef extern from "time.h": - long int time(int) - - srand(time(0)) - print(rand()) - - -pyximport a %%cython --------------------- - -Pro interaktivní práci vJupyter Notebook má Cython vlastní „magii“. -Na začátku Notebooku můžeme zadat: - - %load_ext cython - -a potom můžeme na začátku kterékoli buňky zadat `\%\%cython`: - -(Omlouváme se za zpětná lomítka – ta tam být nemají, ale Edux používá znak -procenta pro speciální účel a bez nich se to rozbije. -Uvítáme fix v pull requestu.) - - %%cython - - cpdef int mul(int a, int b): - return a * b - -Kód v takové buňce pak Notebook zkompiluje Cythonem, a funkce/proměnné v něm -nadefinované dá k dispozici. - -Můžeme použít i `\%\%cython --annotate`, což vypíše anotace přímo do Notebooku. - -Další zkratka je modul `pyximort`, který dává možnost importovat moduly `.pyx` -přímo: hledají se podobně jako `.py` nebo `.so`, a před importem se zkompilují. -Zapíná se to následovně: - - import pyximport - pyximport.install() - - import demo - -Video ------ - -Před nedávnem měl @hroncok na Středisku unixových technologií nahrávanou ukázku přepsání -úlohy ruksaku z předmětu MI-PAA z Pythonu do Cythonu (včetně nepříjemného záseku a live -ukázky debugování problému). -Na [video] se můžete podívat, mohlo by vám prozradit spustu tipů, které se vám mohou hodit -ke splnění úlohy. -K obsahu jen dodáme, že místo `malloc` a `free` je lepší použít `PyMem_Malloc` a -`PyMem_Free` z ukázky výše. - -[video]: https://www.youtube.com/watch?v=Ksv4RA6yhkY - - -Úkol -==== - -Vaším úkolem je zrychlit pomocí Cythonu úkol z předchozího cvičení tak, aby zvládal řešit i bludiště o rozměrech v řádech *nižší jednotky tisíců* × *nižší jednotky tisíců* na moderním počítači (srovnatelném s těmi ve školní učebně) v průměrném čase maximálně 1 sekundu (a volání `.path()` by se u takového bludiště mělo stihnout za sekundu aspoň 50). Úkol musí splňovat všechny náležitosti z minulého týdne + podmínku na čas. - -Následující příkazy musí po instalaci závislostí z `requirements.txt` fungovat: - - python setup.py build_ext -i # sestaví modul napsaný v Cythonu - python -m pytest # pustí testy - python -c 'from maze import analyze; analyze(...)' # lze importovat a použít z Pythonu - -Nepoužívejte pyximport. - -Pokud řešení úlohy z minula nemáte, nebo si např. chcete rozšířit testy o ty naše, -můžete použít [naše řešení minulé úlohy](https://github.com/encukou/maze). -Nezapomeňte na splnění podmínek licence. diff --git a/lessons/intro/distribution/index.md b/lessons/intro/distribution/index.md deleted file mode 100644 index 009f354147..0000000000 --- a/lessons/intro/distribution/index.md +++ /dev/null @@ -1,418 +0,0 @@ -Moduly -====== - -Zatím jsme tvořili programy v Pythonu tak nějak na divoko, tedy v jednom nebo -více souborech bez nějakého zvláštního řádu. Na dnešním cvičení se podíváme na -to, jak tvořit redistribuovatelné moduly, které jdou instalovat pomocí pipu. - -Za příklad si vezmeme kód Ondřeje Caletky, který umožňuje určit české svátky -v zadaném roce. Jako příklad je ideální, protože obsahuje jak funkce, které -můžeme volat z Pythonu, tak lze volat z příkazové řádky. - - * [oskar456/isholiday.py](https://gist.github.com/oskar456/e91ef3ff77476b0dbc4ac19875d0555e) - - -Volání z příkazové řádky, pomocí příkazu `python isholiday.py` nebo -`python -m isholiday`, zajišťuje blok `if __name__ == '__main__':`. -Toto je rychlý způsob, jak napsat modul který jde jak importovat, tak spustit. -Když nějaký modul importujeme, má v proměnné `__name__` k dispozici své jméno. -„Hlavní” modul ale není importován, a jeho jméno není vždy k dispozici -(např. v `cat isholiday.py | python`). -Python proto `__name__` „hlavního” modulu nastavuje na `'__main__'`, -čehož se často využívá. - -Později se podíváme na elegantnější způsob jak to zařídit; teď se vraťme -zpět k balíčkování. - -setup.py --------- - -Základním stavebním kamenem Python balíčku je soubor `setup.py`, který -obsahuje všechna potřebná metadata ve volání funkce `setup()` z modulu -`setuptools`. - -Pojďme vytvořit jeho minimální variantu: - - from setuptools import setup - - - setup( - name='isholiday', - version='0.1', - description='Finds Czech holiday for given year', - author='Ondřej Caletka', - author_email='ondrej@caletka.cz', - license='Public Domain', - url='https://gist.github.com/oskar456/e91ef3ff77476b0dbc4ac19875d0555e', - py_modules=['isholiday'], - ) - -Všimněte si, že jsme balíček pojmenoval stejně jako soubor se zdrojovým kódem. -Je to dobrá konvence, ale není to technicky nutné. - -Balíček můžeme zkusit nainstalovat do virtualenvu: - - $ python3.5 -m venv env - $ . env/bin/activate - (env)$ python setup.py install - ... - (env)$ python - >>> import isholiday - >>> - (env)$ python -m pip freeze - isholiday==0.1 - -Přes `setup.py` můžeme dělat další věci, například vytvořit archiv s balíčkem: - - (env)$ python3 setup.py sdist - ... - warning: sdist: standard file not found: should have one of README, README.rst, README.txt - ... - -Extra soubory do zdrojového balíčku ------------------------------------ - -Jak vidíte, `setuptools` si stěžuje, že náš projekt nemá `README`. -Můžeme jej vytvořit a uložit jako `README` přímo v kořenovém adresáři projektu, -tedy tam, kde byste jej nejspíš čekali. - - Czech public holiday checker... - - (env)$ python3 setup.py sdist - -V adresáři `dist` najdete archiv, jeho obsah můžete zkontrolovat, měl by tam -být i soubor `README`. - -Skvělé, pojďme vytvořit i další speciální soubor, `LICENSE`, který bude -obsahovat text licence, v tomto případě Public Domain. -Obsah najdete třeba na [CC0]. - -[CC0]: https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt - -Pokud ale se souborem `LICENSE` vytvoříte zdrojový balíček, soubor v archivu -nebude. Je to proto, že se standardně do archivu přidávají jen některé soubory. -Další soubory lze přidat pomocí souboru `MANIFEST.in`, dle [dokumentace]. - -[dokumentace]: https://docs.python.org/3/distutils/sourcedist.html#specifying-the-files-to-distribute - -V tomto případě takto: - - include LICENSE - (env)$ python3 setup.py sdist - ... - hard linking LICENSE -> isholiday-0.1 - hard linking MANIFEST.in -> isholiday-0.1 - hard linking README -> isholiday-0.1 - ... - -Více argumentů pro setup() --------------------------- - -Na chvíli se vrátíme k volání funkce `setup()` a přidáme co nejvíc dalších -položek ([jejich vysvětlení](https://packaging.python.org/distributing/#setup-args)). - - from setuptools import setup - - - with open('README') as f: - long_description = ''.join(f.readlines()) - - - setup( - name='isholiday', - version='0.1', - description='Finds Czech holiday for given year', - long_description=long_description, - author='Ondřej Caletka', - author_email='ondrej@caletka.cz', - keywords='holiday,dates', - license='Public Domain', - url='https://gist.github.com/oskar456/e91ef3ff77476b0dbc4ac19875d0555e', - py_modules=['isholiday'], - classifiers=[ - 'Intended Audience :: Developers', - 'License :: Public Domain', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Topic :: Software Development :: Libraries', - ], - zip_safe=False, - ) - -Všimněte si několika věcí. V první řadě v `long_description` vidíte, že jsme -pořád ještě v Pythonu a můžeme si ušetřit duplikaci nějakých informací pomocí -malého kousku kódu. Dalším zajímavým argumentem je `classifiers`. Jsou to -v podstatě takové tagy nebo strukturované informace o balíčku. -Nevymýšlíme je sami, ale hledáme je v -[seznamu](https://pypi.python.org/pypi?%3Aaction=list_classifiers). -Tyto informace jsou později vidět na [PyPI](https://pypi.python.org/pypi). - -Argument `zip_safe=False` zajistí, že se modul nainstaluje do adresáře. -Setuptool totiž mají nepříjemný zlozvyk instalovat moduly jako `zip`, -což komplikuje práci s datovými soubory jako *templates*. -Je proto lepší `zip_safe=False` uvést. - - -Více souborů s Python kódem ---------------------------- - -Doteď jsme vytvářeli balíček jen z jednoho zdrojového souboru `isholiday.py`. -Co ale dělat, pokud je náš projekt větší a obsahuje souborů více? -Teoreticky je možné je přidat všechny do `py_modules`, ale není to dobrý nápad. - -V takovém případě uděláme modul ve formě složky. V našem případě soubor -`isholiday.py` zatím přesuneme do `isholiday/__init__.py`: - - (env)$ tree - . - ├── isholiday - │   └── __init__.py - ├── LICENSE - ├── MANIFEST.in - ├── README - └── setup.py - - 1 directory, 5 files - -Soubor `__init__.py` jednak značí, že adresář `isholiday` je Pythoní modul, -také obsahuje kód, který se spustí při importu modulu `isholiday`. - -Musíme ještě mírně upravit `setup.py`: - - diff --git a/setup.py b/setup.py - index 3a69792..6b453ab 100644 - --- a/setup.py - +++ b/setup.py - @@ -11,7 +11,7 @@ setup( - keywords='holiday,dates', - license='Public Domain', - url='https://gist.github.com/oskar456/e91ef3ff77476b0dbc4ac19875d0555e', - - py_modules=['isholiday'], - + packages=['isholiday'], - classifiers=[ - 'Intended Audience :: Developers', - 'License :: Public Domain', - -Případně, což je ještě lepší, můžeme použít `find_packages()`: - - from setuptools import setup, find_packages - - setup( - ... - packages=find_packages(), - ... - ) - -Momentálně máme všechen kód přímo v `__init__.py`, což sice funguje, -ale ideální to není. Dobré je mít kód v samostatných souborech a v `__init__.py` -pouze importovat veřejné rozhraní, například takto: - - from .holidays import getholidays, isholiday - - __all__ = ['getholidays', 'isholiday'] - - -Do `__init__.py` ideálně nepatří žádný kód kromě tohoto. - -Tečka v příkazu `import` není chyba: je to zkratka pro aktuální modul. -Můžeme psát i `from isholiday.holidays import ...`, -což ale trochu ztěžuje případné přejmenování modulu. - - -Spouštění balíčku ------------------ - -Pokusíme-li se teď program spustit pomocí `python -m isholiday`, -narazíme na problém: na rozdíl od souboru se složka s kódem takto spustit nedá. -Namísto spuštění souboru (typicky s blokem `if __name__ == '__main__':`) totiž -Python v tomto případě hledá *soubor* pojmenovaný `__main__.py`, a spustí ten. - -Soubor `__main__.py` není určený k tomu, aby se z něho importovalo, proto -by měl obsahovat co nejméně kódu – ideálně jen volání funkce, která je -definovaná jinde. Vytvořte proto `__main__.py` s následujícím obsahem: - - from .holidays import main - - main() - -a v `holidays.py` zaměňte `if __name__ == '__main__':` za `def main():`. - -Skript teď bude možné použít pomocí `python -m isholiday`. - - -Programy pro příkazovou řádku ------------------------------ - -Pokud chcete, aby váš modul umožňoval spouštění z příkazové řádky, měli byste -použít [entrypoints]: - -[entrypoints]: https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins - - setup( - entry_points={ - 'console_scripts': [ - 'executable_name = isholiday.holidays:main', - ], - }, - ) - -`isholiday.holidays:main` je cesta k funkci ve tvaru `modul:funkce`, funkce může -být v modulu definovaná nebo importovaná. - -Skript bude možné použít, je-li aktivní prostředí kde je nainstalován, jen -zadáním jména *entrypointu*: - - $ executable_name - - -Specifikace závislostí ----------------------- - -Balíčky na PyPI mohou záviset na dalších balíčkách. V případě `isholiday` to -potřeba není, ale v úlohách z minulých cvičení ano. - -Možná jste se setkali se souborem `requirements.txt`. Pomocí `setuptools` to -jde ale lépe. Existuje několik úrovní závislostí, ve většině případů si -vystačíte s argumentem `install_requires`: - - setup( - install_requires=['Flask', 'click>=6'], - ) - -Kromě závislostí v `setup.py` se u Pythoních projektů často setkáme se souborem -`requirements.txt`, který obsahuje přesné verze všech závislosti, včetně -tranzitivních – t.j. závisí-li náš balíček na `Flask`, a `Flask` na `Jinja2`, -najdeme v `requirements.txt` mimojiné řádky: - - Flask==0.11.1 - Jinja2==2.8 - -Tento soubor se používá, když je potřeba přesně replikovat prostředí, kde -program běží, například mezi testovacím strojem a produkčním nasazením -webové aplikace. -Tento soubor se dá vygenerovat z aktuálního prostředí zadáním -`python -m pip freeze > requirements.txt`, a nainstalovat pomocí -`python -m pip install -r requirements.txt`. -My ho používat nebudeme, vystačíme si s volnější specifikací závislostí -v `setup.py`. - - -Upload na PyPI --------------- - -Balíček jde zaregistrovat a nahrát na PyPI. Původně k tomu sloužily příkazy -`setup.py` `register` a `upload`, ale tyto příkazy používají HTTP, což není -bezpečné. Prototo je lepší použít program `twine` (instalovatelný přes pip). - -Budete potřebovat [účet na PyPI](https://pypi.python.org/pypi?%3Aaction=register_form). -[účet na testovací PyPI](https://testpypi.python.org/pypi?%3Aaction=register_form) -a konfigurační soubor `~/.pypirc`: - - [distutils] - index-servers= - pypi - pypitest - - [pypi] - repository = https://pypi.python.org/pypi - username = - password = - - [pypitest] - repository = https://testpypi.python.org/pypi - username = - password = - -Hesla můžete vynechat, pokud je budete chtít pokaždé zadávat. - -Používáte-li Windows, je potřeba nastavit proměnnou prostředí `HOME` na adresář -se souborem `.pypirc`. - -Registrace projektu a nahrání na testovací PyPI se provádí pomocí: - - (env)$ twine register -r pypitest dist/ - Registering package to https://testpypi.python.org/pypi - Registering - (env)$ twine upload -r pypitest dist/ - Uploading distributions to https://testpypi.python.org/pypi - Uploading - [================================] 8379/8379 - 00:00:02 - -Registrace se zdaří jen pokud jméno projektu již není zabrané. -Po úspěšném nahrání lze nahrát už jen novější verze modulu. - -Pro nahrání na opravdovou PyPI stačí vynechat `-r pypitest`. - -Instalace pomocí pip --------------------- - -Projekt nahraný na PyPI by mělo jít nainstalovat pomocí pipu. -V případě použití ostré verze PyPI stačí k instalaci zadat název balíčku: - - (env)$ python -m pip install - -Pokud však použijeme testovací PyPI, je nutné pipu říct, aby balíček hledal tam. -[Postup](https://wiki.python.org/moin/TestPyPI) uvedený v dokumentaci není -v tomto případě nejvhodnější, protože z testovací PyPI vezme jak náš balíček, -tak i případné závislosti, které mohou být zastaralé, rozbité či jinak škodlivé. - -Lepší by bylo, kdyby pip nainstaloval závislosti z ostré PyPI a na testovací -hledal jen náš projekt. Toho se dá docílit přepínačem `--extra-index-url`. - - (env)$ python -m pip install --extra-index-url https://testpypi.python.org/pypi - -V tomto případě pip nejdřív prohledá ostrou PyPI, a pokud nenajde požadovaný -balíček, použije testovací PyPI. Zde je potřeba dávat pozor na název projektu, -protože případné konflikty mezi ostrou a testovací PyPI se nekontrolují. -Pokud tedy máme projekt na testovací PyPI a na ostré existuje projekt se -stejným názvem, nainstaluje se ten z ostré verze. - -V případě, že tento problém nastane, je možné ho částečně obejít specifikací -verze instalovaného balíčku: - - (env)$ python -m pip install --extra-index-url https://testpypi.python.org/pypi ==0.3 - -Pokud u duplicitního projektu na ostré PyPI neexistuje požadovaná verze, -nainstaluje se náš projekt z testovací PyPI. - -Jiná možnost je zadat přímo cestu k archivu s balíčkem místo jeho názvu. -Zde pak na umístění balíčku ani verzi nezáleží: - - (env)$ python -m pip install https://testpypi.python.org/packages/.../-0.3.tar.gz - -Další ------ - - * [instalace datových souborů](https://docs.python.org/3/distutils/setupscript.html#installing-package-data) - * [obsáhlá dokumentace](https://packaging.python.org/) - -Úkol ----- - -Vaším úkolem je udělat z vašeho dosavadního projektu balíček -instalovatelný přes pip a nahrát jej na testovací nebo opravdovou PyPI. - -Na opravdovou PyPI prosím nahrávejte pouze s rozumným názvem a pokud jde o dílo -s nějakou open-source licencí. - -Podmínky (je jich hodně, ale jsou triviální): - - * Váš balíček musí fungovat (viz zadání předchozích úkolů) po instalaci pomocí pipu do "prázdného" virtualenvu. - * Musí instalovat potřebné závislosti. - * Musí obsahovat rozumný počet classsifiers a voleb pro `setup.py`. - * Podpříkaz `sdist` nesmí skončit chybou ani vyvolat varování. - * Musí splňovat zde uvedené konvence. - * Hlavní skript musí jít spouštět pomocí entry pointu i pomocí `-m`. - * Modul musí obsahovat `__init__.py` a logiku importovat z ostatních souborů. - * Zabalený modul musí obsahovat soubor s textem licence (`LICENSE`, `COPYING`) \* - * `long_description` musí být načten z `README` - -\* Vhodnou licenci můžete najít na [choosealicense.com]. -V případě, že váš kód nechcete šířit pod svobodnou licencí, -napište to do souboru vlastní podmínky. Nevymýšlejte si ale prosím vlastní -open-source licence. - -[choosealicense.com]: http://choosealicense.com/ diff --git a/lessons/intro/docs/index.md b/lessons/intro/docs/index.md deleted file mode 100644 index 3d100c7dbb..0000000000 --- a/lessons/intro/docs/index.md +++ /dev/null @@ -1,430 +0,0 @@ -Dokumentace -=========== - -Jednou ze zásadních součástí každého kvalitního Python projektu je dokumentace. -Protože chceme, abyste vytvářeli kvalitní projekty, podíváme se tedy i na -dokumentaci. - -Sphinx ------- - -Nejpoužívanějším nástrojem na vytváření dokumentace Python projektů je [Sphinx]. -Když jste se dívali do dokumentace Flasku, requests, clicku, flexmocku, pytestu, -betamaxu či Pythonu samotného, viděli jste dokumentaci vytvořenou ve Sphinxu. - -[Sphinx]: http://www.sphinx-doc.org/ - -Pro vytvoření základní kostry dokumentace se používá jednoduchý průvodce: - - $ . env/bin/activate - (env) $ python -m pip install sphinx - (env) $ python -m sphinx.quickstart - Welcome to the Sphinx 1.4.8 quickstart utility. - ... - Enter the root path for documentation. - > Root path for the documentation [.]: docs - ... - The project name will occur in several places in the built documentation. - > Project name: coolthing - > Author name(s): Pythonista Dokumentarista - - Sphinx has the notion of a "version" and a "release" for the - software. Each version can have multiple releases. For example, for - Python the version is something like 2.5 or 3.0, while the release is - something like 2.5.1 or 3.0a1. If you don't need this dual structure, - just set both to the same value. - > Project version: 0.5 - ... - Please indicate if you want to use one of the following Sphinx extensions: - > autodoc: automatically insert docstrings from modules (y/n) [n]: y - > doctest: automatically test code snippets in doctest blocks (y/n) [n]: y - ... - -V ukázce jsou zobrazeny jen věci, kde nestačí nechat výchozí hodnota. -K modulům `autodoc` a `doctest` se dostaneme později. - -Průvodce vytvoří složku docs a v ní několik souborů. -Do gitu patří všechny nyní vytvořené soubory, kromě složky `docs/_build`, -která by měla být ignorována. - -Zatím se nebudeme zabývat obsahem těchto souborů, ale zkusíme základní kostru -dokumentace sestavit do HTML. - -**Poznámka:** Sphinx umí generovat dokumentaci ve více formátech (LaTeX, -manuálové stránky atd.), pro nás bude podstatné především HTML. - - (env) $ cd docs - (env) $ make html - ... - Build finished. The HTML pages are in _build/html. - -Ve zmíněné složce byste měli najít `index.html`, ten si můžete prohlédnout -v prohlížeči. - -Textový obsah v dokumentaci ---------------------------- - -Text dokumentace začíná v souboru `index.rst` a píše se ve značkovacím formátu -[reStructuredText] neboli rst. Bohužel nelze psát v Markdownu, ačkoli existují -složité triky, jak docílit nějaké konverze. - -reStructuredText se od Markdownu liší v syntaxi, která je komplikovanější na -psaní, ale umožňuje dělat komplexnější věci. - -Pro přehled o tom, co reStructuredText umí a jakou má syntaxi, -můžete použít [tahák]. - -[reStructuredText]: http://www.sphinx-doc.org/en/stable/rest.html -[tahák]: https://github.com/ralsina/rst-cheatsheet - -V `index.rst` je seznam kapitol: - - .. toctree:: - :maxdepth: 2 - -Tam můžete přidat další kapitoly: - - .. toctree:: - :maxdepth: 2 - - intro - tutorial/foo - tutorial/bar - ... - -Soubory s kapitolami je třeba vytvořit ve složce `docs` s příponou `.rst`. -Obsah (ve smyslu *content*) lze pak přidávat samozřejmě do těchto souborů i do -`index.rst`. - -Chcete-li odkazovat na některou sekci, označíme si ji pomocí `.. _label:`: - - .. _my-reference-label: - - Section to cross-reference - -------------------------- - - This is the text of the section. - -Poté na ni lze odkazovat, odkudkoli z dokumentace, odkazovat pomocí -[konstrukce ref]: - - It refers to the section itself, see :ref:`my-reference-label`. - It could refer to a different section as well :) - -[konstrukce ref]: http://www.sphinx-doc.org/en/1.4.8/markup/inline.html#role-ref - -doctest -------- - -`doctest` je modul ze standardní knihovny, který najde v dokumentaci bloky kódu -a otestuje, jestli se váš kód chová tak, jak je to ukázáno v dokumentaci. - -V kombinaci se Sphinxem se dá použít rozšíření `doctest`, které jsme v průvodci -aktivovali. Můžete to dělat dvěma způsoby. První je mít v dokumentaci -příklad vypadající jako interaktivní konzole. -Takový příklad nemusí být odsazený ani ničím uvozený; stačí `>>>` na začátku. - - >>> 1 + 1 - 2 - -Doctest v tomto případě otestuje, že vše funguje, jak má. -V tomto případě se provede součet a zkontroluje se, zda výsledek je 2. - -Druhý způsob je mít v dokumentaci nejdříve kód: - - print('foo') - -A dále někde jinde výstup volání: - - foo - -K tomu všemu složí několik direktiv: - -### .. testsetup:: - -Direktiva pro potřebný kód, který se musí provést, aby příklad fungoval, ale -nebude v dokumentaci zobrazen (např. kód pro vytvoření falešného objektu, -import...). - -### .. testcleanup:: - -Podobná direktiva jako `.. testsetup::` provedená po skončení testů. -V dokumentaci nebude kód zobrazen. - -### .. doctest:: - -Test s interaktivní konzolí. V dokumentaci bude zobrazen, pokud nepoužijete flag -`:hide:`. - -### .. testcode:: - -Kód testu bez interaktivní konzole, co chcete kontrolovat, musíte dát na -standardní výstup. V dokumentaci bude zobrazen, pokud nepoužijete flag -`:hide:`. - -### .. testoutput:: - -Výstup posledního testcode bloku. V dokumentaci nebude kód zobrazen, pokud -nepoužijete flag `:hide:`. - -### Kompletní příklad - - The parrot module - ================= - - .. testsetup:: - - class Parrot: - def voom(self, voltage): - print('This parrot wouldn\'t voom if you put {} volts through it!'.format(voltage)) - - def die(self): - return 'RIP' - - - parrot = Parrot() - - The parrot module is a module about parrots. - - Doctest example: - - .. doctest:: - - >>> parrot.voom(3000) - This parrot wouldn't voom if you put 3000 volts through it! - - Test-Output example: - - .. testcode:: - - parrot.voom(3000) - - This would output: - - .. testoutput:: - - This parrot wouldn't voom if you put 3000 volts through it! - - You can use other values: - - .. testcode:: - - parrot.voom(230) - - .. testoutput:: - :hide: - - This parrot wouldn't voom if you put 230 volts through it! - - - .. testcleanup:: - - parrot.die() - -Testy se také dají zařazovat do skupin, více -v [dokumentaci](http://www.sphinx-doc.org/en/1.4.8/ext/doctest.html). - - (env) $ make doctest - ... - Document: intro - --------------- - 1 items passed all tests: - 3 tests in default - 3 tests in 1 items. - 3 passed and 0 failed. - Test passed. - 1 items passed all tests: - 1 tests in default (cleanup code) - 1 tests in 1 items. - 1 passed and 0 failed. - Test passed. - - Doctest summary - =============== - 3 tests - 0 failures in tests - 0 failures in setup code - 0 failures in cleanup code - ... - -### Import z vlastního kódu - -Pokud nemáte nainstalovaný vlastní balíček, a budete z něj chtít v doctestu -importovat, pravděpodobně dostanete `ImportError`. -V takovém případě pomůže drobná editace `conf.py`: - - # If extensions (or modules to document with autodoc) are in another directory, - # add these directories to sys.path here. If the directory is relative to the - # documentation root, use os.path.abspath to make it absolute, like shown here. - # - import os - import sys - sys.path.insert(0, os.path.abspath('..')) - -### Travis CI - -Neexistuje žádný unifikovaný způsob, jak specifikovat závislosti pro sestavení -dokumentace. Proto, pokud chcete mít nějaký jednoduchý způsob, jak pouštět -doctesty na Travisu, vytvořte například soubor `docs/requirements.txt` -a do něj dejte závislosti potřebné pro sestavení dokumentace. -Je na vás, jestli tam budou pouze extra závislosti oproti těm v `setup.py` -(většinou pouze `sphinx`), nebo všechny závislosti, aby šel použít soubor -samostatně. - -Poté na Travisu můžete udělat něco jako: - - language: python - python: - - '3.5' - install: - - python setup.py install - - pip install -r docs/requirements.txt - script: - - python setup.py test --addopts -v - - cd docs && make doctest - -autodoc -------- - -Pro dokumentaci API lze použít Sphinx rozšíření `autodoc`, které jsme povolili -v průvodci (případně to lze udělat v souboru `conf.py`), a to takto: - - .. automodule:: mymodule - :members: - -Tento příklad na dané místo vygeneruje dokumentaci složenou z dokumentačních -řetězců jednotlivých funkcí, tříd a metod v modulu `mymodule`. - -Pokud chcete selektivně vybrat, dokumentaci čeho chcete generovat, -můžete použít i -[jiné direktivy](http://www.sphinx-doc.org/en/1.4.8/ext/autodoc.html#directive-automodule). - -Pro vygenerování hezké struktury si můžete pomoci příkazem `apidoc`: - - (env) $ python -m sphinx.apidoc -o docs mymodule - -V dokumentačních řetězcích samozřejmě můžete použít [reStructuredText] a je to -dokonce žádoucí. - -Zde je ukázka z betamaxu (*Copyright 2013 Ian Cordasco*): - - class Betamax: - - """This object contains the main API of the request-vcr library. - - This object is entirely a context manager so all you have to do is: - - .. code:: - - s = requests.Session() - with Betamax(s) as vcr: - vcr.use_cassette('example') - r = s.get('https://httpbin.org/get') - - Or more concisely, you can do: - - .. code:: - - s = requests.Session() - with Betamax(s).use_cassette('example') as vcr: - r = s.get('https://httpbin.org/get') - - This object allows for the user to specify the cassette library directory - and default cassette options. - - .. code:: - - s = requests.Session() - with Betamax(s, cassette_library_dir='tests/cassettes') as vcr: - vcr.use_cassette('example') - r = s.get('https://httpbin.org/get') - - with Betamax(s, default_cassette_options={ - 're_record_interval': 1000 - }) as vcr: - vcr.use_cassette('example') - r = s.get('https://httpbin.org/get') - """ - -Existují různé způsoby, jak dokumentovat argumenty, návratové hodnoty apod. -Zvídavým studentům doporučujeme podívat se na rozšíření [Napoleon]. - -[Napoleon]: http://www.sphinx-doc.org/en/1.4.8/ext/napoleon.html - - -Odkazy na třídy a moduly ------------------------- - -Máte-li zdokumentovaný modul, funkci, třídu, metodu apod., je možné na ni -odkázat pomocí konstrukce `:mod:`, `:func:`, `:cls:`, `:meth:` a dalších -ze Sphinxové [domény Python]: - - To test the parrot's electrical resistance, use :meth:`parrot.voom()`. - -V této části dokumentace Sphinxu též najdete způsob, jak dokumnetovat API -bez použití `autodoc`. - -Všechny zdokumnetované objekty se automaticky přidávají do rejstříku. -Chcete-li do rejstříku přidat něco navíc, použijte direktivu [index]. - -[domény Python]: http://www.sphinx-doc.org/en/1.4.8/domains.html#cross-referencing-python-objects -[index]: http://www.sphinx-doc.org/en/1.4.8/markup/misc.html#index-generating-markup - - -README.rst ----------- - -Když už se stejně zabýváme [reStructuredText]em, je dobré váš README přepsat -nebo převést do stejného formátu. Na PyPI pak bude váš projekt vypadat lépe. - -Při přejmenování na `README.rst` dejte pozor na patřičné změny v `setup.py`. - -Read the Docs -------------- - -Pokud svůj repositář na GitHubu změníte na veřejný -(což je v tuto chvíli již z hlediska předmětu v pořádku), můžete využít službu -[Read the Docs] k hostování dokumentace ve Sphinxu. -Dokumentace se sestaví při každém pushnutí na GitHub. - -Pokud Read the Docs použijete, nezapomeňte na dokumentaci odkázat -z `README.rst`. - -[Read the Docs]: https://readthedocs.org/ - -Úkol ----- - -Vaším úkolem je vytvořit pomocí Sphinx dokumentaci k vašemu projektu. - -Měla by obsahovat textovou část, ze které bude jasné, co je, k čemu je, -jak se nainstaluje a jak se používá vaše aplikace. -Můžete předpokládat, že uživatel ví, co je to Twitter, tweet, hashtag, retweet -apod.; GitHub, issue, pull request, label, repozitář apod. -Nepředpokládejte ale, že ví, kde najde API klíče či tokeny, že ví, co to je, -jak se k nim chovat apod. U GitHubu nepředpokládejte, že ví, co je to webhook. - -Pokud hypoteticky ukážeme dokumentaci kolegům, kteří nikdy neviděli zadání -vašeho úkolu, musí to pro ně být stejně pochopitelné. - -Dále by dokumentace měla obsahovat textovou část s ukázkami kódu, které se -testují pomocí doctestu. Tato část může vysvětlovat, jak váš kód použít pro -výrobu jiné aplikace, nebo může popisovat, jak aplikace uvnitř funguje. - -V dokumentaci by měla existovat kapitola s kompletní API dokumentací vašich -modulů, tříd, funkcí apod. Všechny tyto věci musí mít v kódu dokumentační -řetězce, které v dokumentaci musí být zobrazeny (t.j. změna dokumentačního -řetězce se automaticky promítne ve vygenerované dokumnetaci). - -Jak sestavit a testovat dokumentaci by mělo být jasné z `README.rst` -(a to musí mít reStructuredText syntaxi). - -Generování dokumentace ani doctesty nesmí způsobit chybu ani varování. -Potlačení chybových a varovných hlášek (např. konfigurací, přesměrováním -*stderr*, apod.) je povoleno jen po konzultaci s cvičícím. - -Na Travis CI spouštějte dokumentační testy. - -Dokumentace musí být v angličtině. - -Dosavadní funkcionalita aplikace musí být samozřejmě zachována. diff --git a/lessons/intro/flask/index.md b/lessons/intro/flask/index.md deleted file mode 100644 index d7fc5ad446..0000000000 --- a/lessons/intro/flask/index.md +++ /dev/null @@ -1,377 +0,0 @@ -Webové aplikace: Flask -====================== - -Python je víceúčelový jazyk. -Na minulém cvičení jsme tvořili aplikace pro příkazovou řádku, -nyní se podíváme na aplikace webové. - -Webových frameworků pro Python je více, mezi nejznámější patří [Django], -[Flask] nebo [Pyramid]. - -Pro naše účely použijeme Flask, protože je nejrychlejší na pochopení a -nevyžaduje striktně použití [MVC] paradigmatu. - -[Django]: https://www.djangoproject.com/ -[Flask]: http://flask.pocoo.org/ -[Pyramid]: http://www.pylonsproject.org/ -[MVC]: https://cs.wikipedia.org/wiki/Model-view-controller - -Flask ------ - -Flask opět můžete nainstalovat do virtualenvu, nejlépe použít projekt -z minulého cvičení: - - $ cd project - $ . env/bin/activate - (env)$ python -m pip install Flask - -Základní použití Flasku je poměrné primitivní: - - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def hello(): - return 'MI-PYT je nejlepší předmět na FITu!' - - if __name__ == '__main__': - app.run(debug=True) - - (env)$ python hello.py - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with stat - * Debugger is active! - * Debugger pin code: 189-972-345 -Na zmíněné adrese byste měli v prohlížeči vidět použitý text. - -V příkladu jsme vytvořili flaskovou aplikaci (`app`), pomocí dekorátoru -`@app.route` jsme vytvořili takzvanou routu (cestu). Říkáme tím, že na adrese -`/` bude k dispozici obsah, který vrátí definovaná funkce. -Více různých cest lze vytvořit jednoduše přidáním další funkce. - - @app.route('/') - def index(): - return 'Index Page' - - @app.route('/hello') - def hello(): - return 'Hello, World' - -Pomocí `app.run()` jsme aplikaci spustili na lokálním počítači. -V případě reálného nasazení pak aplikaci předáme nějakému webovému serveru. -Argument `debug` slouží k zjednodušení debugování -(např. případné výjimky uvidíte přímo v prohlížeči), -pro reálné nasazení by však tento režim neměl být zapnut, kvůli bezpečnosti a -dopadům na výkon. - -### Dynamické routy - -Když vytváříte dynamický web, ne vždy můžete všechna URL znát dopředu. -Pokud například chcete zobrazit informace o uživatelích na adrese -`/user/hroncok` apod., musíte použít dynamické routy: - - @app.route('/user/') - def profile(username): - return 'User {}'.format(username) - -Proměnnou část cesty ohraničíte lomenými závorkami a použijte jako parametr -funkce. Pokud chcete, můžete specifikovat, na jaký obsah se pravidlo vztahuje: - - @app.route('/post/') - -Můžete použít různá pravidla, např.: - - * `string` akceptuje jakýkoliv text bez lomítek (výchozí) - * `int` akceptuje celá čísla - * `float` akceptuje i desetinná čísla s tečkou - * `path` akceptuje text i s lomítky - -### Získání URL - -Opačným způsobem jak k routám přistupovat, je když potřebujete získat URL -nějaké stránky, například protože potřebujete zobrazit odkaz. -K tomu se používá funkce `url_for()`: - - from flask import url_for - ... - url_for('profile', username='hroncok') - -Tuto funkci jde použít jen uvnitř funkce obsluhující cestu, pokud ji chcete -vyzkoušet například v interaktivní konzoli, můžete použít speciální kontext -manager: - - >>> with app.test_request_context(): - ... print(url_for('profile', username='hroncok')) - ... - /user/hroncok - -Možná si říkáte, proč tu cestu prostě nevytvořit ručně, ale mohli byste narazit -na problém, pokud cestu později změníte. - -### Šablony - -Zatím jsou naše webové stránky poměrně nudné, protože nepoužívají HTML. -Klidně byste mohli udělat něco jako: - - @app.route('/') - def hello(): - return '...' - -...ale asi by to nebylo příliš příjemné. -Lepší je použít šablony: - - from flask import render_template - - @app.route('/hello/') - @app.route('/hello/<name>') - def hello(name=None): - return render_template('hello.html', name=name) - -Pak je třeba vedle souboru vytvořit složku `templates` a v ní `hello.html`: - - <!doctype html> - <title>Hello from Flask - {% if name %} -

Hello {{ name }}!

- {% else %} -

Hello, World!

- {% endif %} - -Šablony používají v Pythonu velmi oblíbený [Jinja2]. -(Základní použití najdete na odkaze.) - -Veškerý kontext do šablony musí přijít z volání `render_template()`, -navíc můžete automaticky použít např, `url_for()`. - -[Jinja2]: http://jinja.pocoo.org/docs/dev/templates/ - -Pro debugování je vhodné nastavit automatické načítání změn šablon: - - if __name__ == "__main__": - app.config['TEMPLATES_AUTO_RELOAD'] = True - app.run(debug=True) - -#### Filtry - -Není úplně elegantní vzít nějaká data (např. tweety z Twitter API) a před -předáním šabloně do nich cpát svoje úpravy (např. HTML). -Od toho jsou tu filtry. Filtr je funkce na transformaci řetězce, kterou lze -použít v šabloně. - -Zde je například filtr, který načte čas v určitém formátu a převede do jiného: - - @app.template_filter('time') - def convert_time(text): - """Convert the time format to a different one""" - dt = datetime.strptime(text, '%a %b %d %H:%M:%S %z %Y') - return dt.strftime('%c') - -V šabloně: - - {{ tweet.created_at|time }} - -#### Escaping - -V textu, který se vkládá do šablon, jsou automaticky nahrazeny znaky, které -mají v HTML speciální význam. -Zabraňuje se tak bezpečnostním rizikům, kdy se vstup od uživatele interpretuje -jako HTML. - -Například když v aplikaci výše navštívíme URL `/hello/