Czwarta część serii o zmianach w Pythonie. Ta część obejmuje wersję 3.14: t-stringi, leniwe adnotacje, subinterpretery w bibliotece standardowej, kompresję Zstandard i oficjalnie wspierany tryb bez GIL.

T-stringi

Wersja3.14
KategoriaSkładnia
PEPPEP 750
OpisLiterał t"..." ma składnię f-stringa, ale zamiast gotowego str zwraca obiekt Template z osobnymi częściami statycznymi i wartościami.

F-string skleja tekst od razu, więc o bezpieczeństwie decyduje wywołujący: wystarczy jedno zapomniane escape() w szablonie HTML albo wartość wklejona do zapytania SQL. T-string oddaje części statyczne i wartości osobno funkcji przetwarzającej (silnikowi szablonów, sterownikowi bazy, loggerowi). To ona decyduje, jak je połączyć: zamienia znaki specjalne HTML na encje, zamienia wartości na parametry zapytania albo formatuje je dopiero wtedy, gdy wpis logu faktycznie zostanie zapisany.

Przed 3.14:

from html import escape

user = "<script>alert(1)</script>"
f"<p>Witaj, {escape(user)}!</p>"    # escape trzeba pamiętać przy każdej wartości

Od 3.14:

from html import escape
from string.templatelib import Interpolation, Template

def html(template: Template) -> str:
    parts = []
    for item in template:
        if isinstance(item, Interpolation):
            parts.append(escape(str(item.value)))
        else:
            parts.append(item)
    return "".join(parts)

user = "<script>alert(1)</script>"
html(t"<p>Witaj, {user}!</p>")   # '<p>Witaj, &lt;script&gt;alert(1)&lt;/script&gt;!</p>'

Leniwe wyliczanie adnotacji

Wersja3.14
KategoriaTypowanie
PEPPEP 649, PEP 749
OpisAdnotacje są wyliczane dopiero przy odczycie, więc typ może odwoływać się do klasy zdefiniowanej później. Do odczytu służy nowy moduł annotationlib.

Przed 3.14 odwołanie do klasy, która jeszcze nie istnieje, wymagało cudzysłowów albo from __future__ import annotations:

class Node:
    def __init__(self, next: "Node | None" = None) -> None:   # bez cudzysłowów: NameError
        self.next = next

Od 3.14:

import annotationlib

class Node:
    def __init__(self, next: Node | None = None) -> None:
        self.next = next

def load(config: Config) -> None: ...   # Config jeszcze nie istnieje i to nie jest błąd

annotationlib.get_annotations(load, format=annotationlib.Format.FORWARDREF)
# {'config': ForwardRef('Config', ...), 'return': None}

except bez nawiasów

Wersja3.14
KategoriaSkładnia
PEPPEP 758
Opisexcept ValueError, TypeError: przy kilku typach, o ile nie ma as.

Przed 3.14:

try:
    value = int(text)
except (ValueError, TypeError):
    value = 0

Od 3.14:

try:
    value = int(text)
except ValueError, TypeError:
    value = 0

Ostrzeżenie dla return, break i continue w finally

Wersja3.14
KategoriaSkładnia
PEPPEP 765
OpisKompilator zgłasza dla nich SyntaxWarning, bo potrafią po cichu połknąć wyjątek.
def save():
    try:
        raise ValueError("ważny błąd")
    finally:
        return "ok"     # SyntaxWarning: 'return' in a 'finally' block

save()   # 'ok', a ValueError zniknął bez śladu

compression.zstd

Wersja3.14
KategoriaBiblioteka
PEPPEP 784
OpisKompresja Zstandard. Nowy pakiet compression grupuje też istniejące moduły (compression.gzip, compression.lzma itd.).

Przed 3.14 potrzebny był zewnętrzny pakiet zstandard:

import zstandard

packed = zstandard.ZstdCompressor().compress(b"python " * 1000)

Od 3.14:

from compression import zstd

raw = b"python " * 1000
packed = zstd.compress(raw)
print(len(raw), len(packed))          # 7000 25
assert zstd.decompress(packed) == raw

Subinterpretery w bibliotece standardowej

Wersja3.14
KategoriaBiblioteka
PEPPEP 734
OpisModuł concurrent.interpreters i concurrent.futures.InterpreterPoolExecutor: wiele interpreterów w jednym procesie, każdy z własnym GIL.

Subinterpretery leżą pomiędzy wątkami a procesami. Podobnie jak procesy, wykonują kod Pythona równolegle i mają odizolowany stan (własne moduły, obiekty i GIL). Działają jednak w jednym procesie systemu, więc startują szybciej i zużywają mniej pamięci niż osobne procesy. W przeciwieństwie do trybu bez GIL działają w zwykłej kompilacji Pythona. Ograniczenie: rozszerzenia C muszą obsługiwać wiele interpreterów, a dane między nimi przechodzą przez kopiowanie lub kolejki (interpreters.create_queue()).

Przed 3.14 równoległe obliczenia CPU wymagały osobnych procesów:

from concurrent.futures import ProcessPoolExecutor

def cpu(n: int) -> int:
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=4) as pool:
        print(list(pool.map(cpu, [1_000_000] * 4)))

Od 3.14:

from concurrent import interpreters
from concurrent.futures import InterpreterPoolExecutor

def cpu(n: int) -> int:
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    interp = interpreters.create()
    interp.exec("print('pozdrowienia z subinterpretera')")
    print(interp.call(cpu, 10))      # 285
    interp.close()

    with InterpreterPoolExecutor(max_workers=4) as pool:   # każdy z własnym GIL
        print(list(pool.map(cpu, [1_000_000] * 4)))

Podłączanie do działającego procesu

Wersja3.14
KategoriaBiblioteka
PEPPEP 768
OpisBezpieczny interfejs dla zewnętrznych debuggerów: python -m pdb -p PID oraz python -m asyncio ps PID / pstree PID pokazujące zadania asyncio.

Ta funkcja najwięcej daje przy procesie, który zawiesił się albo zachowuje się dziwnie na produkcji i którego nie można zrestartować bez utraty stanu. Debugger podłącza się do niego w bezpiecznym punkcie wykonania, bez modyfikowania kodu. Dotąd wymagało to gdb z rozszerzeniami Pythona albo zewnętrznych narzędzi, które czytają pamięć procesu.

Przed 3.14 do kodu trzeba było wcześniej dodać punkt przerwania i uruchomić proces od nowa:

breakpoint()        # pdb startuje dopiero, gdy wykonanie tu dotrze

Od 3.14:

python app.py &                    # [1] 4321
python -m pdb -p 4321              # debugger w działającym procesie
python -m asyncio pstree 4321      # drzewo zadań asyncio

Z poziomu Pythona to samo umożliwia sys.remote_exec(pid, "skrypt.py"), który wykonuje plik w docelowym procesie.


uuid.uuid7()

Wersja3.14
KategoriaBiblioteka
PEP—
OpisUUID uporządkowane czasowo, dobre jako klucz w bazie. Doszły też uuid6() i uuid8().

Przed 3.14 standardem był losowy uuid4(). Kolejne wartości trafiają w losowe miejsca indeksu B-drzewa, co przy kluczu głównym spowalnia wstawianie:

import uuid

a = uuid.uuid4()
b = uuid.uuid4()
a < b          # True albo False, kolejność przypadkowa

Od 3.14:

import uuid

a = uuid.uuid7()
b = uuid.uuid7()
a < b          # True, kolejność zgodna z czasem utworzenia
a.version      # 7
str(a)         # '01a1b1f5-69c9-727a-bec6-01be832e7e78'

Kopiec maksymalny w heapq

Wersja3.14
KategoriaBiblioteka
PEP—
OpisFunkcje heapify_max(), heappush_max(), heappop_max() i pokrewne.

Przed 3.14 kopiec maksymalny budowało się z zanegowanych wartości:

import heapq

tasks = [-t for t in [3, 1, 4, 1, 5]]
heapq.heapify(tasks)
-heapq.heappop(tasks)          # 5
heapq.heappush(tasks, -9)
-tasks[0]                      # 9

Od 3.14:

import heapq

tasks = [3, 1, 4, 1, 5]
heapq.heapify_max(tasks)
heapq.heappop_max(tasks)       # 5
heapq.heappush_max(tasks, 9)
tasks[0]                       # 9, największy element na szczycie

Kolory w REPL i narzędziach CLI

Wersja3.14
KategoriaInterpreter
PEP—
OpisKolorowanie składni w REPL oraz kolorowe wyjście json, argparse, unittest, calendar.
python -m json data.json            # kolorowany JSON (zamiast json.tool)
python -m calendar                  # kalendarz z wyróżnionym dniem dzisiejszym
python -m unittest                  # zielone OK, czerwone FAIL
PYTHON_COLORS=0 python -m unittest  # bez kolorów

W argparse kolorową pomoc kontroluje parametr ArgumentParser(color=True), domyślnie włączony.


Oficjalny tryb bez GIL i JIT w binarkach

Wersja3.14
KategoriaInterpreter
PEPPEP 779
OpisTryb bez GIL, wprowadzony eksperymentalnie w 3.13, przestał być eksperymentalny i jest oficjalnie wspierany, choć nadal jako osobna kompilacja (python3.14t). Oficjalne binarki dla Windows i macOS zawierają eksperymentalny JIT, włączany zmienną PYTHON_JIT=1.
python3.14t -c "import sys; print(sys._is_gil_enabled())"     # False

python3.14 -c "import sys; print(sys._jit.is_enabled())"      # False
PYTHON_JIT=1 python3.14 -c "import sys; print(sys._jit.is_enabled())"   # True