Obsah
1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
2. Duplikace u specifikací datových typů
3. Definice nových datových typů
4. Upravený kód demonstračního příkladu s definicí vlastního datového typu
5. Striktní kontrola typů nástrojem Mypy
6. Použití typově bezpečného generického datového typu
8. Zpětná kompatibilita se staršími verzemi Pythonu
9. Upravený kód demonstračního příkladu, který již projde striktní typovou kontrolou
10. Selektivní zjištění typu proměnné či třídy nástrojem Mypy
11. Selektivní zjištění typu funkce nástrojem Mypy
13. Explicitní definice typu versus typová inference
14. Zjištění typů všech lokálních proměnných
15. Podpora neměnitelných hodnot (immutable)
16. Příloha 1: základní dostupné datové typy
17. Příloha 2: dostupné generické typy
18. Příloha 3: rozdíly mezi Pythonem < 3.9 a novějšími verzemi
19. Repositář s demonstračními příklady
1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (3)
Ve třetím článku o statických typových kontrolách v Pythonu založených na nástroji Mypy navážeme na předchozí článek, v němž jsme si mj. ukázali, jakým způsobem je možné postupně do existujících zdrojových kódů psaných v Pythonu přidávat typové informace. Připomeňme si, že poslední zdrojový kód, který jsme si minule ukázali, byl doplněn o většinu typových informací, byly v něm opraveny nalezené chyby a výsledek vypadal následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from typing import Tuple, List 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: Tuple[int, int, int] = (0, 0, 0) RED: Tuple[int, int, int] = (255, 0, 0) GRAY: Tuple[int, int, int] = (128, 128, 128) YELLOW: Tuple[int, int, int] = (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: int, y: int): # 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) -> pygame.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[pygame.sprite.Group, pygame.sprite.Group, BlockySprite]: # Objekt sdružující všechny sprity all_sprites: pygame.sprite.Group = pygame.sprite.Group() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player: pygame.sprite.Group = 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: pygame.sprite.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: pygame.Surface, background_color: Tuple[int, int, int], sprite_group: pygame.sprite.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: pygame.sprite.Group, hit_list: List[pygame.sprite.Sprite]) -> 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: pygame.sprite.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: pygame.Surface, clock: pygame.time.Clock, all_sprites: pygame.sprite.Group, all_sprites_but_player: pygame.sprite.Group, player: BlockySprite) -> None: 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() -> None: # 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
2. Duplikace u specifikací datových typů
V předchozím zdrojovém kódu je možné najít hned několik míst, u nichž specifikujeme shodný datový typ, resp. přesněji řečeno shodný typ z hlediska typového systému Pythonu – například jiný typový systém by tato místa považoval za specifikaci zcela unikátních a nekompatibilních typů:
BLACK: Tuple[int, int, int] = (0, 0, 0) RED: Tuple[int, int, int] = (255, 0, 0) GRAY: Tuple[int, int, int] = (128, 128, 128) YELLOW: Tuple[int, int, int] = (255, 255, 0) def __init__(self, color: Tuple[int, int, int], size: int, x: int, y: int): def draw_scene(display: pygame.Surface, background_color: Tuple[int, int, int], sprite_group: pygame.sprite.Group) -> None:
Je možné nějakým způsobem tyto opakující se části kódu sjednotit? V jiných programovacích jazycích se statickým typovým systémem se tato operace provádí jednoduše – definicí nového datového typu. V Mypy tomu je podobně, i když způsob definice nového datového typu nám může připadat poněkud neobvyklý.
3. Definice nových datových typů
Pro definici nového datového typu se v Pythonu (Mypy) nepoužívá speciální syntaxe (ta prozatím neexistuje). Namísto toho se volá funkce NewType:
NewType(name, tp) NewType creates simple unique types with almost zero runtime overhead. NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy function that simply returns its argument. Usage:: UserId = NewType('UserId', int) def name_by_id(user_id: UserId) -> str: ... UserId('user') # Fails type check name_by_id(42) # Fails type check name_by_id(UserId(42)) # OK num = UserId(5) + 1 # type: int
Prvním parametrem této funkce je tedy jméno nově definovaného typu (použitého pro Mypy), druhým parametrem pak nadtyp, z něhož je nový datový typ odvozen. Vrácenou hodnotou je funkce, která pouze vrací svůj argument:
>>> real = NewType("real", float) >>> real(3.14) 3.14 >>> type(real(3.14)) <class 'float'> >>> type(real("foo")) <class 'str'>
V našem konkrétním případě si vytvoříme nový datový typ, který se bude jmenovat RGB:
# Definice nových datových typů RGB = NewType("RGB", Tuple[int, int, int])
Tento nový typ použijeme na následujících řádcích:
RGB = NewType("RGB", Tuple[int, int, int]) BLACK: RGB = RGB((0, 0, 0)) RED: RGB = RGB((255, 0, 0)) GRAY: RGB = RGB((128, 128, 128)) YELLOW: RGB = RGB((255, 255, 0)) def __init__(self, color: RGB, size: int, x: int, y: int): def draw_scene(display: pygame.Surface, background_color: RGB, sprite_group: pygame.sprite.Group) -> None:
4. Upravený kód demonstračního příkladu s definicí vlastního datového typu
Upravený kód předchozího demonstračního příkladu, nyní ovšem s definicí a použitím nového datového typu, lze získat na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites5.py. Tento kód vypadá následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from typing import NewType, Tuple, List import pygame import sys # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * # Velikost okna aplikace WIDTH = 320 HEIGHT = 240 # Definice nových datových typů RGB = NewType("RGB", Tuple[int, int, int]) # Konstanty s n-ticemi představujícími základní barvy BLACK: RGB = RGB((0, 0, 0)) RED: RGB = RGB((255, 0, 0)) GRAY: RGB = RGB((128, 128, 128)) YELLOW: RGB = RGB((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: RGB, size: int, x: int, y: int): # 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) -> pygame.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[pygame.sprite.Group, pygame.sprite.Group, BlockySprite]: # Objekt sdružující všechny sprity all_sprites: pygame.sprite.Group = pygame.sprite.Group() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player: pygame.sprite.Group = 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: pygame.sprite.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: pygame.Surface, background_color: RGB, sprite_group: pygame.sprite.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: pygame.sprite.Group, hit_list: List[pygame.sprite.Sprite]) -> 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: pygame.sprite.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: pygame.Surface, clock: pygame.time.Clock, all_sprites: pygame.sprite.Group, all_sprites_but_player: pygame.sprite.Group, player: BlockySprite) -> None: 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() -> None: # 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
5. Striktní kontrola typů nástrojem Mypy
Nyní je již zdrojový kód zdánlivě typově zcela bezpečný. Pokusme se tedy otestovat, zda je tomu tak i ve skutečnosti. Zavoláme nástroj mypy s parametrem –strict:
$ mypy --strict sprites5.py
V kódu bylo nalezeno celkem devět chyb, které se týkají stejného datového typu Group, resp. přesněji řečeno pygame.sprite.Group:
sprites5.py:69: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:71: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:74: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:111: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:132: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:142: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:152: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:164: error: Missing type parameters for generic type "Group" [type-arg] sprites5.py:165: error: Missing type parameters for generic type "Group" [type-arg] Found 9 errors in 1 file (checked 1 source file)
Chyby byly nalezeny z toho důvodu, že pygame.sprite.Group je kontejnerem, který může obsahovat sprity. Jde tedy o generický datový typ, u něhož by bylo vhodné specifikovat typy prvků. Rychlá, ale nedokonalá oprava spočívá v tom, že jako hodnoty prvků použijeme typ Any:
pygame.sprite.Group[Any]
To však není typově bezpečné (vlastně jen obcházíme typový systém a přitom se tváříme, jak dobře ho využíváme). Pokud se zamyslíme nad logikou programu, zjistíme, že skupiny spritů budou vždy obsahovat instance třídy BlockySprite a můžeme tedy psát:
pygame.sprite.Group[BlockySprite]
6. Použití typově bezpečného generického datového typu
Úpravu zdrojového kódu příkladu je nutné provést pouze na několika místech, která jsou zvýrazněna pod tímto odstavcem:
def createSprites() -> Tuple[pygame.sprite.Group[BlockySprite], pygame.sprite.Group[BlockySprite], BlockySprite]: all_sprites: pygame.sprite.Group[BlockySprite] = pygame.sprite.Group() all_sprites_but_player: pygame.sprite.Group[BlockySprite] = pygame.sprite.Group() def move_sprites(sprite_group: pygame.sprite.Group[BlockySprite], playground_width: int, playground_height:int) -> None: def draw_scene(display: pygame.Surface, background_color: RGB, sprite_group: pygame.sprite.Group[BlockySprite]) -> None: def change_colors(sprite_group: pygame.sprite.Group[BlockySprite], hit_list: List[pygame.sprite.Sprite]) -> None: def check_collisions(player: BlockySprite, sprite_group: pygame.sprite.Group[BlockySprite]) -> None: all_sprites: pygame.sprite.Group[BlockySprite], all_sprites_but_player: pygame.sprite.Group[BlockySprite], player: BlockySprite) -> None:
7. Seznam či sekvence spritů?
V programovém kódu navíc provedeme ještě dvě úpravy. V prvním kroku nahradíme seznam spritů (List[pygame.sprite.Sprite]) za sekvenci spritů, tedy za Sequence[pygame.sprite.Sprite], čímž efektivně zamezíme modifikaci sekvence spritů (tu stejně nechceme dělat):
from typing import NewType, Tuple, List, Sequence def change_colors(sprite_group: pygame.sprite.Group[BlockySprite], hit_list: Sequence[pygame.sprite.Sprite]) -> None:
8. Zpětná kompatibilita se staršími verzemi Pythonu
Ve druhém kroku jako první import uvedeme:
from __future__ import annotations
Tento programový řádek zamezí runtime chybám po spuštění programu ve starších verzích Pythonu. Pokud bude řádek chybět a použijeme například Python 3.8, běh programu skončí s touto chybou:
pygame 2.4.0 (SDL 2.26.4, Python 3.8.10) Hello from the pygame community. https://www.pygame.org/contribute.html Traceback (most recent call last): File "sprites6.py", line 69, in def createSprites() -> Tuple[pygame.sprite.Group[BlockySprite], pygame.sprite.Group[BlockySprite], BlockySprite]: TypeError: 'type' object is not subscriptable shell returned 1
9. Upravený kód demonstračního příkladu, který již projde striktní typovou kontrolou
Upravený kód předchozího demonstračního příkladu, nyní ovšem upraveného do takové podoby, že projde striktní typovou kontrolou, je možné získat na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites6.py. Tento kód vypadá následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from __future__ import annotations from typing import NewType, Tuple, List, Sequence import pygame import sys # Nutno importovat kvůli konstantám QUIT atd. from pygame.locals import * # Velikost okna aplikace WIDTH = 320 HEIGHT = 240 # Definice nových datových typů RGB = NewType("RGB", Tuple[int, int, int]) # Konstanty s n-ticemi představujícími základní barvy BLACK: RGB = RGB((0, 0, 0)) RED: RGB = RGB((255, 0, 0)) GRAY: RGB = RGB((128, 128, 128)) YELLOW: RGB = RGB((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: RGB, size: int, x: int, y: int): # 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) -> pygame.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[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() # Objekt sdružující všechny sprity kromě hráče all_sprites_but_player: pygame.sprite.Group[BlockySprite] = 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: pygame.sprite.Group[BlockySprite], 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: pygame.Surface, background_color: RGB, sprite_group: pygame.sprite.Group[BlockySprite]) -> 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: pygame.sprite.Group[BlockySprite], hit_list: Sequence[pygame.sprite.Sprite]) -> 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: pygame.sprite.Group[BlockySprite]) -> 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: pygame.Surface, clock: pygame.time.Clock, all_sprites: pygame.sprite.Group[BlockySprite], all_sprites_but_player: pygame.sprite.Group[BlockySprite], player: BlockySprite) -> None: 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() -> None: # 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
10. Selektivní zjištění typu proměnné či třídy nástrojem Mypy
V mnoha případech nastává situace, kdy není (mj. i díky dále zmíněné typové inferenci) zcela zřejmé, jakého typu je nějaká proměnná či třída. Nástroj Mypy pro tyto účely podporuje použití (pseudo)funkce nazvané reveal_type, která dokáže vypsat typ objektu. Má to ovšem dva háčky: jedná se o kód, který je zavolán v průběhu statické typové kontroly nástrojem Mypy a současně se nejedná o skutečnou funkci (proto ji ani neimportujeme). Při spuštění programu v interpretru Pythonu dojde k chybě, protože se jedná o neznámý symbol (tuto pseudofunkci tedy musíme z produkčního kódu odstranit).
Podívejme se nyní na jednoduchý příklad, v němž reveal_type použijeme pro zjištění typu libovolné hodnoty:
x = 42 reveal_type(x) y = "foo" reveal_type(y) z = (1, "abc", 3.14, True) reveal_type(z) class X(): pass w = X() reveal_type(w)
Nástroj Mypy vypíše následující informace s přesnými informacemi o typech tak, jak je lze zjistit statickou analýzou:
$ mypy reveal_type1.py reveal_type1.py:2: note: Revealed type is "builtins.int" reveal_type1.py:5: note: Revealed type is "builtins.str" reveal_type1.py:8: note: Revealed type is "Tuple[builtins.int, builtins.str, builtins.float, builtins.bool]" reveal_type1.py:14: note: Revealed type is "reveal_type1.X" Success: no issues found in 1 source file
Naproti tomu pokud volání reveal_type neodstraníme, nebude možné tento kód běžnými prostředky spustit:
$ python3 reveal_type1.py Traceback (most recent call last): File "reveal_type1.py", line 2, in reveal_type(x) NameError: name 'reveal_type' is not defined
11. Selektivní zjištění typu funkce nástrojem Mypy
Nejvíce problémů se v souvislosti s datovými typy řeší u funkcí a metod (a to mj. i díky kontravarianci atd.). Podívejme se tedy na to, jak lze reveal_type použít pro typ funkce:
def add(a:int, b:int) -> int: return a+b reveal_type(add) reveal_type(add(1, 2))
Zjištění typu funkce a typu návratové hodnoty této funkce, a to pochopitelně bez jejího volání – jedná se o statickou typovou kontrolu:
$ mypy reveal_type2.py reveal_type2.py:4: note: Revealed type is "def (a: builtins.int, b: builtins.int) -> builtins.int" reveal_type2.py:5: note: Revealed type is "builtins.int"
Tentýž příklad, ovšem nyní pro funkci bez explicitního uvedení typů:
def add(a, b): return a+b reveal_type(add) reveal_type(add(1, 2))
Výsledek ukazuje, že se nepoužila typová inference, ale všude, kde je to možné, byl dosazen univerzální datový typ Any:
$ mypy reveal_typeX.py reveal_type2.py:4: note: Revealed type is "def (a: Any, b: Any) -> Any" reveal_type2.py:5: note: Revealed type is "Any"
A konečně si vyzkoušejme poněkud složitější funkci, resp. přesněji řečeno funkci se složitějšími typy a návratovými hodnotami:
from typing import List, Set def add(a:List[Set[int]], b:List[Set[int]]) -> List[Set[int]]: return a+b reveal_type(add)
Výsledek:
func.py:7: note: Revealed type is "def (a: builtins.list[builtins.set[builtins.int]], b: builtins.list[builtins.set[builtins.int]]) -> builtins.list[builtins.set[builtins.int]]"
12. Typová inference
Díky existenci pseudofunkce reveal_type si můžeme ověřit, jakým způsobem Mypy automaticky odvozuje datové typy proměnných. Jedná se o technologii nazvanou typová inference. Nejprve vytvoříme slovník se třemi dvojicemi klíč:hodnota, přičemž klíči budou vždy řetězce a hodnotami vždy celá čísla:
d = {} d["foo"] = 1 d["bar"] = 3 d["baz"] = 10 reveal_type(d)
Pseudofunkcí reveal_type si necháme vypsat typ, který Mypy odvodil:
$ mypy reveal_type3.py reveal_type3.py:7: note: Revealed type is "builtins.dict[builtins.str, builtins.int]" Success: no issues found in 1 source file
Druhý příklad bude z pohledu Mypy složitější, protože klíči slovníků nyní budou jak řetězce, tak i celá čísla a i hodnoty budou různého typu:
d = {} reveal_type(d) d["foo"] = 1 d["bar"] = 3.14 reveal_type(d) d[10] = 10 d[42] = "answer" reveal_type(d)
Opět si necháme zobrazit, jaký typ slovníku Mypy v tomto případě odvodil:
$ mypy reveal_type4.py reveal_type4.py:1: error: Need type annotation for "d" (hint: "d: Dict[<type>, <type>] = ...") [var-annotated] reveal_type4.py:3: note: Revealed type is "builtins.dict[Any, Any]" reveal_type4.py:6: error: Incompatible types in assignment (expression has type "float", target has type "int") [assignment] reveal_type4.py:8: note: Revealed type is "builtins.dict[builtins.str, builtins.int]" reveal_type4.py:10: error: Invalid index type "int" for "Dict[str, int]"; expected type "str" [index] reveal_type4.py:11: error: Invalid index type "int" for "Dict[str, int]"; expected type "str" [index] reveal_type4.py:11: error: Incompatible types in assignment (expression has type "str", target has type "int") [assignment] reveal_type4.py:13: note: Revealed type is "builtins.dict[builtins.str, builtins.int]" Found 5 errors in 1 file (checked 1 source file)
13. Explicitní definice typu versus typová inference
Samozřejmě nám nic nebrání (spíše naopak) explicitně zapsat typ slovníku, tj. typ klíčů i typ hodnot. Například můžeme specifikovat, že typ klíčů bude vždy řetězec a typ hodnot buď celé číslo, číslo s plovoucí řádovou čárkou nebo řetězec:
from typing import Dict, Union d:Dict[str, Union[int, float, str]] = {} reveal_type(d) d["foo"] = 1 d["bar"] = 3.14 d["baz"] = "*" reveal_type(d) d[10] = 10 d[42] = "answer" reveal_type(d)
A takto bude vypadat výsledek statické typové kontroly nástrojem Mypy. Podle očekávání budou detekovány chyby na posledních dvou přiřazeních do slovníku (řádky 13 a 14):
$ mypy reveal_type5.py reveal_type5.py:5: note: Revealed type is "builtins.dict[builtins.str, Union[builtins.int, builtins.float, builtins.str]]" reveal_type5.py:11: note: Revealed type is "builtins.dict[builtins.str, Union[builtins.int, builtins.float, builtins.str]]" reveal_type5.py:13: error: Invalid index type "int" for "Dict[str, Union[int, float, str]]"; expected type "str" [index] reveal_type5.py:14: error: Invalid index type "int" for "Dict[str, Union[int, float, str]]"; expected type "str" [index] reveal_type5.py:16: note: Revealed type is "builtins.dict[builtins.str, Union[builtins.int, builtins.float, builtins.str]]" Found 2 errors in 1 file (checked 1 source file)
14. Zjištění typů všech lokálních proměnných
Kromě výše popsané pseudofunkce reveal_type můžeme při vývoji použít další pseudofunkci nazvanou releal_locals. Jak název této pseudofunkce napovídá, zobrazí se v místě jejího použití při statické typové kontrole typy všech lokálních proměnných, resp. přesněji řečeno proměnných z daného bloku. Ostatně si tuto funkcionalitu nástroje Mypy můžeme ukázat na několika demonstračních příkladech. Začneme proměnnými globálními v rámci daného balíčku:
x = 42 y = "foo" z = (1, "abc", 3.14, True) class X(): pass w = X() reveal_locals()
Výsledek vyprodukovaný nástrojem Mypy:
$ mypy reveal_locals1.py reveal_locals1.py:12: note: Revealed local types are: reveal_locals1.py:12: note: w: reveal_locals1.X reveal_locals1.py:12: note: x: builtins.int reveal_locals1.py:12: note: y: builtins.str reveal_locals1.py:12: note: z: Tuple[builtins.int, builtins.str, builtins.float, builtins.bool] Success: no issues found in 1 source file
Zobrazení typů parametrů a lokálních proměnných ve funkci:
def add(a:int, b:int) -> int: reveal_locals() return a+b reveal_type(add) reveal_type(add(1, 2))
Nyní bude výsledek následující:
$ mypy reveal_locals2.py reveal_locals2.py:2: note: Revealed local types are: reveal_locals2.py:2: note: a: builtins.int reveal_locals2.py:2: note: b: builtins.int reveal_locals2.py:5: note: Revealed type is "def (a: builtins.int, b: builtins.int) -> builtins.int" reveal_locals2.py:6: note: Revealed type is "builtins.int" Success: no issues found in 1 source file
A konečně si ukažme ještě nepatrně složitější příklad s funkcí vyššího řádu, konkrétně s funkcí, která jako svůj parametr akceptuje jinou funkci:
from typing import Callable def printIsPositive(condition:Callable[[int], bool]) -> None: reveal_locals() if condition(5): print("Positive") else: print("Negative") def positiveFloat(x:float) -> bool: return x > 0.0 def positiveInt(x:int) -> bool: return x > 0 printIsPositive(positiveFloat) printIsPositive(positiveInt)
Výsledky budou v tomto případě vypadat následovně:
$ mypy reveal_locals3.py reveal_locals3.py:5: note: Revealed local types are: reveal_locals3.py:5: note: condition: def (builtins.int) -> builtins.bool Success: no issues found in 1 source file
15. Podpora neměnitelných hodnot (immutable)
Již minule jsme se, i když jen okrajově, zmínili o podpoře neměnitelných (immutable) hodnot, přesněji řečeno typů, u nichž je specifikováno, že se jejich hodnoty nebudou měnit. Konkrétně je možné použít kvalifikátor Final, o němž se podrobněji zmíníme příště, nebo v případě seznamů lze seznamy nahradit za typ Sequence a slovníky za typ Mapping. Jak Sequence tak i Mapping při statické typové kontrole představují immutable hodnoty, ovšem při běhu (runtime) se jedná o běžné seznamy nebo slovníky!
Ostatně si to můžeme velmi snadno ověřit. Začneme seznamy a typem Sequence:
from typing import List, Sequence l1: List[bool] = [True, False] l1.append(True) l1.append(False) print(l1) l2: Sequence[bool] = [True, False] l2.append(True) l2.append(False) print(l2)
Výsledek po spuštění v interpretru Pythonu neukáže žádný podstatný rozdíl:
$ python3 list_sequence.py [True, False, True, False] [True, False, True, False]
Výsledek statické typové kontroly ovšem již rozdíly pochopitelně ukazuje:
$ mypy list_sequence.py list_sequence.py:12: error: "Sequence[bool]" has no attribute "append" [attr-defined] list_sequence.py:13: error: "Sequence[bool]" has no attribute "append" [attr-defined] Found 2 errors in 1 file (checked 1 source file)
Prakticky totéž platí pro slovníky a typ Mapping:
from typing import Dict, Mapping d1:Dict[str, float] = {} d1["foo"] = 1 d1["bar"] = 3.14 d1["baz"] = 0.0 print(d1) d2:Mapping[str, float] = {} d2["foo"] = 1 d2["bar"] = 3.14 d2["baz"] = 0.0 print(d2)
Výsledek po spuštění v interpretru Pythonu opět neukáže žádný podstatný rozdíl:
$ python3 dict_mapping.py {'foo': 1, 'bar': 3.14, 'baz': 0.0} {'foo': 1, 'bar': 3.14, 'baz': 0.0}
Výsledek statické typové kontroly ovšem již rozdíly pochopitelně ukazuje:
$ mypy dict_mapping.py dict_mapping.py:13: error: Unsupported target for indexed assignment ("Mapping[str, float]") [index] dict_mapping.py:14: error: Unsupported target for indexed assignment ("Mapping[str, float]") [index] dict_mapping.py:15: error: Unsupported target for indexed assignment ("Mapping[str, float]") [index] Found 3 errors in 1 file (checked 1 source file)
16. Příloha 1: základní dostupné datové typy
V Mypy je definováno sedm základních datových typů, z nichž se mohou odvozovat další datové typy, popř. lze tyto typy zkombinovat s generickými typy vypsanými v navazující kapitole:
# | Typ | Stručný popis | Podtyp z |
---|---|---|---|
1 | int | celá čísla | |
2 | float | čísla s plovoucí řádovou čárkou | |
3 | bool | pravdivostní hodnoty | int |
4 | str | sekvence Unicode znaků | |
5 | bytes | sekvence bajtů | |
6 | object | libovolný objekt | object (bázová třída) |
7 | Any | libovolný typ |
17. Příloha 2: dostupné generické typy
Základní datové typy popsané v předchozí kapitole je možné zkombinovat se standardními generickými datovými typy, které jsou, společně s příklady, vypsány v následující tabulce. Většinu těchto typů již známe z předešlých článků:
# | Generický typ | Stručný popis |
---|---|---|
1 | list[str] | seznam hodnot typu řetězec |
2 | list[Any] | seznam hodnot libovolného typu |
3 | tuple[int, int] | dvojice hodnot typu celé číslo |
4 | tuple[int, str] | dvojice hodnot, první hodnotou bude celé číslo, druhou hodnotou řetězec |
5 | tuple[()] | prázdná n-tice |
6 | tuple[int, …] | n-tice s libovolným množstvím hodnot typu int |
7 | dict[str, int] | slovník s klíči typu řetězec a hodnotami typu celé číslo |
8 | dict[str, Any] | slovník s klíči typu řetězec a hodnotami libovolného typu |
9 | Iterable[int] | objekt, který je „iterovatelný“ (lze jím procházet s využitím for-each) |
10 | Sequence[int] | obdoba seznamů, ovšem neměnitelná (immutable) |
11 | Mapping[str, int] | obdoba slovníků, ovšem neměnitelná (immutable) |
18. Příloha 3: rozdíly mezi Pythonem < 3.9 a novějšími verzemi
Již několikrát jsme se setkali s rozdílným způsobem zápisu typů v Pythonu 3.9 a jakékoli vyšší verze na jedné straně a staršími verzemi Pythonu 3 na straně druhé. Tyto rozdíly jsou zvýrazněny v následující tabulce (přičemž platí, že typy začínající velkým písmenem je nutné importovat z balíčku typing):
Původní zápis typu | Nový zápis typu |
---|---|
List[str] | list[str] |
Tuple[int, int] | tuple[int, int] |
Tuple[int, …] | tuple[int, …] |
Dict[str, int] | dict[str, int] |
Další rozdíl spočívá v zápisu „složeného“ typu Union a Optional:
Původní zápis typu | Nový zápis typu |
---|---|
Union[int, str] | int | str |
Optional[int, str] | int | None |
19. Repositář s demonstračními příklady
Všechny Pythonovské skripty, které jsme si v dnešním článku (i v obou předchozích článcích) ukázali, naleznete na adrese https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady (pro jejich spuštění je pochopitelně nutné mít nainstalován balíček mypy společně s Pythonem alespoň 3.7):
20. Odkazy na Internetu
- 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/