Przejdź do treści
← wszystkie przedmioty

Python dla inżynierów: głęboka semantyka 3.12-3.14

Zaawansowana, aktualna semantyka Pythona 3.12-3.14: GIL i współbieżność, generatory, dekoratory, dopasowanie wzorców, modelowanie danych, typowanie statyczne i wydajność dla praktykujących inżynierów.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • Mutowalne argumenty domyślne: dlaczego ewaluują się raz i jak tego uniknąć wzorcem None-sentinel.
  • GIL i free-threaded build (PEP 703/779): kiedy threading, multiprocessing, a kiedy asyncio.
  • Generatory vs listy: leniwa ewaluacja i pamięć O(1) kontra O(n).
  • Dekoratory sparametryzowane i functools.wraps dla poprawnej introspekcji.
  • match/case: capture pattern, wzorce klas/sekwencji/mapowań, guard i OR-pattern.
  • Modelowanie danych (dataclass/NamedTuple/Pydantic) i exception groups (except*).

1 · Domyślne argumenty i wyrażenia przypisania

★ egzamin

Argument domyślny w Pythonie jest ewaluowany dokładnie raz, w momencie wykonania instrukcji def, a nie przy każdym wywołaniu funkcji. Rezultat tej jednorazowej ewaluacji trafia do atrybutu __defaults__ obiektu funkcji i jest reużywany przy każdym wywołaniu bez podanego argumentu. Dla typów mutowalnych (list, dict, set) to prosta droga do buga produkcyjnego: różne wywołania dzielą ten sam obiekt.

Pułapka
Klasyczna pułapka: def add_item(item, bucket=[]): bucket.append(item); return bucket. Każde wywołanie bez drugiego argumentu mutuje tę samą listę utworzoną raz przy definicji funkcji, kolejne wywołania widzą śmieci z poprzednich.
Bug i poprawka: mutowalny argument domyślny python
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

add_item("a")  # ['a']
add_item("b")  # ['a', 'b'] - ta sama lista, nie nowa!

def add_item_fixed(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

add_item_fixed("a")  # ['a']
add_item_fixed("b")  # ['b'] - nowa lista przy kazdym wywolaniu
Dlaczego tak działa
Definicja funkcji to zwykła instrukcja wykonywana raz przy imporcie modułu. Wyrażenie w sygnaturze (np. []) jest ewaluowane wtedy i zapisane jako pojedynczy obiekt w function.__defaults__, współdzielony między wszystkimi wywołaniami.
Termin kluczowy
Walrus operator (:=) - Wyrażenie przypisania wprowadzone w PEP 572 (Python 3.8): pozwala przypisać wartość do zmiennej i użyć jej w tym samym wyrażeniu, np. wewnątrz warunku while, if czy listy składanej, bez osobnej linii przypisania.
Walrus operator w pętli i comprehension python
# bez walrusa: dwa odczyty tej samej wartosci
chunk = file.read(8192)
while chunk:
    process(chunk)
    chunk = file.read(8192)

# z walrusem: jeden odczyt, czytelniejszy warunek
while (chunk := file.read(8192)):
    process(chunk)

# w liscie skladanej: policz raz, uzyj dwa razy
results = [y for x in data if (y := expensive(x)) is not None]
Pułapka
Zmienna przypisana operatorem := wewnątrz listy składanej wiąże się w zasięgu otaczającej funkcji (lub modułu), nie w prywatnym zasięgu comprehension jak zwykła zmienna pętli for. To celowe zachowanie z PEP 572, ale zaskakuje: po wykonaniu powyższego przykładu zmienna y istnieje poza listą składaną.
  • Używaj walrusa gdy unikasz podwójnej ewaluacji tego samego wyrażenia (np. odczyt, regex match)
  • Nie nadużywaj go do upychania wielu przypisań w jednej linii kosztem czytelności
  • W warunkach while i if wewnątrz comprehension jest to idiomatyczne, w zwykłym kodzie liniowym zwykłe przypisanie bywa czytelniejsze
Mnemonik
Domyślny argument mutowalny to wspólna szafka firmowa: kto pierwszy coś w niej zostawi, każdy kolejny to zastanie. None-sentinel to własna szafka na każde wejście.

2 · GIL, wątki, procesy i asyncio

★ egzamin

CPython (referencyjna implementacja) chroni swoje wewnętrzne struktury danych jednym globalnym zamkiem: GIL (Global Interpreter Lock). W danym momencie tylko jeden wątek może wykonywać bytecode Pythona, niezależnie od liczby rdzeni CPU. To nie jest szczegół implementacyjny bez znaczenia, to fundamentalnie kształtuje wybór modelu współbieżności.

Termin kluczowy
GIL (Global Interpreter Lock) - Blokada wewnątrz interpretera CPython serializująca wykonanie bytecode Pythona do jednego wątku naraz. Chroni liczniki referencji i struktury interpretera przed race condition bez potrzeby drobnoziarnistego blokowania w każdym obiekcie.
PEP 703 / PEP 779, stan na 2026
Free-threaded build (interpreter bez GIL, opcjonalny) przeszedł fazę eksperymentalną w Python 3.13 (opt-in, binarka python3.13t) do statusu oficjalnie wspieranego w Python 3.14 zgodnie z PEP 779. GIL wciąż jest domyślnie włączony, standardowa binarka python3.14 dalej go używa; przejście na domyślny brak GIL to faza 3 planu PEP 703, bez ustalonej daty.
Pułapka
Powszechne mylne przekonanie: 'Python 3.13/3.14 usunęły GIL'. Nieprawda, usunięcie GIL wymaga świadomej instalacji free-threaded builda (np. python3.13t) i sprawdzenia, czy używane rozszerzenia C są z nim kompatybilne. Domyślna instalacja Pythona nadal ma GIL włączony.
Sprawdzenie stanu GIL w free-threaded buildzie (3.13+) python
import sys

# w standardowym buildzie atrybut moze nie istniec / zawsze True
if hasattr(sys, "_is_gil_enabled"):
    print(sys._is_gil_enabled())  # False w binarce python3.13t / 3.14t bez ext. wymuszajacych GIL
threading: dobre dla I/O-bound (GIL zwalniany na czas blokujacego wywolania) python
from concurrent.futures import ThreadPoolExecutor
import httpx

def fetch_size(url: str) -> int:
    return len(httpx.get(url).content)

urls = ["https://example.com"] * 20
with ThreadPoolExecutor(max_workers=20) as pool:
    sizes = list(pool.map(fetch_size, urls))
multiprocessing: realne rownolegle CPU dla zadan CPU-bound python
from concurrent.futures import ProcessPoolExecutor

def is_prime(n: int) -> bool:
    return n > 1 and all(n % i for i in range(2, int(n ** 0.5) + 1))

# guard obowiazkowy: worker (spawn, domyslny na macOS/Windows) re-importuje ten modul
if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        results = list(pool.map(is_prime, range(10_000_000, 10_000_200)))
asyncio: tysiace wspolbieznych operacji I/O w jednym watku python
import asyncio
import httpx

async def fetch_size(client: httpx.AsyncClient, url: str) -> int:
    response = await client.get(url)
    return len(response.content)

async def main(urls: list[str]) -> list[int]:
    async with httpx.AsyncClient() as client:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(fetch_size(client, u)) for u in urls]
    return [t.result() for t in tasks]

asyncio.run(main(["https://example.com"] * 1000))
ModelNajlepszy dlaJednostka izolacjiTypowy narzutKluczowe ograniczenie
threadingI/O-bound (sieć, dysk, DB)wątek OS, współdzielona pamięćniskiGIL serializuje bytecode - brak realnego przyspieszenia CPU-bound
multiprocessingCPU-bound (obliczenia, ML preprocessing)proces OS, osobna pamięćwysoki (start procesu, pickling, IPC)brak współdzielonego stanu, komunikacja przez kolejki/pickle
asynciomasowa współbieżność I/O (tysiące połączeń)coroutine w jednej pętli zdarzeńbardzo niskiwymaga bibliotek async-native, blokujące wywołanie zawiesza całą pętlę
Pułapka
Wywołanie blokującej funkcji synchronicznej (np. time.sleep, requests.get) wewnątrz coroutine asyncio zamraża całą pętlę zdarzeń, nie tylko bieżące zadanie. Każda operacja I/O w kodzie async musi mieć wariant async-native (httpx.AsyncClient, asyncpg) albo być odpalona w wątku przez asyncio.to_thread.
Mnemonik
threading = wspólne biurko, wielu ludzi, jeden długopis (GIL) do pisania kodu Pythona na raz. multiprocessing = osobne biurka, brak wspólnego długopisu, ale trzeba nosić dokumenty (pickling) między nimi. asyncio = jedna osoba, wiele zadań, przełącza się tylko gdy czeka na coś zewnętrznego.

3 · Generatory i iteratory: pamięć kontra listy

★ egzamin

Lista buduje i przechowuje wszystkie elementy naraz w pamięci, zużycie rośnie liniowo z rozmiarem danych (O(n)). Generator produkuje elementy leniwie, jeden na żądanie, przez protokół iteratora, zużycie pamięci pozostaje stałe (O(1)) niezależnie od rozmiaru źródła danych.

Termin kluczowy
Protokół iteratora - Obiekt implementujący __iter__ (zwraca siebie) i __next__ (zwraca kolejny element lub rzuca StopIteration) jest iteratorem. Funkcja z instrukcją yield automatycznie zwraca obiekt generatora, który implementuje ten protokół bez pisania klasy ręcznie.
Generator: przetwarzanie pliku bez wczytywania go w całości python
def read_large_log(path: str):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")

errors = sum(1 for line in read_large_log("app.log") if "ERROR" in line)
Pamięć: generator expression vs list comprehension
sum(x * x for x in range(10**8)) trzyma w pamięci jeden element na raz (kilkadziesiąt bajtów). sum([x * x for x in range(10**8)]) najpierw materializuje pełną listę ~10**8 elementów (setki megabajtów) zanim sum w ogóle zacznie sumować.
Ten sam wynik, radykalnie inny profil pamięci python
# ~0.8 GB tymczasowej listy zanim sum zacznie liczyc
total = sum([x * x for x in range(10**8)])

# stala, minimalna pamiec - jeden element generatora na raz
total = sum(x * x for x in range(10**8))
Pułapka
Generator jest jednorazowy: po jednym pełnym przejściu jest wyczerpany i kolejna iteracja zwraca od razu StopIteration (pustą sekwencję), nie resetuje się. Generator też nie ma len() ani dostępu po indeksie, to nie jest sekwencja, tylko strumień.
  • itertools.islice(gen, n) - leniwe pobranie n elementów bez materializacji reszty
  • itertools.chain(a, b) - leniwe polaczenie kilku iterowalnych w jeden strumien
  • itertools.groupby(data, key) - grupowanie kolejnych elementow (wymaga posortowanych danych wg klucza)
  • itertools.pairwise(data) - kolejne pary sasiadujacych elementow (od Python 3.10)
itertools.pairwise - kolejne pary sasiednich elementow python
from itertools import pairwise

list(pairwise([1, 2, 3, 4]))
# [(1, 2), (2, 3), (3, 4)]
Mnemonik
Lista to pełny magazyn zamówiony z góry, generator to dostawa na żądanie, jedna paczka na raz, nigdy nie zapełnia całego magazynu.

4 · Dekoratory i functools

★ egzamin

Dekorator to funkcja wyższego rzędu przyjmująca funkcję i zwracająca (zwykle) inną funkcję ją opakowującą. Składnia @decorator nad definicją funkcji to czysty cukier składniowy dla func = decorator(func), wykonywany raz, w momencie definicji.

Dekorator bez functools.wraps psuje introspekcje python
def timed(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timed
def compute(n: int) -> int:
    """Sum of squares up to n."""
    return sum(i * i for i in range(n))

compute.__name__  # 'wrapper' - zle, powinno byc 'compute'
compute.__doc__   # None - docstring zgubiony
Pułapka
Bez functools.wraps opakowana funkcja traci __name__, __doc__, __module__ i __wrapped__. Łamie to introspekcję (help(), inspect.signature), generowanie dokumentacji, a czasem serializację i debugging, stack trace pokazuje 'wrapper', nie oryginalną nazwę.
Poprawka: functools.wraps zachowuje metadane python
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timed
def compute(n: int) -> int:
    """Sum of squares up to n."""
    return sum(i * i for i in range(n))

compute.__name__  # 'compute'
compute.__doc__   # 'Sum of squares up to n.'
Termin kluczowy
Dekorator sparametryzowany - Fabryka dekoratorów: funkcja przyjmująca własne argumenty (np. liczbę prób, timeout) i zwracająca właściwy dekorator, który dopiero opakowuje docelową funkcję. Wymaga jednego dodatkowego poziomu zagnieżdżenia funkcji względem zwykłego dekoratora.
Parametrized decorator: retry z konfigurowalna liczba prob python
import functools
import time

def retry(times: int = 3, delay: float = 0.5):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ConnectionError as exc:
                    last_exc = exc
                    time.sleep(delay)
            raise last_exc
        return wrapper
    return decorator

@retry(times=5, delay=1.0)
def call_flaky_api() -> dict:
    ...
Kanoniczny dekorator stdlib
functools.lru_cache(maxsize=...) (lub functools.cache bez limitu, od 3.9) memoizuje wynik funkcji na podstawie hashowalnych argumentów. To najczęściej używany dekorator produkcyjny do prostego cache'owania w pamięci procesu.
  • @staticmethod / @classmethod - metody nie/związane z instancją
  • @functools.cached_property - property liczone raz i zapamiętane na instancji
  • @functools.singledispatch - dekorator generyczny dispatchujący po typie pierwszego argumentu
  • @functools.total_ordering - dogenerowuje pozostale operatory porownania z jednego (__eq__ + __lt__)

5 · Menedżery kontekstu i contextlib

Instrukcja with gwarantuje wywołanie sprzątania niezależnie od tego, czy blok zakończył się normalnie, czy wyjątkiem. Kontrakt jest prosty: __enter__ zwraca wartość dla as, __exit__ dostaje informację o wyjątku (albo None) i decyduje, czy go wygasić.

Termin kluczowy
Protokół menedżera kontekstu - Klasa implementująca __enter__(self) i __exit__(self, exc_type, exc_value, traceback). __exit__ zwraca True aby wygasić wyjątek, False (lub None) aby pozwolić mu propagować dalej.
Klasowy menedzer kontekstu: transakcja bazy danych python
class Transaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.conn.execute("BEGIN")
        return self.conn

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.conn.execute("COMMIT")
        else:
            self.conn.execute("ROLLBACK")
        return False  # nie tlumimy wyjatku, propagujemy dalej

with Transaction(conn) as cur:
    cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
contextlib.contextmanager: to samo bez pisania klasy python
from contextlib import contextmanager

@contextmanager
def transaction(conn):
    conn.execute("BEGIN")
    try:
        yield conn
        conn.execute("COMMIT")
    except Exception:
        conn.execute("ROLLBACK")
        raise

with transaction(conn) as cur:
    cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
contextlib.suppress
contextlib.suppress(FileNotFoundError) to zwięzła alternatywa dla try/except Pass: wygasza wskazany wyjątek w bloku with bez dodatkowych linii try/except.
ExitStack: dynamiczna liczba menedzerow kontekstu python
from contextlib import ExitStack

def process_files(paths: list[str]):
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        # wszystkie pliki zamkniete automatycznie przy wyjsciu, w tym przy wyjatku
        return [f.read() for f in files]
Pułapka
__exit__ zwracające True (lub dowolną wartość truthy) wygasza wyjątek całkowicie, nawet jeśli był niezamierzony, np. AssertionError z bloku testowego zniknie bez śladu. Zwracaj True świadomie, tylko dla konkretnego, oczekiwanego typu wyjątku.
  • contextlib.closing(obj) - wywoluje obj.close() na wyjsciu dla obiektow bez pelnego protokolu with
  • contextlib.suppress(*exc_types) - ignoruje wskazane wyjatki
  • contextlib.redirect_stdout/redirect_stderr - tymczasowe przekierowanie strumieni
  • contextlib.nullcontext() - placeholder gdy menedzer kontekstu jest opcjonalny

6 · Dopasowanie wzorców: match/case

★ egzamin

match/case (PEP 634-636, od Python 3.10) to strukturalne dopasowanie wzorców, nie prosty switch po wartości. Potrafi jednocześnie sprawdzić kształt danych (typ, długość, obecność kluczy) i wydobyć z nich wartości do zmiennych w jednym kroku.

Dopasowanie literalow i wildcard python
def http_status_label(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "OK"
        case 400 | 404 | 422:
            return "Client error"
        case 500:
            return "Server error"
        case _:
            return "Unknown"
Wzorzec klasy: dopasowanie po typie i atrybutach python
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def describe(shape) -> str:
    match shape:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"on y-axis at {y}"
        case Point(x=x, y=y) if x == y:
            return "on diagonal"
        case Point():
            return "generic point"
        case _:
            return "not a point"
Wzorzec sekwencji i mapowania python
def handle(event: dict | list) -> None:
    match event:
        case {"type": "click", "x": x, "y": y}:
            print(f"click at ({x}, {y})")
        case {"type": "key", "code": code}:
            print(f"key {code}")
        case [first, *rest] if rest:
            print(f"batch starting with {first}, {len(rest)} more")
        case []:
            print("empty batch")
Termin kluczowy
Guard clause (if w case) - Dodatkowy warunek po wzorcu, sprawdzany dopiero gdy wzorzec się dopasuje, pozwala zawęzić dopasowanie o logikę niewyrażalną samą strukturą, np. case Point(x=x, y=y) if x == y.
Termin kluczowy
OR-pattern (|) - Łączy kilka alternatywnych wzorców w jednym case, dopasowuje jeśli którykolwiek pasuje, np. case 400 | 404 | 500:.
Pułapka
Goła nazwa zmiennej w case (np. case x:) jest zawsze wzorcem przechwytującym (capture pattern), wiąże x z dopasowaną wartością i pasuje do wszystkiego, nigdy nie porównuje z istniejącą zmienną o tej nazwie. Aby porównać ze stałą (np. enumem), użyj nazwy kropkowanej: case Color.RED:.
  • Uzywaj match/case gdy masz kilka strukturalnie roznych ksztaltow danych do rozroznienia (np. rozne typy zdarzen)
  • Dla prostego porownania kilku wartosci skalarnych zwykle if/elif jest rownie czytelne i nie wymaga uczenia sie skladni wzorcow
  • Klasa musi definiowac __match_args__ (dataclass generuje to automatycznie) aby wspierac pozycyjne wzorce klas jak Point(0, 0)

7 · Modelowanie danych i typowanie statyczne

★ egzamin

Wybór między @dataclass, typing.NamedTuple a Pydantic BaseModel to wybór między trzema różnymi kompromisami: zero zależności i brak walidacji (dataclass), niemutowalność i lekkość krotki (NamedTuple), albo walidacja w runtime kosztem zależności zewnętrznej (Pydantic).

dataclass: mutowalny domyslnie, field(default_factory) dla mutowalnych domyslnych python
from dataclasses import dataclass, field

@dataclass(slots=True, frozen=True)
class Config:
    name: str
    tags: list[str] = field(default_factory=list)
    retries: int = 3
NamedTuple: niemutowalny, lekki, oparty na krotce python
from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(1, 2)
p.x  # 1
p[0]  # 1 - dziala tez jak zwykla krotka
Pydantic: walidacja typow w runtime python
from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    email: str
    age: int = Field(ge=0, le=150)

UserCreate(email="a@b.com", age=200)
# ValidationError: age - Input should be less than or equal to 150

dataclass / NamedTuple

  • Czesc stdlib, zero zewnetrznych zaleznosci
  • Zero narzutu walidacji w runtime - zle dane wchodza bez ostrzezenia
  • Optymalne gdy dane pochodza z zaufanego zrodla (wewnetrzna logika)

Pydantic BaseModel

  • Wymaga zaleznosci (pydantic), rdzen walidacji napisany w Rust (pydantic-core) dla wydajnosci
  • Waliduje i konwertuje typy w runtime, rzuca ValidationError przy zlych danych
  • Optymalne na granicy systemu: API request/response, parsowanie configu, dane z zewnatrz
Termin kluczowy
typing.Protocol - Definiuje typowanie strukturalne (duck typing sprawdzany statycznie): klasa spełnia Protocol jeśli ma pasujące metody/atrybuty, bez jawnego dziedziczenia po nim. Do isinstance() w runtime wymaga dekoratora @runtime_checkable.
Protocol: strukturalne typowanie bez dziedziczenia python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Renderable(Protocol):
    def render(self) -> str: ...

class Button:
    def render(self) -> str:
        return "<button/>"

def show(item: Renderable) -> None:
    print(item.render())

show(Button())  # OK - Button pasuje strukturalnie, nie dziedziczy po Renderable
Termin kluczowy
TypedDict - Opisuje statycznie oczekiwany kształt słownika (nazwy kluczy i typy wartości) dla narzędzi jak mypy/pyright. W przeciwieństwie do Pydantic nie waliduje niczego w runtime, to zwykły dict z adnotacją.
TypedDict: typowany ksztalt slownika bez walidacji runtime python
from typing import TypedDict

class UserPayload(TypedDict):
    id: int
    email: str
    is_admin: bool

def handle(payload: UserPayload) -> None:
    print(payload["email"])  # pyright/mypy znaja typ, ale runtime nie sprawdza niczego
Generyki bez importu TypeVar (PEP 695, od 3.12)
class Stack[T]: ... i def first[T](items: list[T]) -> T: ... to nowa, krotsza skladnia generykow wbudowana w jezyk. Starsza forma (T = TypeVar('T'); class Stack(Generic[T])) dalej dziala i jest potrzebna dla kodu wspierajacego wersje przed 3.12.
NarzedzieTwórca / statusDomyslna surowoscTypowe uzycie
mypyReferencyjny checker PEP 484, communityumiarkowana, konfigurowalna stopniowoduze bazy kodu z bogatym ekosystemem pluginow (Django, SQLAlchemy)
pyrightMicrosoft, uzywany w VS Code (Pylance)wyzsza domyslnie, szybszy silnikszybka petla feedbacku w edytorze, CI

8 · Nowoczesna składnia, pakowanie i wydajność

★ egzamin

Od Python 3.11 do 3.14 seria mniejszych, ale wysoko wykorzystywanych w praktyce zmian: bogatsza gramatyka f-stringow, grupy wyjatkow do obslugi wspolbieznych awarii, deklaratywne pakowanie przez pyproject.toml i tansze narzedzia do profilowania.

Termin kluczowy
PEP 701: nowa gramatyka f-stringow (3.12) - Znosi stare ograniczenia f-stringow: mozna reuzywac ten sam znak cudzyslowu co w otaczajacym stringu, pisac wyrazenia wieloliniowe, komentarze i backslashe wewnatrz {}.
f-string: to co bylo niemozliwe przed 3.12 python
name = "world"
# ten sam znak cudzyslowu wewnatrz wyrazenia f-stringa - blad przed 3.12
greeting = f"hello {"mr. " + name}"

# wyrazenie wieloliniowe z komentarzem wewnatrz {}
value = f"{
    name.upper()  # normalizuj przed wstawieniem
}"
Termin kluczowy
ExceptionGroup i except* (PEP 654, od 3.11) - ExceptionGroup opakowuje wiele niezaleznych wyjatkow zgloszonych rownolegle (np. przez asyncio.TaskGroup gdy kilka zadan zawiedzie naraz). except* dopasowuje i wydziela pasujace wyjatki z grupy, reszte propaguje dalej.
except*: obsluga wielu rownoleglych awarii z TaskGroup python
import asyncio

async def fetch(url: str) -> str:
    if "bad" in url:
        raise ValueError(f"invalid url: {url}")
    return url

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            for url in ["ok1", "bad1", "ok2", "bad2"]:
                tg.create_task(fetch(url))
    except* ValueError as eg:
        print(f"{len(eg.exceptions)} bledne URL-e")

asyncio.run(main())
PEP 758 (3.14)
Klauzule except i except* moga od Python 3.14 pomijac nawiasy przy wielu typach wyjatkow: except ValueError, TypeError: dziala tak samo jak except (ValueError, TypeError):.
Termin kluczowy
pyproject.toml (PEP 518 + PEP 621) - PEP 518 (2016) wprowadza pyproject.toml jako standardowy plik z tabela [build-system] (backend budowania i jego zaleznosci). PEP 621 (2020) dodaje do tego samego pliku standardowa tabele [project] z metadanymi pakietu (nazwa, wersja, zaleznosci). Razem zastapily rozproszona kombinacje setup.py, setup.cfg i MANIFEST.in jako jeden deklaratywny standard ekosystemu.
Minimalny pyproject.toml toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "tenzan-inspector"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.7",
]
  • cProfile (stdlib) - profilowanie deterministyczne calego wywolania, python -m cProfile -s cumulative script.py
  • timeit (stdlib) - mikrobenchmarki krotkich fragmentow kodu, izoluje szum systemowy
  • py-spy - profiler probkujacy dolaczany do dzialajacego procesu bez zmian w kodzie, bezpieczny na produkcji
  • sys.monitoring (PEP 669, od 3.12) - nowe, tansze API zdarzen dla profilerow/debuggerow/narzedzi coverage, zastepuje kosztowny sys.settrace
cProfile: szybki start bez zmian w kodzie bash
python -m cProfile -s cumulative pipeline.py
Pułapka
Profilowanie na malym, lokalnym zbiorze danych dev czesto wskazuje zle 'wąskie gardło'. Realny koszt (np. deserializacja duzego JSON-a, latencja sieci do bazy) ujawnia sie dopiero na danych i infrastrukturze zblizonej do produkcyjnej.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic