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 înfirst, restul înrest.if code >= 500e 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
- PEP 498: Literal String Interpolation: propunerea f-string.
- PEP 572: Assignment Expressions: walrus, inclusiv discuția care aproape a sfâșiat comunitatea în 2018.
- PEP 634: Structural Pattern Matching: Specification: specificația formală.
- PEP 636: Structural Pattern Matching: Tutorial: companionul citibil.