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

Wersja3.10
KategoriaSkładnia
PEPPEP 634, PEP 635, PEP 636
OpisTo nie jest switch z C. Wzorce rozkładają sekwencje, słowniki i obiekty, przypisują fragmenty do zmiennych i obsługują warunki if.
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

Wersja3.10
KategoriaTypowanie
PEPPEP 604
Opisint | None zamiast Optional[int], działa też w isinstance().
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

Wersja3.10
KategoriaSkładnia
PEP—
OpisKilka with w jednej instrukcji można rozpisać na wiele linii, ujmując je w nawiasy.
with (
    open("wejscie.txt") as src,
    open("wyjscie.txt", "w") as dst,
):
    dst.write(src.read())

zip(..., strict=True)

Wersja3.10
KategoriaBiblioteka
PEPPEP 618
OpisZgłasza ValueError, gdy sekwencje mają różną długość.
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()

Wersja3.10
KategoriaBiblioteka
PEP—
OpisKolejne pary elementów: ABCD → AB, BC, CD.
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)

Wersja3.10
KategoriaBiblioteka
PEP—
OpisDataclassy ze __slots__ i polami przekazywanymi wyłącznie po nazwie.
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()

Wersja3.10
KategoriaBiblioteka
PEP—
OpisWyszukiwanie binarne po kluczu oraz wbudowane odpowiedniki iter() i next() dla iteratorów asynchronicznych.
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

Wersja3.10
KategoriaTypowanie
PEPPEP 612
OpisTypowanie dekoratorów, które zachowują sygnaturę dekorowanej funkcji. Wcześniej Callable[..., R] gubił typy argumentów.
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

Wersja3.10
KategoriaTypowanie
PEPPEP 613
OpisJawne oznaczenie aliasu typu, żeby type checker nie wziął go za zwykłą zmienną. Od 3.12 zastępuje go instrukcja type.
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

Wersja3.10
KategoriaTypowanie
PEPPEP 647
OpisWłasne funkcje zawężające typ. Gdy zwrócą True, type checker traktuje argument jako typ podany w 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

Wersja3.10
KategoriaInterpreter
PEP—
OpisKomunikaty wskazują dokładne miejsce problemu, np. niezamknięty nawias zamiast błędu w następnej linii, oraz podpowiadają nazwy przy literówkach.
$ 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*

Wersja3.11
KategoriaSkładnia
PEPPEP 654
OpisExceptionGroup przenosi wiele wyjątków naraz, a except* obsługuje każdy typ z grupy osobno. Głównym odbiorcą jest kod współbieżny.
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

Wersja3.11
KategoriaBiblioteka
PEPPEP 678
Opisexc.add_note("kontekst") dopisuje informację, która pojawia się w tracebacku.
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

Wersja3.11
KategoriaBiblioteka
PEP—
OpisStrukturalna współbieżność. Błąd jednego zadania anuluje pozostałe, a wszystkie błędy trafiają do jednej ExceptionGroup.
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()

Wersja3.11
KategoriaBiblioteka
PEP—
OpisMenedżer kontekstu z limitem czasu dla bloku kodu asynchronicznego.
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

Wersja3.11
KategoriaBiblioteka
PEPPEP 680
OpisOdczyt plików TOML, np. pyproject.toml. Zapisu nie ma.
import tomllib

with open("pyproject.toml", "rb") as f:
    project = tomllib.load(f)
print(project["project"]["requires-python"])

enum.StrEnum

Wersja3.11
KategoriaBiblioteka
PEP—
OpisEnum, którego wartości są napisami. auto() nadaje wartość równą nazwie zapisanej małymi literami.
from enum import StrEnum, auto

class Color(StrEnum):
    RED = auto()
    GREEN = auto()

Color.RED == "red"          # True
f"kolor: {Color.GREEN}"     # 'kolor: green'

contextlib.chdir()

Wersja3.11
KategoriaBiblioteka
PEP—
OpisMenedżer kontekstu tymczasowo zmieniający katalog roboczy. Po wyjściu z bloku przywraca poprzedni, także po wyjątku.
import os
from contextlib import chdir

with chdir("/tmp"):
    print(os.getcwd())      # /tmp
print(os.getcwd())          # z powrotem poprzedni katalog

typing.Self

Wersja3.11
KategoriaTypowanie
PEPPEP 673
OpisTyp bieżącej instancji. Metoda zwracająca self w podklasie ma typ podklasy, bez ręcznego TypeVar związanego z klasą.

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

Wersja3.11
KategoriaTypowanie
PEPPEP 675
OpisNapis złożony wyłącznie z literałów w kodzie. Chroni przed wstrzyknięciem danych użytkownika, np. do zapytań SQL.
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

Wersja3.11
KategoriaTypowanie
PEPPEP 655
OpisOznaczanie pojedynczych kluczy TypedDict jako wymaganych lub opcjonalnych, bez dzielenia klasy na dwie z total=False.
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

Wersja3.11
KategoriaTypowanie
PEPPEP 646
OpisGeneryki o zmiennej liczbie parametrów typu, np. kształt tablicy w bibliotekach numerycznych.
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

Wersja3.11
KategoriaInterpreter
PEPPEP 659
OpisAdaptacyjny interpreter ze specjalizacją bajtkodu. Według dokumentacji 3.11 jest średnio o 25% szybszy od 3.10, w zależności od kodu od 10% do 60%.
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

Wersja3.11
KategoriaInterpreter
PEPPEP 657
OpisTraceback wskazuje konkretne wyrażenie w linii, które spowodowało błąd.
Traceback (most recent call last):
  File "app.py", line 2, in <module>
    print(user["address"]["city"].upper())
          ~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable