diff --git a/lessons/intro/async/index.md b/lessons/intro/async/index.md index f32c76600a..fa93a9b883 100644 --- a/lessons/intro/async/index.md +++ b/lessons/intro/async/index.md @@ -2,9 +2,11 @@ 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. +Můžete použít virtualenv z minula, nebo PyQt5 nainstalovat znovu (viz [minulá lekce]). (Nejde-li to, nevadí – úplně nezbytné dnes PyQt nebude.) +[minulá lekce]: {{ lesson_url('intro/pyqt') }} + Další knihovny pro dnešní den: python -m pip install --upgrade pip @@ -39,28 +41,32 @@ který pracuje se dvěma druhy objektů: s *iterovatelnými objekty* a s *iterá 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]) - +```python +>>> 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 +```python +>>> 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. @@ -83,52 +89,60 @@ 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') +```python +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() - +```python +>>> 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 +```python +>>> 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 +```python +import contextlib - @contextlib.contextmanager - def ctx_manager(): - print('Entering') - yield 123 - print('Exiting') +@contextlib.contextmanager +def ctx_manager(): + print('Entering') + yield 123 + print('Exiting') - with ctx_manager() as obj: - print('Inside context, with', obj) +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. @@ -144,21 +158,25 @@ 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 +```python +def generator(a, b): + """Yield two numbers and return their sum""" + yield a + yield b + return a + b +``` + +```python +>>> 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 @@ -169,18 +187,20 @@ Oproti normálním iterátorům, které hodnoty jen poskytují, mají generátor 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 +```python +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 @@ -191,19 +211,23 @@ 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 +```python +def report_exception(): + try: + yield + except BaseException as e: + print('Death by', type(e).__name__) + yield 123 +``` + +```python +>>> 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. @@ -212,14 +236,16 @@ 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 +```python +>>> 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ů @@ -227,83 +253,95 @@ 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' +```python +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) +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' +```python +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 +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) +for action in dance(): + print(action) +``` Tohle počtu řádků příliš nepomohlo. Existuje lepší způsob – místo: +```python for action in dance_hands(): yield action +``` můžeme delegovat vytváření podsekvence na jiný generátor pomocí: +```python 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 +```python +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) +``` + +```python +>>> 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 @@ -350,71 +388,75 @@ 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 +```python +import random +import time - def print_blinky(blinky): - print(blinky, end='\r') +def print_blinky(blinky): + print(blinky, end='\r') - class Blinky: - def __init__(self): - self._face = '(o.o)' +class Blinky: + def __init__(self): + self._face = '(o.o)' - def __str__(self): - return self._face + def __str__(self): + return self._face - def set_face(self, new): - self._face = new - print_blinky(self) + 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)) + 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() +Blinky().run() +``` Chceme-li spustit několik takových animací, můžeme to udělat ve vláknech: - import random - import time - import threading +```python +import random +import time +import threading - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') +def print_blinkies(): + for blinky in blinkies: + print(blinky, end=' ') + print(end='\r') - class Blinky: - def __init__(self): - self._face = '(o.o)' +class Blinky: + def __init__(self): + self._face = '(o.o)' - def __str__(self): - return self._face + def __str__(self): + return self._face - def set_face(self, new): - self._face = new - print_blinkies() + 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)) + 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)] +blinkies = [Blinky() for i in range(10)] - for blinky in blinkies: - threading.Thread(target=blinky.run).start() +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. @@ -422,11 +464,13 @@ Nám stačí malá změna ve funkci `print_blinkies` (podobná funkce by v reál 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') +```python +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í @@ -437,63 +481,65 @@ Kdykoli je potřeba pozastavit běh některé úlohy, tak zbytek úlohy naplánu 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 +```python +import random +import time - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') +def print_blinkies(): + for blinky in blinkies: + print(blinky, end=' ') + print(end='\r') - blinkies = [] +blinkies = [] - class Blinky: - def __init__(self): - self.open_eyes() +class Blinky: + def __init__(self): + self.open_eyes() - def __str__(self): - return self._face + def __str__(self): + return self._face - def set_face(self, new): - self._face = new - print_blinkies() + 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 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) + 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: +# 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]) +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)] +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() +# 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) + # 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 + # Decrease remaining time for all tasks by the time waited + for entry in task_entries: + entry[0] -= wait_time - # Run the actual task - task() + # 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. @@ -513,58 +559,60 @@ 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 +```python +import random +import time - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') +def print_blinkies(): + for blinky in blinkies: + print(blinky, end=' ') + print(end='\r') - class Blinky: - def __init__(self): - self._face = '(o.o)' +class Blinky: + def __init__(self): + self._face = '(o.o)' - def __str__(self): - return self._face + def __str__(self): + return self._face - def set_face(self, new): - self._face = new - print_blinkies() + 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) + 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: +# Scheduling via a list of [remaining time, generator] pairs: - blinkies = [Blinky() for i in range(10)] +blinkies = [Blinky() for i in range(10)] - task_entries = [[0, b.run()] for b in blinkies] +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] +# 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) + # 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 + # 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 + # 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. @@ -624,55 +672,61 @@ Tuto syntaxi použijeme i tady; máte-li starší Python, podívejte se na potř Náš příklad s animací vypadá v `asyncio` takto: - import random - import time - import asyncio +```python +import random +import time +import asyncio - def print_blinkies(): - for blinky in blinkies: - print(blinky, end=' ') - print(end='\r') +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()) +class Blinky: + def __init__(self): + self._face = '(o.o)' + asyncio.ensure_future(self.run()) - def __str__(self): - return self._face + def __str__(self): + return self._face - def set_face(self, new): - self._face = new - print_blinkies() + 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)) + 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)] +blinkies = [Blinky() for i in range(10)] - loop = asyncio.get_event_loop() - loop.run_forever() - loop.close() +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 ... +```python +async def ...: + await ... +``` psát: - @asyncio.coroutine - def ...: - yield from ... +```python +@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`, @@ -719,35 +773,37 @@ jestli je operace hotová se dá zjistit pomocí `done()`. 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 +```python +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 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 +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() +future = asyncio.Future() - # Schedule the "set_future" task (explained later) - asyncio.ensure_future(set_future(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() +# Run the "get_future" coroutine until complete +loop = asyncio.get_event_loop() +result = loop.run_until_complete(get_future(future)) +loop.close() - print(result) +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, @@ -763,9 +819,11 @@ 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) +```python +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 @@ -789,21 +847,23 @@ Nevýhoda async funkcí spočívá v tom, že na každé zavolání async funkce 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 +```python +import asyncio - async def add(a, b): - await asyncio.sleep(1) - return a + b +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! +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() +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á: @@ -812,10 +872,12 @@ Na to ma dokonce `asyncio` speciální funkci `ensure_future`, která: * 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! +```python +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ě @@ -869,10 +931,12 @@ Typický příklad je u databází: začátek a konec transakce i získávání řá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) +```python +async with database.transaction(): + await database.execute('UPDATE ...') + async for row in (await database.execute('SELECT ...')): + handle(row) +``` Komunikace @@ -909,24 +973,26 @@ 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 +```python +import asyncio +import aiohttp - async def main(url): - # Use a a session - session = aiohttp.ClientSession() - async with session: +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: + # 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) + # 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() +loop = asyncio.get_event_loop() +loop.run_until_complete(main('http://python.cz')) +loop.close() +``` A další @@ -968,15 +1034,19 @@ 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 +```python +from quamash import QEventLoop - app = QtWidgets.QApplication([]) - loop = QEventLoop(app) - asyncio.set_event_loop(loop) +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() +```python +loop.run_forever() +``` Jednotlivé asynchronní funkce se pak používají jako v čistém `asyncio`: pomocí `asyncio.ensure_future`, `await`, atd. @@ -986,35 +1056,38 @@ pomocí `asyncio.ensure_future`, `await`, atd. Ukázka: - import asyncio +```python +import asyncio - from PyQt5 import QtGui, QtWidgets - from quamash import QEventLoop +from PyQt5 import QtGui, QtWidgets +from quamash import QEventLoop - app = QtWidgets.QApplication([]) - loop = QEventLoop(app) - asyncio.set_event_loop(loop) +app = QtWidgets.QApplication([]) +loop = QEventLoop(app) +asyncio.set_event_loop(loop) - display = QtWidgets.QLCDNumber() - display.setWindowTitle('Stopwatch') +display = QtWidgets.QLCDNumber() +display.setWindowTitle('Stopwatch') - display.show() +display.show() - async def update_time(): - value = 0 - while True: - display.display(value) - await asyncio.sleep(1) - value += 1 +async def update_time(): + value = 0 + while True: + display.display(value) + await asyncio.sleep(1) + value += 1 - asyncio.ensure_future(update_time()) +asyncio.ensure_future(update_time()) + +loop.run_forever() +``` - 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. +Vaším úkolem za 5 bodů 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: @@ -1041,7 +1114,7 @@ Do vizualizátoru bludiště doplňte funkcionalitu hry. V režimu hry: 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) + * [class Actor]({{ static('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á: @@ -1180,6 +1253,10 @@ 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.) +Nakolik je chování netriviální, určujeme při hodnocení my. +V případě pochybností se zeptejte pomocí issue nebo na cvičení. + + Odevzdání, deadliny apod. ------------------------- @@ -1188,4 +1265,8 @@ 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. +A zároveň je to poslední úkol kromě semestrálky. + +Proto na něj máte čas až do středy 28.12. 11:00. Nekažte si ale prosím kvůli úkolu Vánoce (a ani od nás v průběhu svátků nečekejte velkou komunikaci). + +Odevzdávejte standardně s tagem `v0.4`. 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/async/static/actor.py b/lessons/intro/async/static/actor.py new file mode 100644 index 0000000000..f9de368352 --- /dev/null +++ b/lessons/intro/async/static/actor.py @@ -0,0 +1,150 @@ +"""This file has been placed in the public domain""" +import asyncio +import contextlib +import time + + +class Actor: + def __init__(self, grid, row, column, kind): + """Coroutine-based actor on a grid + + :param grid: + + The grid this actor moves on. Must have the following attributes: + + * ``update_actor(actor)``: Called to mark the space currently + occupied by the actor as "dirty" (needs redrawing) + * ``cell_size``: size of a grid cell, in pixels (used to optimize + animations) + * ``directions``: a NumPy array of directions (ASCII bytes), + containing ``b'v'``, ``b'<'``, ``b'>'``, ``b'^'`` depending + on where the actor should move to from a particular cell. + Only used in the default implementation of ``behavior``. + + This is stored in an attribute with the same name. + + :param row: + :param column: + + The initial position of the actor. + These are stored in attributes with the same names, and are updated + regularly. + When the actor is currently moving, the values are floats. + + :param kind: + + Any data for use by the grid drawing code. + This is stored in an attribute with the same name. + + The attribute ``task`` will hold an ``asyncio.Task`` object + corresponding to the actor's behavior. Cancel it when done. + """ + self.row = row + self.column = column + self.kind = kind + self.grid = grid + self.task = asyncio.ensure_future(self.behavior()) + + async def behavior(self): + """Coroutine containing the actor's behavior + + The base implementation follows directions the actor is standing on. + If there is no directions (e.g. standing on a wall, unreachable space, + or on the goal), the actor jumps repeatedly. + + To be reimplemented in subclasses.. + """ + while True: + shape = self.grid.directions.shape + row = int(self.row) + column = int(self.column) + if 0 <= row < shape[0] and 0 <= column < shape[1]: + direction = self.grid.directions[row, column] + else: + direction = b'?' + + if direction == b'v': + await self.step(1, 0) + elif direction == b'>': + await self.step(0, 1) + elif direction == b'^': + await self.step(-1, 0) + elif direction == b'<': + await self.step(0, -1) + else: + await self.jump() + + def _progress(self, duration): + """Iterator that yields progress from 0 to 1 based on time + + In each iteration, yields a number based on the current time: + + * 0.0 at the time the generator was started; + * 1.0 at start time plus ``duration`` seconds (end time) + * for the time in between, a linearly interpolated number between 0...1 + + It is not guaranteed that 1.0 will be yielded on the last iteration. + + When using this with a for-loop, you probably need to put + a sleep/delay into each iteration. + """ + start = time.monotonic() + while True: + now = time.monotonic() + p = (now - start) / duration + if p > 1: + return + yield p + + @contextlib.contextmanager + def _update_context(self): + """Context manager for updating the actor's position + + Updates the grid widget before and after the contextis entered. + Wrapping any coordinate updates in this context wil ensure the actor + is drawn correctly. + """ + self.grid.update_actor(self) + yield + self.grid.update_actor(self) + + async def step(self, dr, dc, duration=1): + """Coroutine for a step in a given direction + + Smoothly moves ``dr`` tiles in the row-direction and ``dc`` tiles in + the column-direction in ``duration`` seconds. + """ + start_row = self.row + start_col = self.column + + for p in self._progress(duration): + with self._update_context(): + self.row = start_row + dr * p + self.column = start_col + dc * p + + # Sleep amount is based on zoom level: we want to sleep for + # about one pixel's worth of movement. + await asyncio.sleep(duration/self.grid.cell_size) + + # Final update to the exact ending position (this should use integer + # arithmetic, so it avoids rounding errors) + with self._update_context(): + self.row = start_row + dr + self.column = start_col + dc + + async def jump(self, duration=0.2): + """Coroutine for a small jump + + Smoothly moves a bit up and down in ``duration`` seconds. + """ + start_row = self.row + + for p in self._progress(duration): + with self._update_context(): + # jump along a parabola + self.row = start_row - p * (1-p) + + await asyncio.sleep(duration/self.grid.cell_size * 2) + + with self._update_context(): + self.row = start_row diff --git a/lessons/intro/cython/index.md b/lessons/intro/cython/index.md index 2b9f0428e8..a9aa698422 100644 --- a/lessons/intro/cython/index.md +++ b/lessons/intro/cython/index.md @@ -102,52 +102,54 @@ porozumět tomu, co dělá.) demo.c: - #include +```c +#include - PyDoc_STRVAR( - mod_docstring, - "Demo extension module with a Python wrapper for the system(3) function"); +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; +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); + /* 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; } - /* 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); - } + /* 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 @@ -195,19 +197,21 @@ jen jméno a seznam zdrojových souborů: setup.py: - from setuptools import setup, Extension +```python +from setuptools import setup, Extension - module1 = Extension( - 'demo', - sources=['demo.c'], - ) +module1 = Extension( + 'demo', + sources=['demo.c'], +) - setup( - name = 'demo', - version = '0.1', - description = 'Demo package', - ext_modules = [module1] - ) +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. @@ -239,10 +243,12 @@ Základní datová struktura, která reprezentuje jakýkoli objekt Pythonu, je P [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; +```c +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. @@ -260,10 +266,12 @@ Struktura PyObject slouží jako hlavička, za kterou pak následují data inter typu daného objektu. Například pythonní [objekt typu float][float] vypadá následovně: - typedef struct { - PyObject ob_base; - double ob_fval; - } PyFloatObject; +```c +typedef struct { + PyObject ob_base; + double ob_fval; +} PyFloatObject; +``` ...tedy struktura PyObject, za kterou je v paměti číselná hodnota. @@ -378,26 +386,28 @@ Vývojáři Cythonu považují každou odchylku od specifikace jazyka za chybu, 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 +```python +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. @@ -405,19 +415,21 @@ 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', - ], - ) +```python +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 @@ -468,10 +480,12 @@ Začneme u funkce `intmul`, kde doplníme informaci o tom, že parametry `a` a ` 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 +```python +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. @@ -480,11 +494,13 @@ 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 + +```python +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 @@ -502,22 +518,24 @@ Funkce jdou deklarovat třemi způsoby: 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 - ... +```python +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 __cinit__(self, int f): + self.foo = f + ... - def __dealloc__(self): - ... + def __dealloc__(self): + ... - cpdef int method(self): - ... - return self.foo + 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). @@ -533,31 +551,38 @@ 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 +```python +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 - ... +```python +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 - +```python +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 +```python +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 --------- @@ -607,7 +632,7 @@ potřebujeme vyhodit výjimku. 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. +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 pythonní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, @@ -615,67 +640,71 @@ 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 +```python +# 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 +# Definice struktury +cdef struct coords: + int row + int column + char data - MAXSIZE = ... +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() +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 ...: - ... + cdef int used = 0 + for ...: + ... - # - path[used] = coords(row, column, data) - used += 1 + # + path[used] = coords(row, column, data) + used += 1 - # pole můžeme používat - ... + # 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 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 + # 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 +```python +cdef struct coord: + float x + float y + float z - cdef coord a = coord(0.0, 2.0, 1.5) +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 b = coord(x=0.0, y=2.0, z=1.5) - cdef coord c +cdef coord c - c.x = 42.0 - c.y = 2.0 - c.z = 4.0 +c.x = 42.0 +c.y = 2.0 +c.z = 4.0 - cdef coord d = {'x':2.0, - 'y':0.0, - 'z':-0.75} +cdef coord d = {'x':2.0, + 'y':0.0, + 'z':-0.75} +``` Použití knihoven z C -------------------- @@ -684,15 +713,17 @@ 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) +```python +cdef extern from "stdlib.h": + int rand() + void srand(long int seedval) - cdef extern from "time.h": - long int time(int) +cdef extern from "time.h": + long int time(int) - srand(time(0)) - print(rand()) +srand(time(0)) +print(rand()) +``` pyximport a %%cython @@ -701,7 +732,9 @@ pyximport a %%cython Pro interaktivní práci vJupyter Notebook má Cython vlastní „magii“. Na začátku Notebooku můžeme zadat: - %load_ext cython +```python +%load_ext cython +``` a potom můžeme na začátku kterékoli buňky zadat `\%\%cython`: @@ -709,10 +742,12 @@ a potom můžeme na začátku kterékoli buňky zadat `\%\%cython`: procenta pro speciální účel a bez nich se to rozbije. Uvítáme fix v pull requestu.) - %%cython +```python +%%cython - cpdef int mul(int a, int b): - return a * b +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. @@ -723,10 +758,13 @@ Další zkratka je modul `pyximort`, který dává možnost importovat moduly `. 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() +```python +import pyximport +pyximport.install() + +import demo +``` - import demo Video ----- @@ -745,16 +783,20 @@ K obsahu jen dodáme, že místo `malloc` a `free` je lepší použít `PyMem_Ma Ú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. +Vaším úkolem za 5 bodů 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: +Odevzdávejte s tagem v0.2. 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 +``` +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. + +Termín je jako obvykle začátek příštího **prvního** cvičení, tedy středa 11:00. diff --git a/lessons/intro/distribution/index.md b/lessons/intro/distribution/index.md index 009f354147..298c524a65 100644 --- a/lessons/intro/distribution/index.md +++ b/lessons/intro/distribution/index.md @@ -1,4 +1,4 @@ -Moduly +moduly ====== Zatím jsme tvořili programy v Pythonu tak nějak na divoko, tedy v jednom nebo @@ -33,41 +33,47 @@ obsahuje všechna potřebná metadata ve volání funkce `setup()` z modulu Pojďme vytvořit jeho minimální variantu: - from setuptools import setup +```python +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'], - ) +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 +```bash +$ 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 - ... +```bash +(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 ----------------------------------- @@ -76,9 +82,13 @@ 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... +``` +Czech public holiday checker... +``` - (env)$ python3 setup.py sdist +```bash +(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`. @@ -97,13 +107,17 @@ Další soubory lze přidat pomocí souboru `MANIFEST.in`, dle [dokumentace]. 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 - ... +``` +include LICENSE +``` +```bash +(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() -------------------------- @@ -111,36 +125,38 @@ 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, - ) +```python +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í @@ -166,54 +182,62 @@ Teoreticky je možné je přidat všechny do `py_modules`, ale není to dobrý n 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 +```bash +(env)$ tree +. +├── isholiday +│   └── __init__.py +├── LICENSE +├── MANIFEST.in +├── README +└── setup.py - 1 directory, 5 files +1 directory, 5 files +``` -Soubor `__init__.py` jednak značí, že adresář `isholiday` je Pythoní modul, +Soubor `__init__.py` jednak značí, že adresář `isholiday` je Pythonní 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', +```ansi +␛[1mdiff --git a/setup.py b/setup.py␛[m +␛[1mindex 3a69792..6b453ab 100644␛[m +␛[1m--- a/setup.py␛[m +␛[1m+++ b/setup.py␛[m +␛[36m@@ -11,7 +11,7 @@ setup(␛[m + keywords='holiday,dates', + license='Public Domain', + url='https://gist.github.com/oskar456/e91ef3ff77476b0dbc4ac19875d0555e', +␛[31m- py_modules=['isholiday'],␛[m +␛[32m+ packages=['isholiday'],␛[m + 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 +```python +from setuptools import setup, find_packages - setup( - ... - packages=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 +```python +from .holidays import getholidays, isholiday - __all__ = ['getholidays', 'isholiday'] +__all__ = ['getholidays', 'isholiday'] +``` Do `__init__.py` ideálně nepatří žádný kód kromě tohoto. @@ -234,9 +258,11 @@ 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 +```python +from .holidays import main - main() +main() +``` a v `holidays.py` zaměňte `if __name__ == '__main__':` za `def main():`. @@ -251,13 +277,15 @@ 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', - ], - }, - ) +```python +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á. @@ -278,17 +306,21 @@ 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'], - ) +```python +setup( + install_requires=['Flask', 'click>=6'], +) +``` -Kromě závislostí v `setup.py` se u Pythoních projektů často setkáme se souborem +Kromě závislostí v `setup.py` se u Pythonní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 +``` +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 @@ -311,20 +343,22 @@ Budete potřebovat [účet na PyPI](https://pypi.python.org/pypi?%3Aaction=regis [účet na testovací PyPI](https://testpypi.python.org/pypi?%3Aaction=register_form) a konfigurační soubor `~/.pypirc`: - [distutils] - index-servers= - pypi - pypitest +```ini +[distutils] +index-servers= + pypi + pypitest - [pypi] - repository = https://pypi.python.org/pypi - username = - password = +[pypi] +repository = https://pypi.python.org/pypi +username = +password = - [pypitest] - repository = https://testpypi.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. @@ -333,13 +367,15 @@ 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 +```bash +(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. @@ -352,7 +388,9 @@ 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 +```bash +(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í @@ -362,7 +400,9 @@ tak i případné závislosti, které mohou být zastaralé, rozbité či jinak 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 +```bash +(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, @@ -373,7 +413,9 @@ 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 +```bash +(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. @@ -381,7 +423,9 @@ 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 +```bash +(env)$ python -m pip install https://testpypi.python.org/packages/.../-0.3.tar.gz +``` Další ----- @@ -392,12 +436,15 @@ Další Úkol ---- -Vaším úkolem je udělat z vašeho dosavadního projektu balíček +Vaším úkolem za 5 bodů 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í. +Pokud svůj kód za žádných okolností nechcete zveřejnit ani na testovací PyPI, +dejte nám vědět a domluvíme se. + 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. @@ -416,3 +463,8 @@ napište to do souboru vlastní podmínky. Nevymýšlejte si ale prosím vlastn open-source licence. [choosealicense.com]: http://choosealicense.com/ + +Odevzdáte tagem v0.3 v obvyklém repozitáři +(ten můžete klidně přejmenovat podle modulu - na GitHubu v *Settings*). +Odkaz na (testovací) PyPI můžete napsat někam do README, do release na GitHubu apod. +V každém případě bychom ho měli mít možnost jednoduše najít. diff --git a/lessons/intro/docs/index.md b/lessons/intro/docs/index.md index 3d100c7dbb..d555c39b0a 100644 --- a/lessons/intro/docs/index.md +++ b/lessons/intro/docs/index.md @@ -16,29 +16,31 @@ betamaxu či Pythonu samotného, viděli jste dokumentaci vytvořenou ve Sphinxu 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 - ... +``` +$ . 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. @@ -53,10 +55,12 @@ 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. +``` +(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. @@ -79,18 +83,22 @@ můžete použít [tahák]. V `index.rst` je seznam kapitol: - .. toctree:: - :maxdepth: 2 +```rst +.. toctree:: + :maxdepth: 2 +``` Tam můžete přidat další kapitoly: - .. toctree:: - :maxdepth: 2 +```rst +.. toctree:: + :maxdepth: 2 - intro - tutorial/foo - tutorial/bar - ... + 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 @@ -98,18 +106,22 @@ Obsah (ve smyslu *content*) lze pak přidávat samozřejmě do těchto souborů Chcete-li odkazovat na některou sekci, označíme si ji pomocí `.. _label:`: - .. _my-reference-label: +```rst +.. _my-reference-label: - Section to cross-reference - -------------------------- +Section to cross-reference +-------------------------- - This is the text of the section. +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 :) +```rst +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 @@ -124,19 +136,25 @@ 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 +```python +>>> 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') +```python +print('foo') +``` A dále někde jinde výstup volání: - foo +```python +foo +``` K tomu všemu složí několik direktiv: @@ -169,83 +187,87 @@ nepoužijete flag `:hide:`. ### Kompletní příklad - The parrot module - ================= +```rst +The parrot module +================= - .. testsetup:: +.. testsetup:: - class Parrot: - def voom(self, voltage): - print('This parrot wouldn\'t voom if you put {} volts through it!'.format(voltage)) + 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' + def die(self): + return 'RIP' - parrot = Parrot() + parrot = Parrot() - The parrot module is a module about parrots. +The parrot module is a module about parrots. - Doctest example: +Doctest example: - .. doctest:: +.. doctest:: - >>> parrot.voom(3000) - This parrot wouldn't voom if you put 3000 volts through it! + >>> parrot.voom(3000) + This parrot wouldn't voom if you put 3000 volts through it! - Test-Output example: +Test-Output example: - .. testcode:: +.. testcode:: - parrot.voom(3000) + parrot.voom(3000) - This would output: +This would output: - .. testoutput:: +.. testoutput:: - This parrot wouldn't voom if you put 3000 volts through it! + This parrot wouldn't voom if you put 3000 volts through it! - You can use other values: +You can use other values: - .. testcode:: +.. testcode:: - parrot.voom(230) + parrot.voom(230) - .. testoutput:: - :hide: +.. testoutput:: + :hide: - This parrot wouldn't voom if you put 230 volts through it! + This parrot wouldn't voom if you put 230 volts through it! - .. testcleanup:: +.. testcleanup:: - parrot.die() + 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 - ... +``` +(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 @@ -253,13 +275,15 @@ Pokud nemáte nainstalovaný vlastní balíček, a budete z něj chtít v doctes 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('..')) +```python +# 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 @@ -273,15 +297,17 @@ 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 +```yaml +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 ------- @@ -289,8 +315,10 @@ 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: +```rst +.. 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`. @@ -308,43 +336,45 @@ dokonce žádoucí. Zde je ukázka z betamaxu (*Copyright 2013 Ian Cordasco*): - class Betamax: +```python +class Betamax: - """This object contains the main API of the request-vcr library. + """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: + This object is entirely a context manager so all you have to do is: - .. code:: + .. code:: - s = requests.Session() - with Betamax(s) as vcr: - vcr.use_cassette('example') - r = s.get('https://httpbin.org/get') + 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: + Or more concisely, you can do: - .. code:: + .. code:: - s = requests.Session() - with Betamax(s).use_cassette('example') as vcr: - r = s.get('https://httpbin.org/get') + 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. + This object allows for the user to specify the cassette library directory + and default cassette options. - .. code:: + .. 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') + 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') - """ + 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]. @@ -359,12 +389,14 @@ 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()`. +```rst +To test the parrot's electrical resistance, use :meth:`parrot.voom()`. +``` -V této části dokumentace Sphinxu též najdete způsob, jak dokumnetovat API +V této části dokumentace Sphinxu též najdete způsob, jak dokumentovat API bez použití `autodoc`. -Všechny zdokumnetované objekty se automaticky přidávají do rejstříku. +Všechny zdokumentované 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 @@ -395,7 +427,7 @@ z `README.rst`. Úkol ---- -Vaším úkolem je vytvořit pomocí Sphinx dokumentaci k vašemu projektu. +Vaším úkolem za 5 bodů 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. @@ -414,7 +446,7 @@ 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). +řetězce se automaticky promítne ve vygenerované dokumentaci). Jak sestavit a testovat dokumentaci by mělo být jasné z `README.rst` (a to musí mít reStructuredText syntaxi). @@ -428,3 +460,10 @@ 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. + +Úkol odevzdáváte tradičně s tagem v0.5 a nahráním nové verze na +(testovací či pravou) PyPI. +(Nahrání je nutné – čtenář dokumentace k verzi 0.5 se bude dívat po balíčku +této verze.) + +Za fungující publikaci smysluplné dokumentace na [Read the Docs] je bod navíc. diff --git a/lessons/intro/flask/index.md b/lessons/intro/flask/index.md index d7fc5ad446..cab1c226b9 100644 --- a/lessons/intro/flask/index.md +++ b/lessons/intro/flask/index.md @@ -22,27 +22,33 @@ 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 +```bash +$ cd project +$ . env/bin/activate +(env)$ python -m pip install Flask +``` + +Základní použití Flasku je poměrně primitivní: + +```python +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) +``` + +```bash +(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 @@ -50,13 +56,15 @@ V příkladu jsme vytvořili flaskovou aplikaci (`app`), pomocí dekorátoru `/` 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' +```python +@app.route('/') +def index(): + return 'Index Page' - @app.route('/hello') - def hello(): - return 'Hello, World' +@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. @@ -71,14 +79,18 @@ 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) +```python +@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/') +```python +@app.route('/post/') +``` Můžete použít různá pravidla, např.: @@ -93,18 +105,22 @@ Opačným způsobem jak k routám přistupovat, je když potřebujete získat UR 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') +```python +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 +```python +>>> 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. @@ -114,29 +130,35 @@ na problém, pokud cestu později změníte. 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 '...' +```python +@app.route('/') +def hello(): + return '<html><head><title>...' +``` ...ale asi by to nebylo příliš příjemné. Lepší je použít šablony: - from flask import render_template +```python +from flask import render_template - @app.route('/hello/') - @app.route('/hello/<name>') - def hello(name=None): - return render_template('hello.html', name=name) +@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 %} +```html + +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.) @@ -148,9 +170,11 @@ navíc můžete automaticky použít např, `url_for()`. 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) +```python +if __name__ == "__main__": + app.config['TEMPLATES_AUTO_RELOAD'] = True + app.run(debug=True) +``` #### Filtry @@ -161,15 +185,19 @@ 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') +```python +@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 }} +```html +{{ tweet.created_at|time }} +``` #### Escaping @@ -181,38 +209,48 @@ jako HTML. Například když v aplikaci výše navštívíme URL `/hello/