Non usate print!
E soprattutto, non insegnate a usare print. Questa è una comunicazione
di pubblica utilità per chi crede di poter insegnare
Python ai principianti.
Se dovessi dire qual è il singolo elemento di Python che vedo più frainteso,
sbagliato, insegnato male, usato in modo pericoloso: non avrei esitazioni,
vince print a mani basse.
Sospetto che print sia in cima a una curiosa classifica: è l'istruzione
(funzione in Python 3) con il più basso rapporto di utilizzo
professionisti/principianti: ti insegnano a usarla di continuo, ma poi non la
vedi quasi mai nel codice serio che leggi in giro. Forse in questa classifica
se la gioca con input, altra croce e (poca) delizia dei corsi per
principianti.
Ai bei tempi di Python 2, print era anche una insospettabile fonte di
terrore su Windows: quando rinominavi il tuo file in ".pyw" per sopprimere
l'odiatissima
finestrella nera, un print qualsiasi dimenticato nel codice causava un'eccezione non
gestita con conseguente crash e mani nei capelli. Questo in Python 3 non
succede più - peccato!, era sempre un'occasione di farci bella figura a costo
zero, a "divinare" sui forum la causa dell'errore.
Resta il fatto che print è sempre il cocco della maestra di Python.
Purtroppo non c'è libro o corso che non ne abusi allegramente. Dovrei provare
a contare quante volte compare print nelle prime cento pagine di un
manuale qualsiasi di Python: credo che sarebbe un buon indicatore della
qualità (inversa) di quel manuale.
Non mi è chiarissimo perché print sia così popolare tra chi si mette in
testa di insegnare Python. Può darsi che sia l'influenza di
print('hello world'): "guardate com'è facile Python, non vi viene già
voglia di seguire il mio corso?"
Ma credo piuttosto che sia una questione di banale sciatteria didattica.
L'effetto collaterale di usare print, è print.
Qual è il problema con print, insomma? In sostanza: che
print scrive nello standard output e questo è per definizione un
side-effect. E i side-effect, gli effetti collaterali, sono un problema complesso che
non bisognerebbe infilare alla chetichella nelle prime pagine di un manuale
per principianti.
Intendiamoci, Python è pur sempre un linguaggio procedurale, che maneggia i
side-effect nel normale flusso di esecuzione del programma: per modificare il
valore di una variabile globale usiamo la stessa sintassi che usiamo per una
variabile locale, e così via. Le operazioni di input/output sono astratte in
manipolazioni di oggetti intercambiabili tra loro: una funzione può
indifferentemente aggiungere righe a un file o a una lista. Insomma, non
abbiamo paura dei side-effect; non siamo Haskell.
Tuttavia, proprio perché è così facile produrre un side-effect in
Python, bisogna essere doppiamente cauti. E in particolar modo, è
responsabilità dell'insegnante non introdurre inavvertitamente delle
storture che poi restano radicate nella testa degli allievi.
I motivi per cui bisogna maneggiare con cura i side-effect sono noti e non è
questa la sede per un discorso approfondito. Sono difficili da capire: il loro
effetto intrinsecamente non-locale influenza a distanza codice che in quel
momento non hai sotto gli occhi, rendendo faticoso seguire il flusso del
programma. Sono difficili da testare: non vanno d'accordo con il modello degli
unit test, che si basa sul principio dell'isolamento funzionale delle
unità di codice, principio che ovviamente i side-effect violano allegramente.
Sono difficili da gestire nell'ambiente di esecuzione, e quindi difficili da
debuggare: gli stati globali condivisi rendono l'esito dipendente dall'ordine
di esecuzione delle varie unità che vi accedono,
race condition comprese.
Ma il vero problema non è tanto che i side-effect sono difficili: molte cose
sono difficili e comunque utili, in Python come nella vita.
Il problema è che il principiante non può sapere che i side-effect sono
difficili; e non può sapere che print fa qualcosa di concettualmente
molto intricato.
La responsabilità dell'insegnante è di non dare in mano all'allievo una
pistola carica, senza avvertirlo che è carica e senza spiegargli quali sono i
pericoli potenziali di puntarsela su un piede e tirare il grilletto.
Annuncio di pubblica utilità: chi si accinge a scrivere l'ennesimo tutorial o
a registrare l'ennesimo corso su YouTube, dovrebbe cortesemente passare dieci
minuti a riflettere sulla frase precedente. Grazie dell'attenzione.
Print nella shell: inutile.
Molti corsi iniziano dalla shell: è una buona idea. Anzi, in genere il guaio è
che non restano sulla shell abbastanza a lungo.
Ma allora perché molti si sentono obbligati a scrivere
>>> print(2 + 2)
nella shell?
Come tutti sanno, in modalità interattiva l'interprete di Python funziona come
come una
REPL, dove la "P" sta già appunto per "Print". Non c'è alcun bisogno di usare
print in una REPL. Ci pensa già la REPL a "stampare". La shell di
Python valuta le espressioni "sciolte" (non assegnate a variabili) e ne
pubblica il risultato nello standard output. Proprio come farebbe
print, insomma:
>>> 2 + 2
4
Sì, lo sappiamo, è vero: non è proprio la stessa cosa. Print usa
"str()" per convertire in stringa, la shell usa invece "repr()". Ma di fatto
non vi è alcuna differenza tra le due, almeno per i tipi di base che si
spiegano nelle prime lezioni di Python: numeri, liste, tuple, dizionari...
stringhe! E qui vi vedo già col ditino alzato: no, per le stringhe è diverso.
Ma anche qui: è diverso solo per il gioco degli apici semplici e doppi, e in
generale per i caratteri speciali di cui bisogna fare l'escape. In
particolare per gli "a-capo". Francamente non mi sembra che ci sia tutta
questa urgenza di introdurre le stringhe multi-riga, che oltre tutto sono
anche difficili da scrivere nella shell. E in ogni caso si può benissimo
spiegare che
>>> """hello
... world"""
'hello\nworld'
è una rappresentazione poco user-friendly ma "vedremo in seguito che
print ha appunto lo scopo..." eccetera.
E a proposito: se sono così bravo da ritardare print, quando poi la
spiego ci guadagno che ormai dovrei avere già le basi per spiegare la
differenza tra "dato" (valore di un oggetto) e "rappresentazione del dato"
(output di quel valore). E specificare che "str()" e print offrono una
rappresentazione pensata per gli utenti del programma, mentre "repr()" è
rivolta ai programmatori. E vedete quanti frutti posso raccogliere, se solo ho
la pazienza di aspettare che maturino.
Print nei moduli: dannoso.
Quando Python esegue un modulo, d'altra parte, non emette spontaneamente
nessun risultato nello standard output. Se vogliamo vedere qualcosa
dobbiamo chiederlo esplicitamente; e qui entra in gioco print... o no?
Beh... no. Il problema con questo approccio è che porta gli allievi a
disseminare i moduli di print "globali", a livello del modulo appunto.
E tutte le istruzioni "globali" sono eseguite a import time.
Questo vuol dire che i print non sono eseguiti solo quando "fate
girare" il modulo come uno script; ma anche quando lo importate in qualche
altro modulo. L'istruzione "import" in Python non si limita ad analizzare
staticamente il codice di un modulo e "caricare" i nomi che trova: "import"
esegue anche il codice a livello del modulo.
Questo è (indovinate) un side-effect, appunto. Si tratta di un side-effect
voluto, una scelta di design. Ma è pur sempre un side-effect e andrebbe
maneggiato con cura. Il tutorial di Python (che
ho tradotto
di recente!)
mette bene in chiaro
che bisognerebbe limitare attentamente il codice a livello di modulo:
"Un modulo può contenere istruzioni eseguibili oltre a definizioni di
funzioni. Queste istruzioni devono essere intese come un modo di
inizializzare il modulo."
Cioè, va bene mettere degli "import", delle definizioni di costanti utili, e
cose del genere. Ma un print è solo un side-effect indesiderato e non
dovrebbe stare a livello del modulo. Altrimenti, vedrete messaggi surreali che
compaiono quando importate il modulo e quindi anche, per esempio, quando lo
testate.
Naturalmente questo vale non solo per print, ma per qualsiasi
istruzione "globale": e infatti è buona pratica evitarle per quanto possibile.
E, manco a dirlo, i corsi e i tutorial e i video su YouTube vanno
avanti per un sacco di tempo a proporre agli allievi di mettere istruzioni
"globali" nei moduli,
prima finalmente di arrivare a spiegare le funzioni.
"Ma che importanza ha, poi?", chiede a questo punto il docente sciatto e
pigro: "quando si comincia va bene fare così, poi si spiega che però è meglio
non mettere i print globali. Come la fai lunga!".
Ma è appunto questo il problema, vedete. Dopo che sei andato avanti per
duecento pagine a mostrare agli allievi una cosa sbagliata, diventa un
pochino difficile convincerli a smettere. Davvero si insegna con
l'esempio e con l'abitudine. Il docente sciatto pensa che si
possa iniziare programmando provvisoriamente male, e poi si imparerà a
programmare bene. Ma non è così: il progresso va da programmare bene
cose semplici, a programmare bene cose più complesse. Se inizi a programmare
male, continuerai a programmare male.
Prova ne sia che i principianti continuano a riempire i moduli con
filastrocche interminabili di istruzioni a livello di modulo,
anche quando ormai hanno imparato le funzioni. Nella loro testa, le
funzioni servono solo per il codice che bisogna ripetere (chiamare più volte);
per tutto il resto, sanno che le istruzioni globali funzionano, perché le
usano da molto tempo... e se funzionano, che bisogno c'è di complicarsi la
vita?
Ma questa è una responsabilità dell'insegnante, non del principiante. Ho visto
molti corsi in cui addirittura il side-effect del print è una cosa
voluta, ricercata:
# nel modulo foo.py
print('sono foo.py!')
Cosi quando poi faccio "import foo", l'output del print mi dice appunto
che ho importato "foo". Questa è didattica criminale.
La soluzione? Semplicissimo: non spiegate i moduli finché non avete gli
strumenti necessari. Restate sulla shell più a lungo; spiegate
anche le funzioni nella shell. Solo a quel punto potete introdurre i
moduli nel discorso. In questo modo verrà spontaneo organizzare il codice del
modulo in funzioni; e non ci sarà bisogno di usare istruzioni "globali", tra
cui i print.
Se avete già spiegato le funzioni, potete introdurre i moduli come un modo per
raccogliere le funzioni, invece che un modo per raccogliere le istruzioni. A
questo punto le istruzioni globali diventeranno un'eccezione, come appunto
dovrebbe essere. Se avviate gli allievi sulla buona strada, resteranno sulla
buona strada.
Print nelle funzioni: da evitare.
D'accordo, abbiamo capito che non va bene mettere side-effect come
print a livello del modulo. Ma abbiamo anche detto che i moduli hanno
bisogno dei print espliciti per visualizzare il loro output. Quindi va
bene mettere i print all'interno delle funzioni, giusto?
Sbagliato.
Perché non bisogna dimenticare che print è pur sempre un side-effect.
Idealmente una funzione dovrebbe essere una struttura isolata che riceve
valori come parametri ed emette valori di ritorno. Più ci allontaniamo da
questa situazione ideale, più la nostra funzione diventa difficile da testare,
da capire, da usare. Ma soprattutto: se stiamo insegnando le funzioni,
allora dobbiamo fare ogni possibile sforzo per mettere gli studenti
sulla buona strada. Parametri e valori di ritorno: tutto ciò che esce da
questo confine è sospetto. Se insegni qualcosa che esce da questo
confine, è doppiamente sospetto.
Questo non vale solo per print, ovviamente. Usare "global", ovvero
accedere in scrittura a una variabile globale, è una pessima idea. Ma
anche accedere in sola lettura non è proprio ideale:
VAL = 100 # una costante
def foo():
return VAL + 1 # accedo in lettura alla costante
Questo naturalmente va bene e si fa spesso in pratica. Ma non va proprio
benissimo, e onestamente non lo insegnerei. L'ideale sarebbe che tutto
ciò di cui una funzione ha bisogno le venisse passato come argomento. Quindi,
perché non approfittarne al momento di spiegare i valori di default?
VAL = 100
def foo(val=VAL):
return val + 1
Ma torniamo al nostro print. Il modo peggiore in assoluto di usarlo
dentro una funzione è questo:
def check_age(age): # questo è un crimine
if age >= 18:
print('sei maggiorenne')
else:
print('sei minorenne')
Usare print al posto di return è un crimine contro
l'insegnamento e andrebbe punito. Di nuovo: non importa se è solo
"provvisorio", in attesa di spiegare return. La soluzione è spiegare
subito return e usare subito return.
Quante volte avete visto degli orrori del genere in un tutorial, in un corso
su YouTube, ovunque? Il "vantaggio" è che in questo modo sembra che la
funzione "faccia qualcosa" subito, la si può "usare" immediatamente, si vede
subito "il risultato".
Io proprio non capisco tutta questa ansia da prestazione. Il problema, non c'è
bisogno di dirlo, è che una funzione non si usa così. E poco importa se fai
vedere agli allievi che la usi subito, se però fai vedere che la usi
male.
La soluzione? Semplice: la shell!
>>> def check_age(age): # nella shell!
... if age >= 18:
... return True
... else:
... return False
...
>>> check_age(42) # guarda mamma, senza print
True
Come abbiamo detto, la shell non ha bisogno di print. Se vi serve far
vedere una funzione subito e volete anche usarla bene, spiegate
le funzioni nella shell e non nei moduli! Semplice, vero?
Si chiama didattica. Una cosa a cui forse bisognerebbe pensare prima di
mettersi a insegnare.
Quando poi volete cominciare a usare i moduli, potete andare avanti ancora a
lungo semplicemente importandoli nella shell:
>>> import my_module
>>> my_module.check_age(42) # ancora senza print
True
Questo, tra l'altro, vi porta già su un terreno contiguo a molte buone
pratiche, come l'esecuzione di moduli dalla riga di comando, "sys.argv" e così
via. Se incanalate il discorso in questa direzione, i vostri allievi capiranno
istintivamente che l'output è una cosa da rimandare il più possibile: si deve
chiedere solo all'ultimo momento, quando ce n'è effettivamente bisogno:
$ python -m my_module 42
e cose del genere. Di nuovo, vedete quanti frutti si possono raccogliere se
solo si aspetta che maturino.
Invece, manco a dirlo, i corsi e i tutorial e i video su YouTube raccolgono
sempre subito, il più presto possibile. E siccome le cattive pratiche
portano ad altre cattive pratiche, finisce che questi corsi usano
print dentro le funzioni anche quando ormai hanno spiegato
return. Sempre "provvisoriamente", si capisce.
def mydiv(a, b): # terrificante
if b == 0:
print('non puoi dividere per zero')
return a / b
Print usati per gestire casi particolari, per dire qualcosa mentre si restituisce un risultato... Una variegata galleria degli orrori.
Arriva poi il momento in cui, fatti bene i conti e seguite le buone pratiche, davvero occorre emettere qualcosa nello standard output, con un print. (All'inverso, questo succede anche con input, beninteso: non ne parliamo qui per non allungare ulteriormente il discorso.)
Non è sempre sbagliato usare print. Ma se avete messo i vostri allievi sulla strada giusta fin dal principio, quando finalmente arrivate a spiegare print non correte il rischio di mandarli a sbattere. In particolare, print riguarda solo ed esclusivamente l'interfaccia con l'utente del programma. Ora, è davvero necessario spiegare l'interfaccia utente presto nel vostro corso? Potete andare avanti per centinaia di pagine o decine di ore su YouTube senza preoccuparvi di questo aspetto: semplicemente, fate dialogare le funzioni tra loro e chiamatele dalla shell. L'interfaccia utente è un problema separato (e complicato) che può aspettare.
Se sentite comunque l'impellente necessità di introdurre print "subito", quanto meno
- non parlatene prima di aver spiegato le funzioni,
- non usatelo mai nella shell e nei moduli, a livello "globale",
- non usatelo mai al posto di un return, al posto di un'eccezione, o per gestire un caso particolare.
Tutto questo, neanche "provvisoriamente". Ma anche se rispettate queste buone pratiche, non basta ancora:
- non spargete comunque print nelle funzioni.
Una funzione non dovrebbe restituire un risultato e anche occuparsi della sua interfaccia utente. Questa è una violazione molto grave del principio di singola responsabilità: se passate ai vostri allievi questa abitudine, anche inavvertitamente e "provvisoriamente", non riusciranno mai più a scrollarsela di dosso. Davvero, credetemi. Ho visto abbastanza principianti devastati da pessimi corsi su YouTube per saperlo.
Quello che invece potete far vedere, è come creare una singola funzione ("main"), o comunque poche funzioni ben delimitate, che si occupano in modo specifico di emettere l'output prodotto dalle altre funzioni "pure".
def check_age(age):
return age >= 18
def main(): # qui gestisco l'interfaccia
age = int(input('età...'))
if not check_age(age):
print('non hai l'età')
Ovviamente non è sempre possibile fare una cosa così pulita. Tuttavia, in primo luogo: un corso fa dozzine di semplificazioni lungo la strada; perché proprio qui non va bene semplificare? In secondo luogo: anche se ci sono casi più complessi, l'importate è aver passato il concetto che l'interfaccia utente dovrebbe essere separata dal codice "di business" del programma. E siccome print fa parte dell'interfaccia utente, allora print non deve stare nel codice "di business".
Facile no? Non vi viene voglia di seguire un corso che, per una volta, insegna a fare le cose per bene?
Commenti
Posta un commento