Piąta część serii o zmianach w Pythonie. Ta część obejmuje wersję 3.15: leniwe importy, nowe typy wbudowane frozendict i sentinel, UTF-8 jako domyślne kodowanie i nowy profiler próbkujący. Na końcu podsumowanie kierunku zmian od 3.8.

Jawne leniwe importy

Wersja3.15
KategoriaSkładnia
PEPPEP 810
OpisMiękkie słowo kluczowe lazy odkłada załadowanie modułu do pierwszego użycia nazwy. Działa tylko na poziomie modułu. Tryb można też ustawić globalnie przez -X lazy_imports=all.

Zysk widać głównie w narzędziach CLI i dużych aplikacjach, w których większość importowanych modułów nie jest potrzebna w danym uruchomieniu. Przykładowo app --help nie musi ładować pandas ani sterownika bazy. Importy zostają na początku pliku, gdzie widać je razem z zależnościami, a narzędzia do analizy kodu nadal je rozpoznają.

Przed 3.15 import przenosiło się do wnętrza funkcji:

def export(data: dict) -> str:
    import json                 # ładowany dopiero przy pierwszym wywołaniu
    return json.dumps(data)

Od 3.15:

lazy import json
lazy from pathlib import Path
import sys

print("json" in sys.modules)    # False, moduł jeszcze nie załadowany
json.dumps({"ok": True})        # import następuje tutaj
print("json" in sys.modules)    # True

Rozpakowywanie w comprehension

Wersja3.15
KategoriaSkładnia
PEPPEP 798
Opis* i ** działają w listach, zbiorach, słownikach składanych i wyrażeniach generatorowych, co zastępuje podwójne for i itertools.chain().

Przed 3.15:

from itertools import chain

lists = [[1, 2], [3, 4], [5]]
[x for L in lists for x in L]       # [1, 2, 3, 4, 5]
list(chain.from_iterable(lists))    # [1, 2, 3, 4, 5]

dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
{k: v for d in dicts for k, v in d.items()}   # {'a': 3, 'b': 2}

Od 3.15:

lists = [[1, 2], [3, 4], [5]]
[*L for L in lists]             # [1, 2, 3, 4, 5]

dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
{**d for d in dicts}            # {'a': 3, 'b': 2}

frozendict

Wersja3.15
KategoriaBiblioteka
PEPPEP 814
OpisNiemodyfikowalny słownik wbudowany. Jest hashowalny, gdy klucze i wartości są hashowalne, więc może być kluczem innego słownika lub elementem zbioru. Nie dziedziczy po dict.

Przed 3.15 najbliższym odpowiednikiem był MappingProxyType, czyli widok tylko do odczytu, który zmienia się razem z oryginałem i nie jest hashowalny, gdy opakowuje zwykły dict:

from types import MappingProxyType

data = {"host": "localhost", "port": 5432}
config = MappingProxyType(data)
config["port"] = 6543           # TypeError
data["port"] = 6543             # config["port"] też jest teraz 6543
{config: "pula połączeń"}       # TypeError: unhashable type: 'dict'

Od 3.15:

config = frozendict(host="localhost", port=5432)
config["port"] = 6543           # TypeError: 'frozendict' object does not support item assignment

pools = {config: "pula połączeń"}   # hashowalny, więc może być kluczem

sentinel

Wersja3.15
KategoriaBiblioteka
PEPPEP 661
OpisWbudowany typ unikalnych wartości-znaczników z czytelnym repr, zachowujących tożsamość przy kopiowaniu i pickle.

Przed 3.15:

import copy

MISSING = object()

MISSING                             # <object object at 0x7f3a2c1e0b50>
copy.deepcopy(MISSING) is MISSING   # False, kopia to inny obiekt

Od 3.15:

import copy

MISSING = sentinel("MISSING")

def get(key: str, default=MISSING):
    if default is MISSING:
        raise KeyError(key)
    return default

MISSING                             # MISSING
copy.deepcopy(MISSING) is MISSING   # True

UTF-8 jako domyślne kodowanie

Wersja3.15
KategoriaInterpreter
PEPPEP 686
Opisopen() bez encoding używa UTF-8 niezależnie od ustawień systemu. Największa zmiana dotyczy Windowsa, gdzie dotąd domyślne było kodowanie regionalne (np. cp1250). Poprzednie zachowanie przywraca PYTHONUTF8=0.

Przed 3.15 na Windowsie:

import sys

sys.flags.utf8_mode                 # 0
open("dane.txt").encoding           # 'cp1250'
open("dane.txt", encoding="utf-8")  # UTF-8 tylko jawnie

Od 3.15:

import sys

sys.flags.utf8_mode                    # 1
open("dane.txt").encoding              # 'utf-8'
open("stare.txt", encoding="locale")   # świadomie kodowanie regionalne

Przy przejściu na 3.15 warto przejrzeć wywołania open() bez parametru encoding. Kod, który na Windowsie czytał pliki w cp1250, zacznie je czytać jako UTF-8. Jawne encoding="utf-8" (lub encoding="locale", dostępne od 3.10) daje to samo zachowanie na wszystkich wersjach.


Pakiet profiling i profiler Tachyon

Wersja3.15
KategoriaBiblioteka
PEPPEP 799
OpisProfiler próbkujący profiling.sampling podłącza się do działającego procesu bez restartu i praktycznie bez narzutu. Wynik zapisuje m.in. jako flame graph HTML lub w formacie Firefox Profilera.

Przed 3.15 biblioteka standardowa miała tylko profilery deterministyczne: cProfile (od 3.15 także jako profiling.tracing) i wolniejszy, czysto pythonowy profile, który w 3.15 oznaczono jako przestarzały. Profiler deterministyczny rejestruje każde wywołanie funkcji, więc wyraźnie spowalnia program, a profilowanie trzeba zaplanować przed uruchomieniem:

python -m cProfile -o profile.prof app.py

Od 3.15:

python -m profiling.sampling run --flamegraph -o profile.html app.py
python -m profiling.sampling attach --live 1234

Pliki .start

Wersja3.15
KategoriaInterpreter
PEPPEP 829
OpisJawny zamiennik linii import w plikach .pth, wykonywanych przy starcie interpretera.

Przed 3.15 kod startowy umieszczało się w pliku .pth jako linię zaczynającą się od import, wykonywaną przez exec():

# site-packages/mojpakiet.pth
../lib/mojpakiet
import mojpakiet.startup; mojpakiet.startup.init()

Od 3.15:

# site-packages/mojpakiet.pth – tylko ścieżki dodawane do sys.path
../lib/mojpakiet

# site-packages/mojpakiet.start – funkcje wywoływane przy starcie
mojpakiet.startup:init

TypedDict z dodatkowymi kluczami, TypeForm

Wersja3.15
KategoriaTypowanie
PEPPEP 728, PEP 747
OpisTypowane dodatkowe klucze w TypedDict i adnotowanie wyrażeń typów.

Przed 3.15 parametr przyjmujący typ dało się opisać tylko jako type[T], który nie obejmuje unii ani innych wyrażeń typów:

def trycast[T](typ: type[T], value: object) -> T | None: ...

trycast(int | None, "42")       # type checker: int | None to nie type[T]

Od 3.15:

from typing import TypedDict, TypeForm

class Movie(TypedDict, extra_items=int):
    name: str

movie: Movie = {"name": "Diuna", "year": 2021}   # OK: dodatkowe klucze typu int
bad: Movie = {"name": "Diuna", "tag": "sf"}      # type checker: str to nie int

def trycast[T](typ: TypeForm[T], value: object) -> T | None: ...

trycast(int | None, "42")       # type checker wie, że wynik to int | None

Podsumowanie

Kierunek zmian jest wyraźny. Składnia przejęła rozwiązania znane wcześniej z bibliotek i innych języków (match, generyki w nawiasach kwadratowych, t-stringi), a adnotacje typów stały się pełnoprawną częścią języka z własną składnią. Interpreter od 3.11 co roku przyspiesza. W 3.13 eksperymentalnie dostał tryb bez GIL i kompilator JIT, a w 3.14 subinterpretery w bibliotece standardowej i oficjalnie wspierany tryb bez GIL. Wersja 3.15 skupia się na wygodzie i przewidywalności: leniwe importy skracają start programów, frozendict wypełnia lukę wśród typów wbudowanych, a UTF-8 jako domyślne kodowanie usuwa różnice między systemami. Biblioteka standardowa rośnie ostrożnie: dochodzą moduły bez zewnętrznych zależności (zoneinfo, tomllib, compression.zstd, profiling), a przestarzałe moduły są usuwane.