Quando si muovono i primi passi nella programmazione, viene spesso ripetuto di commentare il codice il più possibile. Ma poco dopo capita di leggere un articolo che consiglia esattamente il contrario. Confuso? Questo articolo ti darà un quadro chiaro su quando commentare e quando evitarlo.
Il principio è semplice: lascia che il linguaggio e il runtime garantiscano, il prima possibile, che ciò che documenti sul software corrisponda al vero.

I nostri robot guardiani
Partirò da una funzione non documentata e procederò passo dopo passo, nell'ordine in cui di solito affronto le cose. Prima le basi più semplici, poi i meccanismi automatici che garantiscono la veridicità della documentazione e, infine, le forme più deboli di documentazione, da usare quando un controllo automatico non è possibile.
1\. Nessuna documentazione
Partiamo da questa semplice funzione, p!, il fattoriale, presa direttamente dagli esercizi base di programmazione. L'esempio è in Python, ma il discorso vale sostanzialmente anche per altri linguaggi.
def fact(p):
ret = 1
if x == 0:
return 1
for i in range(1, x + 1):
ret *= i
return ret
Si potrebbe essere tentati di aggiungere un commento del tipo:
""" This is the factorial """
Ma i commenti spesso non vengono letti, e questo diventa ridondante non appena si adotta un naming decente.
2\. Naming
Modifichiamo la funzione così:
def factorial(p):...
Ora la documentazione è incorporata nel codice eseguito, non semplicemente affiancata. Chi vuole usare la tua funzione deve leggerne il nome. È uno dei buoni motivi per cui le funzioni dovrebbero essere brevi e avere un solo scopo: significa che il nome della funzione può stare vicino al codice e descriverlo in modo accurato.
Qualunque altro accorgimento che aiuti il lettore a comprendere il codice (ad esempio, l'uso di membri privati dove ha senso) è una valida forma di documentazione.
A questo punto, rinomina il parametro in n, per ricordare a chi lo usa che si tratta di un intero.
def factorial(n):...
Dopotutto, sebbene il fattoriale sia definito solo per gli interi, può essere esteso anche ai non interi. La funzione Gamma è una sorta di fattoriale esteso.
Un utente esperto potrebbe chiedersi se è quella usata nella nostra implementazione, e a buon diritto: scipy.special.factorial, ad esempio, restituisce effettivamente un risultato per input float non interi, sfruttando l'interpolazione Gamma del fattoriale. Dobbiamo chiarire che non è ciò che stiamo offrendo qui.

La funzione Gamma estende il fattoriale a quasi tutti i numeri reali (e complessi).
3\. Type Hints
Il nome del parametro è già una sorta di commento, e si potrebbe aggiungere un commento per specificare che è un int (come fa scipy.special.factorial). Ma c'è un modo migliore per comunicare al lettore che il parametro è un intero: i type hints.
def factorial3(n: int) -> int:...
Il tooling di sviluppo Python ti segnalerà la maggior parte degli errori. Il principio è questo: lascia che sia il computer a documentare al posto tuo, riducendo al minimo la dipendenza dalla mente umana. Ad esempio, se la tua implementazione non accetta il float 3.0 come se fosse un int, il type hint lo documenterà subito, cosa che altrimenti potresti dimenticare di fare.
I commenti possono essere obsoleti o semplicemente sbagliati. Ad esempio, scipy.special.factorial (non sto criticando SciPy — è una libreria fantastica!) nel commento dice " n : int", ma in realtà accetta anche float e restituisce persino un valore secondo la funzione Gamma.
Tipizzazione a tempo di compilazione
I nostri esempi sono in Python, ma se usi un linguaggio con tipizzazione a tempo di compilazione come Java o C++, dichiarando i tipi il compilatore bloccherà davvero molti errori già in fase di sviluppo, garantendo che questa "documentazione" sia accurata il prima possibile.
Tuttavia, le asserzioni sono più efficaci nel cogliere vincoli a grana fine, come il divieto di numeri negativi in input, a meno che tu non stia usando un linguaggio con un sistema di tipi più evoluto.
4\. Asserzioni, eccezioni o pre- e post-condizioni
Possiamo fare un passo in più rispetto ai type hints. Asserisci che il valore sia un intero e che sia non negativo. In questo modo non è solo il tooling di sviluppo a strizzarti l'occhio: è il runtime stesso a bloccare gli errori non appena si verificano.
def factorial4(n: int) -> int:
assert isinstance(n, int), f"{n} is not an integer and so unsupported."
assert n >= 0, f"{n} is negative and so unsupported."
...
Ci sono diversi modi per sollevare l'errore.
Dato che non ha senso lasciar proseguire errori del genere, tieni le asserzioni abilitate. In alternativa puoi sollevare un ValueError. Se utilizzi un linguaggio o una libreria che supportano la programmazione per contratto, con pre- e post-condizioni (una sorta di asserzione strutturata), va benissimo anche così.
@icontract.require(lambda n: isinstance(n, int) and n>=0, "n must be a nonnegative integer")
def factorial_precondition(n: int) -> int:
return __iterative_factorial(n)
A questo punto, il computer sta davvero verificando il nostro lavoro. Ma le asserzioni o le altre eccezioni hanno un secondo valore: sono una forma di documentazione che non può sbagliare. Il lettore sa che ciò che dice un'asserzione è vero.
Tornando alla nostra piñata, scipy.special.factorial, il commento dice " If n < 0, the return value is 0."
Tecnicamente è vero per questa implementazione, ma è fuorviante per un utente che sa che il fattoriale non è definito sui numeri negativi. Persino la funzione Gamma, che estende il fattoriale, pur essendo definita sui reali e perfino sui complessi, non è definita sugli interi negativi. Molto meglio sollevare un errore e rendere ben chiaro cosa è ammesso e cosa no.
5\. Unit testing
Gli unit test assomigliano alle asserzioni: verificano che il sistema faccia ciò che ti aspetti durante lo sviluppo. Come documentazione sono più deboli delle asserzioni, perché distanti dal codice sia per i file in cui risiedono sia per il momento in cui vengono eseguiti. È meno probabile che gli altri sviluppatori li leggano.
Tuttavia, se gli sviluppatori eseguono i test in modo continuativo, come dovrebbero, vedranno gli errori in tempi brevi, senza dover scavare nei commenti.
Ad esempio, il nostro semplice fattoriale iterativo è bacato! Restituisce il valore 1 per i valori negativi, il che è certamente sbagliato sia per il fattoriale sia per l'estensione Gamma. Questo test mostrerà che ci aspettiamo un errore, ma sfortunatamente otteniamo un valore di ritorno.
def test_factorial():
with pytest.raises(Exception):
factorial(-2)
Puoi anche inserire unit test in miniatura all'interno dei commenti utilizzando doctest, una funzionalità integrata in Python 3. In questo modo il test viene posizionato il più vicino possibile al codice, dove può svolgere al meglio il ruolo di documentazione (anche se rischia di appesantire la lettura).
def factorial_doctest(n) -> int:
"""
Run this with python -m doctest -v factorial.py
>>> factorial_doctest(3)
>>> factorial_doctest(0)
>>> factorial_doctest(1.5)
Traceback (most recent call last):
...
TypeError: 'float' object cannot be interpreted as an integer
"""
return __iterative_factorial(n)
Di conseguenza, i test di integrazione sono ancora più lontani dal codice e meno adatti a documentarlo. Documentano piuttosto il comportamento dell'intero sistema.
6\. Logging
Se sta per succedere qualcosa di brutto e non hai modo di prevenirlo, puoi almeno registrarlo. Torniamo a SciPy.
Se passi un float, ottieni questo sullo standard error:
DeprecationWarning: Using factorial() with floats is deprecated
A quanto pare hanno usato un'implementazione basata su Gamma e in seguito, rendendosi conto che dovevano essere supportati solo gli interi, hanno voluto limitare l'input a int. Pur non potendolo fare a causa delle applicazioni dipendenti, almeno scrivono l'avviso in un punto in cui potrebbe essere letto. Gli sviluppatori magari non leggono la documentazione, ma quando incappano in un problema i log li leggono, eccome.
7\. Commenti
Siamo arrivati alla nostra penultima risorsa: il commento. Ha il suo posto, ma, secondo la mia esperienza, solo in questi tre casi:
a. Le API pubbliche dovrebbero essere documentate con commenti inline, seguiti dalla generazione di HTML con Doxygen per Python, Javadocs per Java, ecc. I commenti vanno strutturati con tag, seguendo il principio per cui, se un computer può controllare la struttura, è proprio lui a doverlo fare.
Un esempio di commenti strutturati in Python:
:param n: A nonnegative integer
:return: The product of all integers from 1 up to and including n; or for 0, return 1.
Ma quando la funzione non è un'API pubblica, usa questi campi strutturati con parsimonia: tali commenti servono a trasmettere un messaggio specifico su qualcosa di insolito, non a documentare meticolosamente ogni aspetto dell'utilizzo.
b. I fatti sorprendenti vanno documentati. Ad esempio, gli utenti potrebbero non sapere che il fattoriale di zero è definito come 1, dato che non sembra rientrare nella definizione di fattoriale come "il prodotto di tutti i numeri da 1 a n". Allo stesso modo, workaround strani e hack poco eleganti vanno commentati nel codice.
c. Gli algoritmi vanno citati per nome, perché il computer non può mostrare e garantire quale algoritmo sia in uso, né il lettore è sempre in grado di identificarlo a colpo d'occhio. Ad esempio, il fattoriale può essere calcolato con la moltiplicazione o approssimato con vari algoritmi per Gamma. Un utente con applicazioni numeriche esigenti potrebbe voler saperlo.
def factorial(n: int) -> int:
"""
This is a simple iterative implementation of factorial.
The input must be a non-negative integer.
Note that the factorial of zero is defined as 1.
"""
8\. Documentazione esterna
I commenti erano la penultima risorsa; la documentazione esterna è davvero l'ultima spiaggia. RTFM è un bello slogan, ma in pochi leggono il manuale: è così lontano dal codice che diventa facilmente obsoleto. Se però ti serve una panoramica delle funzionalità o dei tutorial, puoi inserirli in un documento mantenuto separatamente dal codice.
Ma attenzione: devi rivedere e rileggere costantemente questi documenti se vuoi che rispecchino ciò che è presente in un codice in continua evoluzione.

Più la documentazione è vicina al codice e automatizzata, meglio è — ma ogni soluzione ha la sua utilità.
La documentazione deve dire la verità. Il tooling del tuo linguaggio e il runtime hanno molti modi per garantirne l'affidabilità: usali. Tieni la documentazione il più vicino possibile al codice e fai in modo che la verifica automatica della sua accuratezza avvenga il prima possibile nel processo di sviluppo.
Il codice eseguibile delle varie implementazioni, con gli unit test, è qui: https://github.com/doitintl/commenting
Per restare in contatto, seguici sul DoiT Engineering Blog , sul canale LinkedIn di DoiT e sul canale Twitter di DoiT . Per scoprire le opportunità di carriera, visita https://careers.doit-intl.com .