Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy (2.část)

20. 6. 2023
Doba čtení: 41 minut

Sdílet

 Autor: Python
Ukážeme si práci s typově bezpečnými slovníky, typovou inferencí, práci s typově bezpečnými funkcemi (Callable) a ve druhé polovině pak převod aplikace bez určení datových typů ke kódu s doplněnými typovými informacemi.

Obsah

1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy

2. Základní vlastnosti slovníků (dictionary) v Pythonu

3. Typová anotace u slovníků

4. Specifikace několika povolených typů (union)

5. Povolení hodnoty None

6. Typový systém a funkce

7. Typové kontroly v těle funkcí

8. Typ Callable

9. Postup při úpravě stávajících projektů bez typových anotací

10. Původní zdrojový kód upravovaného demonstračního příkladu před refaktoringem a bez typových informací

11. Refaktoring kódu

12. Výsledek spuštění Mypy ve striktním režimu

13. Využití Mypy pro nalezení chybějících typových anotací

14. Přidání typů návratových hodnot u funkcí, které žádnou hodnotu nevracejí

15. Typové anotace parametrů funkcí a jejich návratových hodnot

16. Výsledný kód s typovými anotacemi

17. Kontrola na reálné chyby související s použitými datovými typy

18. Výsledný kód s typovými informacemi po opravě všech chyb

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Statické typové kontroly zdrojových kódů Pythonu prováděné nástrojem Mypy

Ve druhém článku o statické kontrole zdrojových kódů naprogramovaných v Pythonu prováděné nástrojem Mypy si nejprve popíšeme práci s typově bezpečnými slovníky (dictionary), odvozením datových typů nástrojem Mypy (což je určitá forma typové inference), specifikací typu volatelná funkce (Callable, to je jedna z nejdůležitějších vlastností Mypy) a ve druhé polovině článku si navíc na poněkud delším zdrojovém kódu ukážeme, jakým způsobem je možné postupně převést aplikaci bez určení datových typů ke zdrojovému kódu, v němž jsou na všech potřebných místech doplněny typové informace. Uvidíme, že i kód s doplněnými typovými informacemi je stále stručnější, než by tomu bylo v jazyku typu C++ či Java.

2. Základní vlastnosti slovníků (dictionary) v Pythonu

V úvodním článku jsme si mj. ukázali, jak lze určit typ prvků seznamů a n-tic (pokaždé se přitom jedná o odlišný koncept). Podívejme se nyní na další standardní datovou strukturu programovacího jazyka Python. Jedná se o slovníky. Práce s nimi je snadná; ukažme si například vytvoření prázdného slovníku s následným přidáním tří prvků do slovníku. Povšimněte si, že v tomto konkrétním případě jsou všechny klíče řetězci a všechny hodnoty jsou typu celé číslo:

d = {}
 
d["foo"] = 1
d["bar"] = 3
d["baz"] = 10
 
print(d)

V Pythonu jsou ovšem slovníky heterogenními datovými strukturami, což konkrétně znamená, že jak klíče, tak i hodnoty mohou být (prakticky) libovolného typu, a to i v rámci jednoho slovníku. Opět si to ukažme:

d = {}
 
d["foo"] = 1
d["bar"] = 3.14
d[10] = 10
d[42] = "answer"
 
print(d)

Jak však bude tento kód zkontrolován nástrojem Mypy, pokud použijeme přepínač –strict? Podívejme se nejprve na výsledek statické typové kontroly:

$ mypy --strict dict_type2.py
 
dict_type2.py:4: error: Incompatible types in assignment (expression has type "float", target has type "int")  [assignment]
dict_type2.py:5: error: Invalid index type "int" for "Dict[str, int]"; expected type "str"  [index]
dict_type2.py:6: error: Invalid index type "int" for "Dict[str, int]"; expected type "str"  [index]
dict_type2.py:6: error: Incompatible types in assignment (expression has type "str", target has type "int")  [assignment]
Found 4 errors in 1 file (checked 1 source file)

Z výsledku je patrné, že si Mypy z prvního přiřazení do slovníku odvodil typ slovníku, což je velmi důležitá vlastnost nazývaná typová inference (nejedná se sice o žádnou novinku, protože typová inference je například součástí programovacího jazyka ML z roku 1978, ovšem do dalších programovacích jazyků se tato velmi užitečná vlastnost rozšiřuje teprve postupně). Navíc je z výsledků vypsaných nástrojem Mypy patrné, že v tomto konkrétním případě nemusí být typová inference zcela korektní a bude lepší typ slovníku specifikovat explicitně.

3. Typová anotace u slovníků

Při specifikaci typu slovníku je zapotřebí zadat jak typ klíčů, tak i typ hodnot. V případě, že pouze vyžadujeme, aby nějaká proměnná (či parametr funkce) byla typu slovník s libovolnými klíči a hodnotami, můžeme využít datového typu Any:

from typing import Dict, Any
 
d:Dict[Any, Any] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d[10] = 10
d[42] = "answer"
 
print(d)
Poznámka: mohlo by se zdát, že taková typová anotace nemá valného významu, ale můžeme díky ní omezit přiřazování hodnot do samotné proměnné d – vždy se musí jednat o slovník.

Samozřejmě můžeme hodnoty zapisované do slovníku omezit, například tak, že klíči budou pouze řetězce a hodnotami čísla typu float:

from typing import Dict, Any
 
d:Dict[str, float] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d[10] = 10
d[42] = "answer"
 
print(d)

Mypy pochopitelně v tomto krátkém příkladu nalezne celou řadu chyb, což je naprosto v pořádku:

$ mypy dict_type4.py 
 
dict_type4.py:7: error: Invalid index type "int" for "Dict[str, float]"; expected type "str"  [index]
dict_type4.py:8: error: Invalid index type "int" for "Dict[str, float]"; expected type "str"  [index]
dict_type4.py:8: error: Incompatible types in assignment (expression has type "str", target has type "float")  [assignment]
Found 3 errors in 1 file (checked 1 source file)

4. Specifikace několika povolených typů (union)

V některých situacích budeme chtít specifikovat, že hodnoty ukládané do slovníků mohou být několika zvolených typů. Nechceme tedy určit pouze jediný typ, ale na druhou stranu nám nemusí vyhovovat typ Any. V takových případech je možné použít typ Union, který se ve specifikacích používá následovně:

Union[typ1, typ2, typ3, ...]

Můžeme tedy upravit typ slovníku tak, že klíči mají být stále řetězce, ale hodnotami mohou být celá čísla, čísla typu float či řetězce:

from typing import Dict, Union
 
d:Dict[str, Union[int, float, str]] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d[10] = 10
d[42] = "answer"
 
print(d)

Výsledek typové kontroly nástrojem Mypy:

$ mypy dict_type5.py
 
dict_type5.py:7: error: Invalid index type "int" for "Dict[str, Union[int, float, str]]"; expected type "str"  [index]
dict_type5.py:8: error: Invalid index type "int" for "Dict[str, Union[int, float, str]]"; expected type "str"  [index]
Found 2 errors in 1 file (checked 1 source file)
Poznámka: chyby tedy byly nalezeny kvůli špatnému typu klíčů, nikoli hodnot.

Samozřejmě je možné určit, že klíče mohou být typu int nebo str, a to následovně:

from typing import Dict, Union
 
d:Dict[Union[int, str], Union[int, float, str]] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d[10] = 10
d[42] = "answer"
 
print(d)

Nyní bude statická typová analýza vypadat následovně:

$ mypy dict_type6.py
 
Success: no issues found in 1 source file

5. Povolení hodnoty None

Dalším častým požadavkem je povolení hodnoty None, která je typu None (typ i hodnota se tedy jmenuje stejně). Například tento skript je typově nekorektní:

from typing import Dict
 
d:Dict[str, float] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d["baz"] = None
 
print(d)

Výsledek typové kontroly:

$ mypy dict_type7.py
 
dict_type7.py:7: error: Incompatible types in assignment (expression has type "None", target has type "float")  [assignment]
Found 1 error in 1 file (checked 1 source file)

Pokud skutečně budeme chtít povolit i hodnotu None, samozřejmě můžeme použít Union, ovšem existuje i idiomatičtější přístup – využití typu Optional. Je to snadné a čitelné:

from typing import Dict, Optional
 
d:Dict[str, Optional[float]] = {}
 
d["foo"] = 1
d["bar"] = 3.14
d["baz"] = None
 
print(d)

Nyní bude statická typová analýza skriptu vypadat následovně:

$ mypy dict_type8.py
 
Success: no issues found in 1 source file

6. Typový systém a funkce

Ještě jednou se vraťme k problematice specifikace datových typů u funkcí. Víme již, jak lze specifikovat typ parametrů funkce i typ návratové hodnoty. Mějme například funkci, která akceptuje (nekomplexní) číselné hodnoty a vrátí hodnotu True v případě, že předávaná hodnota bude kladná. Realizace této funkce i s uvedením typových informací je relativně snadná:

def positive(x:float) -> bool:
    return x > 0.0
 
 
x:bool = positive(0.5)
y:int = positive(42)
z:float = positive(False)
w:str = positive(3)

7. Typové kontroly v těle funkcí

K čemu je však zápis plně „typově anotované“ funkce vhodný? Na prvním místě se jedná o informaci, která pomáhá integrovaným vývojovým prostředím. Ovšem možná ještě důležitější je fakt, že Mypy dokáže na základě typů parametrů otestovat, zda se s parametry pracuje uvnitř funkce korektně. Co to ovšem konkrétně znamená? Podívejme se na následující příklad, který bude pochopitelně funkční pouze pro ty objekty, které mají definovánu metodu strip:

def append(a, b):
    return a.strip()+b.strip()
 
print(append(1, 2))

Po spuštění vznikne běhová chyba:

Traceback (most recent call last):
  File "appender1.py", line 4, in
    print(append(1, 2))
  File "appender1.py", line 2, in append
    return a.strip()+b.strip()
AttributeError: 'int' object has no attribute 'strip'

Typově bezpečná varianta bude vypadat následovně:

def append(a:str, b:str) -> str:
    return a.strip()+b.strip()

V upravené variantě tohoto příkladu sice specifikujeme, že funkci se mají předávat parametry typu int, ovšem v těle funkce se (omylem, refaktoringem, chybou copy&paste atd.) s parametry pracuje, jakoby se jednalo o řetězce (či naopak – spletli jsme se v určení typů):

def append(a:int, b:int) -> int:
    return a.strip()+b.strip()

Tento problém nástroj Mypy samozřejmě relativně snadno odhalí, protože má k dispozici informace o tom, jaké metody (a zda vůbec nějaké) jsou pro specifikovaný datový typ dostupné – a metoda strip to pro celá čísla zcela určitě není:

$ mypy appender3.py
 
appender3.py:2: error: "int" has no attribute "strip"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)
Poznámka: to, že tento problém odhalí Mypy znamená, že ho mohou odhalit i integrovaná vývojová prostředí, která chybu dokážou ihned označit – což je ta nejrychlejší a mnohdy i nejpřesnější zpětná vazba, kterou může programátor dostat.

8. Typ Callable

Nyní již dokážeme u funkcí specifikovat jak typy parametrů, tak i typ návratové hodnoty. Ovšem v programovacím jazyku Python jsou i samotné funkce hodnotami a proto je v některých případech vhodné vědět, jakým způsobem se specifikuje typ samotné funkce – resp. typ všech funkcí se stejnými počty a typy parametrů a se stejným typem návratové hodnoty (tak jednoduché to ovšem není – a to díky varianci, o níž se ještě zmíníme). Funkce jsou specifikovány typem Callable, což ovšem není (podobně jako u slovníků) plné určení typu. Musíme ještě doplnit typy parametrů a typ návratové hodnoty. Celý zápis typu funkce by tedy mohl vypadat následovně:

Callable[[typ_parametru_1, typ_parametru_2, ...], typ_návratové_hodnoty]

Jak lze takový typ použít? Například ho můžeme využít ve funkcích vyššího řádu (higher order functions), které jako svůj parametr akceptují jinou funkci či naopak funkci vrací. Podívejme se na jednoduchý příklad, v němž jedné funkci budeme chtít při jejím volání předat funkci jinou. Předávaná funkce bude využita později – zavolá se v těle první funkce:

def printIsPositive(x:float, condition) -> None:
    if condition(x):
        print("Positive")
    else:
        print("Negative")
 
 
def positiveFloat(x:float) -> bool:
    return x > 0.0
 
 
def positiveInt(x:int) -> bool:
    return x > 0
 
 
printIsPositive(4, positiveFloat)
printIsPositive(4, positiveInt)
printIsPositive(-0.5, positiveFloat)
printIsPositive(-0.5, positiveInt)

Typová anotace není v tomto příkladu úplná, na což nás Mypy upozorní, pochopitelně za předpokladu, že použijeme přepínač –strict:

x.py:4: error: Function is missing a type annotation for one or more arguments  [no-untyped-def]
Found 1 error in 1 file (checked 1 source file)

Typ funkcí, které je možné do printIsPositive předat, můžeme omezit, a to právě s využitím Callable. Je to patrné při pohledu na následující úpravu předchozího demonstračního příkladu:

from typing import Callable
 
 
def printIsPositive(x:float, condition:Callable[[float], bool]) -> None:
    if condition(x):
        print("Positive")
    else:
        print("Negative")
 
 
def positiveFloat(x:float) -> bool:
    return x > 0.0
 
 
def positiveInt(x:int) -> bool:
    return x > 0
 
 
printIsPositive(4, positiveFloat)
printIsPositive(4, positiveInt)
printIsPositive(-0.5, positiveFloat)
printIsPositive(-0.5, positiveInt)

Zkusme si zkontrolovat typovou korektnost:

$ mypy callable2.py
 
callable2.py:20: error: Argument 2 to "printIsPositive" has incompatible type "Callable[[int], bool]"; expected "Callable[[float], bool]"  [arg-type]
callable2.py:22: error: Argument 2 to "printIsPositive" has incompatible type "Callable[[int], bool]"; expected "Callable[[float], bool]"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

V této chvíli totiž na scénu (opět) přichází variance, resp. její dva typy. Připomeňme si, že existují čtyři typy variance:

  1. Covariance
  2. Contravariance
  3. Invariance
  4. Bivariance

U typu „funkce“ (tedy Callable) v Pythonu platí: argumenty/parametry jsou kontravariantní, kdežto návratová hodnota je kovariantní. Zajímavá je zejména kontravariance parametrů, takže si předchozí příklad zjednodušme – nebudeme funkci vyššího řádu předávat dva parametry, ale pouze jediný parametr typu Callable:

from typing import Callable
 
 
def printIsPositive(condition:Callable[[float], bool]) -> None:
    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ýsledek typové kontroly:

$ mypy callable3.py
 
callable3.py:20: error: Argument 1 to "printIsPositive" has incompatible type "Callable[[int], bool]"; expected "Callable[[float], bool]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Zde se právě uplatnila (poněkud neintuitivní) kontravariance. Parametr funkce printIsPositive tedy musíme upravit tak, že bude akceptovat funkci s parametrem typu int a nikoli float:

from typing import Callable
 
 
def printIsPositive(condition:Callable[[int], bool]) -> None:
    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)
Poznámka: float je obecnější typ než int; resp. naopak pokud funkce akceptuje funkci s parametrem typu int, zcela jistě bude fungovat i s funkcí akceptující parametr typu float. Naopak (tedy „podle selského rozumu“, s variancí) by to nebylo možné, protože by se interně mohly provádět operace, které nad typem int nemají smysl (například test na nekonečno nebo NaN).

Tentokrát typová kontrola proběhne v pořádku:

$ mypy callable4.py
 
Success: no issues found in 1 source file

9. Postup při úpravě stávajících projektů bez typových anotací

Ve druhé polovině dnešního článku si ukážeme, 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 je možné shrnout do několika bodů:

  1. Refaktoring, ideálně tak, aby se nepoužívaly globální proměnné a globální kód
  2. Použití Mypy pro nalezení chybějících typových anotací
  3. Postupné doplnění typových anotací
  4. Odstranění reálných chyb nalezených nástrojem Mypy
Poznámka: celý postup samozřejmě není nutné aplikovat na celý projekt, ale například pouze na jednu knihovnu (baliček) nebo dokonce jen na jediný zdrojový kód. Projekt je tak možné postupně, v malých a testovatelných krocích, vylepšovat.

10. Původní zdrojový kód upravovaného demonstračního příkladu před refaktoringem a bez typových informací

Podívejme se nejdříve na původní zdrojový kód příkladu tak, jak byl kdysi ukázán v článku o knihovně Pygame. Tento zdrojový kód, který naleznete na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites1.py, neobsahuje typové informace (anotace) a bude ho zapotřebí poněkud refaktorovat:

#!/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
 
 
# 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)
 
 
# Inicializace knihovny Pygame
pygame.init()
 
clock = pygame.time.Clock()
 
# Vytvoření okna pro vykreslování
display = pygame.display.set_mode([WIDTH, HEIGHT])
 
# Nastavení titulku okna
pygame.display.set_caption("Pygame test #22")
 
# 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)
 
# 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)
 
 
# Posun všech spritů ve skupině na základě jejich rychlosti
@profile
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
@profile
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
@profile
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)
@profile
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 = "Pygame test #22: collisions " + str(collisions)
    pygame.display.set_caption(caption)
 
 
@profile
def mainLoop():
    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)
 
 
mainLoop()
 
# finito

11. Refaktoring kódu

Předchozí zdrojový kód je vhodné před doplněním o typové informace ještě náležitě upravit. Postupně některé jeho části 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 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
Poznámka: povšimněte si zejména toho, že funkce createSprites vrací n-tici. Nejedná se o zcela ideální řešení, ovšem alespoň si osvěžíme, jak se v nástroji Mypy s n-ticemi pracuje.

12. Výsledek spuštění Mypy ve striktním režimu

V případě, že spustíme nástroj Mypy s přepínačem –strict, vypíšou se jak všechny chybějící anotace, tak i další potenciální problémy nalezené ve zdrojových kódech:

$ mypy --strict sprites2.py

Výsledek by měl v našem případě konkrétně vypadat takto:

sprites2.py:26: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:46: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:46: note: Use "-> None" if function does not return a value
sprites2.py:50: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:50: note: Use "-> None" if function does not return a value
sprites2.py:54: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:64: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:66: error: Need type annotation for "all_sprites"  [var-annotated]
sprites2.py:69: error: Need type annotation for "all_sprites_but_player"  [var-annotated]
sprites2.py:73: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:74: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:75: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:76: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:77: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:78: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:79: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:80: error: Call to untyped function "BlockySprite" in typed context  [no-untyped-call]
sprites2.py:106: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:127: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:137: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:147: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:151: error: Call to untyped function "change_colors" in typed context  [no-untyped-call]
sprites2.py:158: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:189: error: Call to untyped function "move_sprites" in typed context  [no-untyped-call]
sprites2.py:190: error: Call to untyped function "check_collisions" in typed context  [no-untyped-call]
sprites2.py:191: error: Call to untyped function "draw_scene" in typed context  [no-untyped-call]
sprites2.py:195: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:195: note: Use "-> None" if function does not return a value
sprites2.py:200: error: Call to untyped function "initDisplay" in typed context  [no-untyped-call]
sprites2.py:202: error: Call to untyped function "createSprites" in typed context  [no-untyped-call]
sprites2.py:204: error: Call to untyped function "mainLoop" in typed context  [no-untyped-call]
sprites2.py:208: error: Call to untyped function "main" in typed context  [no-untyped-call]
Found 29 errors in 1 file (checked 1 source file)

13. Využití Mypy pro nalezení chybějících typových anotací

Prozatím se však budeme chtít soustředit na nalezení těch řádků v programovém kódu, kde chybí typové anotace. K tomuto účelu nemusí být striktní režim nejvhodnější. Namísto toho použijeme jiné přepínače (které jsou součástí striktního režimu):

$ mypy --explicit-package-bases --disallow-untyped-calls --disallow-untyped-defs --disallow-incomplete-defs sprites2.py

Výsledek bude nyní vypadat následovně:

sprites2.py:26: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:46: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:46: note: Use "-> None" if function does not return a value
sprites2.py:50: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:50: note: Use "-> None" if function does not return a value
sprites2.py:54: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:64: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:106: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:127: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:137: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:147: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:158: error: Function is missing a type annotation  [no-untyped-def]
sprites2.py:195: error: Function is missing a return type annotation  [no-untyped-def]
sprites2.py:195: note: Use "-> None" if function does not return a value
sprites2.py:208: error: Call to untyped function "main" in typed context  [no-untyped-call]
Found 12 errors in 1 file (checked 1 source file)

Oněch dvanáct nalezených chyb postupně opravíme.

14. Přidání typů návratových hodnot u funkcí, které žádnou hodnotu nevracejí

Nejdříve můžeme mechanicky doplnit typy návratových hodnot u funkcí, které ve skutečnosti žádnou hodnotu nevracejí. Návratovým typem tedy bude None:

    def yellowColor(self) -> None:
 
    def grayColor(self) -> None:
 
def move_sprites(sprite_group, playground_width, playground_height) -> None:
 
def draw_scene(display, background_color, sprite_group) -> None:
 
def change_colors(sprite_group, hit_list) -> None:
 
def check_collisions(player, sprite_group) -> None:
 
def mainLoop(display, clock, all_sprites, all_sprites_but_player, player) -> None:
 
def main() -> None:

15. Typové anotace parametrů funkcí a jejich návratových hodnot

Typové informace přidáme k použitým konstantám – n-ticím:

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)

Dále přidáme typové anotace pro parametry funkcí/metod i pro jejich návratové hodnoty:

    def __init__(self, color: Tuple[int, int, int], size: int, x: int, y: int):
 
def initDisplay(caption: str) -> pygame.Surface:
 
def move_sprites(sprite_group: pygame.sprite.Group, playground_width: int, playground_height:int) -> None:
 
def draw_scene(display: pygame.Surface, background_color: Tuple[int, int, int], sprite_group: pygame.sprite.Group) -> None:
 
def change_colors(sprite_group: pygame.sprite.Group, hit_list: List[pygame.sprite.Sprite]) -> None:
 
def check_collisions(player: BlockySprite, sprite_group: pygame.sprite.Group) -> None:
  
def mainLoop(display: pygame.Surface, clock: pygame.time.Clock,
        all_sprites: pygame.sprite.Group,
        all_sprites_but_player: pygame.sprite.Group, player: BlockySprite) -> None:

Nejsložitější je případ funkce createSprites vracející n-tici:

def createSprites() -> Tuple[pygame.sprite.Group, pygame.sprite.Group, BlockySprite]:

V této funkci navíc určíme i typy lokálních proměnných:

    all_sprites: pygame.sprite.Group = pygame.sprite.Group()
    all_sprites_but_player: pygame.sprite.Group = pygame.sprite.Group()

16. Výsledný kód s typovými anotacemi

A takto bude vypadat kód, do něhož byly (víceméně mechanicky) doplněny typové anotace:

#!/usr/bin/python
# vim: set fileencoding=utf-8
 
from typing import NewType, Tuple, List, Any
 
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

17. Kontrola na reálné chyby související s použitými datovými typy

Pokud nyní Mypy spustíme znovu, nalezne dvě reálné chyby, které se v programu nachází:

sprites3.py:82: error: Argument 3 to "BlockySprite" has incompatible type "float"; expected "int"  [arg-type]
sprites3.py:82: error: Argument 4 to "BlockySprite" has incompatible type "float"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Tyto chyby jsou způsobeny tím, že operace / vrací hodnotu typu float, o čemž se můžeme snadno přesvědčit přímo v interpretru Pythonu:

>>> x=4/2
>>> type(x)
<class 'float'>
 
>>> y=4//2
>>> type(y)
<class 'int'>

Oprava tedy bude snadná a bude se týkat jediného programového řádku:

ict ve školství 24

player = BlockySprite(RED, 40, WIDTH // 2 - 20, HEIGHT // 2 - 20)

18. Výsledný kód s typovými informacemi po opravě všech chyb

Pro úplnost se podívejme na to, jak bude vypadat výsledný kód skriptu po jeho refaktoringu, přidání typových informací i opravě chyb nalezených nástrojem Mypy. Tento kód je dostupný na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites4.py:

#!/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
 
# 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

19. 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 jejich spuštění je nutné mít nainstalován balíček mypy):

# Příklad Stručný popis Adresa
1 adder1.py funkce add bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder1.py
2 adder2.py funkce add s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder2.py
3 adder3.py funkce add volaná s hodnotami TrueFalse https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder3.py
4 adder4.py funkce add akceptující hodnoty typu bool https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder4.py
5 adder5.py zobrazení typových informací pro funkci bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder5.py
6 adder6.py zobrazení typových informací pro funkci s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/adder6.py
       
5 exec_problem1.py funkce add s typovými anotacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem1.py
6 exec_problem2.py korektní detekce volání funkce add s nekompatibilními hodnotami https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem2.py
7 exec_problem3.py příkaz použitý v exec není statickým analyzátorem zachycen https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/exec_pro­blem3.py
       
8 list_type1.py typ seznam, s inicializací (bez prvků), pro Python 3.10 https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type1.py
9 list_type2.py typ seznam, s inicializací (bez prvků), pro starší verze Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type2.py
10 list_type3.py typ seznam, s inicializací (s prvky), pro starší verze Pythonu https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type3.py
11 list_type4.py typ seznam, kontrola použití prvků s nekorektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type4.py
12 list_type5.py typ seznam, kontrola použití prvků s korektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type5.py
13 list_type6.py typ seznam, kontrola použití prvků s korektními typy https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/list_type6.py
       
14 tuple_type1.py typ n-tice (nekorektní specifikace typu) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type1.py
15 tuple_type2.py typ n-tice (korektní specifikace typu) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type2.py
16 tuple_type3.py typ n-tice, v níž má každý prvek odlišný typ https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type3.py
17 tuple_type4.py typ n-tice, v níž má každý prvek odlišný typ https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/tuple_type4.py
       
18 json_check.py delší kód v Pythonu bez typových anotací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/json_check.py
       
19 Variance1.java variance v Javě – korektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance1.java
20 Variance2.java variance v Javě – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance2.java
       
21 Variance1.py variance v Pythonu – korektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance1.py
22 Variance2.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance2.py
23 Variance3.py variance v Pythonu – nekorektní příklad použití https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance3.py
24 Variance4.py použití typu Sequence namísto List https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/Variance4.py
       
25 view_pyc.py jednoduchá prohlížečka souborů .pyc https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/view_pyc.py
       
26 callable1.py funkce s typovými informacemi https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/callable1.py
27 callable2.py variance funkcí https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/callable2.py
28 callable3.py variance funkcí (nekorektní příklad) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/callable3.py
29 callable4.py korektní řešení problému z kódu callable3.py https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/callable4.py
       
30 dict_type1.py slovník bez specifikace informací o typech (homogenní struktura) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type1.py
31 dict_type2.py slovník bez specifikace informací o typech (heterogenní struktura) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type2.py
32 dict_type3.py typově silný slovník (heterogenní struktura) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type3.py
33 dict_type4.py typově silný slovník (homogenní struktura) https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type4.py
34 dict_type5.py použití typu Union https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type5.py
35 dict_type6.py použití typu Union https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type6.py
36 dict_type7.py použití typu Optional https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type7.py
37 dict_type8.py použití typu Optional https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/dict_type8.py
       
38 sprites1.py původní kód před refaktoringem a bez typových informací https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites1.py
39 sprites2.py refaktoring kódu sprites1.py https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites2.py
40 sprites3.py přidání typových informací do kódu sprites2.py https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites3.py
41 sprites4.py oprava chyb nalezených v kódu sprites3.py https://github.com/tisnik/most-popular-python-libs/blob/master/mypy/sprites4.py

20. Odkazy na Internetu

  1. mypy homepage
    https://www.mypy-lang.org/
  2. mypy documentation
    https://mypy.readthedocs.i­o/en/stable/
  3. Mypy na PyPi Optional static typing for Python
    https://pypi.org/project/mypy/
  4. 5 Reasons Why You Should Use Type Hints In Python
    https://www.youtube.com/wat­ch?v=dgBCEB2jVU0
  5. Python Typing – Type Hints & Annotations
    https://www.youtube.com/watch?v=QORvB-_mbZ0
  6. What Problems Can TypeScript Solve?
    https://www.typescriptlang.org/why-create-typescript
  7. How to find code that is missing type annotations?
    https://stackoverflow.com/qu­estions/59898490/how-to-find-code-that-is-missing-type-annotations
  8. Do type annotations in Python enforce static type checking?
    https://stackoverflow.com/qu­estions/54734029/do-type-annotations-in-python-enforce-static-type-checking
  9. Understanding type annotation in Python
    https://blog.logrocket.com/un­derstanding-type-annotation-python/
  10. Static type checking with Mypy — Perfect Python
    https://www.youtube.com/wat­ch?v=9gNnhNxra3E
  11. Static Type Checker for Python
    https://github.com/microsoft/pyright
  12. Differences Between Pyright and Mypy
    https://github.com/microsof­t/pyright/blob/main/docs/my­py-comparison.md
  13. 4 Python type checkers to keep your code clean
    https://www.infoworld.com/ar­ticle/3575079/4-python-type-checkers-to-keep-your-code-clean.html
  14. Pyre: A performant type-checker for Python 3
    https://pyre-check.org/
  15. „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
    https://www.youtube.com/wat­ch?v=uJHD2×yv7×o
  16. Covariance and contravariance (computer science)
    https://en.wikipedia.org/wi­ki/Covariance_and_contrava­riance_(computer_science)
  17. Functional Programming: Type Systems
    https://www.youtube.com/wat­ch?v=hy1wjkcIBCU
  18. A Type System From Scratch – Robert Widmann
    https://www.youtube.com/wat­ch?v=IbjoA5×VUq0
  19. „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
    https://www.youtube.com/wat­ch?v=SWTWkYbcWU0
  20. Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
    https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8
  21. Statická vs. dynamická typová kontrola
    https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/
  22. Typový systém
    https://cs.wikipedia.org/wi­ki/Typov%C3%BD_syst%C3%A9m
  23. Comparison of programming languages by type system
    https://en.wikipedia.org/wi­ki/Comparison_of_programmin­g_languages_by_type_system
  24. Flow
    https://flow.org/
  25. TypeScript
    https://www.typescriptlang.org/
  26. Sorbet
    https://sorbet.org/
  27. Pyright
    https://github.com/microsoft/pyright
  28. Mypy: Type hints cheat sheet
    https://mypy.readthedocs.i­o/en/stable/cheat_sheet_py3­.html
  29. PEP 484 – Type Hints
    https://peps.python.org/pep-0484/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.