PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

YAML, XML, JSON, CSV, INI… Qu’est-ce que c’est et à quoi ça sert ?

samedi 6 juillet 2013 à 07:23

Que voilà de jolis acronymes !

Quand j’ai débuté la programmation, je les rencontrais partout sur le net. On en parlait comme si on parlait d’acheter du pain. Apparemment c’était évident pour tout le monde.

Ça m’a énervé, mais ça m’a énervé !

Et puis j’ai oublié. C’est devenu tellement le quotidien pour moi, tellement banal… Jusqu’à ce que je reçoive ce mail :

Je viens vous quémander un article

Il y a une question que je me pose assez souvent avant de coder quelque chose et bien que j’ai pu me faire quelques opinions au fil du temps, je n’ai jamais trouvé un article ou une conf ou que sais-je qui explique ça clairement.

Ma question concerne le format d’écriture des données.
Entre les fichiers textes genre ini, json, xml, yaml (que j’aime bien), les formats binaires (pickle, voir hdf5 pour les scientifiques et j’en ignore peut être d’autre…), les bases de données mysql, postgre, sqlite.

Je n’ai pas les idées claires sur :
* cas typique d’utilisation
* les plus et les moins
* la corruptibilité
* la sécurité (point fourre-tout)

En gros, mon raisonnement de béotien dit : binaire plus performant que texte mais texte lisible par l’éditeur et ça, ça rassure.

A prendre ou à jeter

Aujourd’hui, je vais donc parler des formats texte, et je ferai un article sur les formats binaires plus tard.

Formats

YAML, XML, JSON, CSV, INI sont des noms de formats de données texte. Il y a plusieurs choses à comprendre ici:

Donnés :
Ca peut être n’importe quoi que vous vouliez sauvegarder ou transmettre : carnet d’adresses, configuration d’un logiciel, contenu d’une base de données, nom/prenom/age/mensurations, etc. Bref, tout groupe d’informations que vous souhaitez pourvoir communiquer, ou relire plus tard.
Format :
Comment sont organisées ces données. Comme on manipule les données avec un ordinateur, il est nécessaire de les ranger données d’une certaine façons. En les rangeant de cette façon, l’ordinateur, si il connait le “format”, est capable d’analyser les données qu’il reçoit. Sinon pour lui elles ne veulent rien dire.
Texte :
En opposition à “binaire” (ce qui est un abus de langage, puisque du texte en informatique, c’est du binaire). Cela signifie que votre format est organisé autour d’un texte lisible par les humains. C’est ce texte qui va dire “ceci est l’age”, “ceci est le nom”, “ceci est la taille de ses ganglions”, etc.

En résumé, un format de données texte, c’est une convention textuelle pour que des ordinateurs puissent échanger des données entre eux et que celui qui reçoit puisse retrouver la même chose que ce que l’on lui a envoyé.

Exemples

Le format XML est une convention qui dit qu’on va mettre les données (les informations qu’on transmet) entre des balises.

Les balises ont la forme suivante : < nomDeBalise >.

Une balise peut contenir une données, ou d’autres balises.

Le choix des balises est laissé à la personne qui créer le XML. Celui qui reçoit le XML doit connaître ces balises pour récupérer les données.

Imaginez un carnet d’adresses avec chaque personne ayant un nom et un numéro de téléphone. Il existe de nombreux moyens de représenter ce carnet d’adresses. L’UN des moyens de possibles, est d’écrire un fichier XML. Par exemple :

<?xml version="1.0" encoding="utf8"?>
<personnes>
    <personne>
        <nom>Sam</nom>
        <numero>555-555-555</numero>
    </personne>
    <personne>
        <nom>Max</nom>
        <numero>1234567890</numero>
    </personne>
    <personne>
        <nom>Bob</nom>
        <numero>666</numero>
    </personne>
</personnes>

Pour vous en tant qu’humain, ça n’apporte rien.

Mais quand vous avez besoin de créer un programme qui peut sauvegarder / lire ces données ou les transmettre à un autre programme (car oui, les programmes doivent pourvoir se parler entre eux, comment vous croyez que cet article arrive sur votre ordinateur ?), il va falloir choisir un format pour ces données.

Chaque format et différent, et possède des avantages et des inconvénients. On peut très bien représenter les mêmes données avec deux formats différents. Ainsi, voici le même carnet d’adresses, mais au format CSV :

"nom";"numero"
"Sam";"555-555-555"
"Max";"1234567890"
"Bob";"666"

Quel format est le meilleur ?

Il n’existe pas de “meilleur” format.

Chaque format ac ses caractéristiques, et il existe de nombreux formats. En fait, il existe même des formats dans les formats, des XML avec des balises qui ont des noms standardisés par exemple. Pire, vous pouvez inventer vos propres formats. Mais à moins d’être très bon et de combler un besoin qui ne l’est pas encore, je ne vous le recommande pas.

Aussi il va vous falloir choisir un format selon votre situation. En général, on choisira parmi ces caractéristiques :

Dans notre cas, nous allons étudier des formats textes ouverts qui sont tous des standards, et lisibles par un humain. Aussi la pérénité de vos données est le moindre de vos soucis, si tant est que le créateur du format est compétent et bienveillant.

Dans cet article, nous allons en effet voir uniquement les formats texte. Je garderai les formats binaires pou un article suivant, et un dernier pour les bases de données.

Au passage, les formats textes ont tous ces caractéristiques en commun:

A noter qu’au passage la sécurité et la corruptibilité ne sont pas des questions liées au format, mais à la méthode de manipulation de ces formats. Donc le choix du format ne prend pas en compte ces questions.

Le format CSV

L’acronyme CSV signifie Coma Separated Values, littéralement valeurs séparées par des virgules. C’est un des formats les plus simple que l’on puisse trouver.

Dans sa version la plus basique, c’est un format ligne à ligne, et chaque ligne représente une entrée (par exemple une personne dans un carnet d’adresses, un objet dans un catalogue, des groupes d’ingrédients dans une liste de recettes, etc).

Pour chaque entrée, il y a une série de valeur, chaque valeur est séparée par des virgules. Reprenons l’exemple du carnet d’adresse :

sam,555-555-555,5 rue des lilas
max,1234567890,thailande
bob,666,7eme cercle

Mais il peut se complexifier dès qu’on veut rajouter des valeurs plus complexes. Par exemple si elles contiennent des virgules, alors il est d’usage d’entourer les valeurs de quotes :

"sam","555-555-555","5, rue des lilas"
"max,"1234567890","Patong, thailande"
"bob","666","7eme cercle"

Le séparateur (la virgule) et les quotes peuvent être aussi un point-virgule et un quote simple, et on peut avoir n’importe quelle combinaison :

'sam','555-555-555','5, rue des lilas'
'max,'1234567890','Patong, thailande'
'bob','666','7eme cercle'
'sam';'555-555-555';'5, rue des lilas'
'max;'1234567890';'Patong, thailande'
'bob';'666';'7eme cercle'
sam;555-555-555;5, rue des lilas
max;1234567890;Patong, thailande
bob;666;7eme cercle

Il existe aussi des caractères d’échappement, des exceptions de retour à la ligne, et des programmes qui produisent des lignes avec un séparateur, puis des lignes avec un autre. Autant dire que d’un format simple, on arrive parfois à quelque chose de complexe. C’est d’ailleurs pour ça que je recommande de ne pas traiter le CSV à la main, mais d’utiliser des modules spécialisés comme csv en Python.

Sachez néanmoins que le format le plus courant est celui supporté par les tableurs (Excel, LibreOffice Calc…) utilisant des guillemets doubles et des point-virgules :

"Sam";"555-555-555";"5, rue des lilas"
"Max";"1234567890";"Patong, thailande"
"Bob";"666";"7eme cercle"

C’est donc ce format que je recommande car il est du coup très facile à lire et à modifier. Il est aussi possible de donner un nom à chaque colonne sur la première ligne :

"nom";"numero";"adresse"
"Sam";"555-555-555";"5, rue des lilas"
"Max";"1234567890";"Patong, thailande"
"Bob";"666";"7eme cercle"

Vous voudrez utiliser ce format quand :

Le format INI

Le format INI, utilisé pour l’INItialisation, est surtout un format pour stocker des configurations de logiciel. On le trouve souvent dans des fichiers avec des paramètres, portant l’extension .ini, .cfg, .conf ou .txt.

Il est constitué de deux types d’élément :

Les fichiers INI ne sont pas fait pour stocker des données répétitives comme des personnes d’un carnet d’adresses. Généralement, chaque entrée est unique, et représente un paramètre du programme :

[utilisateur]
nom=Sam
dossier=/home/sam
 
[dernier_access]
jour=2013-07-06
fichier='le_plus_dur_est_derriere_toi.avi'

C’est un format simple à manipuler (et il existe un module Python pour ça) et généralement on le lit, on modifie une valeur, et on la sauvegarde.

Vous voudrez utiliser ce format quand :

Le format JSON

A la base JSON, qui signifie JavaScript Object Notation, n’était pas un format destiné à être échangé, mais seulement la représentation textuelle des objets Javascript.

Il se trouve qu’avec l’age d’or du Web, et l’utilisation massive d’AJAX, il a été utilisé pour communiquer entre le navigateur et le serveur, et les gens se sont apperçu qu’il était en fait très très très pratique.

Aujourd’hui, JSON est utilisé un peu pour tout, et si vous ne savez pas trop quoi choisir comme format, choisissez JSON, il y a peu de chance de se planter.

Voilà à quoi ressemble le carnet d’adresses en JSON:

[
    {
        'nom': 'Sam',
        'numero': '555-555-555',
        'adresse': '5, rue des lilas'
    },
    {
        'nom': 'Max',
        'numero': '1234567890',
        'adresse': 'Patong, thailande'
    },
    {
        'nom': 'Bob',
        'numero': '666',
        'adresse': '7eme cercle'
    }
]

JSON est extrêmement facile à manipuler de nos jours, et l’immense majorité des langages ont un module pour ça. Python n’échappe pas à la règle.

Il est rare que JSON soit une mauvaise idée, donc je vais plutôt faire une liste de quand vous ne voulez PAS utiliser JSON :

Sinon, allez-y, prenez du JSON. On peut l’utiliser pour les fichiers de configuration (comme le fait Sublime Text), comme API pour son service (ce que font presque tous les grands services du monde), pour communiquer entre plusieurs sites Web (JSONP), pour exporter / importer ses données (fixtures Django)…

Le format YAML

YAML, l’humoristique “Yet Another Markup Language” est a des buts et qualités similaires au JSON, mais avec un format différent.

Voici le fichier INI, traduit en YAML (les espaces sont significatifs) :

---
utilisateur:
    nom: Sam
    dossier: /home/sam
 
dernier_access:
    jour: 2013-07-06
    fichier: 'le_plus_dur_est_derriere_toi.avi'
...

Le format YAML est néanmoins plus riche que le JSON:

Globalement le YAML a été créé pour être facilement lisible et éditable par un humain, et pour cette raison la communauté Ruby l’a adopté pour ses fichiers de configuration. On retrouve donc YAML dans RubyOnRail.

Utilisez YAML quand :

Contrairement à JSON, on utilisera donc plus YAML pour la configuration que l’envoie de données.

Personnellement je ne suis pas un aficionado de YAML. Mon expérience est que sa syntaxe complexe (j’admet que l’exemple ne le laisse pas paraitre) amène souvent des grattements de tête suite à une édition malheureuse. Frustrant pour un simple fichier de config.. De plus, il faut souvent installer une lib additionnelle pour le lire. Par ailleurs, JSON – qui est en fait un subset de YAML – fait généralement très bien le boulot.

Le format XML

La fameux eXtensible Markup Language. Je ne vais pas vous faire un cours complet sur XML, car on pourrait y passer des mois, au sens propre. Ce format, aux apparences simples, a été utilisé pour des choses extrêmement complexes.

Revenons à notre exemple :

<?xml version="1.0" encoding="utf8"?>
<personnes>
    <personne>
        <nom>Sam</nom>
        <numero>555-555-555</numero>
    </personne>
    <personne>
        <nom>Max</nom>
        <numero>1234567890</numero>
    </personne>
    <personne>
        <nom>Bob</nom>
        <numero>666</numero>
    </personne>
</personnes>

< ?xml version="1.0" encoding="utf8"? > est l’en-tête du fichier, il annonce quel format de XML on va utiliser, le reste est le contenu.

Ici nous n’avons que quelques balises, mais bien entendu, les balises peuvent contenir des balises qui peuvent contenir des balises… En prime, XML autorise des attributs (nom=”valeur”), c’est à dire des valeurs sur les balises qui modifient la signification de celle-ci. Par exemple :

...
<personne>
    <nom>Bob</nom>
    <numero type="shortcode">666</numero>
</personne>
...

Enfin, XML permet ce qu’on appelle des namespaces, afin de dire que les balises correspondent à un dialecte et pas un autre.

Exemple, j’ai deux fois nom dans ce XML :

<?xml version="1.0" encoding="utf8"?>
<personnes>
    <personne>
        <nom>Sam</nom>
        <numero>555-555-555</numero>
        <ville>
            <nom>Metro peau lisse</nom>
            <coord>3.14,6.56</coord>
        </ville>
    </personne>
    <personne>
        <nom>Max</nom>
        <numero>1234567890</numero>
        <ville>
            <nom>Patong</nom>
            <coord>4.2,6.9</coord>
        </ville>
    </personne>
    <personne>
        <nom>Bob</nom>
        <numero>666</numero>
        <ville>
            <nom>Sodome</nom>
            <coord>-12,-13</coord>
        </ville>
    </personne>
</personnes>

Comment savoir pour ma machine ce que signifie, “nom” ?

Et bien je peux les namespacer, c’est à dire les lier à une URL qui pointe vers la documentation qui dit ce que signifie chaque balise, ou au moins le site de l’auteur du XML.

<?xml version="1.0" encoding="utf8"?>
<personnes xmlns:sm="http://sametmax.com" xmlns:osm="http://openstreetmap.org">
    <sm:personne>
        <sm:nom>Sam</sm:nom>
        <sm:numero>555-555-555</sm:numero>
        <sm:ville>
            <osm:nom>Metro peau lisse</osm:nom>
            <osm:coord>3.14,6.56</osm:coord>
        </sm:ville>
    </sm:personne>
    <sm:personne>
        <sm:nom>Max</sm:nom>
        <sm:numero>1234567890</sm:numero>
        <sm:ville>
            <osm:nom>Patong</osm:nom>
            <osm:coord>4.2,6.9</osm:coord>
        </sm:ville>
    </sm:personne>
    <sm:personne>
        <sm:nom>Bob</sm:nom>
        <sm:numero>666</sm:numero>
        <sm:ville>
            <osm:nom>Sodome</osm:nom>
            <osm:coord>-12,-13</osm:coord>
        </sm:ville>
    </sm:personne>
</personnes>

Et si on veut se marrer un peu plus, on peut utiliser ce qu’on appelle des DTD (ou le format plus moderne XSD) : un XML, qui définit comment doit être formé un autre XML et qui permet de vérifier cela automatiquement. Il existe également un format appelé XSLT, qui permet de définir, en XML, comment transformer un autre document XML, en un document dans un troisième format de votre choix.

Bref, le XML peut devenir très très compliqué. Si compliqué en fait, que des formats en XML, même avec leurs spécifications, deviennent difficiles à lire.

XML a été historiquement le premier format texte à être souple, extensible et interopérable. Aussi a-t-il été longtemps un format de choix pour communiquer entre machines et pour sauvegarder les configurations complexes. Aujourd’hui, sa verbosité et sa complexité ont amener les gens à se tourner massivement vers JSON, et XML n’est maintenant plus utilisé que pour les données très riches ou des raisons historiques (RSS, SOAP, etc).

Il reste un terrain où XML brille, c’est la validation de données. En effet, grâce au DTD / XSD, on peut publier un document qui permet à tout langage avec la bibliothèque appropriée, de vérifier si un XML est valide, comme un formulaire. On publie les besoins de validation une fois, et plein de langages peuvent en faire usage. C’est un vrai plus.

Je recommande rarement d’utiliser XML, c’est lourd, difficile à manipuler (malgré un très bon support général de la plupart des langages), verbeux… Difficile à un débutant de mettre les mains dans votre système quand la courbe d’apprentissage est aussi hardos.

Utilisez XML si :

Maintenance, versioning, documentation

Ce n’est pas le tout de choisir un format, il faut maintenant s’assurer qu’on puisse l’utiliser.

Car mettre des labels à ses données, c’est bien beau, mais si le mec qui reçoit vos données ne sait pas ce que signifie ces labels, il ne peut rien en faire.

Il va donc falloir écrire une documentation pour cela. Et oui, on ne documente pas seulement son code, mais aussi ses formats !

Je vous invite aussi fortement à versionner vos formats, c’est à dire à toujours accompagner vos données (dans le fichier, via le protocole d’échange, ou dans le changelog de votre application) d’un numéro de version, ceci afin que les utilisateurs / développeurs / admin système ne se retrouvent pas couillonnés quand vous décider de modifier un peu votre format.

Sur le long terme, comme les commentaires de code, cela va vous aider vous. Mais cela rendra aussi votre projet plus accessible et attirant pour les gens de l’extérieur, ou tout simplement vos collègues.

En écrivant cela je viens de m’apercevoir que l’on a rien fait de tout cela pour 0bin. La honte…

Prochainement, les formats binaires.

flattr this!

Vous n’avez pas besoin de l’utilisateur lambda

vendredi 5 juillet 2013 à 18:47

Cet article va à l’encontre de beaucoup de choses que l’on a dit sur le blog. Ce n’est pourtant pas une contradiction, simplement l’énoncé d’une réalité à plusieurs facettes.

D’un côté, on vous a dit et martelé d’arrêter de faire vos geeks. De vous rapprocher un peu plus des utilisateurs lambda. Parce que ce sont eux qui paient votre salaire. Parce qu’ils représentent le monde réel, pas celui de vos fantasmes nerds. Et c’est toujours vrai.

Aujourd’hui, je viens vous dire l’inverse.

Vous n’avez pas besoin d’eux.

Et c’est aussi vrai.

Internet est né et a grandi grâce à une minorité de marginaux, c’est seulement ensuite que se sont greffées toutes les Mme Michu. Il n’y a jamais eu besoin d’elles pour exister. Bien entendu, son ampleur actuelle est liée à l’arrivée du main stream.

Mais vous n’avez pas besoin de l’ampleur actuelle du réseau pour avoir un impact.

Des technologies comme Linux, PHP, ou HTML ont commencé par concerner peu de personnes, avec peu de moyens, et ont eu une influence majeure sur la Terre entière. Aujourd’hui, d’autres technos ou groupes tels que Bitcoin, Wikileak, Anonymous, Tor, etc. concernent un minorité de gens mais ont un potentiel de provoquer des ruptures dans notre société.

Tout ça pour dire qu’à votre niveau, et en visant uniquement les geeks, vous pouvez avoir un impact majeur.

D’ailleurs, même sans viser des effets de grande magnitude, ce que vous allez faire, si c’est utile à un échantillon de la population, aura des conséquences positives.

Aussi le message du jour est : ne vous souciez pas de savoir si ce que vous faites ne servira pas à monsieur tout-le-monde. OSEF si “personne n’utilise le RSS”, “le chiffrement c’est compliqué”, “il a trop à lire” ou tout ce qui peut se résumer ça “cela demande un effort pour le danseur de lambada”.

Faites le truc comme vous le sentez.

Bien sûr, si un jour vous voulez faire de l’argent, ou tout simplement vous assurer de propager les bénéfices de ce que vous faites à un panel plus large d’utilisateurs, alors il faudra simplifier, amputer, réduire…

Cependant, tout n’a pas besoin de viser le grand public. Vous pouvez faire quelque chose qui vise uniquement les geeks, maintenant, et pour toujours. Ils seront toujours là. Et ils ont besoin de choses faites pour eux, comme tout le monde.

flattr this!

Les vierges, c’est chiant

jeudi 4 juillet 2013 à 14:59

D’où vient ce fantasme des vierges ?

Quel intérêt de coucher avec une personne sans expérience ?

La plupart du temps elle va être timide, ne connait ni le corps de l’homme, ni le sien, et ne sait pas vraiment quoi faire.

Je ne dis pas que le status de vierge pose problème. On a tous été débutant.

Je ne dis pas qu’il ne faut pas encourager les débutants, et même s’envoyer en l’air avec. Il faut bien qu’ils apprennent, et on apprend toujours moins vite entre débutants.

Mais quid des mecs qui sont excités par les pucelles, sérieusement ? Je ne parle même pas des pédophiles, alors là je trouve ça aussi logique que de vouloir faire un bras de fer avec un ouistiti.

En plus, et à moins d’être un gros connard, on se tape la responsabilité de sa première fois. Autant dire qu’on s’amuse pas des masses. Surtout que souvent, c’est douloureux pour la nana. Génial ! Qu’est-ce qu’on se marre !

Je pense à ça car j’étais en train de lire un truc sur les martyrs qui vont au ciel accueilli par 72 vierges.

Arg. Tu viens de t’exploser en mille morceaux, et en plus pour te détendre t’as le choix entre 72 boulets ?

Ils auraient pas plutôt pu en mettre une seule bien coquine qui a connu 72 bites ? Ça m’aurait paru plus rentable comme deal.

Enfin, même comme ça, le sexe pour l’éternité, c’est pas fantastique comme contrat. Ok, se faire lécher les couilles, c’est sympas. Mais dans l’immense panel des expériences possibles, ça manque un chouilla de diversité pour y passer le reste de ta mort.

flattr this!

Qu’est-ce que WSGI et à quoi ça sert ?

mercredi 3 juillet 2013 à 12:36

On va dire que je me répète, mais WSGI est typiquement le cas d’une notion simple et super mal expliquée sur le Web.

C’est terrible ça, une vrai maladie. Ça me donne envie de faire des vidéos comme le joueur du Grenier pour râler à voix haute.

Bref.

Donc WSGI a le même but que FastCGI, SCGI, or AJP : c’est une norme qui sert à définir comment un serveur Python et son application peuvent discuter. Ça été pondu pour que tous les serveurs et toutes les applications Python qui respectent la norme soient garantis de pouvoir s’interfacer.

Ça, c’est la définition que vous voyez partout. Super, mais concrètement ça veut dire quoi ?

WSGI, c’est juste une convention de discussion

Un bon dessin vaut mieux…

Schéma du fonctionnement de WSGI

C'est compliqué de faire un schéma comme ça sérieux ? Ah ça pond une norme de 30 pages mais ça peut pas faire 3 ronds dans paint.

D’un côté vous avez des serveurs comme ceux de cherrypy, de paste, de werkzeug ou comme la commande runserver de Django, ou tels que les serveurs gunicorn, Apache (avec mod_wsgi). Ces logiciels ont pour boulot d’accueillir une requête HTTP et de renvoyer la réponse. Ils ont des problématiques comme : gérer les sockets, gérer plusieurs processus en parallèle, éviter qu’un pirate se balade sur votre serveur, etc.

De l’autre côté vous avez votre application. 9 fois sur 10, votre site Web, donc votre code. Sa problématique c’est de recevoir une requête, la comprendre, et générer une réponse en tapant dans la base de données, et en faisant sa popotte HTML.

Il faut donc que votre serveur dialogue avec votre app, qu’il lui passe la requête, que votre app la gère, retourne la réponse, et file la réponse au serveur. Alors le serveur envoie la réponse au navigateur.

C’est la chose la plus mal expliquée : il y a deux parties à WSGI. Une pour le serveur, et une pour l’application.

Un serveur est dit “compatible WSGI” quand il est capable de transmettre une requête HTTP normale à votre application via le protocole WSGI, et qu’il est capable de récupérer une réponse HTTP depuis votre application via le protocole WSGI pour en faire une requête HTTP normale.

Une application est dite “compatible WSGI” quand elle est capable de recevoir une requête HTTP transformée en un objet Python selon la norme WSGI, et qu’elle retourne une réponse HTTP sous la forme d’un objet Python selon la norme WSGI.

J’avais demandé concrètement, show me the fucking code !

Côté serveur WSGI, on doit lui passer le point d’entrée de votre app.

Le point d’entrée est un module Python qui contient une variable nommé application.

C’est tout.

La variable application doit contenir votre application compatible WSGI.

Une application compatible WSGI a un certain nombre de méthodes pour accepter en paramètres un objet requête HTTP, et retourner un objet réponse HTTP, mais la bonne nouvelle, c’est que vous n’avez absolument pas besoin de savoir comment ça marche.

En effet, tous les frameworks compatibles WSGI (bottle, django, cherrypy, etc) ont une fonction qui retourne cette application toute faite.

Par exemple, avec bottle, mon fichier wsgi va contenir :

from site import app
 
application = app

C’est tout. app, le décorateur qui permet de faire @app.route('/') dans le code du site bottle, est déjà une application WSGI.

Pour Django, ça va donner un truc comme :

import os
# ont dit quel est le module de settings Django
os.environ["DJANGO_SETTINGS_MODULE"] = "project.settings"
 
# et on fabrique l'application WSGI avec une fonction
# que Django nous donne
from django.core.wsgi import get_wsgi_application
 
application = get_wsgi_application()

Le simple fait qu’il y ait une variable nommée application fait que le serveur va se débrouiller. Tout ce qu’il y a à faire, c’est passer en paramètre ce module au serveur, et comment le faire dépend du serveur.

Par exemple pour gunicorn, c’est le premier paramètre de la commande qu’on utilise pour lancer le serveur :

gunicorn nom_du_module

Il faut que le module soit importable (on peut passer l’option --pythonpath pour s’en assurer.)

Pour Apache et mod_wsgi, ça se fait dans le fichier de config apache.conf:

WSGIScriptAlias / /chemin/version/module.wsgi

Bref, faire dialoguer un serveur et une application WSGI, c’est con comme la lune :

  1. Faire un module qui contient une variable nommée application contenant l’app WSGI.
  2. Dire au serveur où se trouve le fichier

Avantages et inconvénients de WSGI

Parmi les avantages, on retrouve :

Quant aux inconvénients :

Pour la culture

Voici à quoi ressemble un hello world en WSGI :

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    yield 'Hello World\n'

Et oui, c’est très simple, et il n’y a rien à importer. La norme WSGI est une convention : il doit y avoir une variable nommée application (ici la variable c’est le nom de la fonction), et cette variable doit contenir une application WSGI.

Mais une application WSGI, c’est juste un callable (ici une fonction) qui accepte en paramètres environ (la requête), start_response (une fonction qui écrit l’en-tête de la réponse) et qui retourne un générateur de string. C’est tout.

Il n’y a rien de magique. C’est une simple interface sur laquelle tout le monde s’est mis d’accord.

Quand on fait en Django:

application = get_wsgi_application()

En fait Django ne fait que fabriquer une fonction comme on vient de voir, qui derrière appelle votre application Django.

En revanche, comme on utilise souvent Django derrière nginx, le setup ressemble très souvent à ça :

Schém d'utilistation de WSGI avec Nginx

Quand on voit "socket", ça fait peur, mais c'est juste une ligne de config qui point généralement vers localhost:8000.

flattr this!

Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 8.

mardi 2 juillet 2013 à 09:31

8ème et dernier chapitre sur la programmation orientée objet en Python. Nous allons voir l’ultime réalité, le secret cosmique de nirvana Pythonique, le truc dont personne ne se sert avant d’avoir au moins codé 3 moteurs de blogs, un bot twitter et une IA de real doll.

J’ai nommé…

Les métaclasses.

Comme tous les articles sur la question, je commence avec la citation standard :

Les métaclasses sont une magie trop obscure pour que 99% des utilisateurs s’en préoccupe. Si vous vous demandez si vous en avez besoin, ce n’est pas le cas (les gens qui en ont vraiment besoin le savent avec certitude et n’ont pas besoin d’explications sur la raison).

Tim Peters, les mec qui a écrit le PEP20.

En résumé, cet article ne vous servira probablement pas. Je me suis servi, en 10 ans, deux fois des métaclasses dans ma vie. C’est pour le sport, quoi.

Pré-requis:

Musique.

Au début, il y avait les objets

Et Dieu Guido vit que cela était bon, et qu’il n’y avait pas de raison de ne pas se marrer un bon coup, alors il décida que le classes, qui servaient à fabriquer les objets, seraient aussi des objets.

Dans la plupart des langages, une classe est vraiment juste un plan pour produire un objet, un bout de code, une syntaxe élaborée pour dire “voici ma classe”. On peut l’imaginer comme cela en Python également, et vivre très heureux et avoir beaucoup de bons moments :

>>> class CreateurDObject(object):
...     pass
...
>>> mon_objet = CreateurDObject()
>>> print(mon_objet)
<__main__.CreateurDObject object at 0x1c22fd0>

Sauf qu’en Python, les classes sont aussi des objets.

Je vous laisse maturer ça 1 minute. Prenez une inspiration.

Quand l’interpréteur Python lit le mot class, il crée un objet. Dans le code ci-dessus, en mémoire, Python crée un objet CreateurDObject.

Cet objet est une classe, il permet de créer d’autres objets – ses instances – mais ça reste un objet. Et comme tous les objets…

On peut l’assigner à une variable :

>>> ReferenceACreateurDObjet = CreateurDObject
>>> ReferenceACreateurDObjet()
<__main__.CreateurDObject object at 0x1c320d0>

On peut le passer en paramètre :

>>> def afficher_une_class(cls):
...     print("Hey, on m'a passé la classe %s" % cls)
...
>>> afficher_une_class(CreateurDObject)
Hey, on m'a passé la classe <class '__main__.CreateurDObject'>

On peut lui ajouter des attributs à la volée :

>>> CreateurDObject.nouvel_attribut = 'nouvelle valeur'
>>> hasattr(CreateurDObject, 'nouvel_attribut')
True
>>> CreateurDObject.nouvel_attribut
u'nouvelle valeur'

Le 7eme jour, on se faisait grave chier alors on a créé des classes dynamiquement

Peut être vous souvenez-vous qu’on peut créer des fonctions à la volée ?

Parce qu’on peut faire pareil avec les classes :

>>> def fabriquer_une_class(nom):
...     if nom == 'bulbizarre':
...         class Bulbizarre(object):
...             pass
...         return Bulbizarre # je retourne Bulbizarre, PAS Bulbizarre()
...     else:
...         class Salameche(object): # qui prenait carapuce, sérieux ?
...             pass
...         return Salameche
>>> Pokemon = fabriquer_une_class('autre')
>>> type(Pokemon) # Pokemon est une CLASSE, pas une instance
<type 'type'>
>>> nouveau_pokemon = Pokemon() # la je créer une instance
>>> type(nouveau_pokemon)
<class '__main__.Salameche'>

Une classe n’est pas quelque chose de figé dans le marbre, comme tout le reste en Python, on peut les fabriquer dynamiquement, les modifier en cours de route, les malmener, etc.

Aucun pokemon n’a cependant été blessé pendant la rédaction de cet article. Tiens je me demande si il y a des sites zoophiles spécialisés dans les Pokemons. Qu’est-ce que je raconte ? Évidement qu’il y en a. D’ailleurs Pikachu est comme une shocking flech torch avec piles incluses quand on y pense. Ou un appareil à abdos. Il faut que je fasse des abdos, j’ai pris du bide. Heu… où j’en étais ?

A oui. Classes. Dynamiques.

Comme toutes les opérations courantes idées à la con, Python vous permet de faire ça complètement à la main. Maintenant vient la troisième Révélation Des Métaclasses : la fonction type() a deux usages.

Dans une première vie type() est une fonction ordinaire, elle sert à retourner le type d’un objet :

>>> print type(1)
<type 'int'>
>>> print type("1")
<type 'str'>
>>> print type(CreateurDObject)
<type 'type'>
>>> print type(CreateurDObject())
<class '__main__.CreateurDObject'>

Et propose à sa logeuse de descendre ses poubelles. Mais elle a une autre vie électronique, elle est aussi capable de créer une classe.

Oui c’est très con d’avoir choisit la même PUTAIN DE FONCTION pour faire deux trucs qui n’ont rien à voir. Mais les deux usages ont un avenir et il va falloir faire avec, se caler ça où je pense et oubliez votre avocat.

Bref, ça marche comme ça :

type(nom de la classe, tuple des parents de la classe, dictionnaire contenant les attributs)

Le nom de la classe, je pense que vous avez pigé.

Le tuple des parents, il peut être vide. C’est dans le cas où vous souhaitez un héritage : vous pouvez passer des références aux classes parentes.

Le dictionnaire est assez simple : chaque clé est un nom d’attribut, chaque valeur est une valeur d’attribut.

Exemple, cette classe :

>>> class Pokeball(object):
...     pass

Peut se créer ainsi :

>>> Pokeball = type('Pokeball', (), {})

Et s’utiliser tout pareil :

>>> print(Pokeball)
<class '__main__.Pokeball'>
>>> print(Pokeball())
<__main__.Pokeball object at 0x1c32450>

Vous aurez noté que le nom de classe passé en paramètre est le même que celui de la variable qui va recevoir la classe ainsi créée. Oui, ils peuvent être différents. Non, ça ne sert à rien.

Et je sais que certains auraient préféré MyLittlePoney, mais je fais avec ma maigre culture G. Faudrait que je tente avec des noms de Pogs.

Un petit exemple avec des attributs :

>>> class Pokeball(object):
...     couleurs = ('rouge', 'blanc')

Ce qui donne :

>>> Pokeball = type('Pokeball', (), {'couleurs': ('rouge', 'blanc')})
>>> Pokeball.couleurs
(u'rouge', u'blanc')

Étant donné que les méthodes sont des attributs…

>>> class Pokeball(object):
  ...     def attraper(self):
  ...         print("ratééééééééééééé")
  ...

Peut se traduire par :

>>> Pokeball = type('Pokeball', (), {'attraper': lambda self: print("ratéééééééééééé")})
>>> Pokeball().attraper()
ratéééééééééééé

Au passage, toute classe créée par type hérite automatiquement de object, pas besoin de le préciser.

Petite démo avec de l’héritage :

>>> class Masterball(Pokeball):
...     def attraper(self):
...         print("yeahhhhhhhhhh")
...

Se transforme en :

>>> Masterball = type('Masterball', (Pokeball,), {'attraper': lambda self: print("yeahhhhhhhhhh")})
>>> Masterball().attraper()
yeahhhhhhhhhh

Je répète les Révélations Divines :

  1. Les classes sont des objets.
  2. On peut créer les classes à la volée.
  3. type() permet de créer une classe manuellement.
  4. Keanu Reeves est un mauvais acteur.

Vous voyez où je veux en venir, là, non ?

Et le tout puissant déclara : tu créeras des classes avec des classes

Et tu n’aimeras pas ça. Alors au début tu le feras avec des fonctions parce que c’est plus facile.

Une métaclasse, c’est seulement le nom du “truc” qui fabrique une classe. C’est tout.

C’est pour ça qu’on appelle les métaclasses, les classes des classes.

Puisque les classes sont des objets ?

Vous suivez ?

Non ?

Ah.

o

En deux lignes alors :

UneClasse = MetaClass()
un_objet = UneClasse()

Mieux ?

Vous avez vu la fonction type() créer des classes ? En fait la fonction type() est une métaclasse. C’est la métaclasse dont Python se sert pour créer tous les objets classes, quand vous tapez le keyword class.

Il est facile de voir ça en regardant l’attribut __class__, qui indique quelle classe a servi à créer un objet :

>>> pokemon_capture = 149
>>> pokemon_capture.__class__
<type 'int'>
>>> nom = 'magicarp'
>>> nom.__class__
<type 'str'>
>>> def attrapez_les_presque_tous(): pass
>>> attrapez_les_presque_tous.__class__
<type 'function'>
>>> class Pokedex(object): pass
>>> gadget = Pokedex()
>>> gadget.__class__
<class '__main__.Pokedex'>

Mais quelle est le __class__ de tout __class__ ?

>>> attrapez_les_presque_tous.__class__.__class__
<type 'type'>
>>> pokemon_capture.__class__.__class__
<type 'type'>
>>> nom.__class__.__class__
<type 'type'>
>>> gadget.__class__.__class__
<type 'type'>

Normalement c’est à ce moment là que vous avez la Grande Révélation.

Faites “Ahhhhhhhhhhhhhhhhh”.

Donc la métaclasse, c’est le machin qui fabrique les classes, c’est une factory de classe. type est la métaclasse utilisée par défaut, mais vous pensez bien, chers amis, qu’on peut fabriquer ses propres métaclasses. Sinon ça serait pas marrant.

Ça marche comme ça :

class UneClasse(object):
  __metaclass__ = votre métaclasse
  [ le reste du code de la classe ]

Quand Python va voir ça, il ne va pas créer la classe immédiatement, il d’abord suivre le chaîne d’héritage pour trouver qu’elle métaclasse utiliser pour fabriquer la classe :

Il va regarder d’abord vérifier si il y a __metaclass__ de déclarée. Si ce n’est pas le cas, il va chercher dans les parents si ils ont un attribut __metaclass__. Si ce n’est pas le cas, il va utiliser type. Si Python trouve __metaclass__, alors il utilisera son contenu à la place de type.

Dans les deux cas, il va collecter toutes les informations sur la classe (le nom, les parents, les attributs), et les passer en paramètres à type ou votre métaclasse, et récupérer le résultat. Puis, seulement à ce moment là, il enregistre votre classe en mémoire.

Prenez votre temps sur ces paragraphes, c’est un peu la clé de tout le tuto.

Bon, nouvelle révélation : une métaclasse n’a pas besoin d’être une classe.

Je sais, c’est idiot d’appeler un truc “métaclass” si ça n’a pas besoin d’être une classe. Mais on parle de la fonctionnalité qui utilise type pour un truc qui n’a rien à avoir. Ça devait être le jour du beaujolais nouveau quand ils ont introduit la feature.

En fait, une métaclasse peut être n’importe quel callable qui retourne une classe, donc une fonction toute conne fait très bien l’affaire.

Le but de la métaclasse, c’est d’intercepter la création de la classe afin de la modifier, et voici ce que ça donne :

# une métaclasse DOIT avoir la même signature que type() puisque
# Python va lui passer tout ça automatiquement en paramètre
 
def prefixer(nom, parents, attributs):
    """
        On va créer une métaclasse qui prend tous les noms de méthodes,
        et les prefixe du mot "attaque". Oui ça ne sert à rien. Mais
        les usages des métaclasses qui servent à quelques choses sont
        généralement très compliqués.
    """
 
    # On crée un nouveau dictionnaire d'attributs avec les noms préfixés
    # On fait gaffe à pas modifier les __méthodes_magique__.
    nouveaux_attributs = {}
    for nom, val in attributs.items():
        if not nom.startswith('__'):
            nouveaux_attributs['attaque_' + nom] = val
        else:
            nouveaux_attributs[nom] = val
 
    # On délègue la création de la classe à type() :
    return type(nom, parents, nouveaux_attributs)

L’utilisation est toute simple, on écrit une classe normale, et on lui rajoute __metaclass__:

>>> class Ronflex(object):
 
    __metaclass__ = prefixer
 
    def armure(self):
        print("defense +15")
 
    def dodo(self):
        print("zzzzzz")
>>> r = Ronflex()
>>> r.attaque_armure()
defense +15
>>> r.attaque_dodo()
zzzzzz
>>> r.dodo()
Traceback (most recent call last):
  File "<ipython-input-58-90dd54234d5d>", line 1, in <module>
    r.dodo()
AttributeError: 'armure' object has no attribute 'dodo'

Voilà, c’est à peut prêt tout ce qu’il y a à comprendre des métaclasses :

  1. On intercepte la création classe.
  2. On modifie les paramètres.
  3. On retourne la classe customisée.

Maintenant, la grande question : à quoi ça sert ?

Généralement, ça sert à faire de jolies APIs. Par exemple, les ORM (peewee, SQLAlchemy, l’ORM de Django) utilisent les métaclasses pour avoir un style déclaratif :

class Article(Model):
    titre = model.CharField()

Ici, Model va contenir une métaclasse, comme c’est un parent, la métaclasse sera aussi appelée pour Article, et elle peut donc agir pour modifier titre à la volée.

En effet, quand vous faites :

>>> art = Article.get(id=13)
>>> art.titre
u'Python pour les méca-scriptophiles'

Avec un ORM, vous récupérez la valeur de titre dans la base de données et non un CharField(). C’est le but : cacher des requêtes SQL et donner l’impression de manipuler des objets.

Ici le rôle de la métaclasse, c’est de prendre tous les attributs de types xxxField(), et modifier la classe pour qu’accéder à l’attribut fasse la requête voulue à la base de données.

On peut aussi utiliser les métaclasses pour faire des vérifications : s’assurer que la classe n’utilise pas un nom que vous voulez réserver, ou qu’elle contient bien un attribut.

Il y a même des implémentations d’interfaces (ou plutôt de l’équivalent des classes abstraites en Java) pour Python utilisant les métaclasses. Je ne suis pas convaincus par leur utilité, mais c’est possible.

Soyez prophètes

C’est pas le tout, mais maintenant que vous avez compris, il est temps de limer les bords pour que vous alliez porter la bonne parole.

Donc déjà, bon à savoir : metaclass est un paramètre de classe en Python 3. Ca donne ça:

class DePython3(metaclass=votre_metaclasse)

Ensuite, il y des conventions de nommage. Tout comme on nomme self le premier paramètre des méthodes, et *args / **kwargs les paramètres dynamiques, les paramètres des métaclasses ont des noms conventionels. La métaclasse précédente s’écrirait donc plus proprement :

def prefixer(name, bases, dct):
    nouveaux_attributs = {}
    for nom, val in dct.items():
        if not nom.startswith('__'):
            nouveaux_attributs['attaque_' + nom] = val
        else:
            nouveaux_attributs[nom] = val
 
    # On délègue la création de la classe à type() :
    return type(name, bases, nouveaux_attributs)

Enfin, souvenez-vous que les métaclasses ne fonctionnent qu’avec les New Style classes, donc celles qui héritent de object.

Par ailleurs, vous croisez peut être des fois __metaclass__ en plein milieu d’un module (pas dans une classe donc). C’est une vieille façon de faire, qui affecte toutes les classes du module. Ce n’est plus recommandé.

Terminons sur une version de métaclasse qui fait la même chose que prefixer(), mais sous forme de classe. Parce que sinon à quoi ça sert que ça s’appelle une métaclasse, hein ?

# Oui, on peut hériter de type(), car c'est une classe, enfin une métaclasse.
# Mais pas sous forme de fonction. Sous forme de classe. Mais son nom
# est en minuscule, comme celui des classes int() et str(). Qui ne sont
# pas des métaclasses. Souvenez-vous : Beaujolais.
class Prefixer(type):
 
    # __new__ est le vrai constructeur en Python, et il retourne l'instance
    # en cours d'une classe. Or, qu'elle est l'instance d'une métaclasse ?
    # Une classe !
    def __new__(cls, name, bases, dct):
 
        nouveaux_attributs = {}
        for nom, val in dct.items():
            if not nom.startswith('__'):
                nouveaux_attributs['attaque_' + nom] = val
            else:
                nouveaux_attributs[nom] = val
 
        # On délègue la création de la classe à son parent.
        # Notez que cls doit être passé de bout en bout, ce qui n'est pas
        # le cas d'habitude avec 'self'
        return super(Prefixer, cls).__new__(cls, name, bases, nouveaux_attributs)

Et ça s’utilise pareil

>>> class Ronflex(object):
    __metaclass__ = Prefixer
    def armure(self):
        print("defense +15")
    def dodo(self):
        print("zzzzzz")
...
>>> r = Ronflex()
>>> r.attaque_dodo()
zzzzzz
>>> r.attaque_armure()
defense +15
>>> r.dodo()
Traceback (most recent call last):
  File "<ipython-input-66-90dd54234d5d>", line 1, in <module>
    r.dodo()
AttributeError: 'Ronflex' object has no attribute 'dodo'

Pourquoi utiliser une classe plutôt qu’une fonction alors que c’est vraiment plus compliqué ?

Et bien l’avantage d’utiliser une classe est qu’elle peut avoir des attributs et hériter. Car bien entendu, on peut avoir des metaclasses, qui héritent de metaclasses, et ainsi de suite, jusqu’à ce que du Lisp paraisse avoir du sens en comparaison.

On peut même faire plus vicieux et utiliser __call__ au lieu de __new__:

# on hérite plus de type, rien à foutre
class Prefixer(object):
 
    def __init__(self, prefix='attaque'):
        self.prefix = prefix
 
    # __call__ est la méthode appelée automatiquement quand on ajoute () après
    # un nom d'objet. Ca permet de rendre un objet callable. En gros, ça nous
    # permet d'utiliser une instance de Prefixer() comme une fonction
    def __call__(self, name, bases, dct):
 
        nouveaux_attributs = {}
        for nom, val in dct.items():
            if not nom.startswith('__'):
                nouveaux_attributs[self.prefix + nom] = val
            else:
                nouveaux_attributs[nom] = val
 
        return type(name, bases, nouveaux_attributs)

Et c’est sympa car du coup on peut passer des paramètres à notre métaclasse :

>>> class Ronflex(object):
    __metaclass__ = Prefixer(prefix='tatayoyo_')
    def armure(self):
        print("defense +15")
    def dodo(self):
        print("zzzzzz")
...         
>>> r = Ronflex()
>>> r.tatayoyo_dodo()
zzzzzz

Voilà, vous avez vu le visage de Dieu en face. C’est comme ça qu’il fabrique le monde.

Ah mais attendez ? Si type() est la métaclasse de tous les objets ? Qu’est-ce qui et la métaclasse de type() ?

>>> type.__class__
<type 'type'>

Je vous laisse méditer sur cette découverte métaphysique.

flattr this!