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
| Wersja | 3.15 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 810 |
| Opis | Mię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
| Wersja | 3.15 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 814 |
| Opis | Niemodyfikowalny 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 661 |
| Opis | Wbudowany 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Interpreter |
| PEP | PEP 686 |
| Opis | open() 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 799 |
| Opis | Profiler 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Interpreter |
| PEP | PEP 829 |
| Opis | Jawny 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
| Wersja | 3.15 |
|---|---|
| Kategoria | Typowanie |
| PEP | PEP 728, PEP 747 |
| Opis | Typowane 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.