PROJET AUTOBLOG


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

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

⇐ retour index

Django, une app à la fois – faire une app de base

samedi 18 janvier 2014 à 16:22

Ca faisait longtemps que je n’avais pas fait avancer le projet django, an app at a time.

Voici donc un nouveau morceau qui montre comment écrire une petite app de base avec un template et une vue réutilisables qui serviront donc dans les prochaines apps du projet.

Comme d’hab, vous pouvez récupérer le code sur la teub de Guy et il y a une version française et anglaise.

flattr this!

En attendant asyncio

vendredi 17 janvier 2014 à 15:09

La programmation asynchrone arrive en force avec la version 3.4, mais celle-ci n’est pas encore en version stable. En attendant, Python 3 possède déjà de quoi faire de la programmation asynchrone, et même parallèle, avec une bien plus grande facilité qu’en Python 2.

Si vous avez oublié le principe ou l’intérêt de la programmation asynchrone, il y a un article pour ça ©.

Pour montrer l’intérêt de la chose, nous allons utiliser un bout de code pour télécharger le code HTML de pages Web.

Sans programmation asynchrone

Le code est simple et sans chichi :

# -*- coding: utf-8 -*-
 
import datetime
from urllib.request import urlopen
 
start_time = datetime.datetime.now()
 
URLS = ['http://sebsauvage.net/',
        'http://github.com/',
        'http://sametmax.com/',
        'http://duckduckgo.com/',
        'http://0bin.net/',
        'http://bitstamp.net/']
 
for url in URLS:
    try:
        # j'ignore volontairement toute gestion d'erreur évoluée
        result = urlopen(url).read()
        print('%s page: %s bytes' % (url, len(result)))
    except Exception as e:
        print('%s generated an exception: %s' % (url, e))
 
elsapsed_time = datetime.datetime.now() - start_time
 
print("Elapsed time: %ss" % elsapsed_time.total_seconds())

Ce qui nous donne:

python sans_future.py
http://sebsauvage.net/ page: 9036 bytes
http://github.com/ page: 12582 bytes
http://sametmax.com/ generated an exception: HTTP Error 502: Bad Gateway
http://duckduckgo.com/ page: 8826 bytes
http://0bin.net/ page: 5551 bytes
http://bitstamp.net/ page: 51996 bytes
Elapsed time: 25.536095s

Erreur 500 sur S&M… Mon script qui se fout de ma gueule en plus…

Avec programmation asynchrone

On utilise le module future, qui, comme sont nom l’indique, implémente des outils pour manipuler des “futures” en Python. Il inclut notamment un context manager pour créer, lancer et arrêter des workers automatiquement, et leur envoyer des tâches, puis récupérer les résultats de ces tâches sous forme de “futures”.

Pour rappel, une “future” est juste un objet qui représente le résultat d’une opération asynchrone (puisqu’on ne sait pas quand elle se termine). Cet objet contient des méthodes pour vérifier si le résultat est disponible à un instant t, et obtenir ce résultat si c’est le cas.

# -*- coding: utf-8 -*-
 
import datetime
import concurrent.futures
 
from urllib.request import urlopen
from concurrent.futures import ProcessPoolExecutor, as_completed
 
start_time = datetime.datetime.now()
 
URLS = ['http://sebsauvage.net/',
        'http://github.com/',
        'http://sametmax.com/',
        'http://duckduckgo.com/',
        'http://0bin.net/',
        'http://bitstamp.net/']
 
 
def load_url(url):
    """
        Le callback que vont appeler les workers pour télécharger le contenu
        d'un site. On peut appeler cela une 'tâche'
    """
    return urlopen(url).read()
 
# Un pool executor est un context manager qui va automatiquement créer des
# processus Python séparés et répartir les tâches qu'on va lui envoyer entre
# ces processus (appelés workers, ici on en utilise 5).
with ProcessPoolExecutor(max_workers=5) as e:
 
    # On e.submit() envoie les tâches à l'executor qui les dispatch aux
    # workers. Ces derniers appelleront "load_url(url)". "e.submit()" retourne
    # une structure de données appelées "future", qui représente  un accès au
    # résultat asynchrone, qu'il soit résolu ou non.
    futures_and_url = {e.submit(load_url, url): url for url in URLS}
 
    # "as_completed()" prend un iterable de future, et retourne un générateur
    # qui itère sur les futures au fur et à mesures que celles
    # ci sont résolues. Les premiers résultats sont donc les premiers arrivés,
    # donc on récupère le contenu des sites qui ont été les premiers à répondre
    # en premier, et non dans l'ordre des URLS.
    for future in as_completed(futures_and_url):
 
        # Une future est hashable, et peut donc être une clé de dictionnaire.
        # On s'en sert ici pour récupérer l'URL correspondant à cette future.
        url = futures_and_url[future]
 
        # On affiche le résultats contenu des sites si les futures le contienne.
        # Si elles contiennent une exception, on affiche l'exception.
        if future.exception() is not None:
            print('%s generated an exception: %s' % (url, future.exception()))
        else:
            print('%s page: %s bytes' % (url, len(future.result())))
 
 
elsapsed_time = datetime.datetime.now() - start_time
 
print("Elapsed time: %ss" % elsapsed_time.total_seconds())

Et c’est quand même vachement plus rapide :

python3 avec_future.py # notez qu'on utilise Python 3 cette fois
http://duckduckgo.com/ page: 8826 bytes
http://sebsauvage.net/ page: 9036 bytes
http://github.com/ page: 12582 bytes
http://sametmax.com/ page: 50998 bytes
http://0bin.net/ page: 5551 bytes
http://bitstamp.net/ page: 52001 bytes
Elapsed time: 3.480596s

Même si vous retirez les commentaires, le code est encore très verbeux, ce qui explique pourquoi j’attends avec impatience asyncio qui, grâce à yield from, va intégrer l’asynchrone de manière plus naturelle au langage.

Mais ça reste beaucoup plus simple que de créer son process à la main, créer une queue, envoyer les tâches dans la queue, s’assurer que le process est arrêté, gérer les erreurs et le clean up, etc.

Notez qu’on peut remplacer ProcessPoolExecutor par ThreadPoolExecutor si vous n’avez pas besoin d’un process séparé mais juste de l’IO non bloquant.


Télécharger le code de larticle : avec future / sans future.

flattr this!

Importer des données, retour d’expérience

jeudi 16 janvier 2014 à 16:49

Dédicaçons la chanson de notre article au plus barbu de mes amis poneys.

J’ai importé des données un très grand nombre de fois dans ma vie. Depuis des APIs, des XML, des CSV, du filesystem, des formats binaires, des formats batards, etc

Pour tous les jobs d’import, Python est probablement le meilleur langage au monde. Autant j’aime Python, autant je suis lucide sur le fait qu’en dev Web, Ruby et Javascript sont d’excellentes alternatives. En programmation concurrente, Go dépasse Python. En IA, Lisp est le top de la concurrence.

Mais pour l’import de données, Python est simplement le meilleur langage au monde. Sa capacité à lire énormément de formats facilement, sa force de manipulation de données numériques et texte, sa philosophie d’itération, ses nombreuses libs en font un outil incroyablement souple et puissant.

Malgré cela, on retrouve toujours des grosses difficultés dans l’import de données. Elles sont les mêmes pour tous les langages.

Voici mes 2 centimes.

N’accordez aucune confiance à la donnée

Partez du principe que tout champ peut manquer. Que toute donnée peut être mal formée ou corrompue. Ou fausse.

Même si le service en face est sérieux. J’ai bossé avec des données du service de santé américain, de France Télécom, de startups, d’outils Open Source, de scrapping de sites, de mon pote Maurice, et de mes propres scripts

Vous savez ce qu’ils ont tous en commun ? Aucun ne sont fiables. Aucun.

Ils ont tous des données merdiques à un moment où à un autre.

Ayez donc une approche défensive. Pour CHAQUE champ, posez vous la question : que doit faire le script d’import si il manque ? Si la donnée contenue est foireuse ?

Les outils d’abstraction sont vos amis

Un import, c’est typiquement le genre de taff où les surcouches vont énormément vous aider. ORM, DSL, XML objectify et toute lib qui peut vous éviter de travailler trop proche du format va vous faire gagner un temps fou.

Prenez un peu de temps pour les mettre en place. Même si ils vous font perdre un peu en perf, un gros script d’import devient TRÈS VITE un sac de nœud. Et vous voulez que les problèmes soient facilement identifiables.

Pour cette même raison, virez toute la logique de l’insertion des données en dehors du script. Votre script doit avoir une logique découpée en 3 parties :

  1. Le script d’acquisition, qui charge les données et les passe sous forme brute à un importeur.
  2. Un importeur, qui est capable d’extraire des données raffinées à partir d’un format de données brutes et appeler le bon code d’insertion.
  3. Du code d’insertion, qui attend en paramètre une donnée toujours propre (sans aucun check), et qui se charge uniquement de prendre cette donnée et la mettre dans votre système de stockage (généralement la base de données).

Le script d’acquisition doit rester très simple. Une suite d’instructions logiques pour récupérer la donnée, l’énumérer et la passer à l’importeur. C’est lui qui fait les appels API, qui se connecte au FTP, qui ouvre le CSV, qui parse le XML, etc. Ainsi vous pouvez facilement interchanger les importeurs ou voir si il y a une couille dans la récupération des données.

L’importeur est généralement le code le plus crade. C’est une série de try / except, de logique métier, d’assainissement des données. Vous ne voulez pas de code d’insertion là dedans, car vous voulez que ce code, qui est difficile à débugger et va être celui qui va être modifié toutes les 5 minutes au fur et à mesure que vous découvrez toute les merdes, soit dédié à une seule logique : obtenir de la donnée saine. Ce code sera spaghetti, vous n’y pouvez rien. Mais vous pouvez l’isoler et le commenter à mort.

Le code d’insertion est un code réutilisable. Il se fout de savoir d’où vient la donnée. Il attend un format, et un seul, et toujours de données correctes, et propres. C’est le but des importeurs de lui filer une entrée normalisée et pertinente. Ce code est propre, et doit être très bien testé via des tests unittaires. Il va vous servir plusieurs fois, car c’est le même code qui sera utilisé que vous importiez d’un service X ou un service Y. Il représente VOTRE logique métier. N’insérez pas ce code dans le code d’une autre abstraction (type ORM), ainsi, si vous changez d’outils, vous changez simplement ce code, et l’interface reste la même pour vos importeurs.

Debugging

Votre script va planter. Beaucoup. Souvent.

Un champ absolument indispensable – que la spec papier notait comme toujours présent – va manquer. Un autre champ noté de type int dans le xld contient une lettre. L’encoding n’est pas le bon, alors qu’il l’a toujours été pendant 5 mois.

Ce n’est pas une question de si, c’est une question de quand.

Donc déjà, blindez votre script de log. Quand je dis blindez, je veux dire que chaque if, chaque résultat de check, doit être accompagné d’une ligne affichant l’action en cours, et son contexte (la donnée traitée, de préférence avec un truc pour l’identifier, genre un ID). Quand il plantera à 3 heure du matin sur un truc hubuesque et que le relancer pour obtenir le même état prendra une demi-journée, le log sera votre seul chance de réparer la panne sans engager un psy.

Mettez aussi un gros try / except générique qui loggue toute exception, pour pouvoir faire un debug post mortem. Idéalement, faites le dumper locals() et envoyez-vous un mail d’alerte. Vous ne voulez pas que le script ne tourne pas pendant une journée sans que vous le sachiez.

Mettez des options dans votre scripts pour pouvoir débugger plus facilement. Par exemple, si vous avez de code d’insertion qui est sous forme de tâche asynchrone (type celery), mettez une option --synchronous qui insère le code inline afin de pouvoir utiliser pdb sur tout le script en cas de besoin. Ou alors, si vous avez une grosse archive à décompresser, mettez une option --nozip pour pouvoir sauter cette étape.

Et si vous insérez un break point, mettez le dans une condition du genre :

if id_du_champ_ou_autre_moyen_identifiant == "valeur":
    send_mail('Alerte, on est peut être au bug. Bouge ton fion.')
    import ipdb; ipdp.set_trace()

Comme ça vous pouvez retourner à vos moutons le temps que le breakpoint s’active, ce qui, sur des gros jeux de données, peut prendre énormément de temps.

Enfin, je sais qu’on a tendance à être fainéant et vouloir toujours débugger en direct. Mais faites des mocks. Faites un faux XML, une API bidon, bref, un truc qui vous permet d’insérer des cas d’import avec une données dans le format attendu, et testez votre code avec ça. Pour les petits imports, c’est une perte de temps, mais pour les gros imports, ça va vous faire gagner des jours. Ainsi vous pouvez tester des cas isolé, rajouter des bugs rencontrés, etc. Et en plus ça sert de documentation.

Problèmes courants

Il y a mille et une manière d’avoir un import qui plante, mais il y a généralement 6 grosses foirades qu’on retrouve tout le temps.

Service défaillant

Le service (FTP, API, humain en face, NAS, etc) qui doit vous fournir les données est indisponible. Vous n’y pouvez rien. Envoyez-vous un SMS pour vous prévenir, aujourd’hui c’est facile et ça coûte presque rien. Ainsi vous pourrez dialoguer rapidement avec les personnes responsables de problème.

Champs manquants

Grand classique. Tout champ peut manquer. Tout. Même un ID unique sans lequel la donnée n’a aucune sens. Faites vous un wrapper du genre :

def get_data(champ):
    try:
        # extraire le champ
    except ChampAbsent, ChampMalFormé:
        return None

Et utilisez le partout. Et décidez ce que doit faire votre programme si il rencontre None. Pour TOUS les champs. Si None est une valeur possible, utilisez Ellipsis. Si Ellipsis est une valeur possible, faites vous une classe InvalidData.

Mauvais encoding

Super vicieux. Je vous renvoie à l’article sur l’encoding pour cela.

Donnée aberrante

Impossible à prévoir, très difficile à identifier. Donnée de mauvais type, date ou nombre hors limites, texte dans la mauvaise langue, etc. Vous ne pouvez pas tout prévoir. Pour ça, il faudra faire au fur et à mesure des plantages.

Données mal formatée et malicieuses

En plus de get_data, il vous faut un clean_data. Qui check si on peut processer la donnée sereinement. Pour tous les champs également. C’est con, mais si LEUR système n’escape pas les entrées utilisateurs, c’est VOTRE système dans lequel se retrouve les injections de code.

Performances

La vitesse de votre script sera généralement limitée par 3 facteurs :

Ces 3 facteurs sont en général très liés.

La meilleur stratégie, c’est d’extrapoler un max de données, et de cacher tout ce qui est cacheable. Par exemple, copiez les données brutes sur vos serveurs (genre si c’est un fichier sur le leur). Copiez les références externes, même si vous n’en avez pas besoin afin d’éviter une query de plus, vous les supprimerez plus tard. Pré-calculez les champs, par exemple age, si vous avez la date de naissance, etc.

Si vous avez beaucoup de checks à faire pour l’assainissement des données, mettez vos données en cache (par exemple dans redis), pour que les looks up soient rapides, ou au moins, ajoutez les index qui vont bien dans la DB (on peut avoir des perfs X10 rien qu’avec ça).

Ensuite, partez du principe que ça va planter souvent, donc :

Ah oui, et faites des copies de sauvegarde des données brutes ET des données importées. Pour les premières, parce qu’on est pas à l’abri un “rm -fr” malencontreux. Ne rigolez pas, ça m’est arrivé la semaine dernière. Une semaine à tout DL à nouveau. Pour les secondes, parce que tant que le dernier import n’est pas terminé, on peut toujours corrompre toute sa base avec une couille de dernière minute. Comme un encoding qui change aléatoirement sur une donnée non datée.

Bon sens

Évidement, je parle ici d’un synthèse des problématiques rencontrées. Vous ne pouvez pas appliquer TOUT ça, ou en tout cas, pas au début, ou pas sur des petits scripts, etc. Selon le sérieux de votre source de données, il faudra plus ou moins être défensif. L’expérience et la douleur vous permettra de trouver la juste dose de morphine.

flattr this!

Un gros guide bien gras sur les tests unitaires en Python, partie 1

mercredi 15 janvier 2014 à 16:26

La zik maintenant traditionelle :

Les tests unitaires font partie de ces “bonnes pratiques” que tout le monde semble appliquer sur le net. Tous les devs hypes parlent de tests unitaires : les conférences, les blogs, les tutos, les livres, whooooo !

Dans la vraie vie vivante, on croise pourtant peu de gens qui les utilisent vraiment. On les retrouvent surtout dans les gros projets et les grosses boîtes, et encore.

Il y a plusieurs raisons à cela. D’une part, beaucoup, beaucoup, beaucoup de développeurs n’ont aucune idée de ce qu’est un test unitaire. Ceux qui savent, ne voient pas forcément l’intérêt, et ceux qui en voient l’intérêt n’ont pas forcément l’expérience nécessaire à leur mise en œuvre.

Je connais des tas de dev qui codent des tas d’excellents projets sans le moindre test unitaires.

L’adage selon lequel un code sans test unitaire est un code buggé est parfaitement faux puisque existe bien d’autres manières de tester son code. De plus, même un code bien testé est un code buggé. Je le sais, je l’ai codé.

Malgré cela, vous devriez maitriser l’usage des tests unitaires, car quand vous arrivez à vous sortir les extrémités digitales de la terminaison dorsale afin de les mettre en place, le bénéfice est très important. Mais aussi parce que certains projets ne peuvent pas s’en passer, et donc que vous ne pourrez pas travailler dessus sans savoir en faire. Certains projets sur Github n’acceptent pas de pull request sans couverture de tests, et certaines personnes n’utiliseront pas votre lib si elle n’est pas testée. C’est un gage de qualité.

Je n’en ferai pas une question morale ou de principe, les projets que l’on publie sur Sam et Max sont parfaitement exempt de tests unitaires, et d’ailleurs, la plupart des projets pros avec Max n’ont aucun tests non plus.

En revanche, en tant que freelance, je prends généralement le temps d’en faire.

Pas de dogmatisme du test donc, mais passé le goût de crabe dans la bouche, ça vaut le coup, alors lisez ce guide.

Qu’est-ce qu’un test unitaire

Le test unitaire est un bout de code qui fait exactement ce que son nom dit : il teste une unité de code.

Le problème c’est quoi tester, qu’est-ce qu’une “unité de code”, ce n’est pas quelque chose d’évident à définir, et vient avec la pratique. En théorie c’est un bout de code minimaliste, que l’on ne peut pas réduire plus. En pratique, on choisit avec pragmatisme un truc assez petit, mais pas trop, parce que merde, hein.

Mais alors que veut-on dire par “tester” ?

Et bien c’est d’une banalité affligeante : on donne des entrées au code, et on vérifie que ses sorties sont celles attendues pour ces entrées.

Bref, généralement (mais pas toujours) on teste une fonction. Souvent avec une autre fonction. Et c’est d’un manque d’originalité terrible.

Le test unitaire le plus bête qu’on puisse avoir en Python :

# Fichier de code
def fonction_a_tester(param1, param2):
    return param1 + param2
# Fichier de test
 
from fichier_de_code import fonction_a_test
 
assert fonction_a_tester(1, 1) == 2  # test de l'addition
assert fonction_a_tester(1, -1) == 0 # test avec chiffre négatif
assert fonction_a_tester(4, 2) == 6 # test avec autre chose que des 1
assert fonction_a_tester(4., 2) == 6. # test avec des floats

Deux constats :

assert est un mot clé qui lève l’exception AssertionError quand l’expression évaluée ne retourne pas True. L’utilisation d’assert n’est pas le sujet de l’article, ici on s’en sert pour faire un test unitaire tout simplement parce que la première ligne qui ne renverra pas True fera planter le programme. C’est le test unitaire du pauvre.

Un test unitaire, ce n’est que ça. Un répétition bête et emmerdante de vérifications généralement très connes.

C’est minable ! A quoi ça sert ?

Là normalement vous vous dites “je sais ce que fait mon code, surtout une unité minimaliste, je n’ai pas besoin d’écrire des évidences pour le tester”. Et c’est pour cela que je ne suis pas dogmatique sur les tests unitaires, car c’est en partie vrai. Beaucoup de codes sont suffisamment simples ou peu critiques pour ne pas avoir besoin d’être renforcés par des tests unitaires. Et même si il faut des tests, tout le code n’a pas nécessairement besoin d’être testé.

Lancer un blog pour sa cousine n’est pas la même chose qu’une site de rencontre pour un grand compte.

Mais le test unitaire a plusieurs bénéfices. Le premier c’est qu’il vous oblige à réfléchir aux entrées et sorties de vos fonctions, et à l’API de votre code en général. Vous vous apercevrez à l’usage qu’un code est plus ou moins facile à tester selon la manière dont vous l’avez organisé, et ce faisant, vous serez forcé d’écrire un code plus souple, propre, extensible.

Écrire des tests fait de vous un meilleur développeur.

Cependant ce n’est pas le principal intérêt. Le véritable gain tient dans ce que vous gagnez dans le futur : quand vous allez modifier votre code, vous pourrez rapidement voir si il n’est pas cassé. En effet, votre code va grossir, et vous ne vous souviendrez pas de toutes les dépendances, de tous les effets de bords, de toutes les interactions. Certains dev sont meilleurs que d’autres à tout garder dans la tête, mais même Cortex a ses limites. Au bout d’un moment, le code est plus fort que vous.

À partir de là, vous allez tout de même avoir besoin de factoriser le code, bouger des choses, en ajouter d’autres, corriger un bug, faire un petit ajustement. À chaque fois que vous le faites, vous prenez le risque de casser un truc. Au début du projet, le risque est faible, et même si ça arrive, ça se répare vite. Après 2 mois de dev, les tests seront votre filet de sécurité. Vous pouvez les lancer après chaque modif, et voir que vous n’avez rien pété. Vous pouvez les lancer après une contribution d’un autre dev, et voir que ça tourne toujours. Vous pouvez les lancer après un changement d’environnement (OS, base de données, système de fichier, format, etc) et vous assurer que ça n’a pas d’impacts.

Particulièrement, des tests unitaires ont beaucoup de valeur sur un projet avec beaucoup de participants, tels que des logiciels libres populaires ou des systèmes de grandes sociétés.

Par exemple, sur notre dernière fonction bidon, on décide de faire une petite modification :

# Fichier de code
def fonction_a_tester(param1, param2):
    return int(param1) + int(param2)

On peut maintenant passer une string, et elle sera convertie en entier.

On lance notre batterie de tests, et là, au milieu de centaines d’autres tests, celui là foire :

assert fonction_a_tester(4., 2) == 6. # test avec des floats

On voit très vite que notre idée était pourrie, car on a un use case qui ne sera plus compatible. Si quelqu’un a utilisé des floats avec notre fonction, on va casser son code.

En l’essence, c’est ça l’intérêt des tests unitaires : vous faire sauter au yeux quand quelque chose casse. On appelle ça des “tests de régression”, et c’est l’usage le plus courant.

Plus tard vous verrez qu’on utilise aussi les tests pour développer son code (TDD), pour définir un comportement du produit avec le client (BDD) ou tout simplement pour servir de documentation.

Mais l’usage de base, c’est ça. S’assurer qu’on est pas en train de merder.

Résumé

  1. N’écoutez pas les Papes du test vous disant que si vous n’avez pas des tests unitaires à 50 ans, vous avez raté votre vie. Les tests, c’est bien. Un projet livré, c’est mieux. Une documentation est plus importante que des tests. Les 3, évidement, c’est l’idéal.
  2. Un test, c’est une suite parfaitement chiante d’énonciations d’évidences. Il n’y a généralement rien de compliqué dans les tests. Vous vous sentirez parfois insulté en les écrivant tellement c’est con.
  3. L’intérêt majeur des tests est d’avoir une alerte rouge qui se lance quand vous avez pété un truc. Ça arrive bien plus souvent que vous ne le croyez sans que vous ne vous en aperceviez car vous n’avez pas de tests.

Ces bases posées, la prochaine partie fera la démonstration du module unittest afin de créer vos premiers tests unitaires en Python, puis on enchaînera, partie par partie, sur les applications pratiques, les variantes, les girafes lesbiennes et tout ce qui fait un bon article de s&m.

flattr this!

Sam on the road again

lundi 30 décembre 2013 à 01:46

Je vous laisse, j’ai des km à parcourir.

Retour dans une ou deux semaines.

flattr this!