Sottoprogrammi
Scopri i pilastri della programmazione procedurale in Python: dalle funzioni e l'organizzazione modulare del codice tramite package, fino all'uso ottimale di strutture dati, comprehension e slicing.
La programmazione procedurale in Python si basa sulla scomposizione in funzioni, che sono oggetti di prima classe manipolabili come dati e supportano la documentazione tramite docstring. Python impiega un modello di "pass-by-assignment" per i parametri delle funzioni, dove le modifiche a oggetti mutabili nello scope della funzione si riflettono esternamente, a differenza degli oggetti immutabili.
Introduzione
La programmazione procedurale costituisce uno dei pilastri fondamentali nello sviluppo software in Python. Basata sulla scomposizione di un problema complesso in blocchi di codice riutilizzabili e modulari, questo paradigma permette di scrivere applicazioni manutenibili, efficienti e facilmente testabili.
In questa guida analizzeremo i concetti chiave della programmazione procedurale in Python, la creazione di sottoprogrammi, l'organizzazione del codice tramite moduli e package, fino all'utilizzo ottimale dei tipi di dati sequenziali e strutturati.
Sottoprogrammi e architettura delle funzioni in Python
In Python tutti i sottoprogrammi sono formalmente definiti come funzioni. Non esiste una distinzione sintattica rigida tra "procedure" e "funzioni", qualsiasi blocco di codice definito tramite la parola chiave def restituisce sempre un valore. Quando una funzione non contiene un'istruzione return esplicita, il linguaggio restituisce automaticamente l'oggetto None.
Dal punto di vista architetturale, le funzioni in Python sono oggetti di prima classe (first-class objects). Questo significa che possono essere assegnate a variabili, passate come argomenti ad altre funzioni o restituite come valori di ritorno, consentendo l'implementazione di pattern avanzati come le closure.
Sintassi e documentazione delle funzioni
La definizione di una funzione richiede una sintassi chiara. È buona norma integrare immediatamente sotto la firma della funzione un literal stringa denominato docstring, utilizzato per documentare lo scopo dell'algoritmo secondo la convenzione PEP 257.
Sintassi base di definizione:
def nome_funzione(parametro1: float, parametro2: float) -> float:
"""Descrizione sintetica del comportamento della funzione."""
risultato = parametro1 + parametro2
return risultato
Esempio pratico per il calcolo del discriminante di un'equazione di secondo grado:
def discriminante(a: float, b: float, c: float) -> float:
"""Calcola il discriminante (delta) di un'equazione di secondo grado."""
return (b ** 2 - 4 * a * c)
Valutazione e passaggio dei parametri
Il modello di passaggio dei parametri in Python segue la semantica del pass-by-assignment (noto anche come pass-by-object-reference). Gli argomenti passati a una funzione sono riferimenti a oggetti in memoria. Se l'oggetto passato è immutabile, ad esempio interi, stringhe o tuple, le modifiche all'interno della funzione non altereranno la variabile originale. Se l'oggetto è mutabile, ad esempio liste o dizionari, le modifiche apportate nello scope della funzione si rifletteranno sull'oggetto esterno.
Python offre grande flessibilità nella gestione degli argomenti, consentendo di impostare valori di default e di utilizzare sia la notazione posizionale che quella nominale (keyword arguments).
def welcome(nome: str, cdl: str = 'Informatica', anno: str = 'primo') -> str:
"""Genera una stringa formattata di benvenuto per lo studente."""
return f"{nome} è uno studente del {anno} anno del cdl in {cdl}"
Invocazioni valide della funzione con differenti combinazioni di argomenti:
messaggio1 = welcome('Mario', 'Ing. Informatica', 'secondo')
messaggio2 = welcome('Mario')
messaggio3 = welcome(nome='Mario', cdl='Ing. Informatica', anno='secondo')
messaggio4 = welcome('Mario', anno='secondo')
Separatori dei parametri per il controllo delle API
Per garantire una chiara separazione delle responsabilità e proteggere l'interfaccia delle librerie, Python permette di limitare l'uso dei parametri attraverso i separatori / e *.
| Posizione del parametro | Comportamento consentito |
|---|---|
| Prima di / | Esclusivamente posizionale |
| Tra / e * | Sia posizionale che nominale |
| Dopo * | Esclusivamente nominale |
def wlc(nome: str, /, cdl: str, *, anno: str = 'I') -> str:
"""Funzione con vincoli rigidi sulla tipologia di argomenti."""
return f"{nome} - {cdl} - {anno}"
Esempi di chiamate valide ed errate:
wlc('Mario', 'Informatica', anno='II')
wlc('Mario', cdl='Informatica')
Funzioni con numero variabile di argomenti
Oltre ai parametri posizionali e nominali con vincoli rigidi, Python consente di definire funzioni capaci di accettare un numero arbitrario di argomenti tramite le notazioni *args e **kwargs. Questo pattern è ampiamente utilizzato nelle librerie standard e di terze parti per costruire interfacce flessibili.
def somma_valori(*args: float) -> float:
"""Somma un numero arbitrario di valori numerici."""
return sum(args)
def stampa_dettagli(**kwargs: str) -> None:
"""Stampa una coppia chiave, valore per ogni argomento nominale ricevuto."""
for chiave, valore in kwargs.items():
print(f"{chiave}, {valore}")
Modularità del codice, moduli e package
Man mano che la dimensione del software cresce, l'organizzazione del codice all'interno di un unico file diventa inefficace. Python gestisce la modularità tramite moduli e package.
Moduli in Python
Un modulo è semplicemente un file di testo contenente codice Python con estensione .py. Per utilizzare le funzioni e le variabili definite in un altro modulo, si utilizza l'istruzione import.
Supponendo che il file equazioni.py contenga la definizione della funzione discriminante, è possibile importare e utilizzare il codice con diverse sintassi:
import equazioni
risultato = equazioni.discriminante(1, -5, 6)
import equazioni as eq
risultato = eq.discriminante(1, -5, 6)
from equazioni import discriminante
risultato = discriminante(1, -5, 6)
from equazioni import discriminante as discr
risultato = discr(1, -5, 6)
L'uso dell'istruzione from modulo import * è fortemente sconsigliato dalle linee guida ufficiali PEP 8, in quanto inquina il namespace locale e rende difficile tracciare l'origine dei simboli utilizzati.
Una buona pratica quando un modulo viene sia importato che eseguito direttamente è racchiudere il codice destinato all'esecuzione diretta all'interno del blocco condizionale seguente, così da evitarne l'esecuzione involontaria in fase di import.
if __name__ == "__main__":
risultato = discriminante(1, -5, 6)
print(risultato)
Risoluzione dei moduli e organizzazione in package
Quando viene eseguita un'istruzione import, l'interprete Python ricerca il modulo richiesto seguendo un ordine preciso.
- La cartella corrente di esecuzione del file.
- I percorsi specificati nella variabile d'ambiente
PYTHONPATH. - I percorsi standard delle librerie di sistema installate nell'ambiente Python, elencati in
sys.path.
Un package è una directory organizzata che raggruppa più moduli correlati. Per essere riconosciuta come package strutturato nelle versioni tradizionali di Python, la directory deve contenere un file di inizializzazione denominato __init__.py, che può anche essere vuoto.
Esempio di importazione da una struttura a directory nidificata:
import sound.effects.echo
sound.effects.echo.echofilter(input_bytes, output_bytes)
Tipi di dati sequenziali e strutture di collezioni
Python mette a disposizione una serie di tipi di dati nativi pensati per memorizzare collezioni di elementi. La comprensione di queste strutture è fondamentale per ottimizzare le prestazioni e l'allocazione della memoria.
Panoramica dei tipi sequenziali
Ogni tipo strutturato supporta sia una sintassi letterale rapida che l'uso della propria funzione costruttrice, come str(), list(), tuple(), set() e dict().
Il tipo set
Un set è una collezione non ordinata di elementi univoci, particolarmente utile quando è necessario garantire l'assenza di duplicati o eseguire in modo efficiente controlli di appartenenza e operazioni insiemistiche.
prodotti_attivi = {'server', 'switch', 'router'}
prodotti_dismessi = {'hub', 'switch'}
| Operazione | Descrizione |
|---|---|
| s.add(x) | Aggiunge l'elemento x all'insieme, se non già presente |
| s.remove(x) | Rimuove l'elemento x, sollevando un'eccezione se assente |
| s.discard(x) | Rimuove l'elemento x senza sollevare eccezioni se assente |
| s1 union s2 | Restituisce l'unione tra due insiemi |
| s1 intersection s2 | Restituisce l'intersezione tra due insiemi |
prodotti_attivi.add('firewall')
tutti_prodotti = prodotti_attivi.union(prodotti_dismessi)
comuni = prodotti_attivi.intersection(prodotti_dismessi)
Grazie alla struttura interna basata su tabelle hash, l'operazione di verifica di appartenenza, ad esempio 'switch' in prodotti_attivi, risulta generalmente più efficiente rispetto alla stessa verifica eseguita su una lista di dimensioni comparabili.
Il tipo dict
Un dict è una collezione di coppie chiave, valore che consente di associare a ciascun elemento un identificatore univoco anziché una posizione numerica, risultando particolarmente adatto a rappresentare dati strutturati e configurazioni.
studente = {'nome': 'Mario', 'cdl': 'Informatica', 'anno': 'secondo'}
| Operazione | Descrizione |
|---|---|
| d[chiave] | Accede al valore associato alla chiave, sollevando un'eccezione se assente |
| d.get(chiave, default) | Accede al valore associato alla chiave, restituendo un default se assente |
| d.keys() | Restituisce le chiavi presenti nel dizionario |
| d.values() | Restituisce i valori presenti nel dizionario |
| d.items() | Restituisce le coppie chiave, valore presenti nel dizionario |
| d.update(altro_dict) | Aggiorna il dizionario con le coppie chiave, valore di un altro dizionario |
corso = studente.get('cdl', 'Non specificato')
for chiave, valore in studente.items():
print(f"{chiave}, {valore}")
A partire da Python 3.7 l'ordine di inserimento delle chiavi in un dizionario viene mantenuto in modo stabile, una caratteristica che ha reso il dict una struttura sempre più utilizzata anche in contesti dove in passato si sarebbe preferita una lista di tuple.
Restituzione di valori multipli tramite tuple
Un pattern ampiamente utilizzato nella programmazione procedurale in Python è la restituzione di più valori da una singola funzione. Mentre in linguaggi come C o Java occorre definire strutture rigide, in Python la funzione può semplicemente restituire una tupla, sfruttando la tecnica dell'unboxing, detta anche tuple unpacking.
Esempio di ricerca del valore massimo in una lista di misurazioni:
def massima(temperature: list) -> tuple:
"""Individua l'indice e il valore massimo all'interno di una sequenza."""
pos_max = 0
for i in range(1, len(temperature)):
if temperature[i] > temperature[pos_max]:
pos_max = i
return pos_max, temperature[pos_max]
Invocazione e spacchettamento diretto del risultato:
dati_meteo = [18.2, 22.5, 19.1, 25.4, 21.0]
indice_max, valore_max = massima(dati_meteo)
Comprehension per la costruzione di collezioni
Accanto ai cicli espliciti, Python offre le comprehension come pattern idiomatico per costruire liste, dizionari e insiemi in modo più conciso e generalmente più performante rispetto a un ciclo for equivalente.
dati_meteo = [18.2, 22.5, 19.1, 25.4, 21.0]
sopra_soglia = [t for t in dati_meteo if t > 20]
indici_valori = {i: t for i, t in enumerate(dati_meteo)}
Operazioni avanzate con le liste e slicing
La lista è la struttura dati mutabile più utilizzata in Python. Consente l'annidamento arbitrariamente complesso, con liste multi-dimensionali, e mette a disposizione una serie di metodi nativi per la manipolazione degli elementi.
numeri = [1, 2, 3, 4]
matrice = [[1, 2], [3, 4]]
valore = matrice[0][0]
Operazioni fondamentali sulle liste
| Operazione | Descrizione |
|---|---|
| l[i] e l[-i] | Accesso diretto tramite indice positivo o negativo, partendo dal fondo |
| l.append(x) | Inserisce l'elemento in coda alla lista |
| l.insert(i, x) | Posiziona l'elemento all'indice i |
| l.pop() e l.pop(i) | Rimuove e restituisce l'ultimo elemento o quello in posizione i |
| l.clear() | Svuota completamente la lista |
| len(l) | Restituisce il numero di elementi della lista |
| l.sort(key=None, reverse=False) | Ordina la lista in-place |
prodotti = ['server', 'switch', 'router']
prodotti.append('firewall')
elemento_rimosso = prodotti.pop(1)
prodotti.sort(reverse=True)
La tecnica dello slicing
Lo slicing è un meccanismo che permette di estrarre sotto-sequenze da liste o stringhe tramite la notazione lista[start:stop]. L'elemento corrispondente all'indice stop è sempre escluso.
Regole operative dello slicing:
- Il valore di default di
startè0. - Il valore di default di
stopè la lunghezza totale della sequenza, ottenibile conlen(l). - La notazione
lista[:i]estrae gli elementi dall'inizio fino all'indicei-1. - La notazione
lista[i:]estrae gli elementi dall'indiceifino alla fine. - L'espressione
lista[:]crea una copia superficiale, detta shallow copy, dell'intera lista.
A differenza dell'accesso diretto tramite indice singolo, che solleva l'eccezione IndexError in caso di sconfinamento, lo slicing è protetto e gestisce in modo sicuro gli indici fuori intervallo.
stringa = 'Prova'
sub1 = stringa[0:3]
sub2 = stringa[2:100]
sub3 = stringa[7:99]
La comprensione dell'indicizzazione a separatori costituisce la chiave visiva per padroneggiare lo slicing, gli indici non vanno intesi come posizioni sugli elementi, ma come delimitatori posizionati prima e dopo ciascun carattere o elemento della sequenza.