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
| Wersja | 3.14 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 750 |
| Opis | Literał 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, <script>alert(1)</script>!</p>'
Leniwe wyliczanie adnotacji
| Wersja | 3.14 |
|---|---|
| Kategoria | Typowanie |
| PEP | PEP 649, PEP 749 |
| Opis | Adnotacje 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 758 |
| Opis | except 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Składnia |
| PEP | PEP 765 |
| Opis | Kompilator 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 784 |
| Opis | Kompresja 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 734 |
| Opis | Moduł 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Biblioteka |
| PEP | PEP 768 |
| Opis | Bezpieczny 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()
| Wersja | 3.14 |
|---|---|
| Kategoria | Biblioteka |
| PEP | — |
| Opis | UUID 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Biblioteka |
| PEP | — |
| Opis | Funkcje 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Interpreter |
| PEP | — |
| Opis | Kolorowanie 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
| Wersja | 3.14 |
|---|---|
| Kategoria | Interpreter |
| PEP | PEP 779 |
| Opis | Tryb 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