PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Introduction au currying   Recently updated !

vendredi 12 décembre 2014 à 20:37

Le currying (ou Curryfication pour les frencofans) est le nom donné à une technique de programmation qui consiste à créer une fonction à partir d’une autre fonction et d’une liste partielle de paramètres destinés à celle-ci. On retrouve massivement cette technique en programmation fonctionnelle puisqu’elle permet de créer une fonction pure à partir d’une autre fonction pure. C’est une forme de réutilisabilité de code.

La forme la plus simple de currying est de réécrire une fonction appelant l’autre. Par exemple, soit une fonction pour multiplier tous les éléments d’un itérable :

def multiply(iterable, number):
    """ Multiplie tous les éléments d'un itérable par un nombre.
 
        Exemple :
 
            >>> list(multiply([1, 2, 3], 2))
            [2, 4, 6]
    """
    return (x * number for x in iterable)

On peut ensuite créer une fonction qui multipliera par 2 tous les éléments d’un itérable :

def doubled(iterable):
    """ Multiplie tous les éléments d'un itérable par un 2.
 
        Exemple :
 
            >>> list(doubled([1, 2, 3]))
            [2, 4, 6]
    """
    return multiply(iterable, 2)

C’est une forme de currying. On créé une fonction qui fait ce que fait une autre fonction, mais avec des arguments par défaut.

Python possède une fonction pour faire ça automatiquement avec n’importe quelle fonction :

>>> from functools import partial 
>>> tripled = partial(multiply, number=3) # on curryfie ici
>>> list(tripled([1, 2, 3])) # nouvelle fonction avec un seul argument
[3, 6, 9]

Cela marche car, je vous le rappelle, les fonctions sont des objets en Python. On peut mettre une fonction (je ne parle pas de son résultat) dans une variable, passer une fonction en paramètre ou retourner une fonction dans une autre fonction. Les fonctions sont manipulables.

Il n’est pas rare d’utiliser les fonctions anonymes comme outils curryfication. En Python, on ferait ça avec une lambda :

>>> tripled = lambda x: multiple(x, 3) 
>>> list(tripled([1, 2, 3]))
[3, 6, 9]

Certains outils, comme Ramda en Javascript, vont plus loin, et exposent des fonctions qui se curryfient automatiquement.

Pour ce faire, il faut inverser l’ordre qu’on mettrait intuitivement aux arguments dans la déclaration d’une fonction :

# au lieu de multiply(iterable, number), on a :
def multiply(number, iterable=None):
    # Si on a pas d'itérable passé, on curryfie
    if iterable is None:
        return partial(multiply, number=number)
    return (x * number for x in iterable)

Ainsi :

>>> list(multiply(2, [1, 2, 3])) # pas de currying
[2, 4, 6]
>>> quintuple = multiply(5) # currying automatique
>>> list(quintuple([1, 2, 3]))
[5, 10, 15]

L’intérêt de ce style, c’est qu’on peut composer des traitements à partir de plusieurs sous traitements, presque déclarativement :

def remove(filter, iterable=None):
    """ Retire tous les éléments d'un itérable correspondant au filtre.
 
        Exemple :
 
            >>> list(remove(lambda x: x >= 4, [1, 2, 3, 4, 5]))
            [1, 2, 3]
    """
    if iterable is None:
        return partial(remove, filter)
 
    return (x for x in iterable if not filter(x))
 
>>> smalls = remove(lambda x: x >= 4)
>>> list(smalls(tripled([0, 1, 2, 3, 4]))) # le traitement est auto descriptif
[0, 3]

Néanmoins, il faut savoir que ce style n’est pas pythonique. En effet, en Python on préférera généralement utiliser des suites suite de générateurs. Soit par intention, soit via yield.

Notre exemple serait alors :

>>> tripled = (x * 3 for x in [0, 1, 2, 3, 4])
>>> smalls = (x for x in tripled if x <= 4)
>>> list(smalls)
[0, 3]

De plus, cette technique suppose qu’on ne profitera pas de certaines fonctionnalités, comme les paramètres par défaut des fonctions Python.

C’est toutefois une bonne chose à connaître. C’est occasionnellement utile en Python et peut produire des solutions très élégantes. C’est également une bonne chose à comprendre pour aborder d’autres langages plus fonctionnels qui les utilisent bien plus comme le Javascript, le Lisp, ou carrément le Haskell.

Des bouts de Python cachés   Recently updated !

jeudi 11 décembre 2014 à 10:23

J’aime bien fouiner pour trouver des petits trucs insolites dans mes techos. Cet excellent post sur SO est un bon début, mais on découvre toujours de nouvelles choses.

ChainMap

Vous vous souvenez de ChainMap ? C’est top, mais uniquement en Python 3.

Et bien il existe une version cachée en Python 2 :

from ConfigParser import _Chainmap as ChainMap

C’est pas aussi complet (il n’y pas les méthodes new_child et parents), mais ça peut servir.

Si néanmoins, vous avez besoin de l’implémentation complète, il existe un backport installable avec pip :

pip install Py2ChainMap

str.startswith, str.endswith et un tuple

Pour beaucoup de situations, une regex en Python, c’est overkill. On a in pour vérifier qu’une chaîne en contient une autre. Et on a str.startswith() et str.endswith() pour vérifier qu’on chaîne commence ou finit par une autre. Avec des strip()/split()/lower(), on finit souvent par s’en sortir, et l’usage de toutes ces méthodes sont en général plus performantes que d’utiliser re.

Mais si vous avez plusieurs chaînes à vérifier ? Pas de problème, str.startswith() et str.endswith() acceptent aussi des tuples :

>>> "Je suis une chaîne qui a du caractère".startswith('Tu')
False
>>> "Je suis une chaîne qui a du caractère".startswith('Je')
True
>>> "Je suis une chaîne qui a du caractère".startswith(('Tu', 'Je'))
True
>>>

Les espaces entre une instance et son attribut sont ignorés

Bien que la deuxième ligne soit très moche, elle est parfaitement valide :

>>> "Yolo".upper()
'YOLO'
>>> "Yolo"           .upper()
'YOLO'

Or on peut intercepter l’accès aux attributs à la volée en Python grâce aux méthodes magiques.

Ca veut dire qu’on peut implémenter des “symboles” en Python.

class SymbolMaker():
 
    symbols = set()
 
    def __getattr__(self, value):
        symbols.add(value)
 
>>> make_symbol = SymbolMaker()
>>> make_symbol   .start
>>> make_symbol   .end
>>> SymbolMaker.symbols
{'end', 'start'}

Je n’ai pas dit qu’on devrait le faire, j’ai dit qu’on pouvait :) D’ailleurs, y en a qui l’utilisent pour implémenter GOTO en Python. The troll is strong with this “feature”.

Quelle est la différence entre “bloquer” et “en cours d’exécution” ?   Recently updated !

mardi 9 décembre 2014 à 17:57

On vous dit qu’il faut faire attention en utilisant des technologies non bloquantes, car si on bloque dans la boucle d’événement, on bloque tout le programme, et on perd l’intérêt de l’outil.

C’est vrai, mais que veut dire “bloquer” ?

Car si je fais :

for x in range(1000000):
    print(x)

Mon programme va tourner longtemps, et la boucle d’événement va bloquer, n’est-ce pas ?

En fait, “bloquer” et un abus de langage car il y a plusieurs raisons pour bloquer. Dans notre contexte, il faudrait dire “bloquer en attente d’une entrée ou d’une sortie”. D’où l’appellation “Aynschronous non blocking I/O” des technos types NodeJS, Twisted, Tornado, Gevent, etc.

En effet, il faut distinguer deux causes d’attente à votre programme :

Le premier cas est impossible à éviter. Tout au mieux pouvons-nous répartir la charge du programme sur plusieurs cœurs, processeurs voire machines. Le code devra toujours attendre qu’il se termine, mais ça ira plus vite.

Dans le contexte de la programmation non bloquante telle qu’on vous en a parlé, on est donc dans le deuxième cas.

Il ne s’agit alors pas de s’interdire de faire des boucles ou autre opération longue (ou plutôt, c’est un problème d’optimisation ordinaire qui n’a rien à voir avec le fait de bloquer), il s’agit de ne pas “attendre à ne rien faire” quand une opération extérieure est en cours.

C’est ce que font naturellement NodeJS, Twisted, Tornado, Gevent & Co. Quand on fait un échange HTTP, le bout de données part, puis le reste du code continue de tourner, passant à la tâche suivante, en attendant que le paquet traverse le réseau, atteigne l’autre machine, qui vous répond finalement. C’est ce temps, incompressible, sans contrôle de votre côté, durant lequel il ne faut pas bloquer. Le gain de perf est que votre programme ne se la touche pas pendant les temps d’attente, mais bien entendu que VOTRE, lui, code va prendre du temps et “bloquer” le processeur. Il faut bien qu’il s’exécute.

Ce qu’on entend donc par “il ne faut pas faire d’opération bloquante dans un code qui est déjà non bloquant” c’est “il ne faut pas utiliser un outil à l’API bloquante au milieu d’autres outils non bloquants”.

Par exemple, n’utilisez pas requests avec Twisted, car requests est codé pour attendre sans rien faire jusqu’à obtenir une réponse à chaque requête, bloquant Twisted. Utilisez plutôt treq. C’est pareil pour la lecture d’un fichier, une requête de base de données, etc. Et il existe des boucles d’événements ailleurs que sur le serveur : une page Web possède sa propre boucle (c’est pour cela que tout JS est asynchrone), un toolkit GUI comme QT ou GTK aussi (c’est pour ça qu’ils utilisent la programmation événementielle), etc.

Maintenant vous allez me dire : mais pourquoi bloquer alors ? Pourquoi ne pas toujours éviter de bloquer ?

Et bien parce que si on ne bloque pas, on ne peut pas écrire un programme ligne à ligne. On est obligé d’adopter un style de programmation asynchrone puisqu’on ne sait pas quand le résultat de certaines lignes va arriver. Ça veut dire des callbacks, ou des futures, ou des coroutines, ou du message passing… Bref, un truc plus compliqué. Or, on n’a pas forcément besoin de ce niveau de performance. En fait, la grande majorité des programmes n’ont pas besoin de ce niveau de performance. Donc, on bloque en attendant, non pas Godot, mais l’I/O, parce que c’est plus simple à écrire. Pour pas se faire chier.

Il y a bien des moyens de contourner ce problème : les threads, le multiprocessing, les coroutines, etc. Parfois même, on ignore le problème : bloquer quelques ms au milieu d’une boucle d’événements une fois par seconde n’est pas un drame. Une fois que j’ai fini le dossier sur les tests unitaires, je vous ferai un dossier sur la programmation non bloquante, avec aussi une esquisse de la parallélisation.

En attendant, ne stressez pas parce que votre code “bloque” parce qu’il travaille longtemps, assurez-vous juste que les APIs que vous utilisez ne bloquent pas pendant l’I/O, et vous êtes ok.

Et comment savoir ? Et bien si une donnée rentre ou sort de votre programme (ça ne fait pas parti du code source), c’est de l’I/O. Si votre code ressemble à ça :

res = faire_operation_sur_IO()
faire_un_truc_avec_le_res(res)

Alors votre outil est bloquant, puisque qu’il compte sur le fait que la deuxième ligne sera exécutée à coup sûr quand la première sera terminée. Un outil non bloquant exigera quelque chose pour gérer le retour du résultat plus tard: un callback, une promesse, un yield

0bin.net est de nouveau en ligne   Recently updated !

lundi 8 décembre 2014 à 22:02

0bin était down, on a supprimé la page en cause, et on l’a remis up.

On ne sait pas trop comment lutter contre ça. Même TPB a dû avoir 40 noms de domaine pour s’en sortir, et franchement on n’a pas envie de se taper autant de boulot pour un projet qui ne nous rapporte rien.

Que faire donc ?

D’abord, créer une admin pour 0bin pour supprimer une page plus facilement. Ça nous évitera de dépasser les délais la prochaine fois, car je n’étais pas dispo et Max ne savait pas comment faire. C’est pas que c’est compliqué, mais ça prend du temps, faut lire la doc, se connecter au serveur, etc. Et forcément, c’est du temps qu’on préfère passer sur Dota ou des projets qui rapportent des sous.

Ensuite, prendre le temps de vous rappeler que 0bin.net n’est qu’une instance de 0bin. Un exemple. Multiplier les instances est encore le meilleur moyen d’avoir l’outil à disposition. Et le process est plutôt bien documenté.

Mais une fois qu’on a posté son truc sur une instance, si elle tombe, que fait-on ?

Pour le moment on est baisé.

On pourrait donc imaginer de rajouter une fonction de distribution à 0bin. L’idée serait de se lier via une API simple à d’autres 0bin en qui on a confiance. Si quelqu’un poste sur l’un, tous les autres reçoivent une copie. Ainsi dans le lot, il y en aura bien un qui résistera.

Je me tâte à faire ça dans la semaine. Déjà 0bin a besoin d’un portage qui supporte Python 3. Ensuite quelques tests unitaires ne feraient pas de mal. Une fois que c’est fait, je freeze cette version, et je lance 1bin.net, la version suivante. Avec du crossbar.io, du angularjs et du mode distribué. Le truc sera plus lourd, uniquement compatible 2.7 et plus compliqué, donc je préfère en faire une version à part.

Ça se tente, voir si j’arrive à débloquer un peu de temps pour le faire, mais c’est pas impossible. Stay tuned.

Un gros guide bien gras sur les tests unitaires en Python, partie 4   Recently updated !

samedi 6 décembre 2014 à 21:34

Python est un langage très pro, et il y a beaucoup, beaucoup d’outils pour faire des tests.

Après avoir vu pytest, un outil typiquement pythonique sont les doctests, des tests unitaires intégrés dans les docstrings.

Pour rappel, les docstrings, ce sont ces chaînes de caractères qu’on retrouve au début des modules, sous la signature des fonctions ou dans la définition des classes. Elles servent à la documentation de ceux-ci, ainsi on peut la lire dans le code, et dans les vraies docs car les outils standards savent les extraire.

Ça ressemble à ça :

def une_fonction():
    """ Ceci est une docstring.
 
        On peut la lire dans le code source, avec help() dans le shell ou
        dans les docs générés par pydoc et sphinx.
    """
    pass

Et bien ces docstrings, on peut mettre des tests unitaires dedans formatés comme des sessions de shell Python. Cela permet de donner des exemples d’usage, tout en testant son code. C’est chouette.

Musique ?

Musique.

Hello doctests

Faire des doctests n’est pas bien compliqué car c’est du copier coller. On fait une session shell avec ce qu’on veut tester, et on copie-colle le tout dans la docstring. Fastoche.

# on copie juste la session de shell tel quel
def ajouter(a, b):
    """
        >>> ajouter(1, 2)
        3
    """
    return a + b
 
# et on demande à Python de parser les doctests. Directement dans votre fichier
# de code. Si, si. Pas de fichier de tests à part.
if __name__ == "__main__":
    import doctest
    doctest.testmod()

On lance ensuite directement notre fichier de code :

python mon_module.py

Et ça n’affiche absolument rien. C’est parce qu’il n’y a pas d’erreur. On peut avoir le topo en demandant un peu de verbosité avec -v :

python mon_module.py -v
Trying:
    ajouter(1, 2)
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.ajouter
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Les doctests marchent purement en se basant sur le formatage texte. Python prendre la ligne avec >>>, l’exécuter, si la ligne suivante ne contient pas de >>>, il va comparer le résultat de l’exécution de la ligne précédente avec le contenu de la ligne qui la suit.

Ceci passe :

"""
    >>> ajouter(1, 2)
    3
"""

Mais ceci échoue :

"""
    >>> ajouter(1, 2)
    4
"""

Car le résultat AFFICHÉ dans le shell est 3, et non 4.

En cas d’échec, Python vous en dit un peu plus :

python mon_module.py
**********************************************************************
File "mon_module.py", line 6, in __main__.ajouter
Failed example:
    ajouter(1, 2)
Expected:
    4
Got:
    3
**********************************************************************
1 items had failures:
   1 of   1 in __main__.ajouter

Formater ses doctests

Les doctests sont faits pour s’intégrer de manière transparente aux docstrings. On peut donc en mettre autant qu’on veut, au milieu du texte ordinaire de la docstring. Python se base sur les chevrons (>>>) pour savoir quand commence un test, et le saut de ligne pour savoir quand ça se termine. Au delà de ça, le style est libre.

def ajouter(a, b):
    """ Je peux mettre n'importe quoi ici.
 
        Et ici aussi.
 
        Puis intégrer des tests:
 
        >>> ajouter(1, 2)
        3
        >>> ajouter(2, 2)
        4
 
        Et un saut de ligne indique que les tests sont terminés. Mais je peux
        encore en ajouter après si je veux.
 
        >>> ajouter(0, 0)
        0
 
    """
    return a + b

Néanmoins, l’intérêt des doctests est de documenter son code à travers les tests, et donc on adoptera généralement un format tel que ace;:

def ajouter(a, b):
    """ Additionne deux elements.
 
        Exemple :
 
            >>> # on peut mettre des commentaires ici
            >>> ajouter(1, 2) # ou là
            3
            >>> ajouter(2., 2) # fonctionne sur tous les types de nombre
            4.0
 
        La fonction fonctionne en duck typing, et accepte donc tout objet
        qui possède la méthode magique __add__ :
 
            >>> ajouter('a', 'b')
            'ab'
            >>> ajouter([1], [2])
            [1, 2]
    """
    return a + b

Notez comme il est agréable de lire cette docstring : on comprend tout de suite comment utiliser la fonction. En prime, ce sont des tests unitaires qui garantissent que notre fonction va continuer de fonctionner correctement et nous oblige à garder cette doc à jour.

On peut faire des imports dedans ou utiliser temporairement pdb pour debugger. N’importe quel code de shell est valide mais faites attention à ne pas démarrer des boucles infinies comme les event loops des GUI ou lib async.

Voici ce que donnerait l’exemple des articles précédents avec des docstests :

def get(data, index, default=None):
    """ Implémente l'équivalent de dict.get() pour les indexables.
 
        Example :
 
            >>> simple_comme_bonjour = ('pomme', 'banane')
            >>> get(simple_comme_bonjour, 0)
            'pomme'
            >>> get(simple_comme_bonjour, 1000, 'Je laisse la main')
            'Je laisse la main'
    """
    try:
        return data[index]
    except IndexError:
        return default

Problèmes et solutions

Les doctests ne sont pas la Panacée, particulièrement parce que le test se fera sur le résultat AFFICHÉ dans le shell. Cela peut facilement amener à des erreurs.

Déjà, il faut faire attention à la représentation des objets dans le shell Python. La représentation n’est pas forcément la valeur de saisie :

>>> 1.
1.0
>>> "1"
'1'
>>> {"foo": "bar", "une apostrophe : '": "est échapée ainsi qu'un accent"}
{"une apostrophe : '": "est \xc3\xa9chap\xc3\xa9e ainsi qu'un accent", 'foo': 'bar'}

La solution à ce problème est de tester dans le shell les valeurs de retour, et non de le faire de tête. Faites bien gaffe aux espaces qui sont donc significatifs, surtout ceux en fin de ligne. Mon éditeur est configuré pour les virer par défaut, et ça m’a niqué en écrivant l’article :)

Ensuite, il y a des cas où la représentation ne sera pas la même d’un appel à l’autre.

C’est le cas avec les dictionnaires, puisque l’ordre des éléments n’est pas garanti par nature. Ne faites donc pas :

>>> retourne_un_dico()
{'ordre': 'non garanti', 'le': 'resultat'}

Mais plutôt quelque chose qui vous garantit l’affichage :

"""
>>> res = list(retourne_un_dico().items())
>>> res.sort()
[('le', 'resultat'), ('ordre', 'non garanti')]
>>> # ou
>>> retourne_un_dico() == {'ordre': 'non garanti', 'le': 'resultat'}
True
"""

Parfois, on ne peut juste pas garantir l’affichage. Par exemple avec des nombres non prévisibles comme les hash ou les id des objets :

"""
>>> class Test(): pass
>>> repr(Test())
''
"""

7f4687d30fc8 n’est ici pas prévisible. Python met certains cas spéciaux comme celui-ci des flags activables via le commentaire # doctest: +NOM_DU_FLAG.

Par exemple, le flag ELLIPSIS permet de placer ... dans le résultat en guise de joker :

"""
>>> repr(Test()) # doctest: +ELLIPSIS
''
"""

D’autres problèmes similaires peuvent être résolus ainsi. Le flag SKIP permet de sauter un test que vous voulez mettre là, en exemple, mais qui ne doit pas être testé :

"""
>>> # ce test va être ignoré
>>> repr(Test()) # doctest: +SKIP
''
"""

NORMALIZE_WHITESPACE permet de considérer toute séquence de caractères non imprimables comme un espace. 8 tabs ou 4 espaces dans le résultat seront tous considérés comme un espace.

"""
>>> 'ceci est une assez longue ligne divisible' # doctest: +NORMALIZE_WHITESPACE
'ceci    est     une assez longue    ligne divisible'
"""

Les flags sont cumulables, si on les sépare par des virgules dans le commentaire.

Autre astuce, si votre sortie doit contenir un saut de ligne, Python va l’interpréter comme la fin des tests. On peut pallier cela en utilisant <BLANKLINE> :

"""
>>> print('Un saut de ligne\\n')
Un saut de ligne
 
"""

Faites attention aux antislash et autres caractères spéciaux dans vos docstests puisque toute string est parsée deux fois : une fois à l’écriture de la docstring, puis une fois à son exécution. Ici vous voyez que je suis tenu d’échapper mon \n On peut d’ailleurs utiliser les préfixes r (cf: les raw strings) et u sur les docstrings, si un jour vous êtes bloqué par trop d’échappements ou des caractères non ASCII en pagaille, pensez-y.

Un cas particulier est celui des exceptions. LOL, n’est-il pas ?

Pour y répondre, Python décide qu’une expression est levée si il voit Traceback (most recent call last):. Il ignore ensuite tout le texte – qui est donc optionnel et que vous pouvez omettre – jusqu’à ce qu’il rencontre le nom de l’exception levée. À partir de là, il vérifie que le test passe.

Par exemple, si votre exception génère ce traceback :

Traceback (most recent call last):
  File "", line 1, in 
  File "test.py", line 41, in ajouter
    1 / 0
ZeroDivisionError: integer division or modulo by zero

Vous pouvez faire dans votre doctest :

"""
>>> je_leve_une_exception()
Traceback (most recent call last):
ZeroDivisionError: integer division or modulo by zero
"""

Seule la dernière ligne est comparée.

Il est également possible de mettre les doctests dans un fichier texte à part, mais je ne vous le recommande pas. Cela retire l’intérêt principal des doctests : avoir du code exécutable dans la doc. Si on doit avoir un fichier séparé, autant utiliser des tests normaux, bien plus pratiques et complets.

Car il n’y a pas de tear down, setup ou fixtures avec les docstests. Ca reste un outil basique.

Sachez néanmoins que les doctests sont parfaitement compris par pytest, il suffit juste de lui demander de les exécuter avec l’option suivante :

py.test --doctest-modules

Dans ce cas, il n’est pas nécessaire de faire à la fin de chaque fichier contenant des doctests :

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Quand utiliser les doctests ?

Généralement, on utilise un mélange des tests ordinaires (dans notre cas des tests pytest plutôt que unittest) et des doctests.

On utilisera des doctests pour les objets ou les fonctions simples et indépendantes. J’entends par là, des fonctions et des objets qui prennent uniquement des types basiques en paramètres, et qui retournent uniquement ces types basiques en paramètres. Pour les objets, ils doivent avoir peu de méthodes.

Pour tout le reste, on utilisera des tests ordinaires.

Par exemple, si vous avez une fonction comme notre exemple get(), les doctests sont un bon choix. En revanche, si vous avez un objet Server qui est un serveur HTTP, ou une fonction qui prend un objet Server en paramètre, il vaut mieux utiliser les tests ordinaires.

Il est tout à fait possible, et même agréable, de mettre quelques tests simples en doctests qui aident à la documentation, et de faire les tests les plus compliqués via pytest.

Prochaine étape, les mocks. Parti de là, je pourrais vous dire quelles parties de votre programme tester en priorité, et comment. Au début je voulais faire l’inverse, mais finalement, c’est plus pratique.


Télécharger le code de l’article