Python Python

Ce poate face `match`/`case` și un lanț de `if`/`elif` nu poate în Python

Dima Aug 30, 2026

Pe scurt

Testează forma unei valori și scoate bucățile din ea în același pas. Un lanț if poate evalua doar expresii pe care le scrii tu, deci verificarea unui tuplu de comandă cu trei elemente înseamnă testarea tipului, testarea lungimii, compararea elementului zero și apoi extragerea manuală a restului prin indexare. Un tipar case descrie structura pe care o aștepți — ("move", sku, dest) — iar Python o verifică și leagă sku și dest doar dacă toată forma se potrivește. Acea combinație de potrivire și destructurare este toată funcționalitatea și se rentabilizează oriunde forma datelor poartă înțelesul.


Cum funcționează

Un tipar nu este o expresie. case ("scan", sku, qty): nu evaluează niciodată sku; el descrie o secvență de trei elemente al cărei prim element este egal cu "scan", iar dacă subiectul se potrivește, sku și qty sunt legate de al doilea și al treilea element. Potrivirea și legarea sunt o singură operație, iar legările se întâmplă doar când reușește tot tiparul.

Cinci feluri de tipare acoperă aproape tot. Un tipar literal, precum "scan" sau 200, compară cu ==. Un tipar de capturare este un nume simplu: se potrivește cu orice și îl leagă. Un tipar de secvență, [a, b] sau (a, b), se potrivește cu orice secvență de acea lungime — dar niciodată cu un str sau cu bytes, care sunt excluse în mod deliberat, ca iterarea unui șir caracter cu caracter să nu fie niciodată ce ai cerut din greșeală. Un tipar de mapare, {"type": "scan", "sku": sku}, verifică prezența acelor chei și potrivește valorile lor, ignorând orice alte chei se întâmplă să poarte dicționarul. Un tipar de clasă, Delayed(hours=h), rulează o verificare isinstance și apoi potrivește atribute.

Cazurile sunt încercate de sus în jos, iar prima potrivire câștigă, deci tiparele mai specifice trebuie să vină primele. Un case poate purta o gardă — case Delayed(hours=h) if h > 24: — care este o expresie obișnuită, evaluată după ce tiparul s-a potrivit și și-a legat numele, deci garda le poate folosi. Dacă niciun caz nu se potrivește, instrucțiunea match pur și simplu nu face nimic.

Regula care îi prinde pe toți: un nume simplu capturează întotdeauna. Scrierea case THRESHOLD: nu compară subiectul cu constanta ta — se potrivește cu orice și releagă THRESHOLD. Ca să compari cu o constantă cu nume, folosește un nume cu punct, precum Status.PAID sau config.LIMIT, pe care Python îl tratează ca pe o valoare de comparat, nu ca pe un nume de legat.

De aceea potrivirea de tipare își câștigă locul pe date eterogene și structurate: JSON analizat, încărcături de webhook, uniuni de evenimente, tupluri de comenzi, arbori de sintaxă. Pentru un singur scalar comparat cu câteva valori, un lanț if spune același lucru cu mai puțin ceremonial.


Cum arată la rulare

Creează un fișier:

touch carrier_events.py

Rulează-l cu:

python3 carrier_events.py

Fiecare pas este un program complet; înlocuiește fișierul de fiecare dată.

Potrivirea unei forme și despachetarea ei deodată

Evenimentele de scanare sosesc ca tupluri al căror prim element numește operația și a căror lungime variază odată cu ea.

events = [
    ("scan", "SKU-9921", 3),
    ("scan", "SKU-3157"),
    ("move", "SKU-4408", "dock-7"),
    ("audit",),
]

for event in events:
    match event:
        case ("scan", sku, qty):
            print(f"scan  {sku} x{qty}")
        case ("scan", sku):
            print(f"scan  {sku} x1")
        case ("move", sku, dest):
            print(f"move  {sku} -> {dest}")
        case _:
            print(f"skip  {event!r}")


scan  SKU-9921 x3
scan  SKU-3157 x1
move  SKU-4408 -> dock-7
skip  ('audit',)


Fiecare caz enunță o lungime și un prim element, iar numele din pozițiile rămase sunt legate doar când acea formă se potrivește. Cele două cazuri scan sunt exact ce tratează cel mai prost un lanț if: deosebirea lor cere len(event) == 3 înainte de indexarea lui event[2], iar greșirea acelei ordini ridică IndexError în loc să cadă mai departe. Și aici ordinea contează — cazul scan cu trei elemente trebuie să îl preceadă pe cel cu două, de vreme ce prima potrivire câștigă. case _ este jokerul: se potrivește cu orice și nu leagă nimic.


Potrivirea după forma obiectelor

Aceeași idee, aplicată claselor. O dataclass primește gratuit __match_args__, ceea ce permite unui tipar să listeze atributele pozițional.

from dataclasses import dataclass


@dataclass
class Delivered:
    tracking: str
    signed_by: str


@dataclass
class Delayed:
    tracking: str
    hours: int


@dataclass
class Lost:
    tracking: str


def describe(event):
    match event:
        case Delivered(tracking=ref, signed_by=name):
            return f"{ref}: signed by {name}"
        case Delayed(tracking=ref, hours=hours) if hours > 24:
            return f"{ref}: delayed {hours}h - escalate"
        case Delayed(tracking=ref, hours=hours):
            return f"{ref}: delayed {hours}h"
        case Lost(ref):
            return f"{ref}: lost - open a claim"


for event in [
    Delivered("1Z-4471", "M. Okafor"),
    Delayed("1Z-4408", 6),
    Delayed("1Z-3157", 48),
    Lost("1Z-8890"),
]:
    print(describe(event))


1Z-4471: signed by M. Okafor
1Z-4408: delayed 6h
1Z-3157: delayed 48h - escalate
1Z-8890: lost - open a claim


Delivered(tracking=ref, ...) înseamnă o verificare isinstance plus potrivire de atribute, deci testul de tip și extragerea atributelor sunt un singur pas. Cele două cazuri Delayed diferă doar prin garda if hours > 24, care rulează după ce hours este legat — acea ordonare permite unei gărzi să citească numele tocmai capturate de tipar, iar cazul specific trebuie din nou să vină primul. Lost(ref) se potrivește pozițional pentru că @dataclass pune __match_args__ pe ('tracking',); o clasă scrisă de mână fără el ridică TypeError la un tipar pozițional.


Numele simplu care se potrivește cu orice

Capcana. Un nume dintr-un tipar este o țintă de legat, niciodată o constantă de comparat.

from enum import Enum

THRESHOLD_HOURS = 24

match 6:
    case THRESHOLD_HOURS:
        print("pattern matched; THRESHOLD_HOURS is now:", THRESHOLD_HOURS)


class Status(Enum):
    PENDING = "pending"
    PAID = "paid"


def route(status):
    match status:
        case Status.PENDING:
            return "hold in queue"
        case Status.PAID:
            return "release to carrier"
        case _:
            return "manual review"


for status in (Status.PENDING, Status.PAID, "refunded"):
    print(f"{status} -> {route(status)}")


pattern matched; THRESHOLD_HOURS is now: 6
Status.PENDING -> hold in queue
Status.PAID -> release to carrier
refunded -> manual review


6 s-a potrivit cu un tipar care pare că ar trebui să ceară 24, iar constanta a fost suprascrisă în proces — corpul cazului citește apoi 6. Nimic nu te avertizează, pentru că un tipar de capturare este sintaxă legitimă, care face exact ce este definit să facă. Status.PENDING este corectura: un nume cu punct este un tipar de valoare, comparat cu ==, nu legat, motiv pentru care enumerările și constantele de la nivel de clasă sunt vocabularul natural pentru potrivirea față de mulțimi fixe.

Notă: Python respinge cea mai evidentă versiune a acestei greșeli. O capturare simplă urmată de alte cazuri este o eroare de compilare — SyntaxError: name capture 'PENDING' makes remaining patterns unreachable — deci eșecul tăcut de mai sus supraviețuiește doar când capturarea este ultimul sau singurul caz.


Cazuri limită și capcane

  • Șirurile nu sunt secvențe aici. case [first, second]: nu se potrivește cu "ab". str, bytes și bytearray sunt excluse intenționat din tiparele de secvență, deci un șir cade mai departe, la cazul următor.
  • Tiparele de mapare se potrivesc cu submulțimi. case {"type": "scan"}: se potrivește cu un dicționar cu alte douăzeci de chei. Nu există niciun tipar care să însemne „exact aceste chei"; capturează restul cu **rest și verifică-l tu, dacă asta contează.
  • Un tipar de clasă pozițional are nevoie de __match_args__. Dataclasele și tuplurile cu nume îl definesc automat; o clasă simplă nu, iar case Parcel(ref): pică cu TypeError: Parcel() accepts 0 positional sub-patterns (1 given). Tiparele cu nume funcționează pe orice clasă.
  • Un match fără potrivire este tăcut. Fără niciun case _, o valoare care nu se potrivește cu nimic nu face absolut nimic — nicio eroare. Adaugă un joker care ridică o excepție atunci când intrarea trebuie să fie exhaustivă.
  • Alternativele folosesc |, iar fiecare ramură trebuie să lege aceleași nume. case ("scan", sku) | ("recount", sku): este valid; legarea unui nume într-o alternativă și nu în cealaltă este o SyntaxError.

Rezumat

match testează structura și extrage date într-o singură instrucțiune, ceea ce un lanț if nu poate face fără verificări de tip separate, verificări de lungime și indexare manuală. Tiparele descriu forme — secvențe, mapări, atribute de clasă — și leagă nume doar când toată forma se potrivește, primul caz potrivit câștigând, iar gărzile rulând după legare.

  • Un tipar este o descriere de formă, nu o expresie; potrivirea și legarea se întâmplă împreună, iar legările au efect doar dacă reușește tot tiparul.
  • Un nume simplu este un tipar de capturare, care se potrivește cu orice și releagă acel nume — folosește un nume cu punct, precum Status.PAID, ca să compari cu o constantă.
  • Tiparele de secvență nu se potrivesc niciodată cu str sau bytes, iar tiparele de mapare se potrivesc cu o submulțime de chei, nu cu mulțimea exactă.
  • Tiparele de clasă combină isinstance cu potrivirea de atribute; subtiparele poziționale cer __match_args__, pe care dataclasele și tuplurile cu nume îl oferă.
  • Cazurile sunt încercate în ordine, iar prima potrivire câștigă, deci tiparele specifice și cazurile cu gardă trebuie scrise deasupra celor generale.

Trebuie să fii autentificat pentru a accesa laboratorul cloud.

Autentifică-te