Trzecia część serii o zmianach w Pythonie. Ta część obejmuje wersje 3.12 i 3.13: nową składnię generyków, instrukcję type, sformalizowane f-stringi i nowy interaktywny REPL.
Składnia parametrów typów
| Wersja | 3.12 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 695 |
| Opis | Generyczne funkcje i klasy deklaruje się w nawiasach kwadratowych, bez ręcznego TypeVar. |
Przed 3.12 parametr typu był zmienną modułu, a klasa generyczna musiała dziedziczyć po Generic:
from typing import Generic, TypeVar
T = TypeVar("T")
S = TypeVar("S", str, bytes)
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def first(items: list[T]) -> T:
return items[0]
def longest(a: S, b: S) -> S:
return a if len(a) >= len(b) else b
Od 3.12 parametry deklaruje się przy nazwie klasy lub funkcji, a ich zasięg jest ograniczony do tej definicji:
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def first[T](items: list[T]) -> T:
return items[0]
def longest[S: (str, bytes)](a: S, b: S) -> S: # S tylko str albo bytes
return a if len(a) >= len(b) else b
Instrukcja type
| Wersja | 3.12 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 695 |
| Opis | Definiuje aliasy typów, wyliczane leniwie. |
Przed 3.12 alias był zwykłym przypisaniem z adnotacją TypeAlias, a typ rekurencyjny wymagał cudzysłowów:
from typing import TypeAlias, TypeVar
T = TypeVar("T")
Pair: TypeAlias = tuple[T, T]
Json: TypeAlias = "dict[str, Json] | list[Json] | str | int | None"
Od 3.12:
type Pair[T] = tuple[T, T]
type Json = dict[str, Json] | list[Json] | str | int | None # rekurencja bez cudzysłowów
def swap[T](pair: Pair[T]) -> Pair[T]:
return pair[1], pair[0]
Pair.__value__ # tuple[T, T], wyliczone dopiero przy odczycie
Sformalizowane f-stringi
| Wersja | 3.12 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 701 |
| Opis | Wewnątrz {} można używać tych samych cudzysłowów co na zewnątrz, odwrotnych ukośników, komentarzy i wielu linii. |
Przed 3.12 wewnątrz {} trzeba było użyć innego rodzaju cudzysłowów, a odwrotny ukośnik wymagał zmiennej pomocniczej:
songs = [{"title": "Intro"}, {"title": "Outro"}]
print(f"Utwory: {', '.join(s['title'] for s in songs)}")
newline = "\n"
print(f"{newline.join(s['title'] for s in songs)}")
Od 3.12:
songs = [{"title": "Intro"}, {"title": "Outro"}]
print(f"Utwory: {", ".join(s["title"] for s in songs)}")
print(f"{"\n".join(s["title"] for s in songs)}")
total = f"{
sum(len(s["title"]) for s in songs) # komentarz w wyrażeniu
}"
typing.override
| Wersja | 3.12 |
|---|---|
| Kategoria | Typowanie |
| PEP | PEP 698 |
| Opis | Oznacza metodę nadpisującą metodę klasy bazowej. Type checker zgłasza błąd, gdy w bazie takiej metody nie ma. |
Dekorator chroni przed cichym rozjechaniem się hierarchii przy refaktoryzacji. Po zmianie nazwy metody w klasie bazowej metoda w podklasie przestaje ją nadpisywać, ale kod dalej działa i wywołuje implementację z bazy. Z @override type checker wskaże każdą podklasę, której metoda nie ma już odpowiednika w bazie.
from typing import override
class Base:
def save(self) -> None: ...
class User(Base):
@override
def save(self) -> None: ... # OK
@override
def sav(self) -> None: ... # type checker: w Base nie ma metody sav
Unpack[TypedDict] dla **kwargs
| Wersja | 3.12 |
|---|---|
| Kategoria | Typowanie |
| PEP | PEP 692 |
| Opis | Precyzyjne typowanie **kwargs: każdy argument nazwany ma własny typ. |
Najwięcej daje w funkcjach opakowujących, które przekazują opcje dalej (klient HTTP, wywołanie subprocess, fabryka połączeń). Zestaw opcji opisuje się raz jako TypedDict i używa go w wielu sygnaturach, bez powtarzania listy parametrów i bez gubienia typów w **kwargs.
Przed 3.12 adnotacja **kwargs określała jeden typ dla wszystkich argumentów, bez ich nazw:
def request(url: str, **kwargs: float) -> None: ...
request("https://example.com", timeout=5, retry=3) # OK dla type checkera, literówka niewykryta
Od 3.12:
from typing import NotRequired, TypedDict, Unpack
class Options(TypedDict):
timeout: float
retries: NotRequired[int]
def request(url: str, **kwargs: Unpack[Options]) -> None: ...
request("https://example.com", timeout=5) # OK
request("https://example.com", timeout=5, retry=3) # type checker: nieznany argument retry
itertools.batched()
| Wersja | 3.12 |
|---|---|
| Kategoria | Biblioteka |
| PEP | — |
| Opis | Dzielenie na paczki: batched("ABCDEFG", 3) → ABC, DEF, G. |
Przed 3.12:
from itertools import islice
def batched(iterable, n):
it = iter(iterable)
while chunk := tuple(islice(it, n)):
yield chunk
Od 3.12:
from itertools import batched
list(batched("ABCDEFG", 3)) # [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]
for chunk in batched(range(10_000), 500):
print(len(chunk)) # np. zapis do bazy po 500 wierszy
pathlib.Path.walk(), sys.monitoring
| Wersja | 3.12 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 669 (sys.monitoring) |
| Opis | Odpowiednik os.walk() dla Path oraz lekkie API monitorowania wykonania dla debuggerów i profilerów. |
Przed 3.12 do przejścia drzewa katalogów służył os.walk(), który zwraca ścieżki jako napisy:
import os
from pathlib import Path
for root, dirs, files in os.walk("src"):
dirs[:] = [d for d in dirs if d != "__pycache__"]
for name in files:
if name.endswith(".py"):
print(Path(root) / name)
Od 3.12 root jest od razu obiektem Path:
from pathlib import Path
for root, dirs, files in Path("src").walk():
dirs[:] = [d for d in dirs if d != "__pycache__"] # pomija katalogi
for name in files:
if name.endswith(".py"):
print(root / name)
sys.monitoring jest przeznaczony dla narzędzi: debuggerów, profilerów i mierników pokrycia kodu. Stare sys.settrace() i sys.setprofile() wywołują callback dla każdego zdarzenia w całym programie i spowalniają go wielokrotnie. Nowe API pozwala zapisać się tylko na wybrane zdarzenia, a callback może zwrócić sys.monitoring.DISABLE, żeby wyłączyć zdarzenie w danym miejscu kodu. Kilka narzędzi (np. debugger i coverage) działa naraz, bo każde ma własny identyfikator.
import sys
mon = sys.monitoring
calls = {}
def on_start(code, offset):
calls[code.co_name] = calls.get(code.co_name, 0) + 1
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
mon.use_tool_id(mon.PROFILER_ID, "licznik wywołań")
mon.register_callback(mon.PROFILER_ID, mon.events.PY_START, on_start)
mon.set_events(mon.PROFILER_ID, mon.events.PY_START)
fib(10)
mon.set_events(mon.PROFILER_ID, 0)
mon.free_tool_id(mon.PROFILER_ID)
print(calls) # {'fib': 177}
distutils
| Wersja | 3.12 |
|---|---|
| Kategoria | Usunięte |
| PEP | PEP 632 |
| Opis | Moduł usunięty. Jego rolę przejęły setuptools i inne backendy budowania. |
Nowy interaktywny REPL
| Wersja | 3.13 |
|---|---|
| Kategoria | Interpreter |
| PEP | — |
| Opis | REPL oparty na kodzie z PyPy: edycja wielu linii, historia całych bloków, kolorowe prompty i tracebacki, komendy exit i help bez nawiasów, tryb wklejania (F3). |
Do 3.12 samo exit tylko wypisywało podpowiedź:
$ python3.12
>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit
Od 3.13:
$ python3.13
>>> def greet(name):
... return f"Cześć, {name}!"
...
>>> greet("Ala")
'Cześć, Ala!'
>>> exit
Strzałka w górę przywraca całą funkcję do edycji, a nie pojedynczą linię. Stary REPL włącza zmienna PYTHON_BASIC_REPL=1.
warnings.deprecated
| Wersja | 3.13 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 702 |
| Opis | Dekorator, który zgłasza DeprecationWarning w czasie działania i jest rozpoznawany przez type checkery. |
Przed 3.13 ostrzeżenie zgłaszało się ręcznie w ciele funkcji, a type checker o nim nie wiedział:
import warnings
def connect() -> None:
warnings.warn("Użyj connect_v2()", DeprecationWarning, stacklevel=2)
Od 3.13:
from warnings import deprecated
@deprecated("Użyj connect_v2()")
def connect() -> None: ...
connect() # DeprecationWarning: Użyj connect_v2(), type checker też oznacza wywołanie
Domyślne parametry typów, TypeIs, ReadOnly
| Wersja | 3.13 |
|---|---|
| Kategoria | Typowanie |
| PEP | PEP 696, PEP 742, PEP 705 |
| Opis | Domyślne wartości parametrów typów, precyzyjniejsze zawężanie typu i pola tylko do odczytu w TypedDict. |
Przed 3.13 do własnych funkcji sprawdzających typ służył TypeGuard, który zawęża typ tylko w gałęzi if:
from typing import TypeGuard
def is_str(value: object) -> TypeGuard[str]:
return isinstance(value, str)
def handle(value: int | str) -> None:
if is_str(value):
value.upper() # tu value: str
else:
value + 1 # type checker: value nadal int | str
Od 3.13:
from typing import ReadOnly, TypedDict, TypeIs
class Box[T = int]: ... # Box bez parametru oznacza Box[int]
def is_str(value: object) -> TypeIs[str]:
return isinstance(value, str)
def handle(value: int | str) -> None:
if is_str(value):
value.upper() # tu value: str
else:
value + 1 # tu value: int
class Settings(TypedDict):
name: ReadOnly[str]
def rename(settings: Settings) -> None:
settings["name"] = "x" # type checker: pole tylko do odczytu
Usunięte moduły
| Wersja | 3.13 |
|---|---|
| Kategoria | Usunięte |
| PEP | PEP 594 |
| Opis | Usunięte: cgi, cgitb, crypt, telnetlib, nntplib, audioop i inne. smtpd zniknął już w 3.12. |