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

Post più popolari