Les docstrings en Python


Du fait de la nature du tuto, exceptionellement je ne respecterai pas le nouveau format de rédaction. Mais y aura quand même de la zik :

Une des mes fonctionnalités favorites en Python est son mécanisme de documentation du code : les doctrings. En effet, je crois qu’il est très important de rendre simple les tâches over chiantes comme les tests unitaires ou la doc car moins il y a de frein à le faire, plus il y a de chances qu’on le fasse.

Principe

La docstring est une chaîne de caractères que l’on n’assigne pas, et qui est placée à un endroit spécifique du code pour décrire ce dernier.

La docstring la plus courante est placée sous une fonction. Voici une fonction SANS docstring :

def ajouter(a, b):
    return a + b

Et voici une fonction AVEC docstring :

def ajouter(a, b):
    """
        Ajoute deux nombres l'un à l'autre et retourne
        le résultat.
    """
    return a + b

La chaîne de caractère doit être placée juste en dessous de la signature de la fonction.

Écrire des docstrings offrent de nombreux avantages :

  • La fonction help() affiche cette documentation dans un shell.
  • Les outils de programmation tels que les shells ou les IDE affichent cette documentation quand le développeur qui ne lit pas votre code, mais l’utilise, en a besoin.
  • On peut générer une bonne doc du code avec des commandes qui extraient ces docstrings.
  • C’est un mécanisme standardisé de documentation : tout le monde sait que si c’est là, et que ça a cette forme, c’est une documentation.
  • Le code Python peut utiliser la docstring pour la lire ou l’afficher.
  • On peut mettre des tests dans les docstrings, qui servent alors d’exemples d’utilisation.

Usage

Si vous avez une fonction ainsi faite :

def ajouter(a, b):
    """
        Ajoute deux nombres l'un à l'autre et retourne
        le résultat.
    """
    return a + b

Alors dans un shell, toute personne qui va utiliser votre fonction pourra faire :

>>> help(ajouter)
Help on function ajouter in module __main__:
 
ajouter(a, b)
    Ajoute deux nombres l'un à l'autre et retourne
    le résultat.

Il y a ainsi une documentation de votre fonction DANS le shell, sans avoir à se connecter à Internet ou quoi que ce soit. Il n’a pas à ouvrir le moindre fichier.

On peut documenter également les modules en plaçant la docstring comme première expression Python (qui n’est pas un commentaire) tout en haut du fichier :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
 
"""
    Ceci est un module génial qui va faire 
    plein de trucs super cool.
"""
 
import threading
import multiprocessing
from functools import wraps
from Queue import Empty
 
class BaseAsbtractAdapterStrategyFactoryMock(object):
    pass

On peut aussi documenter une classe et ses méthodes :

class ADallas(object):
    """
        Cette classe vous donne le classe à Dallas
        quand vous en avez vraiment besoin.
    """
 
    def univers_impitoyable(self):
        """
            Retourne un objet univers, prêt
            à être impitoyable.
        """

La plupart des fonctions et modules de la lib standard sont ainsi documentées, vous pouvez donc faire :

>>> import os
>>> help(os)
>>> from functools import partial
>>> help(partial)
>>> help(str)
>>> help('foo'.upper)

Bonnes pratiques

D’abord, et malgré mes exemples à caractère purement pédagogique précédents, votre docstring devrait être en anglais. Même quand vous travaillez uniquement avec des français. L’anglais est la lingua franca (oui, oui, je sais…) de l’informatique, et de plus vous évitez tout problème d’encoding car vous n’avez aucun moyen de savoir si cette doc sera lue dans un shell rêglé avec les pieds (comme celui de Windows).

J’écrirai un article pour motiver les résistants à se mettre à l’anglais une bonne fois pour toute.

L’anglais est votre ami. Il est la novlang de notre métier. C’est pas vendeur ça ?

Bref.

Ensuite, il existe plusieurs manières de formater une docstring, et il y a même un PEP 257 qui ne parle que de ça. En résumé :

def foo():
    """Docstring d'une ligne"""
 
 
def foo():
    """Résumé de la docstring de plusieurs lignes.
 
    Contenu détaillé de la doctstring.
    Contenu détaillé de la doctstring.
    Contenu détaillé de la doctstring.
 
    """

Je ne respecte jamais cette convention. Généralement je fais plutôt :

def foo():
    """
        Docstring d'une ligne.
    """
 
def foo():
    """
        Résumé de la docstring de plusieurs lignes.
 
        Contenu détaillé de la doctstring.
        Contenu détaillé de la doctstring.
        Contenu détaillé de la doctstring.
    """

Je trouve ça immensément plus lisible dans le code. Je ne peux pas vous recommander de faire comme moi, puisque c’est aller à l’encontre du PEP. Tout ce que je peux vous dire c’est que personne ne s’est jamais plaint de cette habitude. En matière de docstring, la plupart des gens sont juste déjà trop heureux qu’il y en ait.

En revanche, tout le monde est d’accord sur le fait qu’une ligne de la docstring ne doit pas faire plus de 80 caractères. Donc indentez en conséquence. Le plugin SublimeText Wrap-Plus permet de le faire automatiquement avec Alt + Q (et bien plus). Un must have.

Usage avancé

Python étant un langage qui aime l’instrospection, la docstring est accessible depuis le code sous la forme de l’attribut __doc__ :

>>> def foo():
...     """
...         Can foo a bar with ease
...     """
...     pass
...
>>> foo.__doc__
'\n        Can foo a bar with ease\n    '
>>>

Vous ne vous en servirez pas souvent, mais c’est utile pour créer le help d’un script (c’est ce que fait clize) ou faire une popup dans un IDE.

Une autre particularité des docstrings, c’est qu’elles sont très utilisées dans les générateurs de documentation comme sphinx. Et ils comprennent généralement très bien le format RST.

Le format RST est une convention de balisage pour formater du texte. Il garde le texte lisible, mais permet de générer du HTML, du PDF et un tas d’autres trucs plus propres. Aussi je vous invite à l’utiliser si vous avez une docstring dont vous sentez qu’elle a besoin d’être aussi complète que possible.

Voici toutes les balises à votre disposition :

:param arg1: description
:param arg2: description
:type arg1: type
:type arg1: type
:return: description de la valeur de retour
:rtype: type de la valeur de retour

:Example:

Un exemple écrit après un saut de ligne.

.. seealso:: Référence à une autre partie du code
.. warning:: Avertissement
.. note:: Note
.. todo:: A faire

On peut aussi utilise ``element`` pour signaler un morceau de code au milieu du texte. Sur les docstrings très longues (il n’est pas rare qu’une docstring soit plus longue que le code qu’elle documente), comme celles des modules, on peut sous-ligner les titres et sous-titres avec des = et des -.

Par exemple :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
"""
    The ``obvious`` module
    ======================
 
    Use it to import very obvious functions.
 
    :Example:
 
    >>> from obvious import add
    >>> add(1, 1)
    2
 
    This is a subtitle
    -------------------
 
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
    You can say so many things here ! You can say so many things here !
 
    This is another subtitle
    ------------------------
 
    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 
 
"""
 
def add(a, b):
    """
        Adds two numbers and returns the result.
 
        This add two real numbers and return a real result. You will want to
        use this function in any place you would usually use the ``+`` operator
        but requires a functional equivalent.
 
        :param a: The first number to add
        :param b: The second number to add
        :type a: int
        :type b: int
        :return: The result of the addition
        :rtype: int
 
        :Example:
 
        >>> add(1, 1)
        2
        >>> add(2.1, 3.4)  # all int compatible types work
        5.5
 
        .. seealso:: sub(), div(), mul()
        .. warning:: This is a completly useless function. Use it only in a 
                      tutorial unless you want to look like a fool.
        .. note:: You may want to use a lambda function instead of this.
        .. todo:: Delete this function. Then masturbate with olive oil.
    """
    return a + b

Aucun champ n’est obligatoire, aucuns ne sont interdépendant. Cela vous donne une grande flexibilité pour savoir jusqu’à quel point vous voulez documenter votre fonction.

La section la plus importante à mon sens est :Example:. Avec ça une personne peut généralement avoir une bonne idée de ce qui se passe, et en plus ça sert de tests (comme on le verra plus loin).

La section la plus inutile est de loin .. todo::. En fait je vous recommande de ne pas l’utiliser. Si vous avez des todos, utilisez plutôt la convention de commentaire :

# TODO: un truc à faire

Car :

  • Je pense que vos TODO n’ont rien à foutre dans la doc.
  • Il faut mieux avoir un TODO le plus proche du truc qu’il doit modifier. Le mettre en haut de la fonction n’a pas toujours de sens.
  • De très nombreux outils et services détectent ce format automatiquement et en font quelque chose d’utile.

Le typage des arguments et de la valeur de retour n’est pas toujours utile, surtout avec Python faisant massivement usage du duck typing. La description est plus importante. Mettez le typage quand le type n’est pas intuitif ou signalez une caratéristique comme : itérable, indexable, file-like object, etc.

EDIT: ah, y aussi un field raises pour déclarer que le code peut lever une exception en particulier. J’avais zappé. On m’a aussi demandé si il y avait des équivalent à @since et @depreciated mais non, en général on fout ça dans .. note:: ou .. warning::

Doc tests

Une fonctionalité controversée des docstrings sont les doctests, des tests unitaires directement intégrés dans la docstring.

Mon conseil : utilisez les docstests pour des petites fonctions simples ou pour quelques exemples sur les fonctions complexes, et complétez les avec des tests ordinnaires. Ce sont des bons compléments, mais pas forcément idéales pour contenir TOUS les tests. Après, si c’est le seul truc qui vous motive pour écrire des tests, mettez tout dedans, il vaut mieux ça que rien du tout.

Une doc test est donc la rédaction d’une partie de la docstring avec la syntaxe d’un shell :

def add(a, b):
    """
        Do I neeed to explain this ?
 
        :Example:
 
        >>> add(1, 1)
        2
        >>> add(2.1, 3.4)  # all int compatible types work
        5.5
 
    """
    return a + b
 
# A la fin de votre script, mettez ce snippet qui va activer les doctest
if __name__ == "__main__":
    import doctest
    doctest.testmod()

Si vous importez ce module, il ne se passera rien. Mais si vous faites python script.py, Python va exécuter add(1, 1) et vérifier que cela affiche bien 2, puis exécuter add(2.1, 3.4) et vérifier que cela affiche bien 5.5.

Si il n’y a aucune erreur, le script se termine silencieusement (il donne des détails si on utilise l’option -v). Sinon, il beugle. Par exemple si je rajoute :

>>> add(1, 1)
3

On obtient en sortie :

$ python script.py
*******************************************
File "script.py", line 7, in __main__.add
Failed example:
    add(1, 1)
Expected:
    3
Got:
    2
*******************************************
1 items had failures:
   1 of   2 in __main__.add
***Test Failed*** 1 failures.

Attention !

Python compare non pas la valeur, mais CE QUI S’AFFICHE. Ça peut être très déroutant. Si j’ai les tests :

>>> print str(add(1, 1))        
2        
>>> str(add(1, 1))        
2

Ça va planter :

$ python script.py
*******************************************
File "script.py", line 10, in __main__.add
Failed example:
    str(add(1, 1))
Expected:
    2
Got:
    '2'
*******************************************
1 items had failures:
   1 of   2 in __main__.add
***Test Failed*** 1 failures.

Il aurait fallu que j’écrive :

>>> print str(add(1, 1))
2
>>> str(add(1, 1))
'2'

Notez les guillemets. C’est ainsi que ça s’afficherait dans le shell. Donc c’est ce que teste Python.

C’est la raison pour laquelle les docstests ne sont pas parfaites pour les gros tests. Si vous testez des caratères d’échappements ou du texte unicode, il vous faudra préfixer vos doctests de ur sinon ça va échouer :

ur"""
    Ceci est une doctring écrite en unicode, sans interprétation des caractères
    d'échappement.
"""

Même problème pour les textes longs. Il faut préciser qu’on veut tester une sortie tronquée avec +ELLIPSIS :

>>> print range(1000) # doctest: +ELLIPSIS
[0, 1, ..., 18, 999]

Car vous allez pas écrire les 1000 entiers pour le fun dans le test. Pareil pour les stacktraces.

Les espaces sont signficatifs, du coup il faut parfois marquer les tests avec +NORMALIZE_WHITESPACE :

>>> print range(20) # doctest: +NORMALIZE_WHITESPACE
[0,   1,  2,  3,  4,  5,  6,  7,  8,  9,
10,  11, 12, 13, 14, 15, 16, 17, 18, 19]

Sinon c’est galère car il faut reformater la sortie à la main correctement.

Enfin, sur les structures de données comme les dicos, l’ordre des éléments n’est pas garanti, donc l’ordre d’affichage non plus. Quand aux données aléatoires…

Bref, les docstests, c’est cool, mais il ne faut pas en abuser.

28 thoughts on “Les docstrings en Python

  • kontre

    Les docstrings et doctests, c’est un des plus gros avantages de python. Sans ça j’aurais clairement jamais fait de tests unitaires ! J’avais déjà été ébloui par doxygen pour le C/C++, mais en python c’est encore le niveau au-dessus. C’est bon, mangez-en !

    Il faut que je teste le ur””, j’avais un problème pour tester les exceptions… (ouais, je documente en français et je vous merde !).

    Je préciserai que les utilistaires de tests genre py.test et nosetests permettent d’exécuter les doctests, et c’est bien pratique.

  • Etienne

    En fait il est pas vide. ElementTree me dit:
    ParseError: not well-formed (invalid token): line 454, column 55

    Mais comme je sais pas ce qu’ElementTree appelle une “ligne”, mon enquête s’arrête là.

    PS:
    Mon reader s’est arrêté de récupérer votre flux après “Bon bah voilà”.

  • Etienne

    @kontre
    Ça y ressemble.

    Je lis les flux avec Mail sur os x (10.7). Peut-être est-il en mousse, j’en sais rien.

    Et j’ai testé avec ElementTree (de la lib standard python 2.7.3). En mousse aussi?

  • Flo

    @Etienne @kontre
    J’utilise RSSOwl et je n’arrive pas non plus a recuperer le flux. Le logiciel m’indique : “Invalid or Malformed tree”.

  • kontre

    C’est une mousse mutante transgénique qui se développe ultra-rapidement, en fait.
    @Etienne: Les problèmes d’encodage en python, c’est tabou !

  • Sam Post author

    Ouai, donc je pense que les caractères spéciaux font planter les parseurs XML stricts. Je suppose que pour ceux que ça marche, leur flux RSS normalize le flux avant de le parser.

    Bah, on va pas debugger wordpress. Au mieux, je peux retirer l’expression qui fait tout planter.

  • sil

    C’est un truc de pervers ce doctest quand même ! Les programmeurs n’ont même plus le droit de raconter des conneries !

  • Etienne

    @kontre
    D’où tu sors qu’il y a des problèmes d’encodage en python? Y’a pas de problème dans python. S’il y a un problème quelque part, c’est sûrement la faute à PHP, ou à WordPress, ou à Max.

  • Anucunnilinguiste

    @kontre

    “Pourtant, chez moi ça marche !™” : j’aime ;)

  • Sam Post author

    @Etienne : au final, c’est toujours la faute de Max. C’est son rôle dans l’équipe.

  • Recher

    Wouah, je savais pas qu’il y avait toutes ces conventions et syntaxes dans les docstrings.

    Pour vous remercier, voici une Olive Oil, qui sera certainement ravie d’aider à effectuer le todo sus-mentionné.

  • kontre

    Après test, le :Example: n’est pas une commande spéciale dans sphinx, on peut mettre n’importe quoi entre :: et ça fait une section.
    Le .. todo:: n’est pas actif par défaut dans sphinx, d’ailleurs, ça rejoint ton avis ! ;) Perso je l’utilise quand même parce que je fais surtout de la doc pour dev.

  • Réchèr

    Pas compris le dernier commentaire de Max. Par conséquent, je requiers un tampon kamoulox.

    Dans la plupart des cas, on a un module, avec une seule classe dedans. La description générale de ce que ça fait, il vaut mieux la mettre dans une docstring au niveau module, ou au niveau classe ?

    (J’ai lu en diagonale le PEP256, je n’ai pas trouvé de convention à ce sujet.)

  • Sam Post author

    La description générale, c’est au niveau du module.

    Dans la description de la classe, tu trouveras des instructions spécifiques à la classe.

    Mais on est pas en java, en Python y a pas forcément une classe par module. Je dirais même qu’architecturalement, c’est pas idéal de tout mettre dans une seule classe.

  • Luigi

    Plutôt que de passer des heures sur le web à chercher les conventions, un petit tour sur S&M et c’est du tout cuit.

    Encore merci les gars !

  • herison

    Je déterre pour partager un truc bizard, si on met des commentaires avant une class ou function, ils montent comme une docstring.

    un_module.py
    ————
    # comment on foo
    # again comment
    def foo():
    pass

    import un_module
    help(un_module.foo)
    foo()
    # comment on foo
    # again comment

    Je trouve ça très laid.

  • Sam Post author

    Oui, et la docstring prend précédence si elle est écrite. Je suppose que c’est pour permettre la compatibilité avec les outils de génération de docs qui, dans les autres langages, attendent la doc au dessus des fonctions sous forme de comment.

  • Siltaar

    Il n’est jamais trop tard : « Ceci est une doctring écrite en unicode, sans interprétation des caractères »

    En fait « Ceci est une docstring … » (avec un ‘s’ en plus donc)

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.