Scrivere codice Python che gira senza errori sul proprio portatile è una cosa. Scrivere architetture in grado di elaborare milioni di righe senza mandare in crash la memoria o gestire il flusso degli errori in un’applicazione di produzione è tutta un’altra storia.
La differenza tra uno script amatoriale e un software professionale si nasconde spesso nei dettagli che diamo per scontati: il modo in cui gestiamo le eccezioni, la comprensione reale di come Python alloca le variabili nello scope, o la scelta tra calcolare un risultato subito o rimandarlo a quando serve davvero.
In questo articolo analizziamo dieci scenari tecnici avanzati ,dalla gestione della memoria con le generator expression alle trappole del late binding nelle closure — non con la teoria da manuale, ma guardando a quello che succede realmente alla memoria e al flusso del programma quando il codice va in esecuzione. Mettiti alla prova con questi dieci quesiti e confronta il tuo ragionamento con le risposte e le riflessioni.
Parte 1: I 10 Quiz di Controllo
Q1 – Decoratori
Vuoi creare un decoratore misura_tempo che calcoli il tempo di esecuzione di una funzione senza alterarne il comportamento originale o il valore di ritorno. Quale codice implementa il pattern corretto?
A)
import time
def misura_tempo(func):
def wrapper(*args, **kwargs):
t0 = time.time()
func(*args, **kwargs)
print(f"Tempo: {time.time() - t0}")
return wrapper
B)
import time
def misura_tempo(func):
def wrapper(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
print(f"Tempo: {time.time() - t0}")
return result
return wrapper
C)
import time
def misura_tempo(func):
t0 = time.time()
result = func()
print(f"Tempo: {time.time() - t0}")
return result
D)
def misura_tempo(func):
return func
Domanda di riflessione: Perché è fondamentale salvare il valore restituito in una variabile result e fare return result alla fine del wrapper?
Q2 – Generatori
Vuoi creare una funzione che calcoli i quadrati dei numeri fino a N. Per ottimizzare la memoria, vuoi che la funzione si comporti come un generatore. Quale opzione rappresenta la soluzione più chiara e Pythonica?
A)
def quadrati(n):
lista = []
for i in range(n):
lista.append(i ** 2)
return lista
B)
def quadrati(n):
for i in range(n):
yield i ** 2
C)
def quadrati(n):
return [i ** 2 for i in range(n)]
D)
def quadrati(n):
i = 0
while i < n:
yield i ** 2
i += 1
Domanda di riflessione: Qual è il vantaggio principale di un generatore quando si lavora con sequenze di dati potenzialmente infinite o molto grandi?
Q3 – Gestione dei File e Context Manager
Devi aprire un file in lettura ed elaborarne il contenuto. Quale sintassi garantisce che il file venga chiuso correttamente in ogni scenario, anche se si verifica un errore improvviso durante la lettura?
A)
f = open("dati.txt", "r")
try:
contenuto = f.read()
finally:
f.close()
B)
with open("dati.txt", "r") as f:
contenuto = f.read()
C)
f = open("dati.txt", "r")
contenuto = f.read()
f.close()
D)
with open("dati.txt", "r") as f:
try:
contenuto = f.read()
except IOError:
print("Errore di lettura")
Domanda di riflessione: Cosa succede nel codice B e nel codice D se il file dati.txt non esiste sul disco (errore in fase di apertura)?
Q4 – Ordinamento personalizzato con Lambda
Hai una lista di tuple che rappresenta un gruppo di persone: persone = [(‘Anna’, 25), (‘Luca’, 18), (‘Sara’, 30)]. Vuoi ordinare la lista in-place (modificando la lista originale) in base all’età (il secondo elemento della tupla) in ordine crescente. Quale codice usi?
A)
persone.sort(key=lambda x: x[1])
B)
persone.sort(key=lambda x: x[0])
C)
persone_ordinate = sorted(persone, key=lambda x: x[0])
D)
persone.sort(key=lambda x: x[1], reverse=True)
Domanda di riflessione: Qual è la differenza strutturale e di allocazione di memoria tra l’uso del metodo list.sort() e la funzione integrata sorted()?
Q5 – Argomenti flessibili (*args, kwargs)
Stai progettando una funzione di logging che deve accettare un numero imprecisato di argomenti posizionali e, contemporaneamente, opzioni di configurazione passate tramite parole chiave (keyword arguments). Quale firma della funzione definisce correttamente questo comportamento?
A)
def log_messaggio(*args, **kwargs):
pass
B)
def log_messaggio(*args):
pass
C)
def log_messaggio(**kwargs):
pass
D)
def log_messaggio(*, configurazione):
pass
Domanda di riflessione: Perché l’ordine dei parametri nella firma è vincolante? Cosa succede se provi a definire una funzione posizionando **kwargs prima di *args?
Q6 – Ereditarietà e Costruttori
Hai una classe base Animale che richiede un parametro nome nel suo costruttore. Vuoi creare una sottoclasse Cane che estenda Animale e aggiunga l’attributo razza. Qual è la sintassi corretta per inizializzare l’oggetto?
A)
class Cane(Animale):
def __init__(self, nome, razza):
super().__init__(nome)
self.razza = razza
B)
class Cane(Animale):
def __init__(self, nome, razza):
Animale.__init__(nome)
self.razza = razza
C)
class Cane(Animale):
def __init__(self, nome, razza):
super().__init__()
self.razza = razza
D)
class Cane(Animale):
def __init__(self, nome, razza):
self.razza = razza
Domanda di riflessione: Quale anomalia si verifica a runtime se ometti completamente la chiamata a super().__init__() nella sottoclasse?
Q7 – Overloading degli Operatori
Stai creando una classe Vettore2D. Vuoi fare in modo che utilizzando l’operatore matematico + tra due istanze della classe (es. v1 + v2) venga restituito un nuovo oggetto Vettore2D che rappresenta la somma algebrica dei due vettori. Quale metodo speciale devi implementare?
A)
class Vettore2D:
def __sum__(self, other):
return Vettore2D(self.x + other.x, self.y + other.y)
B)
class Vettore2D:
def __add__(self, other):
return Vettore2D(self.x + other.x, self.y + other.y)
C)
class Vettore2D:
def __plus__(self, other):
return Vettore2D(self.x + other.x, self.y + other.y)
D)
class Vettore2D:
def __add__(self, other):
return (self.x + other.x, self.y + other.y)
Domanda di riflessione: Se l’utente scrivesse v1 += v2 nel suo codice, Python quale metodo cercherebbe di eseguire di default, e come differisce da __add__?
Q8 – Eccezioni personalizzate
Vuoi definire un’eccezione personalizzata ErroreDiValidazione che estenda la classe base delle eccezioni standard. Quale codice è corretto e strutturalmente pulito?
A)
class ErroreDiValidazione(Exception):
pass
B)
class ErroreDiValidazione():
pass
C)
def ErroreDiValidazione(Exception):
pass
D)
class ErroreDiValidazione(Exception):
def __init__(self, messaggio):
self.messaggio = messaggio
Domanda di riflessione: Perché è importante che le eccezioni personalizzate ereditino esplicitamente da Exception (o da una sua sottoclasse) piuttosto che da BaseException?
Q9 – List comprehension vs Generator expression
Devi elaborare un file di log di grandi dimensioni (nell’ordine di milioni di righe). Vuoi estrarre esclusivamente le righe che contengono una determinata stringa-chiave e poi processarle una per una. Quale approccio evita picchi di allocazione di memoria?
A) righe = [linea for linea in file if ‘parola’ in linea] e poi iterare su righe
B) righe = (linea for linea in file if ‘parola’ in linea) e poi iterare su righe
C) Caricare tutto il file in memoria con file.read().splitlines() e poi filtrare
D) È indifferente, entrambi i costrutti consumano la stessa quantità di memoria
Domanda di riflessione: In quale scenario specifico una list comprehension si rivela più vantaggiosa rispetto a un generatore?
Q10 – Closure
Quale blocco di codice definisce una vera e propria closure che restituisce una funzione deputata a moltiplicare un numero per un fattore fissato esternamente?
A)
def moltiplicatore(fattore):
def interno(x):
return x * fattore
return interno
B)
def moltiplicatore(fattore):
return lambda x: x * fattore
C)
def moltiplicatore(fattore):
def interno(x):
return x * fattore
return interno()
D)
def moltiplicatore(fattore):
def interno(x, fattore=fattore):
return x * fattore
return interno
Domanda di riflessione: Quale variabile viene catturata dallo scope circostante nelle risposte A e B? Quale comportamento stiamo alterando nella variante D tramite la sintassi fattore=fattore?
Parte 2: L’Analisi -Soluzioni
Q1 – Risposta corretta: B
Un decoratore deve comportarsi in modo agnostico e trasparente rispetto alla funzione che avvolge.
La soluzione A commette un errore grave in produzione: esegue la funzione originale ma ne distrugge il valore di ritorno, facendo sì che chiunque chiami la funzione decorata riceva sistematicamente None.
La soluzione B intercetta correttamente il risultato (result), esegue gli effetti collaterali del decoratore (il log del tempo) e infine restituisce il controllo intatto al chiamante.
La C esegue la funzione nel momento stesso in cui il decoratore viene definito, non quando la funzione viene effettivamente invocata.
Risposta alla riflessione: Salvare e restituire result preserva l’interfaccia e il contratto della funzione originale. Senza questo passaggio, il decoratore altera l’esito logico dell’algoritmo avvolto.
Q2 – Risposta corretta: B
L’opzione B implementa il pattern del generatore usando la parola chiave yield in un ciclo lineare pulito. Sebbene anche la D sia un generatore funzionante, l’uso di un ciclo while con incremento manuale dell’indice è meno efficiente e più prono ad errori (come loop infiniti) rispetto al costrutto nativo range(). Le risposte A e C allocano immediatamente intere liste in memoria, vanificando la richiesta di ottimizzazione.
Risposta alla riflessione: Il vantaggio principale risiede nella valutazione lazy (pigra). Un generatore mantiene in memoria solo lo stato interno corrente e calcola il valore successivo su richiesta. Questo permette di elaborare flussi di dati infiniti o file di dimensioni superiori alla RAM fisica con una complessità spaziale costante, pari a O[math]O(1)[/math].
Q3 – Risposta corretta: B
Il costrutto with (Context Manager) rappresenta lo standard industriale in Python. Garantisce l’esecuzione del metodo __exit__ dell’oggetto file (che si occupa di liberare il descrittore di file a livello di sistema operativo) anche se il blocco interno solleva eccezioni critiche. La soluzione A è formalmente corretta ma inutilmente verbosa, mentre la C rischia di lasciare il file aperto indefinitamente se f.read() fallisce. La D introduce un’illusione di sicurezza: posizionare il try dentro il blocco with non cattura gli errori generati durante la fase di apertura stessa del file.
Risposta alla riflessione: Se il file non esiste, sia in B che in D viene sollevata un’eccezione FileNotFoundError prima ancora che il corpo interno venga eseguito. In D, il blocco try interno non catturerà mai questo errore poiché si trova gerarchicamente a valle dell’apertura. Per gestire correttamente lo scenario, l’intero blocco with deve essere racchiuso da un try esterno.
Q4 – Risposta corretta: A
Il metodo list.sort() modifica la struttura dati originaria in-place e restituisce None. La funzione anonima lambda x: x[1] indica a Python di utilizzare il secondo elemento (l’indice 1, ossia l’età) come chiave di confronto. La B ordina per nome. La C usa la funzione sorted(), la quale non modifica la lista originale ma ne alloca una nuova in memoria. La D esegue un ordinamento decrescente a causa di reverse=True.
Risposta alla riflessione: list.sort() ottimizza l’uso della memoria modificando i puntatori della lista esistente (complessità spaziale in-place). sorted() crea una copia speculare ordinata, richiedendo memoria addizionale, ma offre il vantaggio di accettare qualsiasi iterabile (comprese tuple e set, che non possiedono un metodo .sort() nativo).
Q5 – Risposta corretta: A
La sintassi corretta richiede che i parametri posizionali variabili (*args) precedano i parametri chiave variabili (kwargs). L’opzione A è l’unica firma universale in grado di intercettare qualsiasi combinazione di input. Se si inverte questo ordine sintattico, Python solleva un SyntaxError immediato in fase di parsing del file, poiché non sarebbe in grado di tracciare la linea di demarcazione tra i due dizionari di binding.
Risposta alla riflessione: L’ordine rigoroso dei parametri imposto dall’interprete è: argomenti posizionali standard, parametri con valore di default, *args, parametri keyword-only, e infine **kwargs. Invertire questo flusso interrompe l’algoritmo di firma del costrutto, rendendo ambigua l’assegnazione delle variabili.
Q6 – Risposta corretta: A
La funzione super() restituisce un oggetto proxy che delega le chiamate ai metodi alla classe madre della gerarchia (MRO). L’opzione A chiama correttamente __init__ passando l’argomento richiesto nome. La B fallisce perché chiama il metodo della classe direttamente sulla firma della classe (Animale.__init__) omettendo il passaggio esplicito dell’istanza self come primo argomento. La C omette un parametro obbligatorio, mentre la D manca completamente di agganciare la catena di inizializzazione del genitore.
Risposta alla riflessione: Se si omette la chiamata al costruttore della superclasse, gli attributi definiti al suo interno (in questo caso self.nome) non vengono associati all’istanza della sottoclasse. Qualsiasi successivo tentativo di invocare metodi ereditati che fanno affidamento su tali attributi genererà un AttributeError a runtime.
Q7 – Risposta corretta: B
L’operatore aritmetico + è mappato internamente sul metodo speciale __add__. La risposta B implementa correttamente questo metodo e restituisce una nuova istanza coerente di Vettore2D. Le opzioni A e C si affidano a dunder methods inventati che l’interprete ignorerà. La D, pur implementando il metodo correttore, commette un errore architetturale: restituisce una tupla invece di un oggetto della stessa classe, rompendo la catena di polimorfismo (es. non potresti fare v1 + v2 + v3).
Risposta alla riflessione: Quando l’interprete incontra l’operatore di assegnazione composta +=, cerca in prima istanza il metodo __iadd__(self, other). Se questo non è definito, Python ripiega su __add__, calcolando un nuovo oggetto e riassegnandolo alla variabile. Implementare esplicitamente __iadd__ permette di ottimizzare le prestazioni modificando l’oggetto in-place, evitando nuove allocazioni (fondamentale per strutture dati mutabili).
Q8 – Risposta corretta: A
La risposta A rappresenta la struttura minima necessaria e sufficiente quando non vi è la reale necessità di mutare il comportamento del costruttore. La B definisce una classe che non eredita dal framework delle eccezioni, rendendola inutilizzabile con l’istruzione raise. La C compie un errore di sintassi macroscopico, definendo una funzione anziché una classe. Anche la D è sintatticamente valida, ma obbliga a passare una stringa rigida, riducendo la flessibilità nativa della classe base.
Risposta alla riflessione: Ereditare da Exception garantisce che la classe sia integrata nel motore di intercettazione di Python. Non si eredita da BaseException in contesti standard perché essa funge da superclasse anche per eccezioni di sistema come KeyboardInterrupt (es. Ctrl+C) o SystemExit. Se la nostra eccezione custom ereditasse da BaseException, correrebbe il rischio di essere catturata da blocchi di catch troppo generici, impedendo all’utente di arrestare il programma da terminale.
Q9 – Risposta corretta: B
L’utilizzo delle parentesi tonde in righe = (… if …) definisce una generator expression. Questo costrutto non popola alcuna struttura dati in memoria: restituisce un iteratore che estrae e valuta le righe del file una alla volta, mantenendo l’occupazione della RAM costante a prescindere dal volume del file di log. Al contrario, la list comprehension della risposta A (parentesi quadre) alloca istantaneamente spazio per tutte le milioni di righe filtrate, esponendo l’infrastruttura a crash da Out Of Memory.
Risposta alla riflessione: Una list comprehension è preferibile quando la mole di dati è ridotta e si ha la necessità stringente di riutilizzare la sequenza più volte (es. per effettuare accessi casuali tramite indice o per compiere passaggi iterativi multipli). Un generatore, infatti, una volta consumato si svuota e richiede di essere ricreato da capo per una nuova iterazione.
Q10 – Risposta corretta: A
Sia A (definizione esplicita) che B (espressione lambda) realizzano una closure formale, poiché la funzione interna cattura e “racchiude” lo stato della variabile fattore appartenente allo scope locale della funzione esterna, mantenendola in vita anche dopo che moltiplicatore ha terminato la sua esecuzione. Tra le due, la A è considerata la forma più esplicita e leggibile per blocchi strutturati. La C fallisce l’intento poiché esegue immediatamente la funzione interna, restituendo un valore numerico anziché un oggetto callable. La D utilizza un perimetro differente: fissa il valore istantaneamente tramite una configurazione di default.
Risposta alla riflessione: Nelle opzioni A e B, la funzione racchiusa cattura il riferimento alla variabile esterna tramite un puntatore all’ambiente circostante. Nella D, la sintassi fattore=fattore crea un argomento di default valutato eager (immediatamente) al momento della dichiarazione della funzione interna. Questo stratagemma congela il valore corrente della variabile ed è una tecnica difensiva fondamentale per evitare i bug da late binding (legame tardivo) quando si generano funzioni all’interno di cicli iterativi.
Parte 3: Commento Applicativo
Robustezza delle API e Monitoraggio (Q8)
Nel software enterprise, lanciare eccezioni generiche come ValueError o un generico Exception rende i sistemi di monitoraggio ciechi. Se un payload JSON fallisce la validazione dei tipi, sollevare un ErroreDiValidazione dedicato permette al middleware di catturare specificamente quella anomalia, loggarla con un livello di severity corretto (es. WARNING) e rispondere al client con un codice HTTP 400. Se venisse lanciato un errore generico, saremmo costretti a usare blocchi except Exception: omnicomprensivi che rischiano di mascherare bug di sistema ben più gravi (come una disconnessione del database), trasformandoli erroneamente in errori di input.
Sostenibilità e Scalabilità dell’Infrastruttura (Q2, Q9)
Il passaggio da una List Comprehension a una Generator Expression non è una scelta di stile, ma una decisione architetturale. Lavorando nel campo della Data Engineering o della System Security (ad esempio nell’analisi di flussi di log), elaborare un file di grandi dimensioni caricando l’intera struttura in memoria satura istantaneamente i limiti dei container (es. nodi Kubernetes o istanze serverless). Se il container esaurisce la RAM, interviene il Linux OOM (Out Of Memory) Killer che termina bruscamente il processo. L’approccio lazy garantito dai generatori mantiene il consumo di risorse piatto, consentendo allo stesso script di elaborare 10 Megabyte così come 10 Terabyte di dati senza variare l’impatto infrastrutturale.
Factory dinamiche ed evitare i bug da late binding (Q10)
Uno dei bug più subdoli in Python è il late binding, un comportamento che emerge spesso quando si creano funzioni all’interno di un ciclo. In questi casi, le funzioni generate non memorizzano il valore corrente della variabile del ciclo, ma conservano un riferimento alla variabile stessa. Quando vengono richiamate, utilizzano quindi il valore che essa possiede in quel momento, che di solito coincide con l’ultimo assunto al termine del ciclo.
funzioni = []
for i in range(3):
funzioni.append(lambda x: x * i)
# Cosa ci aspetteremmo?
funzioni[0](10) # 0
funzioni[1](10) # 10
funzioni[2](10) # 20
# Cosa otteniamo realmente?
funzioni[0](10) # 20
funzioni[1](10) # 20
funzioni[2](10) # 20
Tutte le funzioni restituiscono lo stesso risultato perché fanno riferimento alla medesima variabile [math]i[/math], che al termine del ciclo vale [math]2[/math].
Per evitare questo comportamento si può “congelare” il valore corrente della variabile utilizzando un parametro di default:
funzioni = []
for i in range(3):
funzioni.append(lambda x, i=i: x * i)
In questo caso ogni funzione conserva una copia del valore di [math]i[/math] esistente al momento della sua creazione, eliminando il problema del late binding.
Comprendere il funzionamento delle closure e sapere quando utilizzare questa tecnica è fondamentale per sviluppare factory di funzioni, sistemi a plugin, callback, router dinamici e, più in generale, tutte quelle architetture software in cui le funzioni vengono create e configurate durante l’esecuzione del programma.
Mappa dei Pattern Architetturali
Dietro molti costrutti sintattici di Python si nascondono principi fondamentali dell’ingegneria del software. Comprenderli come pattern architetturali permette di andare oltre la semplice sintassi e di progettare applicazioni più modulari, manutenibili e robuste. Decoratori, generatori, context manager e closure non sono soltanto caratteristiche del linguaggio: rappresentano strumenti progettuali che aiutano a separare le responsabilità, gestire le risorse e costruire software adatto agli ambienti di produzione.
Il Glossario Architetturale della Produzione
- Pattern: Decorator → Trasparenza dell’interfaccia
- Il Decorator Pattern consente di estendere il comportamento di una funzione o di un componente senza modificarne l’implementazione né alterarne l’interfaccia pubblica. Questo rispetta il Principio Aperto/Chiuso (Open/Closed Principle) e permette di isolare le cosiddette cross-cutting concerns, come logging, monitoraggio, caching, controllo degli accessi, validazione o raccolta di metriche, mantenendo la logica di business indipendente dall’infrastruttura.
- Pattern: Generator / Iterator → Lazy Evaluation
- I generatori implementano il paradigma della lazy evaluation, producendo i dati soltanto quando vengono richiesti dall’iteratore. Dal punto di vista architetturale, consentono di trasformare un’elaborazione monolitica in una pipeline di dati incrementale, particolarmente adatta allo streaming e all’elaborazione di grandi volumi di informazioni. Poiché mantengono in memoria solo lo stato necessario per l’iterazione corrente, permettono di elaborare dataset molto estesi con un consumo di memoria praticamente costante, migliorando la scalabilità delle applicazioni.
- Pattern: Context Manager → Gestione deterministica delle risorse
- Il Context Manager applica in Python gli stessi principi che ispirano il paradigma RAII (Resource Acquisition Is Initialization) del C++, pur attraverso un meccanismo differente basato sul protocollo __enter__() e __exit__(). L’acquisizione e il rilascio di una risorsa vengono legati al ciclo di vita di un blocco with, garantendo che file, connessioni al database, socket, lock o altre risorse vengano sempre rilasciati correttamente, anche in presenza di eccezioni. Questo riduce il rischio di resource leak, blocchi concorrenti e comportamenti imprevedibili nei sistemi di produzione.
- Pattern: Closure → Incapsulamento dello stato
- Le closure rappresentano un meccanismo leggero per mantenere uno stato privato senza ricorrere necessariamente alla definizione di una classe. La funzione interna conserva un riferimento alle variabili dello scope che la racchiude, creando un ambiente persistente anche dopo il termine dell’esecuzione della funzione esterna. Questo costrutto è particolarmente utile per implementare factory di funzioni, callback, strategie configurabili e router dinamici, oltre a costituire uno degli strumenti fondamentali della programmazione funzionale in Python.
Conoscere questi costrutti significa imparare a ragionare non soltanto in termini di sintassi, ma anche di architettura software. La stessa soluzione che in uno script di poche righe appare come una semplice caratteristica del linguaggio diventa, in un sistema reale, uno strumento per migliorare modularità, riuso del codice, gestione delle risorse e scalabilità. È questo passaggio di prospettiva che distingue chi sa scrivere codice Python da chi sa progettare software destinato alla produzione.
Cosa misura davvero questo quiz?
Le domande che seguono non verificano soltanto se conosci la sintassi di Python. Valutano soprattutto la tua capacità di prevedere il comportamento del programma durante l’esecuzione: come vengono allocate le strutture dati, quando vengono liberate le risorse, quali oggetti vengono realmente creati in memoria e come l’interprete gestisce il flusso delle eccezioni. È questa differenza di prospettiva che separa lo scripting occasionale dalla progettazione di software destinato alla produzione.
“`html id=”python_fondamenti01″
Python: dalle strutture dati ai decoratori, con esercizi pratici
👉Test di autovalutazione Python: 20 domande da base ad avanzato
👉Imparare Python: esercizi pratici sulle liste con soluzioni
👉Tuple in Python: teoria, immutabilità ed esercizi
👉Liste, tuple e dizionari in Python: guida completa
👉Funzioni in Python: parametri, return, *args e **kwargs
“`





