Obsah
1. Novinky v typovém systému přidané do Pythonu 3.12
3. Nový způsob zápisu typových parametrů funkcí
4. Zápis většího množství typových parametrů
5. Přepis do syntaxe Pythonu 3.12
6. Chování funkce pair[T, T]() při předání hodnot různých typů
8. Statická kontrola typů u tříd s typovými parametry
9. Přepis do syntaxe Pythonu 3.12
11. Ukázka použití dekorátoru @override
12. Úprava pro starší nástroje
15. Příloha: překlad Pythonu 3.12
16. Repositář s demonstračními příklady
1. Novinky v typovém systému přidané do Pythonu 3.12
V Pythonu 3.12 můžeme najít poměrně velké množství novinek, které jsou shrnuty například na stránce https://docs.python.org/3/whatsnew/3.12.html. Z hlediska syntaxe a sémantiky Pythonu je nejdůležitější hned první zmíněná novinka – podpora nového zápisu typových parametrů a navíc i zavedení nového klíčového slova type, které je možné použít pro definici nových pojmenovaných datových typů. V dnešním článku se s těmito novinkami seznámíme, ovšem ukážeme si taktéž to, do jaké míry jsou tyto novinky využity v nástroji Mypy a jak vypadá starší (kompatibilní) způsob práce s typovými informacemi, které je možné do zdrojových kódů Pythonu přidávat (type hints).
Seznámíme se i s dalšími novinkami, například s existencí dekorátoru @override, který má podobný význam, jako je tomu v Javě.
2. Typové parametry funkcí
Začneme způsobem specifikace typových parametrů u funkcí. Stávající způsob, který je kompatibilní i s nástrojem Mypy, je založen na definici nových typů s využitím TypeVar:
T = TypeVar('T')
Takto definovaný typ lze použít například pro specifikaci typů parametrů funkce i pro specifikaci typu návratové hodnoty (či hodnot). Můžeme si například nadefinovat funkci, která akceptuje dva parametry stejného typu T (přičemž ovšem nespecifikujeme, o jaký konkrétní typ se jedná, jen že oba parametry musí být stejného typu) a současně určíme, že funkce vrátí dvojici hodnot, které budou taktéž stejného typu T:
from typing import TypeVar, Tuple T = TypeVar('T') def pair(first: T, second: T) -> Tuple[T, T]: x = (first, second) return x print(pair("A", "B"))
Nástroje typu Mypy dokážou v takovém případě zajistit, aby oba předávané parametry měly skutečně shodný typ (viz další text) a dokážou samozřejmě odvodit i typ výsledné dvojice.
Ostatně si můžeme snadno ověřit korektnost tohoto příkladu:
$ mypy generics-2.py Success: no issues found in 1 source file
3. Nový způsob zápisu typových parametrů funkcí
Funkci, kterou jsme si ukázali v předchozí kapitole, lze v Pythonu 3.12 zapsat kratším způsobem, který se navíc začíná podobat syntaxi, kterou známe i z jiných mainstreamových programovacích jazyků. Obecné jméno typového parametru (nebo, jak uvidíme dále, více parametrů) se zapíše do hranatých závorek za jménem funkce. Zcela se tedy vynechává volání TypeVar a zápis funkce je tak více odizolován od ostatního programového kódu (což je jen dobře – čím méně kontextu je zapotřebí znát, tím lépe):
from typing import Tuple def pair[T](first: T, second: T) -> Tuple[T, T]: x = (first, second) return x print(pair("A", "B"))
Tento způsob prozatím není v Mypy podporován:
$ mypy generics-1.py generics-1.py:4: error: invalid syntax [syntax] Found 1 error in 1 file (errors prevented further checking)
4. Zápis většího množství typových parametrů
V případě, že budeme chtít nadeklarovat funkci, která akceptuje dva parametry obecně různých typů T a U a vrací dvojici s prvky typu T, U, můžeme použít buď starší zápis kompatibilní s Mypy nebo zápis novější, který je podporován Pythonem 3.12. Ukažme si nejdříve starší zápis, který spočívá v tom, že explicitně nadeklarujeme dva typové identifikátory T a U zavoláním TypeVar. Poté již tyto identifikátory použijeme běžným způsobem:
from typing import TypeVar, Tuple T = TypeVar('T') U = TypeVar('U') def pair(first: T, second: U) -> Tuple[T, U]: x = (first, second) return x reveal_type(pair(1, 2)) reveal_type(pair(0, "B")) reveal_type(pair("A", 42)) reveal_type(pair("A", "B"))
Nástrojem Mypy se nyní můžeme přesvědčit, že typ funkce (při volání) je správně odvozen a že je odvozen i její návratový typ:
$ mypy generics-4.py generics-4.py:12: note: Revealed type is "Tuple[builtins.int, builtins.int]" generics-4.py:13: note: Revealed type is "Tuple[builtins.int, builtins.str]" generics-4.py:14: note: Revealed type is "Tuple[builtins.str, builtins.int]" generics-4.py:15: note: Revealed type is "Tuple[builtins.str, builtins.str]" Success: no issues found in 1 source file
5. Přepis do syntaxe Pythonu 3.12
Příklad z předchozí kapitoly je relativně snadno přepsatelný do podoby využívající novou syntaxi Pythonu 3.12. Povšimněte si, že typové parametry se opět zapisují do hranatých závorek uvedených za jménem funkce a že jsou odděleny čárkou:
from typing import Tuple def pair[T, U](first: T, second: U) -> Tuple[T, U]: x = (first, second) return x print(pair("A", "B"))
6. Chování funkce pair[T, T]() při předání hodnot různých typů
Nyní se zaměřme na možná poněkud matoucí chování typového systému Pythonu. Zkusme funkci, která akceptuje dvojici hodnot stejných typů předat řetězec a celé číslo:
from typing import TypeVar, Tuple T = TypeVar('T') def pair(first: T, second: T) -> Tuple[T, T]: x = (first, second) return x print(pair("A", 42))
Kontrola nástrojem Mypy kupodivu projde naprosto bez problémů:
$ mypy generics-5.py Success: no issues found in 1 source file
Proč tomu tak je odhalí nepatrně upravený příklad, v němž funkci pair postupně předáme různé kombinace parametrů a s využitím reveal_type (nabízené nástrojem Mypy) zjistíme, jakého typu je výsledná n-tice:
from typing import TypeVar, Tuple T = TypeVar('T') def pair(first: T, second: T) -> Tuple[T, T]: x = (first, second) return x reveal_type(pair(1, 2)) reveal_type(pair(0, "B")) reveal_type(pair("A", 42)) reveal_type(pair("A", "B"))
Z výsledků je patrné, že výsledná n-tice buď obsahuje dvojici celých čísel (korektní), dvojici řetězců (taktéž korektní) a u obou „divných“ kombinací se vrací dvojice objektů, protože je to nejbližší typ, který může reprezentovat jak celá čísla (int), tak i řetězce (str):
$ mypy generics-6.py generics-6.py:11: note: Revealed type is "Tuple[builtins.int, builtins.int]" generics-6.py:12: note: Revealed type is "Tuple[builtins.object, builtins.object]" generics-6.py:13: note: Revealed type is "Tuple[builtins.object, builtins.object]" generics-6.py:14: note: Revealed type is "Tuple[builtins.str, builtins.str]" Success: no issues found in 1 source file
Ovšem samotné typy prvků se v runtime nezmění:
from typing import TypeVar, Tuple T = TypeVar('T') def pair(first: T, second: T) -> Tuple[T, T]: x = (first, second) return x p = pair(0, "B") print(type(p[0])) print(type(p[1]))
S výsledky:
<class 'int'> <class 'str'>
7. Typové parametry tříd
Typové parametry lze specifikovat i u tříd. Opět se nejprve podívejme na způsob zápisu, který je kompatibilní i se staršími nástroji typu Mypy, které ještě nepřešly na novou syntaxi Pythonu 3.12. Vytvoříme si třídu představující jednoduchou kolekci se dvěma metodami, přičemž už při konstrukci třídy určíme, prvky jakého typu je možné do takové kolekce přidat a jaké prvky z kolekce získáme. Z důvodu již zmíněné kompatibility se staršími nástroji použijeme TypeVar a zápis bude vypadat následovně:
from typing import TypeVar, Generic from typing import List T = TypeVar('T') class Collection(Generic[T]): def __init__(self) -> None: self.collection : List[T] = [] def append(self, item: T) -> None: self.collection.append(item) def get_all(self) -> List[T]: return self.collection c = Collection[int]() c.append(1) c.append(2) print(c.get_all())
Kontrola nástrojem Mypy by v tomto příkladu neměla nalézt žádné chyby:
$ mypy collection-1.py Success: no issues found in 1 source file
8. Statická kontrola typů u tříd s typovými parametry
Pokusme se nyní vytvořit instanci naší třídy Collection, přičemž budeme vyžadovat, aby prvky kolekce byly typu int. Následně se pokusíme do takto zkonstruované kolekce vložit řetězec:
from typing import TypeVar, Generic from typing import List T = TypeVar('T') class Collection(Generic[T]): def __init__(self) -> None: self.collection : List[T] = [] def append(self, item: T) -> None: self.collection.append(item) def get_all(self) -> List[T]: return self.collection c = Collection[int]() c.append(1) c.append("foo")
Tento prohřešek proti typovému systému je správně rozpoznán při statické typové kontrole:
$ mypy collection-2.py collection-2.py:21: error: Argument 1 to "append" of "Collection" has incompatible type "str"; expected "int" [arg-type] Found 1 error in 1 file (checked 1 source file)
9. Přepis do syntaxe Pythonu 3.12
Přepis skriptu z předchozí kapitoly tak, aby se využila nová syntaxe zavedená v Pythonu 3.12, bude vypadat následovně. Namísto TypeVar a určení předka třídy zapíšeme identifikátor typu do hranatých závorek za jméno třídy:
from typing import List class Collection[T](): def __init__(self) -> None: self.collection : List[T] = [] def append(self, item: T) -> None: self.collection.append(item) def get_all(self) -> List[T]: return self.collection c = Collection[int]() c.append(1) c.append("foo")
Ve skutečnosti můžeme namísto typu typing.List použít přímo list, takže je programový kód ještě kratší a v tomto případě i přehlednější:
class Collection[T](): def __init__(self) -> None: self.collection : list[T] = [] def append(self, item: T) -> None: self.collection.append(item) def get_all(self) -> list[T]: return self.collection c = Collection[int]() c.append(1) c.append("foo")
10. Dekorátor @override
V Pythonu se taktéž objevuje nový dekorátor, který se jmenuje override. Tento dekorátor nalezneme v balíčku typing. Podobný zápis najdeme například v Javě (kde se však jedná o anotaci – což je méně mocný nástroj než dekorátor), kde je určen ke statické kontrole při překladu, zda metoda takto označená skutečně překrývá metodu předka. Pokud například programátor napíše špatné jméno metody (překlep), nemá překladač jinou možnost jak zjistit, že se má jednat o novou metodu nebo jde o špatně zapsané jméno metody předka. V Pythonu je význam stejný, ovšem s tím, že kontrolu neprovádí překladač (CPython je ostatně interpretovaný), ale předpokládá se použití specializovaných nástrojů pro statické typové kontroly.
11. Ukázka použití dekorátoru @override
Použití dekorátoru @override je triviální. Postačuje ho nejprve naimportovat a posléze jím označit ty metody potomka, které překrývají metody předka. Ukažme si jednoduchý příklad, kde jak předek, tak i jeho potomek mají pouze jedinou metodu eat:
from typing import override class Fruit: def eat(self): pass class Apple(Fruit): @override def eat(self): pass
Statická typová kontrola by měla detekovat nekorektní použití dekorátoru (a nebo špatné jméno metody) v tomto případě. Jméno metody v potomkovi se liší od jména metody předka, i když programátor dal najevo, že chce metodu překrýt:
from typing import override class Fruit: def eat(self): pass class Apple(Fruit): @override def eat_apple(self): pass
12. Úprava pro starší nástroje
Pro starší nástroje, například pro Mypy, se dekorátor @override musí naimportovat z balíčku typing_extensions. Výsledný program se tedy liší pouze odlišným importem:
from typing_extensions import override class Fruit: def eat(self): pass class Apple(Fruit): @override def eat(self): pass
Ovšem Mypy bude kontrolu tohoto dekorátoru provádět pouze ve chvíli, kdy jsou u metod uvedeny typy. Opět si samozřejmě ukažme příklad, v němž je tato úprava provedena:
from typing_extensions import override class Fruit: def eat(self) -> None: pass class Apple(Fruit): @override def eat_apple(self) -> None: pass
V tomto případě již Mypy správně odhalí chybu:
override-4.py:9: error: Method "eat_apple" is marked as an override, but no base method was found with this name [misc] Found 1 error in 1 file (checked 1 source file)
13. Klíčové slovo type
S typy se v Pythonu pracuje již relativně dlouho – devět let. Ovšem teprve ve verzi 3.12 se objevuje klíčové slovo, které se přímo k typům váže. Toto slovo asi lehce uhádnete: type. To je možné použít pro zjednodušení deklarace nových datových typů. Použití tohoto klíčového slova je snadné, jak je to ostatně patrné i z následující ukázky, v níž nejdříve nadeklarujeme dva nové datové typy a posléze je použijeme při určení typů parametrů funkce nazvané print_score_table:
type Names = List[str] type Scores = List[int] def print_score_table(names: Names, scores: Scores) -> None: for name, score in zip(names, scores): print(name, score) print_score_table(["aa", "bb"], [1, 2])
Podobným způsobem je samozřejmě možné definovat různé typy, včetně typů odvozených:
type ID = int type Name = str type Surname = str type User = (ID, Name, Surname) u1:User = (42, "Linus", "Torvalds") print(u1)
14. Závěr
Python je stále živým a vyvíjejícím se programovacím jazykem. Není tedy divu, že se do něj postupně přidávají další konstrukce a rozšiřuje se jeho syntaxe i sémantika. Pravděpodobně největší změnou bylo přidání podpory pro strukturální pattern matching (což je po mnoha letech opět módní trend v IT), ale největší změny se dotýkají právě typového systému Pythonu. Jedná se o velmi zajímavý koncept, na druhou stranu se však nejde ubránit dojmu, že se už nejedná o snadno naučitelný jazyk – různých „vychytávek“ je už možná až příliš mnoho.
15. Příloha: překlad Pythonu 3.12
Vzhledem k tomu, že Python 3.12 prozatím nebývá nabízen všemi standardními distribucemi Linuxu, můžeme si provést jeho překlad, slinkování a popř. i instalaci. Nejedná se o nic složitého (otestováno na Fedoře a Mintu).
Nejdříve je nutné nainstalovat tooling jazyka C a některé použité knihovny:
$ sudo dnf install wget yum-utils make gcc openssl-devel bzip2-devel libffi-devel zlib-devel
Dále si stáhněte tarball se zdrojovými kódy Pythonu 3.12 (zde konkrétně se jedná o 3.12.1, v době psaní článku další setinkové verze nebyly k dispozici):
$ wget https://www.python.org/ftp/python/3.12.1/Python-3.12.1.tgz
Rozbalte stažený tarball příkazem:
$ tar xzf Python-3.12.1.tgz
Následuje přesun do adresáře se zdrojovými kódy a konfigurace na základě možností poskytovaných operačním systémem:
$ cd Python-3.12.1 $ ./configure --with-system-ffi --with-computed-gotos
Překlad je proveden příkazem make, kterému je vhodné předat počet paralelně běžících úloh. Ten by měl odpovídat počtu procesorových jader (například 16):
$ make -j 16
Výsledný binární soubor můžete zmenšit příkazem strip:
$ strip python $ ls -l -h python -rwxrwxr-x 1 ptisnovs ptisnovs 6,9M Dec 13 18:47 python
Nově přeložený interpret si můžeme ihned spustit:
$ ./python Python 3.12.1 (main, Dec 13 2023, 18:47:23) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Popř. si můžete právě přeložený interpret Python 3.12 nainstalovat do systému příkazem:
$ sudo make altinstall
Potom je spuštění mnohem jednodušší:
$ python3.12 Python 3.12.1 (main, Dec 13 2023, 18:47:23) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
16. 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 spuštění starších příklad je nutné mít nainstalován balíček mypy společně s Pythonem alespoň 3.7. Nové příklady (u nichž je to napsáno) vyžadují Python 3.12:
17. Odkazy na Internetu
- What’s New In Python 3.12 (official)
https://docs.python.org/3/whatsnew/3.12.html - What’s New In Python 3.12
https://dev.to/mahiuddindev/python-312–4n43 - PEP 698 – Override Decorator for Static Typing
https://peps.python.org/pep-0698/ - typing.override
https://docs.python.org/3/library/typing.html#typing.override - Type Hinting
https://realpython.com/lessons/type-hinting/ - mypy homepage
https://www.mypy-lang.org/ - mypy documentation
https://mypy.readthedocs.io/en/stable/ - Mypy na PyPi Optional static typing for Python
https://pypi.org/project/mypy/ - 5 Reasons Why You Should Use Type Hints In Python
https://www.youtube.com/watch?v=dgBCEB2jVU0 - Python Typing – Type Hints & Annotations
https://www.youtube.com/watch?v=QORvB-_mbZ0 - What Problems Can TypeScript Solve?
https://www.typescriptlang.org/why-create-typescript - How to find code that is missing type annotations?
https://stackoverflow.com/questions/59898490/how-to-find-code-that-is-missing-type-annotations - Do type annotations in Python enforce static type checking?
https://stackoverflow.com/questions/54734029/do-type-annotations-in-python-enforce-static-type-checking - Understanding type annotation in Python
https://blog.logrocket.com/understanding-type-annotation-python/ - Static type checking with Mypy — Perfect Python
https://www.youtube.com/watch?v=9gNnhNxra3E - Static Type Checker for Python
https://github.com/microsoft/pyright - Differences Between Pyright and Mypy
https://github.com/microsoft/pyright/blob/main/docs/mypy-comparison.md - 4 Python type checkers to keep your code clean
https://www.infoworld.com/article/3575079/4-python-type-checkers-to-keep-your-code-clean.html - Pyre: A performant type-checker for Python 3
https://pyre-check.org/ - „Typing the Untyped: Soundness in Gradual Type Systems“ by Ben Weissmann
https://www.youtube.com/watch?v=uJHD2×yv7×o - Covariance and contravariance (computer science)
https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science) - Functional Programming: Type Systems
https://www.youtube.com/watch?v=hy1wjkcIBCU - A Type System From Scratch – Robert Widmann
https://www.youtube.com/watch?v=IbjoA5×VUq0 - „Type Systems – The Good, Bad and Ugly“ by Paul Snively and Amanda Laucher
https://www.youtube.com/watch?v=SWTWkYbcWU0 - Type Systems: Covariance, Contravariance, Bivariance, and Invariance explained
https://medium.com/@thejameskyle/type-systems-covariance-contravariance-bivariance-and-invariance-explained-35f43d1110f8 - Statická vs. dynamická typová kontrola
https://www.root.cz/clanky/staticka-dynamicka-typova-kontrola/ - Typový systém
https://cs.wikipedia.org/wiki/Typov%C3%BD_syst%C3%A9m - Comparison of programming languages by type system
https://en.wikipedia.org/wiki/Comparison_of_programming_languages_by_type_system - Flow
https://flow.org/ - TypeScript
https://www.typescriptlang.org/ - Sorbet
https://sorbet.org/ - Pyright
https://github.com/microsoft/pyright - Mypy: Type hints cheat sheet
https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html - PEP 484 – Type Hints
https://peps.python.org/pep-0484/