> ## Content Index
> Fetch the complete content index at: https://bitstales.eu/llms.txt
> Use this file to discover other available public pages before exploring further.

# Come disinnescare un if: nove attrezzi per raddrizzare il codice a freccia
- URL: https://bitstales.eu/it/come-evitare-gli-if/
- Published: 2025-09-15T17:05:00.000Z
- Updated: 2025-09-15T17:05:00.000Z
- Description: Erediti spedizioni.py: una funzione a freccia con complessità 18. Un pomeriggio, nove attrezzi (guardie, dizionari, bisect, polimorfismo, Null Object, match, EAFP) e la scoperta che in Python il trucco branchless rallenta.
- Author: Vittorio
- Tags: Programming, Python, Software Engineering, #it

Lunedì 15 settembre 2025, primo pomeriggio. Il caffè è ancora caldo quando apri `spedizioni.py`, il modulo che da oggi è tuo. Chi l'ha scritto ha cambiato lavoro, e in eredità ti ha lasciato una funzione, un commento in cima che dice «non toccare» e una forma geometrica: il codice scivola verso destra, un rientro dopo l'altro, come una freccia puntata contro il margine dello schermo.

```python
def calcola_spedizione(ordine, express=False):
    if ordine is not None:
        if ordine.get("righe"):
            peso = sum(r["peso"] for r in ordine["righe"])
            if peso <= 30:
                if ordine["paese"] == "IT":
                    corriere = "BRT"
                elif ordine["paese"] == "FR":
                    corriere = "Colissimo"
                elif ordine["paese"] == "DE":
                    corriere = "DHL"
                elif ordine["paese"] == "ES":
                    corriere = "SEUR"
                else:
                    corriere = "UPS"
                if peso <= 2:
                    costo = 4.90
                elif peso <= 5:
                    costo = 6.90
                elif peso <= 10:
                    costo = 9.90
                elif peso <= 20:
                    costo = 14.90
                else:
                    costo = 19.90
                if express:
                    costo = costo * 2
                if ordine.get("coupon") is not None:
                    if ordine["coupon"]["tipo"] == "percentuale":
                        costo = costo * (1 - ordine["coupon"]["valore"] / 100)
                    elif ordine["coupon"]["tipo"] == "fisso":
                        costo = max(0, costo - ordine["coupon"]["valore"])
                    elif ordine["coupon"]["tipo"] == "gratis":
                        costo = 0
                return corriere, round(costo, 2)
            else:
                raise ValueError("troppo pesante")
        else:
            raise ValueError("ordine vuoto")
    else:
        raise ValueError("nessun ordine")
```

Lo leggi due volte. Funziona, questo va detto: da tre anni calcola il corriere e il costo di ogni pacco che esce dal magazzino. Ma per sapere cosa succede a un ordine francese da 12 chili con un coupon fisso, devi seguire con il dito quattro livelli di rientro. E gli errori sono finiti in fondo, lontanissimi dagli `if` che li hanno causati: il `raise ValueError("nessun ordine")` dell'ultima riga risponde a un `if` che sta 40 righe più su.

Lanci [radon](https://radon.readthedocs.io/en/latest/intro.html?ref=bitstales.eu), che misura la complessità del codice Python, e lui ti risponde con un voto da pagella:

```python
$ radon cc -s spedizioni.py
spedizioni.py
    F 1:0 calcola_spedizione - C (18)
```

Diciotto. Oggi pomeriggio quel numero lo farai scendere, un attrezzo alla volta.

## Che cosa misura quel 18

Il numero si chiama *complessità ciclomatica*. Lo ha inventato Thomas McCabe, che nel 1976 lavorava per il Dipartimento della Difesa americano e lo pubblicò in un articolo intitolato, senza troppa fantasia, *A Complexity Measure*. L'idea è semplice: disegni la funzione come una mappa di incroci e conti le strade indipendenti che vanno dall'ingresso all'uscita. In pratica è il numero di decisioni più uno: ogni `if`, ogni `elif`, ogni `and` e ogni `or` aggiunge una biforcazione.

Perché ti dovrebbe interessare? Perché ogni strada è un caso da provare. Diciotto strade indipendenti significa che per percorrerle tutte almeno una volta servono almeno diciotto test diversi, e nessuno li ha mai scritti. McCabe propose di spezzare le funzioni che superano 10\. Vent'anni dopo, in una guida del NIST sul collaudo del software, lui e Arthur Watson scrivevano che quel limite aveva «un notevole supporto sperimentale», anche se c'era chi usava 15 senza problemi. Diciotto non va bene in nessuno dei due casi.

## Un italiano contro gli if

Non sei la prima persona a provare antipatia per quella freccia. Il 23 novembre 2007, all'Italian Agile Day, Francesco Cirillo presentò la **Anti-IF Campaign**. Sì, è lo stesso Cirillo della tecnica del Pomodoro, quella del timer da cucina a forma di pomodoro per lavorare a blocchi di 25 minuti. Il sito della campagna spiegava così il motivo: «Molti team vogliono essere agili, ma non conoscono le basi per ridurre la complessità del codice». Nel giugno 2008 aderì Kent Beck, il padre dell'Extreme Programming, e dopo di lui un lungo elenco di sviluppatori.

Il bersaglio non era ogni singolo `if`. Lo ha chiarito anche Craig Larman, amico di Cirillo: la campagna «non significa che tutti gli if siano cattivi». Il nemico erano soprattutto gli `if` che controllano *che tipo di cosa* hai in mano per decidere cosa farne. Nel tuo modulo ce ne sono parecchi.

## Prima di tutto, la rete

C'è una regola che vale più di tutti gli attrezzi che stai per usare: non si cambia la forma del codice senza una rete sotto. Prima di toccare una riga, copi la vecchia funzione in un file a parte e scrivi un test che genera ordini a caso (paesi, pesi, coupon, ordini vuoti) e confronta il risultato del vecchio codice con quello del nuovo:

```python
for _ in range(20_000):
    o = ordine_a_caso()
    for express in (False, True):
        vecchio = vecchia.calcola_spedizione(o, express=express)
        nuovo = (spedizione_express if express else spedizione_standard)(o)
        assert vecchio == nuovo, (o, express, vecchio, nuovo)
```

Ventimila ordini, ciascuno in versione normale ed express: quarantamila confronti. La funzione nuova deve dare esattamente gli stessi risultati della vecchia, comprese le eccezioni. Adesso puoi cominciare.

## Attrezzo 1: le guardie all'ingresso

La prima cosa che fai è raddrizzare la freccia. Le tre condizioni di errore (nessun ordine, ordine vuoto, pacco troppo pesante) sono controlli d'ingresso: se l'ordine non è valido non ha senso calcolare niente. Allora le metti in cima, una dopo l'altra, ognuna con il suo `raise` subito sotto, come un buttafuori alla porta della discoteca:

```python
def _costo_base(ordine):
    if not ordine or not ordine.get("righe"):
        raise ValueError("ordine vuoto")
    peso = sum(r["peso"] for r in ordine["righe"])
    if peso > PESO_MAX:
        raise ValueError("troppo pesante")
    # da qui in poi: l'ordine esiste, ha righe, pesa il giusto
    ...
```

Martin Fowler, nel suo catalogo di refactoring, chiama questa operazione *Replace Nested Conditional with Guard Clauses*: sostituire le condizioni annidate con delle clausole di guardia. Nota una cosa onesta: gli `if` non sono spariti. Ma non sono più annidati, e tutto quello che viene dopo può dare per scontato che l'ordine sia valido. È come leggere un libro dove le note sono a piè di pagina invece che in un'appendice di 40 pagine.

## Attrezzo 2: un dizionario al posto della scala

Poi guardi la scala `if/elif` dei corrieri: cinque gradini per dire «se il paese è X, usa il corriere Y». Quella non è logica, è una tabella travestita da codice. E le tabelle in Python hanno un nome: dizionario.

```python
CORRIERI = {"IT": "BRT", "FR": "Colissimo", "DE": "DHL", "ES": "SEUR"}

corriere = CORRIERI.get(ordine["paese"], "UPS")
```

Una riga, e il caso «tutti gli altri paesi» è il secondo argomento di `.get()`. Aggiungere il Portogallo non vuol dire più aggiungere un ramo al codice: vuol dire aggiungere una voce a un elenco.

Questo, per Python, è quasi un dogma. Il linguaggio non ha mai avuto lo `switch` che hanno C e Java, e non per caso. Fu proposto nel 2001 (PEP 275) e di nuovo nel 2006 (PEP 3103, scritta da Guido van Rossum in persona). Nel 2007, durante il suo intervento al PyCon, Guido fece un sondaggio veloce tra il pubblico. Il risultato: la proposta fu respinta perché «non ha alcun sostegno popolare». Per anni la risposta ufficiale a «come faccio uno switch in Python?» è stata: usa un dizionario.

## Attrezzo 3: la tabella e `bisect`

Le fasce di peso sono più insidiose: non sono uguaglianze («il paese è FR»), sono intervalli («fino a 2 kg», «fino a 5 kg»). Un dizionario non basta. Ma la libreria standard ha un modulo che fa esattamente questo, `bisect`, che cerca in quale posizione di una lista ordinata andrebbe un valore. La documentazione ufficiale ne mostra l'uso con un esempio sui voti degli esami:

```python
>>> def grade(score):
...     i = bisect([60, 70, 80, 90], score)
...     return "FDCBA"[i]
```

Quattro soglie, cinque voti, zero `if`. Tu fai lo stesso con le tariffe:

```python
from bisect import bisect_left

SOGLIE_KG = [2, 5, 10, 20, 30]
TARIFFE   = [4.90, 6.90, 9.90, 14.90, 19.90]
PESO_MAX  = SOGLIE_KG[-1]

def tariffa(peso):
    return TARIFFE[bisect_left(SOGLIE_KG, peso)]
```

C'è una trappola, ed è proprio quella in cui stavi per cadere. `bisect` (cioè `bisect_right`) e `bisect_left` si comportano diversamente quando il valore cade esattamente su una soglia:

```python
>>> from bisect import bisect, bisect_left
>>> bisect_left([2, 5, 10, 20, 30], 2)   # 2 kg: prima fascia, 4,90
0
>>> bisect([2, 5, 10, 20, 30], 2)        # 2 kg: seconda fascia, 6,90. Ops.
1
```

Il vecchio codice diceva `peso <= 2`, quindi 2 kg fanno parte della prima fascia, e serve `bisect_left`. Con l'altra versione ogni pacco da esattamente 2, 5, 10 o 20 chili costerebbe una fascia in più. Il test con 40.000 ordini lo trova al primo giro, perché tra i pesi casuali ci sono anche quelli esatti. Questa è la ragione per cui la rete si scrive prima.

Il premio, però, è grande: adesso le tariffe sono *dati*. Possono stare in un file di configurazione o in una tabella del database. Quando il listino cambierà (e cambierà), non toccherai la logica.

## Attrezzo 4: lascia decidere agli oggetti

Arrivi ai coupon, e qui c'è proprio il tipo di `if` contro cui è nata la campagna di Cirillo: «se il coupon è di tipo percentuale fai questo, se è fisso fai quest'altro, se è gratis fai quest'altro ancora». Ogni volta che qualcuno inventa un nuovo tipo di coupon, qualcun altro deve trovare tutti i posti del codice dove c'è quella scala di `if` e aggiungere un gradino. Se ne dimentica uno, il bug è servito.

L'alternativa è che ogni tipo di coupon sappia da solo cosa fare. Fowler la chiama *Replace Conditional with Polymorphism*:

```python
from dataclasses import dataclass

@dataclass
class Percentuale:
    valore: float
    def applica(self, costo): return costo * (1 - self.valore / 100)

@dataclass
class Fisso:
    valore: float
    def applica(self, costo): return max(0, costo - self.valore)

class Gratis:
    def applica(self, costo): return 0
```

Tre classi, ciascuna con un metodo `applica`. Chi usa il coupon non chiede più «di che tipo sei?»: chiama `coupon.applica(costo)` e basta. Python non chiede nemmeno una classe base comune, perché gli basta che il metodo ci sia (è il *duck typing*: se fa «qua» come un'anatra, per Python è un'anatra). Un nuovo tipo di coupon domani sarà una classe nuova, senza toccare quelle che già funzionano.

## Attrezzo 5: l'oggetto che non fa niente

Resta un `if` sui coupon: quello che controlla se il coupon c'è (`is not None`). Questo `if` ha una storia illustre. Nel 1965 Tony Hoare, progettando il linguaggio ALGOL W, introdusse il riferimento nullo «semplicemente perché era così facile da implementare». Nel 2009, a una conferenza a Londra, lo ha chiamato pubblicamente «il mio errore da un miliardo di dollari»: da allora ogni riferimento che *potrebbe* essere nullo costringe a un controllo ogni volta che lo usi. Se lo dimentichi, il programma crolla.

Il rimedio è un po' zen: un coupon che non fa niente.

```python
class NessunCoupon:
    def applica(self, costo): return costo
```

Si chiama *Null Object*, e Fowler lo cataloga come *Introduce Special Case*. Quando l'ordine non ha coupon, invece di `None` ricevi un `NessunCoupon`, che restituisce il costo senza modifiche. Il resto del codice non deve più chiedersi niente. In piccolo, Python lo fa già ogni volta che scrivi `dict.get(chiave, valore_di_default)`, o quando usi un `defaultdict`: il caso «non c'è» diventa un valore normale.

## Attrezzo 6: `match`, ma al confine

A questo punto ti serve un posto dove trasformare il coupon grezzo (un dizionario che arriva dal JSON dell'ordine) in uno degli oggetti di prima. È il lavoro perfetto per `match`, il *pattern matching strutturale* arrivato con Python 3.10 nell'ottobre 2021 (PEP 634):

```python
COUPON = {"percentuale": Percentuale, "fisso": Fisso,
          "gratis": lambda valore=0: Gratis()}

def coupon_da(dati):
    match dati:
        case None:
            return NessunCoupon()
        case {"tipo": tipo, **resto}:
            return COUPON[tipo](**resto)
```

Due casi: o non c'è niente, e restituisci il coupon che non fa niente, o c'è un dizionario con una chiave `tipo`, e il resto delle chiavi va al costruttore giusto. Sia chiaro: `match` non è un modo per evitare le decisioni. È un altro modo di scriverle, e radon lo sa, infatti a `coupon_da` dà 3\. Ma ora la decisione si prende **una volta sola, al confine**, dove i dati entrano nel programma. Da lì in poi il codice lavora con oggetti che sanno cosa fare, e non deve più chiedere niente a nessuno.

È la regola più importante di tutto il pomeriggio: un `if` sul tipo di una cosa va bene una volta, quando la crei. Se lo ripeti ogni volta che la usi, è lì che nasce la freccia.

## Attrezzo 7: via la bandierina

Ti resta il parametro `express=False`. Cerchi nel progetto dove viene chiamata la funzione, e trovi questo:

```python
calcola_spedizione(ordine, True)   # True cosa?
```

`True` cosa? Per scoprirlo bisogna aprire la funzione. Un parametro booleano che cambia il comportamento è una funzione che ne contiene due, con un `if` dentro a decidere quale delle due eseguire. Fowler la chiama *Remove Flag Argument*: dai a ciascun comportamento il suo nome.

```python
def _chiudi(ordine, corriere, costo):
    coupon = coupon_da(ordine.get("coupon"))
    return corriere, round(coupon.applica(costo), 2)

def spedizione_standard(ordine):
    corriere, costo = _costo_base(ordine)
    return _chiudi(ordine, corriere, costo)

def spedizione_express(ordine):
    corriere, costo = _costo_base(ordine)
    return _chiudi(ordine, corriere, costo * 2)
```

Adesso, nel codice che le usa, si legge `spedizione_express(ordine)`. Nessuno deve più chiedersi cosa voglia dire `True`.

## Attrezzo 8: chiedere scusa invece del permesso

Mentre sei lì, nello stesso file trovi una funzioncina per le giacenze che fa un `if` prima di leggere un dizionario. Il glossario ufficiale di Python ha due sigle per questi due stili. **LBYL**, *Look Before You Leap* («guarda prima di saltare»), è lo stile pieno di `if` che controllano prima di agire. **EAFP**, *Easier to Ask for Forgiveness than Permission* («è più facile chiedere scusa che chiedere il permesso»), invece agisce e gestisce l'eccezione se qualcosa va storto.

```python
# LBYL: guarda prima di saltare
if sku in giacenze:
    return giacenze[sku]
return 0

# EAFP: salta, e se cadi chiedi scusa
try:
    return giacenze[sku]
except KeyError:
    return 0

# oppure, ancora più corto
return giacenze.get(sku, 0)
```

Il glossario aggiunge un dettaglio che pochi ricordano: con più thread, LBYL può essere proprio sbagliato. Tra il momento in cui controlli `if sku in giacenze` e quello in cui leggi `giacenze[sku]`, un altro thread potrebbe aver tolto quella chiave. Con EAFP il controllo e la lettura sono un'unica operazione. Un'avvertenza, però: l'eccezione serve per i casi eccezionali, come una chiave che manca o un file che non c'è. Non usarla per le regole di business. Ne abbiamo già parlato [in un altro articolo](https://bitstales.eu/it/try-catch-quando-non-farlo/).

## Attrezzo 9: il trucco senza if (che in Python non funziona)

Sul web ti è capitata più volte una domanda di Stack Overflow del 2012 che è diventata leggendaria: perché un ciclo in C++ su un array *ordinato* è molto più veloce dello stesso ciclo sullo stesso array in disordine? La risposta è la *previsione dei salti* (*branch prediction*). Il processore, per non perdere tempo, prova a indovinare in anticipo come andrà ogni `if`. Con i dati ordinati indovina quasi sempre, con quelli in disordine sbaglia circa una volta su due, e ogni errore costa parecchi cicli di clock, perché il lavoro fatto in anticipo va buttato. Il rimedio, in C, è scrivere codice *branchless*: niente salti, solo aritmetica.

In Python anche i booleani sono numeri (`True + True` fa 2), quindi il trucco si può scrivere:

```python
def con_if(dati):
    t = 0
    for x in dati:
        if x >= 128:
            t += x
    return t

def senza_if(dati):
    t = 0
    for x in dati:
        t += x * (x >= 128)   # True vale 1, False vale 0
    return t
```

Ti viene la curiosità e misuri, con un milione di numeri tra 0 e 255 su Python 3.13\. I risultati sono il tempo migliore su sette prove:

```python
                 casuale   ordinato
con if           32,4 ms   28,1 ms
senza if         42,0 ms   35,6 ms
```

Due sorprese. La versione senza `if` è **più lenta** di circa il 30%: la moltiplicazione e la conversione del booleano costano più del salto che vorresti evitare. E l'array ordinato è più veloce *anche nella versione senza if*, quindi la differenza non viene (solo) dal tuo `if`. Viene dall'interprete, che per eseguire ogni tua riga fa migliaia di scelte sue, e probabilmente da come i dati stanno in memoria. Morale: in Python i trucchi branchless sono folklore. Gli `if` si tolgono per chi legge il codice, non per il processore. E se ti serve davvero la velocità, i cicli si spostano dentro NumPy o in codice compilato.

## L'if che resta

Sono le sei e mezza. Il test gira: quarantamila confronti, tutti identici. Rilanci radon:

```python
$ radon cc -s spedizioni.py
spedizioni.py
    F 41:0 _costo_base - A (5)
    F 29:0 coupon_da - A (3)
    C 11:0 Percentuale - A (2)
    ...
    F 54:0 spedizione_standard - A (1)
    F 59:0 spedizione_express - A (1)
```

La funzione da 18 non esiste più. Al suo posto ci sono funzioni piccole, la più complessa vale 5, e ognuna si legge in un colpo d'occhio. Il file, però, è *più lungo* di prima: da 41 righe a una sessantina. Se qualcuno in azienda misura la produttività contando le righe, oggi hai fatto un ottimo lavoro, ma per i motivi sbagliati.

E alcuni `if` sono rimasti, di proposito. Quello in `_costo_base` che rifiuta i pacchi oltre i 30 chili non è un difetto di progettazione: è una regola del mondo reale, e quell'`if` è il modo più chiaro di scriverla. Non serve trasformarlo in una gerarchia di classi. Allo stesso modo, un `if/else` con due casi non diventa più elegante se lo trasformi in un dizionario con due chiavi. E il polimorfismo ha un costo: la logica che prima stava in una funzione ora è divisa tra diverse classi.

La differenza, alla fine, è tutta qui. Un `if` che esprime una regola del dominio («oltre 30 kg non spediamo») è documentazione che si esegue. Un `if` che chiede a un oggetto «di che tipo sei?» per la decima volta in dieci file diversi è un problema di progettazione, e prima o poi qualcuno ne pagherà il prezzo.

Fai il commit e cancelli il commento «non toccare». Ci hai messo un pomeriggio, cioè, per dirla alla Cirillo, nove pomodori, pause comprese.

Fonti
- Thomas J. McCabe, [A Complexity Measure](https://doi.org/10.1109/TSE.1976.233837?ref=bitstales.eu) (IEEE Transactions on Software Engineering, 1976)
- Arthur H. Watson, Thomas J. McCabe, [Structured Testing](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication500-235.pdf?ref=bitstales.eu) (NIST SP 500-235, 1996), sezione sul limite di 10
- [Anti-IF Campaign](http://web.archive.org/web/20100310120536/http://www.antiifcampaign.com/), il sito originale nell'archivio di Internet Archive (2010)
- Craig Larman, [Anti-If Campaign](https://www.craiglarman.com/wiki/index.php?title=Anti-If%5FCampaign&ref=bitstales.eu)
- Martin Fowler, catalogo dei refactoring: [guard clauses](https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html?ref=bitstales.eu), [polimorfismo](https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html?ref=bitstales.eu), [special case](https://refactoring.com/catalog/introduceSpecialCase.html?ref=bitstales.eu), [flag argument](https://refactoring.com/catalog/removeFlagArgument.html?ref=bitstales.eu)
- [PEP 3103](https://peps.python.org/pep-3103/?ref=bitstales.eu) (switch, respinta) e [PEP 634](https://peps.python.org/pep-0634/?ref=bitstales.eu) (pattern matching)
- Documentazione di Python: [bisect](https://docs.python.org/3/library/bisect.html?ref=bitstales.eu) e [glossario (EAFP, LBYL)](https://docs.python.org/3/glossary.html?ref=bitstales.eu)
- Tony Hoare, [Null References: The Billion Dollar Mistake](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/?ref=bitstales.eu) (QCon London 2009)
- Stack Overflow, [Why is processing a sorted array faster than processing an unsorted array?](https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array?ref=bitstales.eu) (2012)
- [radon](https://radon.readthedocs.io/en/latest/intro.html?ref=bitstales.eu), documentazione