Il tuo post VoAGI Perché non dovresti abusare delle list comprehension in Python.

Il tuo articolo VoiAGI - Perché evitare un uso eccessivo delle list comprehension in Python.

 

In Python, le comprensioni di lista forniscono una sintassi concisa per creare nuove liste da liste esistenti e da altri oggetti iterabili. Tuttavia, una volta che ti abitui alle comprensioni di lista, potresti essere tentato di usarle anche quando non è necessario. 

Ricorda, il tuo obiettivo è scrivere codice semplice e mantenibile; non codice complesso. Spesso è utile rivedere il Zen di Python, un insieme di aforismi per scrivere codice Python pulito ed elegante, in particolare i seguenti:

  • Elegante è meglio che brutto.
  • Semplice è meglio che complesso.
  • La leggibilità è importante.

In questo tutorial, programmeremo tre esempi, ognuno dei quali più complesso del precedente, in cui le comprensioni di lista rendono il codice estremamente difficile da mantenere. Quindi proveremo a scrivere una versione più mantenibile dello stesso codice. 

Quindi cominciamo a programmare!

 

Comprensioni di Lista in Python: Una Veloce Recensione 

 

Iniziamo rivedendo le comprensioni di lista in Python. Supponiamo di avere un oggetto iterabile esistente come una lista o una stringa. E vorremmo crearne una nuova lista. Possiamo scorrere l’oggetto iterabile, elaborare ogni elemento e aggiungere l’output a una nuova lista in questo modo:

nuova_lista = []per elemento in oggetto_iterabile:    nuova_lista.append(output)

 

Ma le comprensioni di lista offrono un’alternativa concisa in una sola riga per fare lo stesso:

nuova_lista = [output for elemento in oggetto_iterabile]

 

Inoltre, è possibile aggiungere anche condizioni di filtro.

Il seguente frammento:

nuova_lista = []per elemento in oggetto_iterabile:    if condizione:        nuova_lista.append(output)

 

Può essere sostituito da questa comprensione di lista:

nuova_lista = [output for elemento in oggetto_iterabile if condizione]

 

Quindi le comprensioni di lista ti aiutano a scrivere codice Pythonico, spesso rendendo il tuo codice più pulito riducendo il rumore visuale. 

Ora prendiamo tre esempi per capire perché non dovresti utilizzare le comprensioni di lista per compiti che richiedono espressioni super complesse. Infatti, in tali casi, le comprensioni di lista – anziché rendere il tuo codice elegante – lo rendono difficile da leggere e mantenere.

 

Esempio 1: Generare Numeri Primi

 

Problema: Dato un numero limite_superiore, genera una lista di tutti i numeri primi fino a quel numero.

Puoi suddividere questo problema in due idee chiave:

  • Verificare se un numero è primo
  • Popolare una lista con tutti i numeri primi 

L’espressione di comprensione di lista per farlo è la seguente:

import mathlimite_superiore = 50  primi = [x for x in range(2, limite_superiore + 1) if x > 1 and all(x % i != 0 for i in range(2, int(math.sqrt(x)) + 1))]print(primi)

 

Ecco l’output:

Output >>>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

 

Ad una prima occhiata, è difficile capire cosa sta accadendo…Rendiamolo migliore.

Forse, meglio?

import mathlimite_superiore = 50  primi = [   x   for x in range(2, limite_superiore + 1)  if x > 1 and all(x % i != 0 for i in range(2, int(math.sqrt(x)) + 1))]print(primi)

 

Più facile da leggere, sicuramente. Ora scriviamo una versione veramente migliore.

Una Versione Migliore

Anche se una list comprehension è in realtà un’ottima idea per risolvere questo problema, la logica per verificare i numeri primi nella list comprehension la rende rumorosa.

Quindi scriviamo una versione più gestibile che sposta la logica per verificare se un numero è primo in una funzione separata is_prime(). E chiamiamo la funzione is_prime() nell’espressione della list comprehension:

import mathdef is_prime(num):    return num > 1 and all(num % i != 0 for i in range(2, int(math.sqrt(num)) + 1))upper_limit = 50  primes = [    x   for x in range(2, upper_limit + 1)  if is_prime(x)]print(primes)

Output >>>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

La versione migliore è abbastanza buona? Questo rende l’espressione della list comprehension molto più facile da capire. Ora è chiaro che l’espressione raccoglie tutti i numeri fino a upper_limit che sono primi (dove is_prime() restituisce True).

Esempio 2: Lavorare con le Matrici

Problema: Data una matrice, trovare quanto segue:

  • Tutti i numeri primi
  • Gli indici dei numeri primi
  • La somma dei numeri primi
  • I numeri primi in ordine decrescente

Per appiattire la matrice e raccogliere l’elenco di tutti i numeri primi, possiamo utilizzare una logica simile all’esempio precedente.

Tuttavia, per trovare gli indici, abbiamo un’altra complessa espressione di list comprehension (ho formattato il codice in modo che sia facile da leggere).

Puoi combinare il controllo dei numeri primi e l’ottenimento dei loro indici in una singola list comprehension. Ma questo non renderà le cose più semplici.

Ecco il codice:

import mathfrom pprint import pprintmy_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]def is_prime(num):    return num > 1 and all(num % i != 0 for i in range(2, int(math.sqrt(num)) + 1))# Appiattire la matrice e filtrare per contenere solo numeri primiprimes = [    x   for row in my_matrix    for x in row    if is_prime(x)]# Trova gli indici dei numeri primi nella matrice originaleprime_indices = [  (i, j)  for i, row in enumerate(my_matrix)  for j, x in enumerate(row)  if x in primes]# Calcola la somma dei numeri primisum_of_primes = sum(primes)# Ordina i numeri primi in ordine decrescentesorted_primes = sorted(primes, reverse=True)# Crea un dizionario con i risultatiresult = {  "primes": primes,   "prime_indices": prime_indices, "sum_of_primes": sum_of_primes, "sorted_primes": sorted_primes}pprint(result)

E l’output corrispondente:

Output >>>{'primes': [2, 3, 5, 7], 'prime_indices': [(0, 1), (0, 2), (1, 1), (2, 0)], 'sum_of_primes': 17, 'sorted_primes': [7, 5, 3, 2]}

Quindi cos’è una versione migliore?

Una Versione Migliore

Ora, per la versione migliore, possiamo definire una serie di funzioni per separare le preoccupazioni. In questo modo, se c’è un problema, saprai a quale funzione tornare e correggere la logica.

import mathfrom pprint import pprintdef is_prime(num):    return num > 1 and all(n % i != 0 for i in range(2, int(math.sqrt(num)) + 1))def flatten_matrix(matrix):    flattened_matrix = []    for row in matrix:        for x in row:            if is_prime(x):                flattened_matrix.append(x)    return flattened_matrixdef find_prime_indices(matrix, flattened_matrix):    prime_indices = []    for i, row in enumerate(matrix):        for j, x in enumerate(row):            if x in flattened_matrix:                prime_indices.append((i, j))    return prime_indicesdef calculate_sum_of_primes(flattened_matrix):    return sum(flattened_matrix)def sort_primes(flattened_matrix):    return sorted(flattened_matrix, reverse=True)my_matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]primes = flatten_matrix(my_matrix)prime_indices = find_prime_indices(my_matrix, primes)sum_of_primes = calculate_sum_of_primes(primes)sorted_primes = sort_primes(primes)result = {  "primes": primes,   "prime_indices": prime_indices, "sum_of_primes": sum_of_primes, "sorted_primes": sorted_primes}pprint(result)

 

Questo codice restituisce lo stesso output di prima.

Output >>>{'primes': [2, 3, 5, 7], 'prime_indices': [(0, 1), (0, 2), (1, 1), (2, 0)], 'sum_of_primes': 17, 'sorted_primes': [7, 5, 3, 2]}

 

È sufficiente la versione migliore? Anche se questa soluzione funziona per una matrice piccola come quella di questo esempio, restituire una lista statica non è generalmente consigliato. E per generalizzare a dimensioni più grandi, puoi utilizzare generatori al loro posto.

 

Esempio 3: Analisi di Stringhe JSON Nidificate

 

Problema: Analizzare una stringa JSON nidificata specificando delle condizioni e ottenere una lista di valori richiesti.

L’analisi delle stringhe JSON nidificate è sfidante perché bisogna considerare i diversi livelli di nidificazione, la natura dinamica della risposta JSON e i diversi tipi di dati nella logica di analisi.

Prendiamo ad esempio l’analisi di una stringa JSON specificando delle condizioni per ottenere una lista di tutti i valori che sono:

  • Interi o liste di interi
  • Stringhe o liste di stringhe

Puoi caricare una stringa JSON in un dizionario Python utilizzando la funzione loads del modulo incorporato json. Quindi avremo un dizionario nidificato su cui possiamo fare una comprehension di lista.

La comprehension di lista utilizza loop nidificati per iterare sul dizionario nidificato. Per ogni valore, si costruisce una lista in base alle seguenti condizioni:

  • Se il valore non è un dizionario e la chiave inizia con ‘inner_key’, si utilizza [inner_item].
  • Se il valore è un dizionario con un ‘sub_key’, si utilizza [inner_item['sub_key']].
  • Se il valore è una stringa o un intero, si utilizza [inner_item].
  • Se il valore è un dizionario, si utilizza list(inner_item.values()).

Dai un’occhiata al frammento di codice qui sotto:

import jsonjson_string = '{"key1": {"inner_key1": [1, 2, 3], "inner_key2": {"sub_key": "value"}}, "key2": {"inner_key3": "text"}}'# Analizza la stringa JSON in un dizionario Pythondata = json.loads(json_string)flattened_data = [   value   if isinstance(value, (int, str))    else value  if isinstance(value, list)  else list(value)    for inner_dict in data.values() for key, inner_item in inner_dict.items()   for value in (      [inner_item]        if not isinstance(inner_item, dict) and key.startswith("inner_key")     else [inner_item["sub_key"]]        if isinstance(inner_item, dict) and "sub_key" in inner_item     else [inner_item]       if isinstance(inner_item, (int, str))       else list(inner_item.values())  )]print(f"Valori: {flattened_data}")

 

Ecco l’output:

Output >>>Valori: [[1, 2, 3], 'valore', 'testo']

 

Come si può vedere, la comprensione della lista è molto difficile da capire. 

Per favore, aiutate voi stessi e gli altri membri del team evitando di scrivere codice del genere.

 

Una versione migliore

 

Credo che il seguente snippet usando cicli for annidati e una scala if-elif sia migliore. Perché è più semplice capire cosa sta succedendo. 

dati_schierati = []for dizionario_interno in dati.values():    for chiave, elemento_interno in dizionario_interno.items():        if not isinstance(elemento_interno, dict) and chiave.startswith("chiave_interna"):            dati_schierati.append(elemento_interno)        elif isinstance(elemento_interno, dict) and "sotto_chiave" in elemento_interno:            dati_schierati.append(elemento_interno["sotto_chiave"])        elif isinstance(elemento_interno, (int, str)):            dati_schierati.append(elemento_interno)        elif isinstance(elemento_interno, list):            dati_schierati.extend(elemento_interno)        elif isinstance(elemento_interno, dict):            dati_schierati.extend(elemento_interno.values())print(f"Valori: {dati_schierati}")

 

Anche questo restituisce l’output atteso:

Output >>>Valori: [[1, 2, 3], 'valore', 'testo']

 

La versione migliore è sufficientemente buona? Beh, non proprio. 

Perché le scale if-elif sono spesso considerate un segnale di cattivo codice. Potresti ripetere la logica tra i rami e l’aggiunta di più condizioni renderà il codice più difficile da mantenere. 

Ma per questo esempio, le scale if-elif e i cicli annidati rendono la versione più facile da capire rispetto all’espressione di comprensione.

 

Conclusione

 

Gli esempi che abbiamo codificato finora dovrebbero darti un’idea di come l’uso eccessivo di una funzionalità Python come la comprensione della lista possa diventare troppo. Questo è vero non solo per le comprensioni delle liste (che sono le più utilizzate, però) ma anche per le comprensioni dei dizionari e degli insiemi.

Dovresti sempre scrivere codice che sia facile da capire e mantenere. Quindi cerca di mantenere le cose semplici anche se significa non utilizzare alcune funzionalità Pythoniche. Continua a programmare!  

[Bala Priya C](https://twitter.com/balawc27) è una sviluppatrice e scrittrice tecnica dell’India. Le piace lavorare all’intersezione tra matematica, programmazione, data science e creazione di contenuti. Le sue aree di interesse e competenza includono DevOps, data science e elaborazione del linguaggio naturale. Ama leggere, scrivere, programmare e bere caffè! Attualmente sta cercando di imparare e condividere le sue conoscenze con la comunità di sviluppatori scrivendo tutorial, guide pratiche, articoli di opinione e altro ancora: