Druga część serii o zmianach w Pythonie. Ta część obejmuje wersje 3.10 i 3.11: dopasowanie wzorców match/case, unie typów X | Y, grupy wyjątków i przyspieszenie interpretera.
Strukturalne dopasowanie wzorców match/case
def handle(command: str) -> str:
match command.split():
case ["go", ("north" | "south") as direction]:
return f"idę na {direction}"
case ["get", item, *rest]:
return f"podnoszę {item}, reszta: {rest}"
case ["quit" | "exit"]:
return "koniec"
case _:
return "nieznana komenda"
handle("go north") # 'idę na north'
handle("get klucz mapa") # "podnoszę klucz, reszta: ['mapa']"
Wzorce klas dopasowują atrybuty obiektów, a strażnik if dodaje warunek:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def where(p: Point) -> str:
match p:
case Point(x=0, y=0):
return "środek"
case Point(x=0, y=y):
return f"oś Y, y={y}"
case Point(x=x, y=y) if x == y:
return "przekątna"
case _:
return "gdzie indziej"
Unie typów X | Y
def parse_port(value: str) -> int | None:
return int(value) if value.isdigit() else None
isinstance(3.5, int | float) # True
Menedżery kontekstu w nawiasach
with (
open("wejscie.txt") as src,
open("wyjscie.txt", "w") as dst,
):
dst.write(src.read())
zip(..., strict=True)
names = ["Ala", "Ola", "Ela"]
ages = [30, 25]
list(zip(names, ages)) # [('Ala', 30), ('Ola', 25)], Ela znika po cichu
list(zip(names, ages, strict=True)) # ValueError: zip() argument 2 is shorter than argument 1
itertools.pairwise()
from itertools import pairwise
temps = [12, 15, 11, 18]
[b - a for a, b in pairwise(temps)] # [3, -4, 7]
dataclass(slots=True, kw_only=True)
from dataclasses import dataclass
@dataclass(slots=True, kw_only=True)
class Config:
host: str
port: int = 5432
cfg = Config(host="db", port=6543)
Config("db") # TypeError: ... takes 1 positional argument but 2 were given
cfg.timeout = 5 # AttributeError: __slots__ blokuje nowe atrybuty
bisect z parametrem key, aiter() i anext()
import asyncio
from bisect import bisect_left
users = [("Ala", 25), ("Ola", 30), ("Ela", 41)] # posortowane po wieku
bisect_left(users, 30, key=lambda u: u[1]) # 1
async def ticks():
for i in range(3):
yield i
async def main():
it = aiter(ticks())
print(await anext(it)) # 0
print([x async for x in it]) # [1, 2]
print(await anext(it, "koniec")) # koniec
asyncio.run(main())
typing.ParamSpec
from functools import wraps
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def logged(fn: Callable[P, R]) -> Callable[P, R]: # sygnatura fn zostaje zachowana
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"wywołanie {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
@logged
def add(a: int, b: int) -> int:
return a + b
add(1, 2) # OK
add("1", 2) # type checker: "1" to nie int
typing.TypeAlias
from typing import TypeAlias
Coords: TypeAlias = tuple[float, float]
Handler: TypeAlias = "Callable[[str], None]" # alias z referencją w przód
def distance(a: Coords, b: Coords) -> float: ...
typing.TypeGuard
from typing import TypeGuard
def is_str_list(values: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(v, str) for v in values)
def join(values: list[object]) -> str:
if is_str_list(values):
return ", ".join(values) # tu type checker wie, że to list[str]
return ""
Lepsze komunikaty błędów
$ python app.py
File "app.py", line 1
data = {"a": 1, "b": 2,
^
SyntaxError: '{' was never closed
>>> length = 3
>>> print(lenght)
NameError: name 'lenght' is not defined. Did you mean: 'length'?
Grupy wyjątków i except*
try:
raise ExceptionGroup("błędy walidacji", [
ValueError("zły wiek"),
TypeError("brak imienia"),
ValueError("zły e-mail"),
])
except* ValueError as group:
print(len(group.exceptions), "błędy wartości") # 2 błędy wartości
except* TypeError as group:
print("TypeError:", group.exceptions[0]) # TypeError: brak imienia
Notatki do wyjątków
def parse(line_no: int, text: str) -> int:
try:
return int(text)
except ValueError as exc:
exc.add_note(f"linia {line_no} pliku dane.csv")
raise
parse(7, "abc")
# ValueError: invalid literal for int() with base 10: 'abc'
# linia 7 pliku dane.csv
asyncio.TaskGroup
import asyncio
async def fetch(n: int) -> int:
if n % 2:
raise ValueError(f"zadanie {n} nie powiodło się")
await asyncio.sleep(0.1)
return n
async def main():
try:
async with asyncio.TaskGroup() as tg:
for n in range(4):
tg.create_task(fetch(n))
except* ValueError as group:
for exc in group.exceptions:
print("ValueError:", exc) # zadanie 1 ..., zadanie 3 ...
asyncio.run(main())
asyncio.timeout()
import asyncio
async def main():
try:
async with asyncio.timeout(1):
await asyncio.sleep(5)
except TimeoutError:
print("przekroczono limit 1 s")
asyncio.run(main())
tomllib
import tomllib
with open("pyproject.toml", "rb") as f:
project = tomllib.load(f)
print(project["project"]["requires-python"])
enum.StrEnum
from enum import StrEnum, auto
class Color(StrEnum):
RED = auto()
GREEN = auto()
Color.RED == "red" # True
f"kolor: {Color.GREEN}" # 'kolor: green'
contextlib.chdir()
import os
from contextlib import chdir
with chdir("/tmp"):
print(os.getcwd()) # /tmp
print(os.getcwd()) # z powrotem poprzedni katalog
typing.Self
Przed 3.11 trzeba było zdefiniować TypeVar związany z klasą bazową i adnotować nim self w każdej metodzie:
from typing import TypeVar
TBuilder = TypeVar("TBuilder", bound="Builder")
class Builder:
def set(self: TBuilder, key: str, value: str) -> TBuilder:
return self
Od 3.11 wystarczy Self:
from typing import Self
class Builder:
def set(self, key: str, value: str) -> Self:
return self
class HtmlBuilder(Builder):
def tag(self, name: str) -> Self:
return self
HtmlBuilder().set("lang", "pl").tag("p") # OK: set() zwraca HtmlBuilder, nie Builder
typing.LiteralString
from typing import LiteralString
def query(sql: LiteralString) -> None: ...
def run(table: str) -> None:
query("SELECT * FROM users") # OK
query(f"SELECT * FROM {table}") # type checker: str to nie LiteralString
Required i NotRequired
from typing import NotRequired, Required, TypedDict
class Movie(TypedDict):
title: str
year: NotRequired[int]
movie: Movie = {"title": "Diuna"} # OK, klucz year jest opcjonalny
class Patch(TypedDict, total=False):
id: Required[int] # wymagany mimo total=False
title: str
typing.TypeVarTuple
from typing import Generic, TypeVarTuple
Shape = TypeVarTuple("Shape")
class Array(Generic[*Shape]): ...
image: Array[int, int, int] # dowolna liczba parametrów typu
vector: Array[int]
Ts = TypeVarTuple("Ts")
def args_to_tuple(*args: *Ts) -> tuple[*Ts]:
return args
args_to_tuple(1, "a") # type checker: tuple[int, str]
Faster CPython
import dis
def add(a, b):
return a + b
for _ in range(1000):
add(1, 2)
dis.dis(add, adaptive=True)
# ...
# BINARY_OP_ADD_INT ← ogólne BINARY_OP wyspecjalizowane pod dodawanie liczb całkowitych
Przyspieszenie nie wymaga żadnych zmian w kodzie ani flag, wystarczy uruchomić program na 3.11 lub nowszym. Interpreter obserwuje, jakie typy trafiają do danej instrukcji, i po kilku wykonaniach zamienia ją na wersję wyspecjalizowaną, np. dodawanie dwóch int. Gdy typy się zmienią, wraca do wersji ogólnej.
dis.dis(..., adaptive=True) przydaje się więc głównie do diagnozy: pokazuje, czy gorąca funkcja została wyspecjalizowana. Największy zysk daje kod, w którym te same miejsca dostają za każdym razem dane tego samego typu. Funkcja raz dodająca liczby, a raz napisy, specjalizuje się gorzej.
Dokładne lokalizacje błędów w tracebackach
Traceback (most recent call last):
File "app.py", line 2, in <module>
print(user["address"]["city"].upper())
~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable