Python, de la zero Lecția 3 / 60

f-strings, walrus, match: noile idiomuri de flow-control

Sintaxa Python moderna care face codul mai clar decat alternativele: cand sa folosesti fiecare si cazurile unde modul vechi e inca mai bun.

Bun venit la lecția a treia. Astăzi acoperim trei caracteristici sintactice care, împreună, au schimbat felul în care un dezvoltator Python fluent scrie cod zi de zi: f-strings (3.6), walrus operator (3.8) și pattern matching (3.10). Niciuna nu e uluitoare în sine. Toate trei împreună, folosite în locurile potrivite, fac diferența dintre cod care se citește ca 2026 și cod care se citește ca interiorul unui manual din 2014.

O temă străbate toate trei: fiecare înlocuiește un idiom vechi mai stângaci, dar fiecare are și cazuri unde idiomul vechi e încă mai bun. Abilitatea constă în a ști care e care. Vom parcurge fiecare caracteristică așa cum o vei folosi efectiv și cazurile unde apelarea la ea înrăutățește lucrurile, nu le îmbunătățește.

f-strings: singura formatare de string-uri spre care să apelezi întâi

Înainte de f-strings (Python 3.6, decembrie 2016), aveai trei moduri să formatezi string-uri:

name = "Alice"
age = 30

# 1. Operatorul "%" în stil C: mai bătrân decât tina, încă funcționează
print("Hello, %s, you are %d" % (name, age))

# 2. Metoda .format(): adăugată în 2.6, era calea „modernă" până în 3.6
print("Hello, {}, you are {}".format(name, age))

# 3. Concatenare de string-uri: calea „abia ieri am învățat Python"
print("Hello, " + name + ", you are " + str(age))

Toate trei încă funcționează, toate trei încă apar în baze de cod vechi și toate trei sunt acum greșite implicit. Răspunsul modern:

print(f"Hello, {name}, you are {age}")

Două caractere de overhead (prefixul f), zero overhead cognitiv, iar variabilele se citesc în ordinea în care le rostești cu voce tare. f-strings nu sunt doar mai scurte decât .format(): sunt vizibil mai rapide (bytecode-ul e mai direct), se integrează mai curat cu debugger-ul și tratează aproape orice caz pe care îl tratau formele vechi.

Setul complet de unelte f-string

# Interpolare de bază
user = "Alice"
greeting = f"Hello, {user}"  # "Hello, Alice"

# Expresii, nu doar variabile
items = [1, 2, 3, 4, 5]
print(f"You have {len(items)} items, summing to {sum(items)}")

# Apeluri de metodă și acces la atribute
import datetime
now = datetime.datetime.now()
print(f"Today is {now.strftime('%A')}")

# Specificatori de format: aceeași sintaxă ca .format(), după două puncte
price = 1234.5678
print(f"Price: {price:.2f}")              # "Price: 1234.57"
print(f"Price: {price:,.2f}")             # "Price: 1,234.57"  (separator de mii)
print(f"Pad: {price:>12.2f}")             # aliniere la dreapta în 12 coloane
print(f"Pct: {0.4567:.1%}")               # "Pct: 45.7%"
print(f"Hex: {255:#x}")                   # "Hex: 0xff"

# Formatare de date
print(f"Date: {now:%Y-%m-%d %H:%M}")      # "Date: 2026-05-01 14:30"

# Sintaxa de debug din 3.8: afișează numele ȘI valoarea
x = 42
print(f"{x=}")                             # "x=42"
print(f"{len(items)=}")                   # "len(items)=5"

Sintaxa {x=} (3.8+) e unul dintre acele lucruri liniștit transformatoare. Înlocuiește zece ani de print("x =", x) cu print(f"{x=}") și economisești apăsări de taste pentru tot restul carierei. Folosește-o constant la debugging.

Când să NU folosești f-strings

Două cazuri. Memorează-le.

Logging. Asta e greșit:

import logging
log = logging.getLogger(__name__)

# NU face asta
log.info(f"Processing user {user_id} with payload {expensive_to_format(payload)}")

f-string-ul e evaluat înainte să fie apelat log.info. Dacă nivelul tău de log e setat pe WARNING și ăsta e un mesaj INFO, mesajul e aruncat, dar expensive_to_format(payload) deja a rulat. Modulul logging folosește deliberat formatare leneșă cu % exact din motivul ăsta:

log.info("Processing user %s with payload %s", user_id, expensive_to_format(payload))

Argumentele sunt formatate doar dacă mesajul chiar va fi emis. Pe un serviciu cu volum mare, contează.

Interogări SQL (și orice string care intră în parser-ul altui limbaj). Asta e o vulnerabilitate:

# NICIODATĂ să nu faci asta. SQL injection.
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")

Folosește interogări parametrizate. Întotdeauna. Fiecare driver de bază de date le suportă. f-strings sunt unealta greșită pentru orice string care traversează o graniță de limbaj într-un parser: SQL, comenzi shell, HTML, orice. Unealta corectă e API-ul proiectat să trateze escaparea pentru tine.

Pentru orice altceva, cele 95% de string-uri pe care le construiești într-un program normal, f-strings sunt corecte.

Walrus operator: atribuire-ca-expresie

Python 3.8 a adăugat :=, oficial numit „assignment expression”, dar universal cunoscut ca walrus operator (fiindcă := arată ca o morsă, dacă îți înclini capul și ai imaginația potrivită).

Face un singur lucru: atribuie o valoare unui nume și returnează valoarea, într-o singură expresie. Sună plictisitor până vezi ce te lasă să eviți.

Cazul clasic: bucle de citește-și-testează

# Înainte de walrus
with open("data.log") as f:
    line = f.readline()
    while line:
        process(line)
        line = f.readline()

Acel f.readline() apare de două ori. Ușor de uitat unul, ușor de introdus un bug dacă vreodată schimbi cum citești. Walrus-ul îl colapsează:

# Cu walrus
with open("data.log") as f:
    while line := f.readline():
        process(line)

O singură referință la f.readline(), fără duplicare, bucla se citește curat: „cât timp line e ceva, procesează-l.”

Alte cazuri genuin bune

Evitarea unui apel de funcție redundant într-un comprehension:

# Fără walrus: apelează expensive_compute(x) de două ori pentru elementele care trec
results = [expensive_compute(x) for x in inputs if expensive_compute(x) > threshold]

# Cu walrus: o dată
results = [y for x in inputs if (y := expensive_compute(x)) > threshold]

Match pe regex compilat:

import re

pattern = re.compile(r"user_(\d+)")

# Fără walrus
match = pattern.match(text)
if match:
    user_id = int(match.group(1))
    process_user(user_id)

# Cu walrus
if match := pattern.match(text):
    process_user(int(match.group(1)))

Corpul lui if folosește clar match, iar variabila e legată doar când chiar există un match.

Când să NU folosești walrus-ul

Oriunde face codul mai greu de citit. Walrus-ul e o unealtă de claritate, nu o unealtă de concizie. Dacă folosirea lui forțează un cititor să facă marșul înapoi ca să-și dea seama de unde a venit o variabilă, l-ai înrăutățit.

# Asta e mai rău decât alternativa.
# Walrus-ul e îngropat, atribuirea e neevidentă,
# iar ai economisit o linie de cod cu prețul unui cititor confuz.
total = sum((y := x * 2) + 1 for x in range(10) if (y > 5))

O regulă bună: dacă nu poți explica ce face walrus-ul într-o propoziție scurtă, nu-l folosi. Cea mai mare parte a codului nu are nevoie de el. Apelează la el când există un câștig real de citibilitate (bucla de citire fișier, match-ul de regex, cazul evită-recalcularea-într-un-comprehension) și sări peste el în restul timpului.

Pattern matching: nu doar un switch elegant

Python 3.10 (octombrie 2021) a adăugat match și case. Cadrul din articolele timpurii, „Python are în sfârșit o instrucțiune switch!”, l-a subevaluat. switch din C/Java e un tabel de salt înnobilat pe constante întregi. match din Python e structural pattern matching: face match pe forma datelor, le destructurează în variabile și se integrează cu clase și dataclasses într-un mod care chiar schimbă cum se scriu unele tipuri de cod.

Forma de bază

def http_error_message(status: int) -> str:
    match status:
        case 200 | 201 | 204:
            return "OK"
        case 301 | 302:
            return "Redirect"
        case 400:
            return "Bad request"
        case 401 | 403:
            return "Unauthorized"
        case 404:
            return "Not found"
        case 500 | 502 | 503 | 504:
            return "Server error"
        case _:
            return f"Unknown status {status}"

Acel case _: e wildcard-ul, echivalent cu default: din C. | separă alternative. Până aici, e într-adevăr un switch elegant.

Unde devine interesant: match pe formă

match strălucește când te ramifici pe forma unei valori, nu doar pe identitatea ei.

def handle_message(msg: dict) -> str:
    match msg:
        case {"type": "ping"}:
            return "pong"
        case {"type": "greeting", "name": name}:
            return f"Hello, {name}"
        case {"type": "command", "action": action, "args": [first, *rest]}:
            return f"Running {action} with {first} and {len(rest)} more args"
        case {"type": "error", "code": code} if code >= 500:
            return f"Server error: {code}"
        case {"type": "error", "code": code}:
            return f"Client error: {code}"
        case _:
            return "Unknown message"

Lucruri de remarcat:

  • {"type": "greeting", "name": name} face match pe un dict cu o cheie "type" egală cu "greeting" și leagă valoarea lui "name" de o variabilă locală numită name.
  • [first, *rest] e destructurare de listă: primul element în first, restul în rest.
  • if code >= 500 e un guard: o condiție suplimentară care trebuie să fie adevărată pentru ca acel case să facă match.
  • Ordinea case-urilor contează. Python le verifică de sus în jos, ia primul match.

E genuin puternic pentru parsarea de mesaje JSON, parcurgerea de noduri AST, dispatch pe înregistrări tip enum sau orice cod care e un lanț de instrucțiuni „dacă acest dict are aceste chei cu aceste forme, fă X.”

Pattern-uri de clasă

Pattern matching se integrează cu clasele prin atributul __match_args__ (setat automat pe dataclasses).

from dataclasses import dataclass

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

@dataclass
class Triangle:
    base: float
    height: float

def area(shape: Circle | Rectangle | Triangle) -> float:
    match shape:
        case Circle(radius=r):
            return 3.14159 * r * r
        case Rectangle(width=w, height=h):
            return w * h
        case Triangle(base=b, height=h):
            return 0.5 * b * h

E structural similar cu algebraic data types din Rust sau Haskell. Nu e la fel de expresiv ca acelea (pattern matching-ul din Python e intenționat mai puțin strict), dar pentru cazurile unde se potrivește, se potrivește frumos. Type checker-ele folosesc pattern-ul match-class și pentru îngustarea automată a tipurilor.

Capture vs comparație: capcana

Există o capcană care îi prinde pe toți exact o dată. Privește:

HOST = "localhost"

def check(target: str) -> str:
    match target:
        case HOST:
            return "matches host"
        case _:
            return "no match"

print(check("anything"))  # "matches host"  (stai, ce?)

case HOST: nu înseamnă „match dacă target este egal cu HOST.” Un nume gol într-un case e un capture pattern: leagă orice era în target de un nume local nou HOST (umbrind pe cel de la nivelul modulului). Fiecare valoare face match.

Ca să compari cu o constantă, trebuie s-o califici:

class Config:
    HOST = "localhost"

def check(target: str) -> str:
    match target:
        case Config.HOST:           # nume cu punct = comparație
            return "matches host"
        case _:
            return "no match"

Sau folosește un literal direct. Sau membri enum.Enum. Regula: numele cu punct compară, numele simple cu litere mici capturează. Odată ce te-a mușcat asta o dată, o să-ți amintești pentru totdeauna.

Când să NU folosești match

Două cazuri.

Un if/elif simplu e mai scurt și mai clar. Dacă faci match pe două sau trei valori întregi fără destructurare, un lanț if/elif e fin și aproape sigur mai citibil. match strălucește la patru sau mai multe ramuri cu match pe formă, nu la „e x egal cu 1, 2 sau 3.”

Datele nu au o formă consistentă. match e strălucit când datele tale au pattern-uri previzibile. Dacă fiecare case e case _ if some_complicated_condition:, ai reinventat if/elif cu pași în plus. Structura trebuie să fie acolo pentru ca match să adauge valoare.

Punându-le împreună

Iată o funcție mică ce folosește toate trei caracteristicile într-un fel care, în 2020, ar fi fost de trei ori mai lungă:

import logging
import re

log = logging.getLogger(__name__)

USER_PATTERN = re.compile(r"user:(\d+)")

def parse_event(event: dict) -> str:
    match event:
        case {"type": "click", "target": target} if (m := USER_PATTERN.match(target)):
            user_id = int(m.group(1))
            log.info("Click on user %s", user_id)
            return f"User clicked: {user_id=}"
        case {"type": "view", "page": page}:
            log.info("Page view: %s", page)
            return f"Viewed {page}"
        case _:
            log.warning("Unknown event: %s", event)
            return "Unknown event"

f-strings pentru valorile de retur orientate spre om și pentru debug-ul {user_id=}. Walrus în interiorul guard-ului if ca să lege match-ul de regex. Pattern matching ca să facă dispatch pe forma evenimentului. Formatare leneșă cu % în apelurile de log fiindcă asta vrea logging.

Încheiere

f-strings: implicit pentru string-uri orientate spre om, exceptând logging-ul și SQL. Folosește {x=} pentru debugging, o să-mi mulțumești. Walrus: apelează la el pe bucle de citește-și-testează și unde elimină duplicare; sări peste el când costă citibilitate. match: strălucește pe match pe formă și date structurate; nu e un switch, nu te gândi la el așa; ține minte capcana captării pe nume gol.

Lecția 4 (vineri) e despre iteratori, generatoare și protocolul de iterație. Sala de mașini propriu-zisă a Python; odată ce o înțelegi, jumătate din biblioteca standard începe brusc să aibă sens.

Lecturi suplimentare

Caută