Obsah
1. Typová inference v Pythonu prováděná v runtime
2. Princip činnosti nástroje MonkeyType
3. Instalace nástroje MonkeyType
4. Modul, který budeme zkoumat
5. Zavolání funkcí z analyzovaného modulu
6. Výsledky analýzy uložené v databázi
7. Vygenerování stub souboru, automatické přidání typových informací do analyzovaného modulu
8. Volání funkcí z analyzovaného modulu s různými parametry
9. Automatické přidání typů k algoritmu výpočtu Ackermannovy funkce
10. Postup při manuální nebo automatizované úpravě stávajících projektů bez typových anotací
11. Zdrojový kód po základním refaktoringu, ovšem bez typových informací
13. Výsledek po přidání typových informací
15. Problematické chování nástroje MonkeyType
17. Pád v případě použití typu Callable na starších verzích Pythonu
18. Repositář s demonstračními příklady
19. Odkazy na další články o typových anotacích v Pythonu
1. Typová inference v Pythonu prováděná v runtime
Na stránkách Roota jsme se již několikrát zabývali problematikou typových anotací (type annotations, type hints) v programovacím jazyku Python (viz též odkazy na příslušné články uvedené v devatenácté kapitole). Připomeňme si, že se jedná o rozšíření jazyka Python o novou syntaxi i sémantiku, která umožňuje dobrovolně (a klidně jen na určitých místech v programovém kódu) specifikovat typy proměnných, argumentů funkcí a metod, návratové typy funkcí a metod, ale i generické typy vztažené ke třídám a funkcím. Tyto přidané typové informace se, i když je standardní CPython prakticky ignoruje, již dnes používají k několika účelům. Především k zajištění korektnosti zdrojových kódů z pohledu typového systému (do funkce akceptující řetězec nelze předat celé číslo atd.), ale taktéž k lepším optimalizacím, které díky těmto poměrně důležitým informacím mohou provádět AOT (ahead of time) a JIT (just in time) překladače Pythonu. A v neposlední řadě je informace o typech užitečným prvkem samodokumentujícího se kódu a využívají ji některá integrovaná vývojová prostředí (a LSP).
Taktéž jsme si již v předchozích článcích řekli, že nástroje typu Mypy do jisté míry dokážou provádět typovou inferenci, tj. zjištění, jakého typu mají být například argumenty funkcí nebo metod na základě detekce, s jakými parametry (resp. jejich typy) je funkce volána. Ovšem tato typová inference je v Mypy omezena na statickou analýzu. Zejména pro starší zdrojové kódy, které neobsahují typové anotace, by však bylo zajímavé a užitečné použít nějaký nástroj, který tyto typové anotace dokáže do zdrojového kódu automaticky či alespoň poloautomaticky přidat, a to na základě analýzy provedené za běhu aplikace (runtime). Tímto způsobem by bylo možné zjistit, jak je programový kód používaný reálně. A právě taková funkcionalita je realizována v nástroji MonkeyType, s nímž se seznámíme v dnešním článku.
2. Princip činnosti nástroje MonkeyType
Pojďme si nyní alespoň ve stručnosti naznačit, jak vlastně nástroj MonkeyType pracuje. Není to ve skutečnosti nic složitého (minimálně ne v ekosystému Pythonu, který nabízí plnohodnotnou introspekci). Nástroji MonkeyType je nutné předat jméno skriptu, který se má spustit. Skript (což může být spouštěcí skript i rozsáhlého projektu) je následně skutečně spuštěn, ovšem MonkeyType přitom sleduje volání všech funkcí a metod. Průběžně si zaznamenává jak konkrétní typy předaných hodnot, tak i typy návratových hodnot. Tyto informace jsou uloženy do lokální databáze SQLite, z níž se posléze mohou přečíst, analyzovat a dále zpracovat. Jakmile je skript dokončen (lze ho totiž pochopitelně spustit vícekrát, například s odlišnými parametry atd.), můžeme MonkeyType nechat provést tři operace:
- Vypsat si jména všech modulů, jejichž funkce a metody byly volány.
- Nechat si vygenerovat takzvaný stub soubor s informacemi o typech parametrů atd. Tento soubor je možné zpracovat dalšími nástroji (například ho podporuje i výše zmíněný Mypy atd.), popř. se může stát součástí projektu – typové informace tedy budou odděleny od programového kódu.
- Alternativně je možné si nechat přidat informace o typech přímo do originálních zdrojových kódů. Výsledek je pochopitelně možné upravit či v některých případech je spíše bude nutné opravit. A výsledek by měl projít (striktní) statickou analýzou nástrojem Mypy.
3. Instalace nástroje MonkeyType
Ve druhé části článku si ukážeme činnost MonkeyType prakticky, na několika demonstračních příkladech. Samotná instalace tohoto nástroje je snadná a rychlá, protože má jen minimum závislostí. Pro jeho instalaci můžeme použít standardní utilitu pip, popř. pip3 (na systémech s více instalacemi Pythonu) a provést instalaci pouze pro aktivního uživatele (přepínač –user):
$ pip3 install --user monkeytype Collecting monkeytype Downloading MonkeyType-23.3.0-py3-none-any.whl (40 kB) |████████████████████████████████| 40 kB 1.2 MB/s Requirement already satisfied: mypy-extensions in ./.local/lib/python3.8/site-packages (from monkeytype) (1.0.0) Collecting libcst>=0.4.4 Downloading libcst-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.2 MB) |████████████████████████████████| 3.2 MB 1.2 MB/s Requirement already satisfied: typing-extensions>=3.7.4.2 in ./.local/lib/python3.8/site-packages (from libcst>=0.4.4->monkeytype) (4.7.1) Requirement already satisfied: pyyaml>=5.2 in /usr/lib/python3/dist-packages (from libcst>=0.4.4->monkeytype) (5.3.1) Collecting typing-inspect>=0.4.0 Downloading typing_inspect-0.9.0-py3-none-any.whl (8.8 kB) Installing collected packages: typing-inspect, libcst, monkeytype Successfully installed libcst-1.1.0 monkeytype-23.3.0 typing-inspect-0.9.0
4. Modul, který budeme zkoumat
První modul naprogramovaný v jazyce Python, který budeme zkoumat a zjišťovat, s jakými typy parametrů jsou volané v něm definované funkce, je velmi malý (až triviální). Obsahuje pouze dvě funkce nazvané add a inc. U těchto funkcí nejsou uvedeny žádné typové informace (což je v Pythonu stále obvyklé), což znamená, že například první funkci můžeme volat s parametry typu celé číslo, s hodnotami s plovoucí řádovou čárkou, seznamy, řetězci, libovolnými objekty, v jejichž třídě je přetížen operátor + atd.:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 def add(a, b): return a + b def inc(a): return a + 1
5. Zavolání funkcí z analyzovaného modulu
Modul adder je uložen v samostatném adresáři se stejným jménem (tedy obsahuje jen dvojici souborů adder/__init.py__ a adder/adder.py). V aktuálním adresáři vytvoříme nějaký pomocný skript, například pojmenovaný test_adder1.py. V tomto skriptu naimportujeme volané funkce z modulu adder a posléze tyto funkce skutečně zavoláme. Povšimněte si, že volaným funkcím předáváme hodnoty typu int (celá čísla):
from adder.adder import add, inc print(add(1, 2)) print(inc(41))
Nyní nastává důležitý okamžik. Spustíme tento testovací skript, ale nikoli přímo (python test_adder1.py), ale nepřímo přes nástroj MonkeyType příkazem run:
$ monkeytype run test_adder_1.py
Skript se zdánlivě spustí obvyklým způsobem; ostatně vypíše i očekávané výsledky:
3 42
Ovšem povšimněte si, že kromě toho vznikl v aktuálním adresáři i soubor monkeytype.sqlite3. Ten využijeme v dalších krocích.
6. Výsledky analýzy uložené v databázi
Příkazem list-modules předaného nástroji MonkeyType si můžeme nechat vypsat plná jména všech modulů, jejichž funkce a metody byly při analýze skriptu (projektu) volány. V našem konkrétním případě bychom měli získat pouze jméno jediného modulu:
$ monkeytype list-modules adder.adder
Informace o volaných funkcích a metodách (včetně jejich modulu) jsou uloženy v lokální SQLite databázi, konkrétně v souboru se jménem monkeytype.sqlite3. Nic nám pochopitelně nebrání v tom si obsah této databáze prohlédnout. Použijeme k tomu klienta sqlite3. Nejprve databázový soubor otevřeme:
$ sqlite3 monkeytype.sqlite3 SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints.
Dále si necháme vypsat jména všech tabulek v databázi:
sqlite> .tables monkeytype_call_traces
Vidíme, že zde existuje jen jediná tabulka, jejíž schéma je do značné míry samopopisné:
sqlite> .schema monkeytype_call_traces CREATE TABLE monkeytype_call_traces ( created_at TEXT, module TEXT, qualname TEXT, arg_types TEXT, return_type TEXT, yield_type TEXT);
Vypišme si obsah této tabulky:
sqlite> select * from monkeytype_call_traces ; 2023-12-30 14:16:19.170512|adder.adder|add|{"a": {"module": "builtins", "qualname": "int"}, "b": {"module": "builtins", "qualname": "int"}}|{"module": "builtins", "qualname": "int"}| 2023-12-30 14:16:19.170533|adder.adder|inc|{"a": {"module": "builtins", "qualname": "int"}}|{"module": "builtins", "qualname": "int"}|
7. Vygenerování stub souboru, automatické přidání typových informací do analyzovaného modulu
S obsahem databáze však většinou není zapotřebí manipulovat přímo, protože příslušné operace jsou již implementovány přímo v nástroji MonkeyType. Nejdříve si necháme vygenerovat takzvaný stub soubor, který obsahuje pouze informace o datových typech proměnných a funkcí, nikoli však jejich těla:
$ monkeytype stub adder.adder
Výsledek by měl vypadat následovně:
def add(a: int, b: int) -> int: ... def inc(a: int) -> int: ...
Kromě stub souboru můžeme nástroj MonkeyType instruovat, aby typové informace přidal přímo do zdrojového kódu analyzovaného modulu. Provádí se to následujícím způsobem:
$ monkeytype apply adder.adder
Kromě zpráv vypsaných na standardní výstup by se měl zdrojový kód upravit do této finální (korektní) podoby:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 def add(a: int, b: int) -> int: return a+b def inc(a: int) -> int: return a+1
8. Volání funkcí z analyzovaného modulu s různými parametry
Ve druhém kroku se pokusíme funkce z modulu adder, tj. funkce nazvané add a inc, volat s odlišnými typy parametrů. Konkrétně to znamená, že funkci add předáme dvojici řetězců a dvojici seznamů. Funkci inc pak předáme celé číslo a hodnotu s plovoucí řádovou čárkou:
from adder.adder import add, inc print(add(1, 2)) print(add("foo", "bar")) print(add([1, 2, 3], [4, 5, 6])) print(inc(41)) print(inc(1.5))
V SQLite databázi by se měly po provedení těchto kroků objevit záznamy o odlišných parametrech. Databáze by měla vypadat zhruba následovně:
sqlite> select * from monkeytype_call_traces; 2023-12-30 14:19:27.525313|adder.adder|add|{"a": {"module": "builtins", "qualname": "int"}, "b": {"module": "builtins", "qualname": "int"}}|{"module": "builtins", "qualname": "int"}| 2023-12-30 14:19:27.525338|adder.adder|add|{"a": {"module": "builtins", "qualname": "str"}, "b": {"module": "builtins", "qualname": "str"}}|{"module": "builtins", "qualname": "str"}| 2023-12-30 14:19:27.525373|adder.adder|add|{"a": {"elem_types": [{"module": "builtins", "qualname": "int"}], "module": "typing", "qualname": "List"}, "b": {"elem_types": [{"module": "builtins", "qualname": "int"}], "module": "typing", "qualname": "List"}}|{"elem_types": [{"module": "builtins", "qualname": "int"}], "module": "typing", "qualname": "List"}| 2023-12-30 14:19:27.525387|adder.adder|inc|{"a": {"module": "builtins", "qualname": "int"}}|{"module": "builtins", "qualname": "int"}| 2023-12-30 14:19:27.525401|adder.adder|inc|{"a": {"module": "builtins", "qualname": "float"}}|{"module": "builtins", "qualname": "float"}|
Důležitější jsou však typové informace na úrovni zdrojového kódu. Ty si můžeme nechat zobrazit (stub soubor) nebo přímo přidat do zdrojového kódu analyzovaného modulu, podobně jako v předchozí kapitole:
$ monkeytype stub adder.adder
Získáme:
from typing import ( List, Union, ) def add(a: Union[int, List[int], str], b: Union[int, List[int], str]) -> Union[int, List[int], str]: ... def inc(a: Union[int, float]) -> Union[int, float]: ...
Modifikace zdrojového kódu:
$ monkeytype apply adder.adder
S výsledkem:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from typing import List, Union def add(a: Union[int, List[int], str], b: Union[int, List[int], str]) -> Union[int, List[int], str]: return a+b def inc(a: Union[int, float]) -> Union[int, float]: return a+1
9. Automatické přidání typů k algoritmu výpočtu Ackermannovy funkce
Naprosto stejným postupem si můžeme nechat přidat typové informace do algoritmu pro výpočet Ackermannovy funkce. Původní podoba zdrojového kódu je následující:
# Výpočet Ackermannovy funkce, založeno na konstrukci if def A(m, n): """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1)) # otestování korektnosti výpočtu Ackermannovy funkce for m in range(4): for n in range(5): print(m, n, A(m, n))
Nástrojem MonkeyType přitom budeme spouštět tento skript:
import ackermann ackermann.A(3, 3)
A po automatické modifikaci kódu bychom měli získat algoritmus s plnými typovými informacemi:
# Výpočet Ackermannovy funkce, založeno na konstrukci if def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1)) # otestování korektnosti výpočtu Ackermannovy funkce for m in range(4): for n in range(5): print(m, n, A(m, n))
10. Postup při manuální nebo automatizované úpravě stávajících projektů bez typových anotací
V článcích o knihovně Mypy jsme si ukázali, jakým způsobem by se mohlo postupovat při úpravě stávajících projektů napsaných v Pythonu, které prozatím nepoužívají typové anotace. Celý postup jsme shrnuli do několika bodů:
- Refaktoring, ideálně tak, aby se nepoužívaly globální proměnné a globální kód
- Použití Mypy pro nalezení chybějících typových anotací
- Postupné doplnění typových anotací
- Odstranění reálných chyb nalezených nástrojem Mypy
Druhý, třetí i čtvrtý bod lze do značné míry automatizovat s využitím nástroje MonkeyType. Ukážeme si to na stejném demonstračním příkladu, do jakého jsme ručně přidávali typové informace. Nyní tuto operaci ponecháme na nástroji MonkeyType a porovnáme výsledek s ruční prací.
11. Zdrojový kód po základním refaktoringu, ovšem bez typových informací
Původní zdrojový kód demonstračního příkladu tak, jak byl kdysi ukázán v článku o knihovně Pygame, naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites1.py. Tento kód pochopitelně neobsahuje žádné typové informace (anotace) a bude ho zapotřebí poněkud refaktorovat.
Při refaktoringu postupně některé části zdrojového kódu vložíme do funkcí a zajistíme, aby se nepoužívaly globální proměnné (pouze globální „konstanty“, i když koncept pravých konstant v Pythonu není). Refaktorovaný kód sice stále nepoužívá typové anotace, ale už je na tuto důležitou změnu již náležitě připraven:
#!/usr/bin/python # vim: set fileencoding=utf-8 import pygame import sys # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * # Velikost okna aplikace WIDTH = 320 HEIGHT = 240 # Konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) RED = (255, 0, 0) GRAY = (128, 128, 128) YELLOW = (255, 255, 0) CAPTION = "Sprites in Pygame" # Třída představující sprite zobrazený jako jednobarevný čtverec. class BlockySprite(pygame.sprite.Sprite): # Konstruktor def __init__(self, color, size, x, y): # Nejprve je nutné zavolat konstruktor předka, # tj. konstruktor třídy pygame.sprite.Sprite: pygame.sprite.Sprite.__init__(self) # Vytvoření obrázku představujícího vizuální obraz spritu: self.image = pygame.Surface([size, size]) self.image.fill(color) # Vytvoření obalového obdélníku # (velikost se získá z rozměru obrázku) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y # Počáteční rychlost spritu self.speed_x = 0 self.speed_y = 0 # Nastavení barvy spritu, který kolidoval s hráčem def yellowColor(self): self.image.fill(YELLOW) # Nastavení barvy spritu, který nekolidoval s hráčem def grayColor(self): self.image.fill(GRAY) def initDisplay(caption): # Vytvoření okna pro vykreslování display = pygame.display.set_mode([WIDTH, HEIGHT]) # Nastavení titulku okna pygame.display.set_caption(caption) return display def createSprites(): # Objekt sdružující všechny sprity all_sprites = pygame.sprite.Group() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player = pygame.sprite.Group() # Vytvoření několika typů spritů # barva x y velikost wall1 = BlockySprite(GRAY, 50, 10, 10) wall2 = BlockySprite(GRAY, 15, 100, 100) wall3 = BlockySprite(GRAY, 15, 100, 150) wall4 = BlockySprite(GRAY, 15, 200, 100) wall5 = BlockySprite(GRAY, 15, 200, 150) wall6 = BlockySprite(GRAY, 15, 150, 100) wall7 = BlockySprite(GRAY, 15, 150, 150) player = BlockySprite(RED, 40, WIDTH / 2 - 20, HEIGHT / 2 - 20) # Přidání několika dalších spritů do seznamu # (jen jeden sprite - ten poslední - bude ve skutečnosti pohyblivý) all_sprites.add(wall1) all_sprites.add(wall2) all_sprites.add(wall3) all_sprites.add(wall4) all_sprites.add(wall5) all_sprites.add(wall6) all_sprites.add(wall7) all_sprites.add(player) # Seznam všech nepohyblivých spritů all_sprites_but_player.add(wall1) all_sprites_but_player.add(wall2) all_sprites_but_player.add(wall3) all_sprites_but_player.add(wall4) all_sprites_but_player.add(wall5) all_sprites_but_player.add(wall6) all_sprites_but_player.add(wall7) return all_sprites, all_sprites_but_player, player # Posun všech spritů ve skupině na základě jejich rychlosti def move_sprites(sprite_group, playground_width, playground_height): for sprite in sprite_group: # Posun spritu sprite.rect.x = sprite.rect.x + sprite.speed_x sprite.rect.y = sprite.rect.y + sprite.speed_y # Kontrola, zda sprite nenarazil do okrajů okna if sprite.rect.x < 0: sprite.rect.x = 0 sprite.speed_x = 0 if sprite.rect.x + sprite.rect.width > playground_width: sprite.rect.x = playground_width - sprite.rect.width sprite.speed_x = 0 if sprite.rect.y < 0: sprite.rect.y = 0 sprite.speed_y = 0 if sprite.rect.y + sprite.rect.height > playground_height: sprite.rect.y = playground_height - sprite.rect.height sprite.speed_y = 0 # Vykreslení celé scény na obrazovku def draw_scene(display, background_color, sprite_group): # Vyplnění plochy okna černou barvou display.fill(background_color) # Vykreslení celé skupiny spritů do bufferu sprite_group.draw(display) # Obnovení obsahu obrazovky (překlopení zadního a předního bufferu) pygame.display.update() # Změna barvy spritu na základě kolize s hráčem def change_colors(sprite_group, hit_list): # Projít všemi sprity ze skupiny, kterou detekovala kolizní funkce for sprite in sprite_group: if sprite in hit_list: sprite.yellowColor() else: sprite.grayColor() # Zjistí kolize spritu se "stěnami" (nepohyblivými sprity) def check_collisions(player, sprite_group): # Vytvoření seznamu spritů, které kolidují s hráčem hit_list = pygame.sprite.spritecollide(player, sprite_group, False) # Změna barev kolidujících spritů change_colors(sprite_group, hit_list) collisions = len(hit_list) # Přenastavení titulku okna caption = CAPTION + ": collisions " + str(collisions) pygame.display.set_caption(caption) def mainLoop(display, clock, all_sprites, all_sprites_but_player, player): while True: # Načtení a zpracování všech událostí z fronty for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() # Stiskem kurzorových kláves je možné měnit směr pohybu spritu elif event.key == pygame.K_LEFT: player.speed_x = -3 elif event.key == pygame.K_RIGHT: player.speed_x = +3 elif event.key == pygame.K_UP: player.speed_y = -3 elif event.key == pygame.K_DOWN: player.speed_y = +3 if event.type == KEYUP: # Puštění kurzorových kláves vede k zastavení pohybu spritu if event.key == pygame.K_LEFT: player.speed_x = 0 elif event.key == pygame.K_RIGHT: player.speed_x = 0 elif event.key == pygame.K_UP: player.speed_y = 0 elif event.key == pygame.K_DOWN: player.speed_y = 0 move_sprites(all_sprites, display.get_width(), display.get_height()) check_collisions(player, all_sprites_but_player) draw_scene(display, BLACK, all_sprites) clock.tick(20) def main(): # Inicializace knihovny Pygame pygame.init() clock = pygame.time.Clock() display = initDisplay(CAPTION) all_sprites, all_sprites_but_player, player = createSprites() mainLoop(display, clock, all_sprites, all_sprites_but_player, player) if __name__ == "__main__": main() # finito
12. Analýza projektu
Analýzu projektu, jehož zdrojový kód byl ukázán v jedenácté kapitole je snadná. Pouze si vytvoříme pomocný skript, který spustí funkci main z projektu:
import sprites sprites.main()
Samotný projekt (demo) spustíme příkazem:
$ monkeytype run run_sprites.py
A nakonec si necháme automaticky upravit zdrojové kódy projektu – přidají se do něj typové informace:
$ monkeytype apply sprites
13. Výsledek po přidání typových informací
A takto vypadá zdrojový soubor po automatickém přidání typových informací nástrojem MonkeyType (níže bude zobrazen diff, který je přehlednější):
#!/usr/bin/python # vim: set fileencoding=utf-8 import pygame import sys # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * from pygame.sprite import Group from pygame.surface import Surface from pygame.time import Clock from typing import Any, List, Tuple, Union # Velikost okna aplikace WIDTH = 320 HEIGHT = 240 # Konstanty s n-ticemi představujícími základní barvy BLACK = (0, 0, 0) RED = (255, 0, 0) GRAY = (128, 128, 128) YELLOW = (255, 255, 0) CAPTION = "Sprites in Pygame" # Třída představující sprite zobrazený jako jednobarevný čtverec. class BlockySprite(pygame.sprite.Sprite): # Konstruktor def __init__(self, color: Tuple[int, int, int], size: int, x: Union[int, float], y: Union[int, float]) -> None: # Nejprve je nutné zavolat konstruktor předka, # tj. konstruktor třídy pygame.sprite.Sprite: pygame.sprite.Sprite.__init__(self) # Vytvoření obrázku představujícího vizuální obraz spritu: self.image = pygame.Surface([size, size]) self.image.fill(color) # Vytvoření obalového obdélníku # (velikost se získá z rozměru obrázku) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y # Počáteční rychlost spritu self.speed_x = 0 self.speed_y = 0 # Nastavení barvy spritu, který kolidoval s hráčem def yellowColor(self) -> None: self.image.fill(YELLOW) # Nastavení barvy spritu, který nekolidoval s hráčem def grayColor(self) -> None: self.image.fill(GRAY) def initDisplay(caption: str) -> Surface: # Vytvoření okna pro vykreslování display = pygame.display.set_mode([WIDTH, HEIGHT]) # Nastavení titulku okna pygame.display.set_caption(caption) return display def createSprites() -> Tuple[Group, Group, BlockySprite]: # Objekt sdružující všechny sprity all_sprites = pygame.sprite.Group() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player = pygame.sprite.Group() # Vytvoření několika typů spritů # barva x y velikost wall1 = BlockySprite(GRAY, 50, 10, 10) wall2 = BlockySprite(GRAY, 15, 100, 100) wall3 = BlockySprite(GRAY, 15, 100, 150) wall4 = BlockySprite(GRAY, 15, 200, 100) wall5 = BlockySprite(GRAY, 15, 200, 150) wall6 = BlockySprite(GRAY, 15, 150, 100) wall7 = BlockySprite(GRAY, 15, 150, 150) player = BlockySprite(RED, 40, WIDTH / 2 - 20, HEIGHT / 2 - 20) # Přidání několika dalších spritů do seznamu # (jen jeden sprite - ten poslední - bude ve skutečnosti pohyblivý) all_sprites.add(wall1) all_sprites.add(wall2) all_sprites.add(wall3) all_sprites.add(wall4) all_sprites.add(wall5) all_sprites.add(wall6) all_sprites.add(wall7) all_sprites.add(player) # Seznam všech nepohyblivých spritů all_sprites_but_player.add(wall1) all_sprites_but_player.add(wall2) all_sprites_but_player.add(wall3) all_sprites_but_player.add(wall4) all_sprites_but_player.add(wall5) all_sprites_but_player.add(wall6) all_sprites_but_player.add(wall7) return all_sprites, all_sprites_but_player, player # Posun všech spritů ve skupině na základě jejich rychlosti def move_sprites(sprite_group: Group, playground_width: int, playground_height: int) -> None: for sprite in sprite_group: # Posun spritu sprite.rect.x = sprite.rect.x + sprite.speed_x sprite.rect.y = sprite.rect.y + sprite.speed_y # Kontrola, zda sprite nenarazil do okrajů okna if sprite.rect.x < 0: sprite.rect.x = 0 sprite.speed_x = 0 if sprite.rect.x + sprite.rect.width > playground_width: sprite.rect.x = playground_width - sprite.rect.width sprite.speed_x = 0 if sprite.rect.y < 0: sprite.rect.y = 0 sprite.speed_y = 0 if sprite.rect.y + sprite.rect.height > playground_height: sprite.rect.y = playground_height - sprite.rect.height sprite.speed_y = 0 # Vykreslení celé scény na obrazovku def draw_scene(display: Surface, background_color: Tuple[int, int, int], sprite_group: Group) -> None: # Vyplnění plochy okna černou barvou display.fill(background_color) # Vykreslení celé skupiny spritů do bufferu sprite_group.draw(display) # Obnovení obsahu obrazovky (překlopení zadního a předního bufferu) pygame.display.update() # Změna barvy spritu na základě kolize s hráčem def change_colors(sprite_group: Group, hit_list: List[Union[Any, BlockySprite]]) -> None: # Projít všemi sprity ze skupiny, kterou detekovala kolizní funkce for sprite in sprite_group: if sprite in hit_list: sprite.yellowColor() else: sprite.grayColor() # Zjistí kolize spritu se "stěnami" (nepohyblivými sprity) def check_collisions(player: BlockySprite, sprite_group: Group) -> None: # Vytvoření seznamu spritů, které kolidují s hráčem hit_list = pygame.sprite.spritecollide(player, sprite_group, False) # Změna barev kolidujících spritů change_colors(sprite_group, hit_list) collisions = len(hit_list) # Přenastavení titulku okna caption = CAPTION + ": collisions " + str(collisions) pygame.display.set_caption(caption) def mainLoop(display: Surface, clock: Clock, all_sprites: Group, all_sprites_but_player: Group, player: BlockySprite): while True: # Načtení a zpracování všech událostí z fronty for event in pygame.event.get(): if event.type == QUIT: pygame.quit() sys.exit() if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.quit() sys.exit() # Stiskem kurzorových kláves je možné měnit směr pohybu spritu elif event.key == pygame.K_LEFT: player.speed_x = -3 elif event.key == pygame.K_RIGHT: player.speed_x = +3 elif event.key == pygame.K_UP: player.speed_y = -3 elif event.key == pygame.K_DOWN: player.speed_y = +3 if event.type == KEYUP: # Puštění kurzorových kláves vede k zastavení pohybu spritu if event.key == pygame.K_LEFT: player.speed_x = 0 elif event.key == pygame.K_RIGHT: player.speed_x = 0 elif event.key == pygame.K_UP: player.speed_y = 0 elif event.key == pygame.K_DOWN: player.speed_y = 0 move_sprites(all_sprites, display.get_width(), display.get_height()) check_collisions(player, all_sprites_but_player) draw_scene(display, BLACK, all_sprites) clock.tick(20) def main(): # Inicializace knihovny Pygame pygame.init() clock = pygame.time.Clock() display = initDisplay(CAPTION) all_sprites, all_sprites_but_player, player = createSprites() mainLoop(display, clock, all_sprites, all_sprites_but_player, player) if __name__ == "__main__": main() # finito
Jak jsme se již zmínili výše, bude pro vývojáře přehlednější se podívat na diff mezi původním zdrojovým kódem a kódem s přidanými typovými informacemi:
--- sprites.py 2023-12-30 15:03:41.000000000 +0100 +++ sprites_patched.py 2023-12-30 15:03:50.000000000 +0100 @@ -18,6 +18,10 @@ # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * +from pygame.sprite import Group +from pygame.surface import Surface +from pygame.time import Clock +from typing import Any, List, Tuple, Union # Velikost okna aplikace WIDTH = 320 @@ -35,7 +39,7 @@ # Třída představující sprite zobrazený jako jednobarevný čtverec. class BlockySprite(pygame.sprite.Sprite): # Konstruktor - def __init__(self, color, size, x, y): + def __init__(self, color: Tuple[int, int, int], size: int, x: Union[int, float], y: Union[int, float]) -> None: # Nejprve je nutné zavolat konstruktor předka, # tj. konstruktor třídy pygame.sprite.Sprite: pygame.sprite.Sprite.__init__(self) @@ -55,15 +59,15 @@ self.speed_y = 0 # Nastavení barvy spritu, který kolidoval s hráčem - def yellowColor(self): + def yellowColor(self) -> None: self.image.fill(YELLOW) # Nastavení barvy spritu, který nekolidoval s hráčem - def grayColor(self): + def grayColor(self) -> None: self.image.fill(GRAY) -def initDisplay(caption): +def initDisplay(caption: str) -> Surface: # Vytvoření okna pro vykreslování display = pygame.display.set_mode([WIDTH, HEIGHT]) @@ -73,7 +77,7 @@ return display -def createSprites(): +def createSprites() -> Tuple[Group, Group, BlockySprite]: # Objekt sdružující všechny sprity all_sprites = pygame.sprite.Group() @@ -115,7 +119,7 @@ # Posun všech spritů ve skupině na základě jejich rychlosti -def move_sprites(sprite_group, playground_width, playground_height): +def move_sprites(sprite_group: Group, playground_width: int, playground_height: int) -> None: for sprite in sprite_group: # Posun spritu sprite.rect.x = sprite.rect.x + sprite.speed_x @@ -136,7 +140,7 @@ # Vykreslení celé scény na obrazovku -def draw_scene(display, background_color, sprite_group): +def draw_scene(display: Surface, background_color: Tuple[int, int, int], sprite_group: Group) -> None: # Vyplnění plochy okna černou barvou display.fill(background_color) # Vykreslení celé skupiny spritů do bufferu @@ -146,7 +150,7 @@ # Změna barvy spritu na základě kolize s hráčem -def change_colors(sprite_group, hit_list): +def change_colors(sprite_group: Group, hit_list: List[Union[Any, BlockySprite]]) -> None: # Projít všemi sprity ze skupiny, kterou detekovala kolizní funkce for sprite in sprite_group: if sprite in hit_list: @@ -156,7 +160,7 @@ # Zjistí kolize spritu se "stěnami" (nepohyblivými sprity) -def check_collisions(player, sprite_group): +def check_collisions(player: BlockySprite, sprite_group: Group) -> None: # Vytvoření seznamu spritů, které kolidují s hráčem hit_list = pygame.sprite.spritecollide(player, sprite_group, False) # Změna barev kolidujících spritů @@ -167,7 +171,7 @@ pygame.display.set_caption(caption) -def mainLoop(display, clock, all_sprites, all_sprites_but_player, player): +def mainLoop(display: Surface, clock: Clock, all_sprites: Group, all_sprites_but_player: Group, player: BlockySprite): while True: # Načtení a zpracování všech událostí z fronty for event in pygame.event.get():
14. Kontrola nástrojem Mypy
Samozřejmě je vhodné výsledný projekt zkontrolovat (co se týče typových informací) nástrojem, který je pro tyto účely navržen. Jedná se o Mypy, který již dobře známe.
Ve skutečnosti kontrola nedopadne se stoprocentní úspěšností:
$ mypy sprites_patched.py sprites_patched.py:54: error: Incompatible types in assignment (expression has type "Union[int, float]", variable has type "int") [assignment] sprites_patched.py:55: error: Incompatible types in assignment (expression has type "Union[int, float]", variable has type "int") [assignment] sprites_patched.py:82: error: Need type annotation for "all_sprites" [var-annotated] sprites_patched.py:85: error: Need type annotation for "all_sprites_but_player" [var-annotated] Found 4 errors in 1 file (checked 1 source file)
Za některé problémy si však můžeme sami, protože první dva typy zmíněné v chybových hlášeních byly odvozeny z těchto programových řádků:
wall1 = BlockySprite(GRAY, 50, 10, 10) wall2 = BlockySprite(GRAY, 15, 100, 100) wall3 = BlockySprite(GRAY, 15, 100, 150) wall4 = BlockySprite(GRAY, 15, 200, 100) wall5 = BlockySprite(GRAY, 15, 200, 150) wall6 = BlockySprite(GRAY, 15, 150, 100) wall7 = BlockySprite(GRAY, 15, 150, 150) player = BlockySprite(RED, 40, WIDTH / 2 - 20, HEIGHT / 2 - 20)
Problém spočívá v tom, že na posledním uvedeném programovém řádku jsou parametry x a y skutečně hodnotami typu float (dělení má v Pythonu 3 neceločíselný výsledek). Musíme tedy opravit původní zdrojový kód!
Striktní typová kontrola dopadne (podle očekávání) ještě hůře:
$ mypy --strict sprites_patched.py sprites_patched.py:54: error: Incompatible types in assignment (expression has type "Union[int, float]", variable has type "int") [assignment] sprites_patched.py:55: error: Incompatible types in assignment (expression has type "Union[int, float]", variable has type "int") [assignment] sprites_patched.py:80: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:82: error: Need type annotation for "all_sprites" [var-annotated] sprites_patched.py:85: error: Need type annotation for "all_sprites_but_player" [var-annotated] sprites_patched.py:122: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:143: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:153: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:163: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:174: error: Function is missing a return type annotation [no-untyped-def] sprites_patched.py:174: error: Missing type parameters for generic type "Group" [type-arg] sprites_patched.py:211: error: Function is missing a return type annotation [no-untyped-def] sprites_patched.py:211: note: Use "-> None" if function does not return a value sprites_patched.py:224: error: Call to untyped function "main" in typed context [no-untyped-call] Found 13 errors in 1 file (checked 1 source file)
Nejvíce nových problémů spočívá v tom, že Group je generickým typem a tedy očekává typový parametr. Tyto informace je nutné doplnit ručně, například do této podoby:
def createSprites() -> Tuple[pygame.sprite.Group[BlockySprite], pygame.sprite.Group[BlockySprite], BlockySprite]: # Objekt sdružující všechny sprity all_sprites: pygame.sprite.Group[BlockySprite] = pygame.sprite.Group()
atd. na dalších řádcích.
15. Problematické chování nástroje MonkeyType
Již v předchozí kapitole jsme mohli vidět některé problémy nástroje MonkeyType. Ty většinou vyžadují ruční zásah do analyzovaného a automaticky modifikovaného zdrojového kódu. Problémů je však ve skutečnosti ještě více. Na dva problémy, na něž jsme narazili v praxi, se pokusím upozornit v navazujících dvou kapitolách.
16. n-tice s mnoha prvky
V některých projektech se využívají n-tice jako forma neměnitelného seznamu. To je pochopitelně zcela legální způsob použití, ovšem typový systém Pythonu pracuje s n-ticemi odlišně než se seznamy. U seznamů se totiž očekává, že všechny prvky budou stejného typu (například int, takže píšeme list[int]), zatímco u n-tic se naopak očekává, že každý prvek může být odlišného typu. To ovšem u dlouhých n-tic způsobuje neúměrně dlouhý zápis typu.
Podívejme se nyní na jednoduchý příklad funkce, která volá funkci print_tuple a předává ji n-tici se 100 prvky typu int:
def print_tuple(x): print(x) def use_tuple(): x = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) print_tuple(x)
Nástroj MonkeyType typ n-tice zjistí přesně a zapíše její typ formou „nekonečného“ řádku:
from typing import Tuple def print_tuple(x: Tuple[int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int]) -> None: print(x) def use_tuple() -> None: x = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) print_tuple(x)
V praxi je výhodnější zápis ručně zkrátit na pouhé (což již MonkeyType neumí):
Tuple[int, ...]
17. Pád v případě použití typu Callable na starších verzích Pythonu
Zkusme si nechat analyzovat tento triviální kód, v němž je použita funkce vyššího řádu foo. To je funkce, do které v argumentu předáváme jinou funkci:
#!/usr/bin/env python def foo(x, y, function): return function(x, y) def main(): print(foo(1, 2, lambda x, y: x+y))
Výsledkem by mělo být odvození typu zhruba ve tvaru:
Callable[[int, int], int]
Ovšem ve starších verzích Pythonu (které se bohužel stále používají na produkci) dojde k pádu, tedy k nezachycené výjimce, a zdrojové kódy se neupraví (možná by lepší bylo přeskočit problematický kód a pokračovat dále):
$ monkeytype apply callable Traceback (most recent call last): File "/home/ptisnovs/.local/bin/monkeytype", line 8, in <module> sys.exit(entry_point_main()) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/cli.py", line 473, in entry_point_main sys.exit(main(sys.argv[1:], sys.stdout, sys.stderr)) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/cli.py", line 458, in main handler(args, stdout, stderr) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/cli.py", line 209, in apply_stub_handler stub = get_stub(args, stdout, stderr) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/cli.py", line 119, in get_stub traces.append(thunk.to_trace()) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/encoding.py", line 203, in to_trace arg_types = arg_types_from_json(self.arg_types) File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/encoding.py", line 151, in arg_types_from_json return {name: type_from_dict(type_dict) for name, type_dict in arg_types.items()} File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/encoding.py", line 151, in <dictcomp> return {name: type_from_dict(type_dict) for name, type_dict in arg_types.items()} File "/home/ptisnovs/.local/lib/python3.8/site-packages/monkeytype/encoding.py", line 126, in type_from_dict typ = typ[elem_types] # type: ignore[index] File "/usr/lib/python3.8/typing.py", line 806, in __getitem__ raise TypeError("Callable must be used as " TypeError: Callable must be used as Callable[[arg, ...], result].
18. Repositář s demonstračními příklady
Všechny Pythonovské skripty, které jsme si v dnešním článku ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady. Pro analýzu kódů je pochopitelně nutné mít nainstalovaný i nástroj monkeytype:
19. Odkazy na další články o typových anotacích v Pythonu
V této kapitole jsou uvedeny odkazy na články, které již na Rootu vyšly a ve kterých jsme se zabývali problematikou typového systému programovacího jazyka Python:
- Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-2-cast/ - Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
https://www.root.cz/clanky/staticke-typove-kontroly-zdrojovych-kodu-pythonu-provadene-nastrojem-mypy-3/ - Novinky v typovém systému přidané do Pythonu 3.12
https://www.root.cz/clanky/novinky-v-typovem-systemu-pridane-do-pythonu-3–12/
20. Odkazy na Internetu
- What’s New In Python 3.12 (official)
https://docs.python.org/3/whatsnew/3.12.html - What’s New In Python 3.12
https://dev.to/mahiuddindev/python-312–4n43 - PEP 698 – Override Decorator for Static Typing
https://peps.python.org/pep-0698/ - typing.override
https://docs.python.org/3/library/typing.html#typing.override - Type Hinting
https://realpython.com/lessons/type-hinting/ - mypy homepage
https://www.mypy-lang.org/ - mypy documentation
https://mypy.readthedocs.io/en/stable/ - Mypy na PyPi Optional static typing for Python
https://pypi.org/project/mypy/ - 5 Reasons Why You Should Use Type Hints In Python
https://www.youtube.com/watch?v=dgBCEB2jVU0 - Python Typing – Type Hints & Annotations
https://www.youtube.com/watch?v=QORvB-_mbZ0 - What Problems Can TypeScript Solve?
https://www.typescriptlang.org/why-create-typescript - How to find code that is missing type annotations?
https://stackoverflow.com/questions/59898490/how-to-find-code-that-is-missing-type-annotations - Do type annotations in Python enforce static type checking?
https://stackoverflow.com/questions/54734029/do-type-annotations-in-python-enforce-static-type-checking - Understanding type annotation in Python
https://blog.logrocket.com/understanding-type-annotation-python/ - Static type checking with Mypy — Perfect Python
https://www.youtube.com/watch?v=9gNnhNxra3E - Static Type Checker for Python
https://github.com/microsoft/pyright - Differences Between Pyright and Mypy
https://github.com/microsoft/pyright/blob/main/docs/mypy-comparison.md - 4 Python type checkers to keep your code clean
https://www.infoworld.com/article/3575079/4-python-type-checkers-to-keep-your-code-clean.html - Pyre: A performant type-checker for Python 3
https://pyre-check.org/ - „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
https://www.youtube.com/watch?v=uJHD2×yv7×o - Covariance and contravariance (computer science)
https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) - Functional Programming: Type Systems
https://www.youtube.com/watch?v=hy1wjkcIBCU - A Type System From Scratch – Robert Widmann
https://www.youtube.com/watch?v=IbjoA5×VUq0 - „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
https://www.youtube.com/watch?v=SWTWkYbcWU0 - Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8 - Statická vs. dynamická typová kontrola
https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/ - Typový systém
https://cs.wikipedia.org/wiki/Typov%C3%BD_syst%C3%A9m - Comparison of programming languages by type system
https://en.wikipedia.org/wiki/Comparison_of_programming_languages_by_type_system - Flow
https://flow.org/ - TypeScript
https://www.typescriptlang.org/ - Sorbet
https://sorbet.org/ - Pyright
https://github.com/microsoft/pyright - Mypy: Type hints cheat sheet
https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html - PEP 484 – Type Hints
https://peps.python.org/pep-0484/ - What is the use of stub files (.pyi ) in python?
https://stackoverflow.com/questions/59051631/what-is-the-use-of-stub-files-pyi-in-python - PEP 561 – Distributing and Packaging Type Information
https://peps.python.org/pep-0561/ - What does „i“ represent in Python .pyi extension?
https://stackoverflow.com/questions/41734836/what-does-i-represent-in-python-pyi-extension