W3docs

Raggruppare le Liste Python

Tre modi per raggruppare liste Python: defaultdict, itertools.groupby e dict comprehension — con esempi eseguibili e insidie comuni.

Raggruppare una lista significa suddividere i suoi elementi in sotto-collezioni che condividono una chiave comune — ad esempio, raggruppare parole per la loro prima lettera, o raggruppare record per un campo categoria. Python offre tre approcci principali: un ciclo manuale con collections.defaultdict, itertools.groupby dalla libreria standard e le dict comprehension. Questo capitolo spiega ciascuna tecnica, quando scegliere l'una rispetto all'altra e le insidie da evitare.

Capitoli correlati: Liste Python · Metodi delle Liste · List Comprehension · Iterare le Liste · Modulo collections

Cosa significa "raggruppare"

Data una lista piatta e una funzione chiave che associa ogni elemento a un'etichetta di gruppo, l'obiettivo è produrre una mappatura da ogni etichetta alla lista degli elementi che le appartengono:

['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
  key = first letter
  →  {'a': ['apple', 'avocado', 'apricot'],
      'b': ['banana', 'blueberry'],
      'c': ['cherry']}

Le tre tecniche seguenti producono tutte questo tipo di risultato. Si differenziano per verbosità, prestazioni e vincoli che impongono sull'input.

Tecnica 1: Ciclo manuale con defaultdict

collections.defaultdict è l'approccio più comune e flessibile. Quando si accede a una chiave che non esiste ancora, un defaultdict(list) crea automaticamente una lista vuota per quella chiave, quindi non è mai necessaria una guardia if key in d.

from collections import defaultdict

words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']

by_letter = defaultdict(list)
for word in words:
    by_letter[word[0]].append(word)

for letter, group in sorted(by_letter.items()):
    print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']

Perché usare defaultdict invece di un dict normale?

Con un dict normale è necessario un controllo esplicito prima del primo append:

# Plain dict — more boilerplate, same result
by_letter = {}
for word in words:
    if word[0] not in by_letter:
        by_letter[word[0]] = []
    by_letter[word[0]].append(word)

Un'alternativa più breve con un dict normale è dict.setdefault:

by_letter = {}
for word in words:
    by_letter.setdefault(word[0], []).append(word)

setdefault va bene per script brevi, ma defaultdict è più veloce (nessuna ricerca ripetuta della chiave) e più esplicito riguardo all'intento.

Raggruppare per una chiave calcolata

La chiave può essere qualsiasi espressione, non solo un attributo. In questo esempio, una lista di interi viene suddivisa in gruppi pari e dispari:

from collections import defaultdict

numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]

by_parity = defaultdict(list)
for n in numbers:
    by_parity['even' if n % 2 == 0 else 'odd'].append(n)

print('even:', sorted(by_parity['even']))  # even: [2, 4, 6]
print('odd:', sorted(by_parity['odd']))    # odd: [1, 1, 3, 3, 5, 5, 5, 9]

Raggruppare una lista di dict

Questo è lo scenario più comune nel mondo reale — raggruppare righe di dati per il valore di un campo:

from collections import defaultdict

data = [
    {'category': 'fruit', 'name': 'apple'},
    {'category': 'vegetable', 'name': 'carrot'},
    {'category': 'fruit', 'name': 'banana'},
    {'category': 'vegetable', 'name': 'broccoli'},
]

grouped = defaultdict(list)
for item in data:
    grouped[item['category']].append(item['name'])

for category, names in grouped.items():
    print(f'{category}: {names}')
# fruit: ['apple', 'banana']
# vegetable: ['carrot', 'broccoli']

Tecnica 2: itertools.groupby

itertools.groupby raggruppa elementi consecutivi che condividono la stessa chiave. È utile quando è necessario preservare la struttura delle sequenze o quando i dati sono già ordinati e si vuole evitare di costruire l'intero dizionario in una volta sola (è lazy/streaming).

from itertools import groupby

words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']

# groupby only groups consecutive elements, so sort first
words_sorted = sorted(words, key=lambda w: w[0])

for letter, group in groupby(words_sorted, key=lambda w: w[0]):
    print(f'{letter}: {list(group)}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']

L'insidia critica: ordinare prima di groupby

groupby raggruppa solo gli elementi consecutivi che condividono la stessa chiave. Se l'input non è ordinato per chiave, si ottengono più gruppi piccoli invece di un gruppo per chiave:

from itertools import groupby

# Unsorted input — groupby produces WRONG results
numbers = [1, 1, 2, 3, 3, 1, 2, 2]
for key, group in groupby(numbers):
    print(f'{key}: {list(group)}')
# 1: [1, 1]   ← first run of 1s
# 2: [2]
# 3: [3, 3]
# 1: [1]      ← second run of 1s — NOT merged with the first!
# 2: [2, 2]

Ordinare sempre con la stessa funzione chiave prima di chiamare groupby:

numbers_sorted = sorted(numbers)
for key, group in groupby(numbers_sorted):
    print(f'{key}: {list(group)}')
# 1: [1, 1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]

Quando groupby eccelle: dati in streaming di grandi dimensioni

Poiché groupby restituisce un iteratore, non carica tutti i gruppi in memoria contemporaneamente. Questo lo rende utile per elaborare file ordinati di grandi dimensioni riga per riga senza costruire un dizionario completo.

from itertools import groupby

# Grouping namedtuple records
from collections import namedtuple

Product = namedtuple('Product', ['category', 'name', 'price'])
products = [
    Product('dairy', 'milk', 1.10),
    Product('fruit', 'apple', 1.20),
    Product('fruit', 'banana', 0.50),
    Product('vegetable', 'broccoli', 1.50),
    Product('vegetable', 'carrot', 0.80),
]
# products is already sorted by category here

for category, group in groupby(products, key=lambda p: p.category):
    items = list(group)
    print(f'{category}: {[p.name for p in items]}')
# dairy: ['milk']
# fruit: ['apple', 'banana']
# vegetable: ['broccoli', 'carrot']

Tecnica 3: Dict comprehension

Una dict comprehension costruisce il dizionario raggruppato in un'unica espressione. È concisa ma ha uno svantaggio: la list comprehension interna riscorre l'intero input per ogni chiave univoca, rendendola O(n × k) dove k è il numero di chiavi univoche. Per liste piccole va bene; per liste grandi, preferire defaultdict.

words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']

# Collect unique keys first, then build each group
letters = sorted(set(w[0] for w in words))
grouped = {letter: [w for w in words if w[0] == letter] for letter in letters}

for letter, group in grouped.items():
    print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']

Questa tecnica è più leggibile quando l'insieme delle chiavi è piccolo e già noto — ad esempio, raggruppare risultati True/False o un insieme fisso di categorie.

Aggregare i gruppi dopo il raggruppamento

Un'operazione comune successiva al raggruppamento è l'aggregazione: calcolare una somma, una media, un minimo o un conteggio per gruppo. Si combina defaultdict(list) con l'aritmetica standard di Python:

from collections import defaultdict

scores = [
    ('Alice', 90), ('Bob', 75), ('Alice', 85),
    ('Bob', 88), ('Carol', 92),
]

by_student = defaultdict(list)
for name, score in scores:
    by_student[name].append(score)

for student, student_scores in sorted(by_student.items()):
    avg = sum(student_scores) / len(student_scores)
    print(f'{student}: scores={student_scores}, avg={avg:.1f}')
# Alice: scores=[90, 85], avg=87.5
# Bob: scores=[75, 88], avg=81.5
# Carol: scores=[92], avg=92.0

Scegliere la tecnica giusta

SituazioneScelta migliore
Raggruppamento generale, qualsiasi ordinedefaultdict(list)
Necessità di fare streaming di dati ordinati di grandi dimensioniitertools.groupby
Lista piccola, soluzione concisa in una rigaDict comprehension
L'input è già ordinatodefaultdict o groupby
Necessità di aggregare (somma, media, ecc.)defaultdict(list) + aritmetica

Insidie comuni

Dimenticare di ordinare prima di groupby. groupby unisce solo le chiavi consecutive identiche. Usare sempre sorted() sull'input con la stessa funzione chiave prima di passarlo a groupby.

Assegnare list(group) immediatamente. L'iteratore del gruppo restituito da groupby si esaurisce non appena il ciclo for esterno avanza alla chiave successiva. Convertirlo in una lista all'interno del corpo del ciclo se è necessario usarlo più di una volta.

Modificare la lista di input durante il raggruppamento. Aggiungere o rimuovere elementi dalla lista durante un ciclo di raggruppamento produce risultati imprevedibili. Costruire prima il dizionario raggruppato, poi modificare gli elementi.

defaultdict che appare nel repr. defaultdict(list, {...}) appare diverso da un semplice dict nel repr. Racchiuderlo con dict(grouped) quando si necessita di un output in formato dict normale.

Pratica

Pratica
What must you do before passing a list to itertools.groupby() to get one group per unique key?
What must you do before passing a list to itertools.groupby() to get one group per unique key?
Was this page helpful?