Python: ordinare con `sorted`, `key` e `itemgetter` (e perché l'ordinamento è stabile)
Ordinare una lista di dizionari per uno o più campi è un'operazione quotidiana. Python la rende semplice, ma ci sono due o tre dettagli che conviene conoscere.
key, non confronti
sorted() accetta key: una funzione applicata una sola volta a ogni elemento, il cui risultato viene usato per confrontare.
from operator import itemgetter, attrgetter
righe = [{"nome": "Anna", "eta": 31}, {"nome": "Bruno", "eta": 25}]
sorted(righe, key=lambda r: r["eta"])
sorted(righe, key=itemgetter("eta")) # equivalente, un po' più veloce
# per gli oggetti: attrgetter("eta")
L'ordinamento è stabile
A parità di chiave, gli elementi restano nell'ordine originale. Vale anche con reverse=True: l'ordine degli elementi uguali non si inverte.
dati = [("b", 1), ("a", 1), ("c", 0), ("d", 1)]
sorted(dati, key=itemgetter(1))
# [('c', 0), ('b', 1), ('a', 1), ('d', 1)]
sorted(dati, key=itemgetter(1), reverse=True)
# [('b', 1), ('a', 1), ('d', 1), ('c', 0)]
Più criteri
Con criteri tutti nello stesso verso basta una tupla (o itemgetter("eta", "nome")). Con versi diversi hai due strade, verificate uguali sui dati di prova:
# 1) chiave con il segno meno (solo per numeri)
sorted(righe, key=lambda r: (-r["eta"], r["nome"]))
# 2) due passaggi, sfruttando la stabilità: prima il criterio secondario
a = sorted(righe, key=itemgetter("nome"))
a = sorted(a, key=itemgetter("eta"), reverse=True)
Il secondo funziona anche con stringhe in verso inverso, dove il segno meno non si può usare.
Trappola: None nei dati
sorted([3, None, 1])
# TypeError: '<' not supported between instances of 'NoneType' and 'int'
Soluzione: includere nella chiave se il valore manca.
sorted([3, None, 1], key=lambda x: (x is None, x if x is not None else 0))
# [1, 3, None]
Quanto costa
100.000 dizionari, Python 3.12.15, minimo di 5 prove:
lambda r: r["eta"]: ~9,5 msitemgetter("eta"): ~7,9 msitemgetter("eta", "nome"): ~34,5 ms- due passaggi con
itemgetter: ~24,6 ms - tupla con
(-eta, nome)via lambda: ~43,1 ms cmp_to_keycon una funzione di confronto: ~79,8 mslist.sort()esorted()sono pari (~8,3 ms):sortrisparmia la copia di memoria, non il tempo.- Rifare il sort su una lista già ordinata: ~2,7 ms contro ~7,9 ms su una casuale (Timsort sfrutta i tratti già ordinati).
Quindi: itemgetter quando basta, i due passaggi sono spesso più veloci di una tupla costruita con lambda, e cmp_to_key solo se non c'è alternativa.
Il codice completo, eseguito per scrivere questo articolo, è nel nostro repository di verifica (sorted_key.py). Le misure dipendono dalla macchina: contano i rapporti, non i millisecondi.