Structural Pattern Matching - Programmare in Python

Structural Pattern Matching

Pubblicato il 11 Giugno 2022 da Michele Saba

Structural Pattern Matching: una delle funzionalità più interessanti introdotte in Python 3.10, simile allo switch statement presente in altri linguaggi di programmazione.

È importante da conoscere perché oltre ad essere molto potente e permettervi di scrivere codice moderno ed elegante, vi capiterà di incontrarla sempre più spesso nel codice di altre persone.

Pensate allo structural pattern matching come a uno di quei giochi per bambini dove lo scopo è inserire delle forme all'interno di ingressi della stessa tipologia.

Abbiamo due nuove parole chiave: match e case.

Un match statement prende un'espressione e confronta il suo valore con i pattern espressi nei blocchi case.

Nel nostro primo esempio vediamo una funzione che, passato come parametro un valore rappresentante un codice di stato, ne restituisce il messaggio relativo.

Preciso che per poter eseguire questo codice correttamente, dovrete disporre di Python con versione minima 3.10.

Qualora vi troviate su Windows potete scaricare l'interprete da questo indirizzo, mentre qualora vi troviate su Linux o MacOS, potete usare un tool chiamato PyEnv, di cui ho già parlato in un tutorial dedicato.

def http_error(status_code):
    match status_code:
        case 200:
            print("Ok") 
        case 404:
            print("Not Found") 
        case 500:
            print("Internal Server Error")
        case _: # wildcard - simile ad un else, deve stare alla fine
            print("Unknown error")

Nel nostro caso la funzione http_error() accetta come parametro status_code.

Il codice in ciscun blocco case (case 200, case 404 ecc) verrà quindi eseguito solamente qualora il valore di status_code combaci col relativo pattern espresso nel case.

Oltre a ciò, fate attenzione al blocco finale dove cui viene usato un underscore (_)

case _:
    print("Unknown error")

Questo rappresenta una "wildcard" (carta jolly), ed è opzionale.

Il codice del blocco relativo verrà eseguito qualora non ci sia stato match sui pattern precedenti.

Associando a status_code il valore 200 otterremo quindi "Ok", associando 500 "Internal Server Error", con 404 otterremo "Not Found" e usando qualsiasi altri valore "Unknown Error"

Come fare a gestire più di un caso contemporaneamente?

Supponiamo di voler ottenere le categorie relative ai vari codici di stato, invece che i singoli messaggi (per informazioni su cosa i codici di stato siano e che si intenda per codice, messaggio e categoria, date uno sguardo a questa pagina di Wikipedia).

Ci serve un sistema che permetta di fare match su più codici di stato, ovvero di creare un pattern che sia in grado di riconoscere più di un codice.

In casi come questi possiamo usare il carattere pipe ( | ) , che permette di creare pattern in OR, come in questa versione aggiornata della funzione http_error()

def http_error(status_code):
    match status_code:
        case 200 | 201 | 202:
            print("Success") 
        case 400 | 401 | 404:
            print("Client Error") 
        case 500 | 501 | 503:
            print("Server Error")
        case _:
            print("Unknown error")

In questo caso, il codice relativo ad ogni blocco verrà eseguito qualora il valore associato a status_code sia uno tra i valori definiti nel pattern.

200, 201 e 202 ad esempio, faranno tutti eseguire il codice print("Success")

Il comportamento del blocco finale in cui si usa carattere wildcard non varierà rispetto agli esempi precedenti.

Pattern e Sotto-Pattern

Facciamo ora un esempio in cui creare dei pattern più complessi e dinamici.

Abbiamo una funzione move() che accetta come parametro action. Ci si aspetta, esprimendo ciò tramite type_hint, che action sia di tipo Stringa.

La funzione move() permette ad un personaggio immaginario, di effettuare due tipologie di azioni: muoversi (go) e saltare (jump).

Il personaggio potrà saltare solamente in alto (up) e avanti (forward).

Potrà però correre in tutte le direzioni: avanti (forward), indietro (backwards), a sinistra (left) e a destra (right).

Ci si aspetta quindi di ricevere come valore di action una stringa formata da due parole, del tipo: "go forward" oppure "jump up".

def move(action: str):
    match action.split():
        case ["go", ("forward" | "backwards" | "left" | "right") as direction]:
            print(f"Character is running ({direction})")
        case ["jump", ("up" | "forward") as direction]:
            print(f"Character is jumping ({direction})")
        case _:
            print("Action not supported")

L'espressione sul match statement usa action.split() in modo da separare la stringa associata ad action in due parole.

split() è infatti un metodo delle Stringhe in Python che si occupa di dividerle in una lista di sotto stringhe, usando come carattere separatore di default lo spazio.

Nel caso di "go forward" otterremo quindi la lista ["go", "forward"].

Python effettuerà anzitutto il controllo sulla prima parola, verificando se questa faccia match, e poi verificherà che la seconda parola sia una delle possibili parole accettate.

Per questo motivo, la stringa "go right" farà match, mentre la stringa "go sideways" non verrà riconosciuta come relativa al pattern.

Lo stesso discorso vale anche per il secondo blocco case, dove la prima parola restituita da action.split() dovrà necessariamente essere "jump", e la seconda potrà essere "up" oppure "forward".

In entrambi i casi, il valore della seconda parola verrà associato a direction, così che possa quindi essere usato all'interno del blocco case relativo.

I patten possono essere formati anche a partire da classi.

Nel prossimo esempio abbiamo una classe Element che dispone di due attributi name e color.

Questa classe è definita usando il decoratore @dataclass: potete trovare informazioni al riguardo qui.

Abbiamo quindi una funzione validate_el(), che accetta come parametro element.

Si otterrà quindi match nel caso alla funzione venga passato un oggetto di tipo Element, che abbia come valore associato a name "Cubo" oppure "Sfera".

Oggetti di altra tipologia, come ad esempio oggetti di tipo Other, non faranno match.

Notate come in questo caso, il valore associato all'attributo color non sia incluso nel pattern.

from dataclasses import dataclass

@dataclass
class Element:
    name: str
    color: str

@dataclass
class Other:
    name: str

def validate_el(element):
    match element:
        case Element(name="Cubo"):
            print("Hai inserito un cubo!")
        case Element(name="Sfera"):
            print("Hai inserito una sfera!")

Volendo è possibile creare pattern che prevedano valori specifici per più attributi, non solo uno!

In questo esempio, vengono infatti accettati cubi di qualsiasi colore, ma solo sfere di colore "Verde".

def validate_el(element):
    match element:
        case Element(name="Cubo"):
            print("Hai inserito un cubo rosso!")
        case Element(name="Sfera", color="Verde"):
            print("Hai inserito una sfera verde!")

Nel caso di pattern creati a partire dai nomi di classi, è possibile usare i valori degli attributi relativi nel codice del blocco case.

In questo esempio notiamo come nel terzo case pattern non siano specificati dei valori per gli attributi name e color, e come questi ultimi vengano inoltre usati all'interno della f string presente nel blocco di codice associato.

In questo caso potremmo far match anche con un oggetto diverso da un cubo o una sfera, come ad esempio Element(name="Cilindro", color="Azzurro")

from dataclasses import dataclass

@dataclass
class Element:
    name: str
    color: str

def validate_el(element):
    match element:
        case Element(name="Cubo"):
            print("Hai inserito un cubo!")
        case Element(name="Sfera", color="Verde"):
            print("Hai inserito una sfera!")
        case Element(name, color):
            print(f"Hai inserito: {name} di colore {color}")
        case _:
            print("Oggetto non valido!")

Se siete interessati a scoprire ancora di più riguardo allo Structural Pattern Matching di Python, date uno sguardo alla pagina relativa al tutorial da uno dei siti ufficiali di Python.