PROJET AUTOBLOG


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

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

⇐ retour index

WAMP et les outils de dev Web Python existants

mercredi 2 juillet 2014 à 07:21

Même si on peut créer un site Web en utilisant uniquement des libs WAMP, tout comme on peut le faire en utilisant uniquement flask ou tornado, il arrive immanquablement le moment où on veut mélanger, intégrer, faire cohabiter les techos ensemble. D’abord parce qu’il y a des sites existants, et qu’on va pas les jeter à la poubelle. Ensuite parce que ce sont des techos qu’on connaît et pour lesquelles ils y a beaucoup d’outils, que l’on veut mettre à profit.

C’est tout naturellement qu’on a fini par me poser (par mail), ze question :

Subject: crossabar et autobahn

Message Body:
Ha bah oui vous l’avez cherché à nous parler de truc comme ça: ça interroge !

Je m’interroge donc sur la manière d’intégrer WAMP à django. Pour la perstistence des données (l’ORM qui simplifie la création des tables tout de même), pour l’authentication et l’authorization, pour la robustesse et versatilité apportée par django…

J’ai pour habitude de mettre pas mal de logique dans mes modèles et je me demandais si il n’y aurait pas moyen de pluguer WAMP dans ceux ci… exposer une méthode update en RPC avec passage de JSON ? Avec namespace automatique à partir de la classe ?

En fait remplacer: Angular+tastypie+django+nginx par Angular+WAMP+django+crossbar avec du coup le bonus de WAMP pour le pubsub que n’a pas AJAX.

Comment vous verriez un (petit – après relecture ça parait dur) mixin WAMP pour des modèles django auto-détectés, auto-exposés en RPC ?

J’ai du mal à voir quelle tactique utiliser. J’ai peur que ça finisse en refaire tout le travail que fait déjà (très bien) tastypie/RESTframework.

Comment modulariser (là ou est sensé briller WAMP) les services déjà offerts par django: celery, authentication, etc ?
Ces services sont tous très dépendant du système de persistance des données (check des users, permissions, query) et donc l’approche plus monolithique de django n’est pas mauvaise car tout est lié et donc facilement manipulable au même endroit… où est le gain de WAMP dans ce cas, pour une application assez classique en fait ?

Merci encore pour cette découverte dans tous les cas. C’est top.

Du coup je me relis et ça part un peu dans tous les sens… désolé.

Comme je me suis fendu d’une réponse bien longue, je vais la paster verbatim :

Bonjour,

C’est une très bonne question.

D’abord, il faut savoir qu’on ne peut pas faire tourner un routeur WAMP dans un process Django (ou tout autre app WSGI) car Django est synchrone. En plus, l’ORM de django est bloquant, donc même sans utiliser django, utiliser son ORM au sein de WAMP va bloquer la boucle d’événements et on perdra tout l’interêt d’avoir une techno temps réel.

(Note a posteriori : y a surement un truc à faire avec gevent ou des threads ici, mais je sais pas encore quoi)

Ici on a donc 3 problèmes à résoudre :

- comment faire communiquer django et son app WAMP ?
- comment utiliser un ORM bloquant avec WAMP ?
- comment auto générer une API WAMP ?

Ces 3 questions n’ont pas encore de réponse définitive puisque, comme je l’ai précisé, WAMP est une techno jeune, et donc il y a beaucoup à faire. Mes articles sont précisément là pour tenter de générer un enthousiasme et pousser les gens à améliorer les outils autour de WAMP.

Prenons les problèmes un par un :

Comment faire communiquer django et son app WAMP ?
====================================

C’est le problème le plus facile à mon sens. Il faut coder une app WAMP qui fasse le bridge entre HTTP et WAMP. Quand on register côté app HTTP, on fait un post sur l’app WAMP (qui écoute aussi sur HTTP du coup) en fournissant une URL de callback. L’app WAMP fait le register, et quand on l’appelle, elle fait l’appel à l’app HTTP via l’url de callback, et retourne le résultat. On peut faire ça pour register, subscribe, call et publish, c’est le même principe.

(Note a posteriori : en me relisant moi-même je m’aperçois à quel point c’est pas clair. De toute façon il faudra que je le code un jour où l’autre, et avec un bon tuto pratique, la pilule passera mieux).

Ce faisant, on pourra appeler du WAMP côté app HTTP, et taper dans l’app HTTP côté client WAMP.

Une amorce de travail a été fait pour coder un tel bridge. Pour le moment il n’y a que le publish :

https://groups.google.com/forum/#!searchin/autobahnws/http/autobahnws/SbobAnoWVlQ/FnGhdYXj9aIJ

Ce n’est pas très dur à coder, c’est juste un boulot chiant à faire.

Cela dit, ça ne résout pas le problème de l’authentification, qu’il faudra à un moment on un autre, se poser. Je pense qu’on va se diriger vers une authentification hybride, qui va utiliser le session ID en cookie, mais l’envoyer via un token. Encore un truc à travailler.

De même, on voudra sûrement créer quelques facilités pour intégrer ça dans les frameworks les plus connus en proposant une app prêt à plugger. Rien d’insurmontable donc, mais pas mal de taff.

Par contre, pour ce qui est des tasks queues, à mon avis une solution de task queue WAMP sera bien plus intéressante qu’une solution type celery car on peut envoyer des messages WAMP depuis les tâches et donc avertir en temps réel de l’avancement du process. Je voterais donc pour coder soi-même une alternative.

Comment utiliser un ORM bloquant avec WAMP ?
===============================

Idéalement, il faudrait avoir des ORM non bloquant, mais on Python, on en a pas. On a quelques drivers non bloquant, notamment pour PostGres et Mongo, mais pas d’ORM, et ils demandent une forme de compilation d’extension C.

C’est là qu’on voit qu’on se traine la culture de l’API synchrone en Python, car côté NodeJS, ils commencent à avoir pas mal de solutions.

En l’occurrence, on a 3 solutions :

- utiliser le bridge dont je viens de parler pour garder les appels dans l’app HTTP. Ca veut dire que quand on veut faire un appel à de la base de données, ça fait WAMP => HTTP => connexion à la base, aller-retour. C’est pas idéal.
- créer une app WAMP pour héberger les appels bloquants et taper dedans en RPC. Une bonne solution à mon avis. Mais assez peu intuitive.
- faire tous les appels dans un threads à part. Le plus simple. Un peu verbeux par contre.

Dans les deux derniers cas, on à quand même le problème des querysets qui sont lazy, notamment au niveau des foreign keys. Il faudra faire particulièrement attention à ne pas accidentellement faire des appels bloquant, par exemple dans le rendu du template. Une solution viable est de créer un wrapper qui fait le rendu du template dans un threads.

Bref, encore pas mal d’outils à developper.

On peut aussi se lancer dans l’écriture d’un ORM non bloquant. Une bonne année de travail avant d’avoir quelque chose qui soit compétitif.

Comment auto générer une API WAMP ?
==========================

Là tu m’en poses une bonne.

C’est la suite logique, évidement, mais je n’avais jamais réfléchi aussi loin. C’est un taff énorme, surtout que ça dépend de l’outil derrière. La solution la plus simple c’est encore de faire un mapper dans le bridge HTTP-WAMP qui va traduire directement un appel WAMP en un appel JSON vers l’API générée par django-rest-framework ou autre.

Mais bon, je suis pas certains de la valeur ajoutée.

Je pense qu’il est difficile pour moi de répondre à cette question pour le moment car :

- je ne suis pas certain que WAMP soit un bon remplacement pour les API REST. Je pense plutôt que c’est un complément.
- il y a toute la question de l’authentification. Encore et toujours.
- il va falloir pas mal d’essais avec plusieurs architectures en prod (séparées, mixtes, mono culture…) pour pouvoir déterminer ce qui rend le mieux.

Mon intuition est qu’on utilise généralement 10% de l’API générée par les frameworks, et que la partie dont on a besoin à peut très bien se faire à la main. La raison pour laquelle les trucs comme django-rest-framework sont si pratiques, c’est qu’ils gèrent des problématiques comme l’authentification, la sérialisation et la pagination.

Je serais plutôt d’avis de s’attaquer à ça pour WAMP, et je pense qu’on s’apercevra que finalement, pour ses propres besoins, un API complète est overkill. Par contre, pour exposer une API au monde, c’est une autre histoire. J’ai eu récemment une discussion à propos de faire des APIs WAMP :) Il y a des possibilités fascinantes. Mais c’est peut être encore un peu loin tout ça.

Je pense que je vais publier cette réponse sur le blog, car tu soulèves des points très importants.

flattr this!

Pourquoi self en Python ?

mercredi 2 juillet 2014 à 06:44

Quand on écrit une méthode dans une classe en Python, vous êtes obligé de faire ceci :

class UneClasse:
   #              ?
   #              |
   #              v
   def __init__(self):
      self.attribut = 'value'
 
   #                ?
   #                |
   #                v
   def une_methode(self):
      print(self.attribut)

Vous êtes tenu de déclarer self, le premier paramètre, qui sera l’instance en cours.

Cela étonne, parfois irrite. Pourquoi dois-je me taper ce self ?

D’abord, petite clarification : le nom self n’est qu’une convention. Le premier paramètre de toutes les méthodes est une instance, soit, mais il n’a pas de nom obligatoire.

Ce code marche parfaitement :

class UneClasse:
 
   def __init__(tachatte):
      tachatte.attribut = 'value'
 
   def une_methode(tachatte):
      print(tachatte.attribut)

Il ne passera probablement pas une code review, mais il est valide.

Il ne passera pas une code review, non pas parce que tachatte n’est pas un nom de variable politiquement correcte – après tout ces mignonnes boules de poils ne sont-elles pas aimées par tous ? – mais parce que self est une convention forte. Tellement forte que les éditeurs de code la prennent en compte.

Mais je suppose que la plus grosse interrogation, c’est pourquoi on se tape le self à la main, et pas :

Il y a de nombreuses raisons.

D’abord, rien comme le C++ ne permettrait pas, en Python, de distinguer une variable locale d’une variable d’un scope supérieur, rendant la lecture difficile. La philosophie de Python étant qu’on lit un code 100 fois plus qu’on l’écrit et qu’il faut donc faciliter la lecture plutôt que l’écriture, cela n’a pas été retenu.

@ comme en Ruby suppose 3 notations. @ pour les variables d’instance, @@ pour les variables de classe, et self pour l’instance en cours (avec un usage aussi pour définir les méthodes de classe car les classes sont des instances, mais je trouve ça super bordélique). Ça introduit beaucoup de mécanismes supplémentaires pour utiliser quelque chose qui existe déjà, et comme en la philosophie de Python c’est qu’il ne devrait y avoir qu’un seul moyen, de préférence évident, de faire quelques chose, utiliser juste une référence aux classes et aux instances a été choisi.

Pour le JS, et son binding de merde, je vais passer mon tour, sinon je vais encore m’énerver.

Reste donc la solution de PHP, Java, etc., une référence explicite this, mais automatiquement présente dans le scope de la méthode.

La réponse courte, est encore une fois philosophique. En Python, on préfère l’explicite plutôt que l’implicite.

Si vous avez ce code :

class UneClasse:
 
   def __init__(self):
      self.attribut = 'value'
 
   def une_methode(self):
      print(self.attribut)

Et que vous faites :

instance = UneClasse()
instance.une_methode()

En réalité vous faites sans le savoir :

instance = UneClasse()
UneClasse.une_methode(instance)

L’interpréteur fait la conversion pour vous (il y a derrière une notion de bound/unbound, mais c’est un autre sujet).

A l’appel de la “méthode”, instance est visiblement présente, c’est assez explicite, et plus court que la version traduite par l’interpréteur. Donc Python vous aide avec cette traduction. Mais au niveau de la déclaration de la méthode, il n’y a pas de mention explicite de la référence à la variable d’instance, donc Guido a choisi, comme en Modula-3, de rendre le passage explicite.

Ce comportement a tout un tas de conséquences forts pratiques.

Python a en effet une fonctionnalité que PHP et Java n’ont pas : l’héritage multiple. Dans ce contexte, le passage explicite du self permet de facilement choisir l’appel de la méthode d’un parent, sans faire appel à des mécanismes supplémentaires (C++ ajoute par exemple un opérateur pour ça):

class Clerc:
   heal = 50
   def soigner(self):
      return self.heal * 2
 
class Paladin:
   heal = 60
   def soigner(self):
      return self.heal * 1.5 + 30
 
class BiClasse(Clerc, Paladin):
   heal = 55
   def soigner(self):
      # Hop, j’appelle les parents distinctement
      # et fastochement en prenant la méthode
      # au niveau de la classe, et en lui passant
      # manuellement l'instance.
      soin_clerc = Clerc.soigner(self)
      soin_palouf = Paladin.soigner(self)
      return (soin_clerc + soin_palouf) / 2

Mais, et peu de gens le savent, il permet aussi de faire de la composition beaucoup plus fine, en ignorant complètement l’héritage.

On peut notamment créer des algo globaux, et ensuite les attacher à des objets:

 
# Une fonction moyenne qui fonctionne de manière générique
# et utilisable normalement. Imaginez que cela puisse être
# un algo complexe. On veut pouvoir l'utiliser hors du
# cadre d'objet.
def moyenne(sequence):
   """ Calcule la moyenne d'une séquence.
 
       Les décimales sont tronquées
   """
   notes = list(sequence)
   return sum(notes) / len(notes)
 
 
class DossierEleve:
 
   # Intégration de l'algo de la fonction "moyenne"
   # à notre dossier, sans se faire chier à faire
   # un héritage. Comme 'self' est passé en premier
   # paramètre, 'sequence' contiendra 'self'. Comme
   # plus bas on rend le dossier itérable, tout va
   # marcher.
   moyenne = moyenne
 
   def __init__(self):
      self.notes = []
 
   # on rend le dossier itérable
   def __iter__(self):
      return iter(self.notes)
 
   # on donne une taille au dossier
   def __len__(self):
      return len(self.notes)
 
# On peut l'intégrer à plusieurs classes.
class CarnetDeClasse:
 
   moyenne = moyenne
 
   def __init__(self):
      self.notes = []
 
   def __iter__(self):
      return iter(self.notes)
 
   def __len__(self):
      return len(self.notes)
 
c = CarnetDeClasse()
c.notes = [12, 14, 13, 15]
print(c.moyenne())
## 13
e = DossierEleve()
e.notes = [9, 8, 17, 1]
print(e.moyenne())
## 8

Vous allez me dire, pourquoi ne pas faire moyenne(eleve) dans ces cas ? Parce que ce code supposerait connaitre de l’implémentation d’élève. Alors que eleve.moyenne() utilise le code encapsulé, sans avoir à s’en soucier. Si le code change (ce qui arrive dans des cas plus complexes qu’une moyenne), pas besoin de changer son API.

Vous me direz, si on avait pas le self explicite, on pourrait faire ça :

class DossierEleve:
   def moyenne(self):
      return moyenne(self)

Mais :

En Python 3, ça va même plus loin. Ce self explicite permet également de partager du code entre objets, sans utiliser l’héritage.

Imaginez un autre scénario, où vous importez une lib gestion_classe.py, qui n’est pas votre code. Vous savez que son algo de calcul de moyenne est très complexe, mais très rapide, et efficace, et vous voulez en bénéficier. Seulement, il est encapsulé dans la classe CarnetDeClasse, et en faire hériter un profil d’élève d’un carnet de classe n’a absolument aucun sens.

Dans gestion_classe.py :

class CarnetDeClasse:
 
   def __init__(self):
      self.notes = []
 
   def __iter__(self):
      return iter(self.notes)
 
   def __len__(self):
      return len(self.notes)
 
   def moyenne(self):
      notes = list(self)
      return sum(notes) // len(notes)

Et dans votre code :

 
from gestion_classe import CarnetDeClasse
 
class DossierEleve:
 
   moyenne = CarnetDeClasse.moyenne
 
   def __init__(self):
      self.notes = []
 
   # on rend le dossier itérable
   def __iter__(self):
      return iter(self.notes)
 
   # on donne une taille au dossier
   def __len__(self):
      return len(self.notes)
 
e = DossierEleve()
e.notes = [9, 8, 17, 1]
print(e.moyenne())
## 8

flattr this!

Rendre un élément scrollable avec Angular

mardi 1 juillet 2014 à 11:30

Petit snippet que j’utilise dans mes apps angular. Ça permet de définir un comportement quand l’utilisateur scrolle au-dessus d’un élément. Typiquement, augmenter la valeur d’un champ, faire défiler un carousel, etc. Il faut, bien entendu, éviter que la page scrolle elle-même.

Implémentation

app.directive('wheelable', function() {
"use strict";
 
  /* On définit sur quels attributs on va mettre les callbacks */
  var directive = {
      scope: {
          'onWheelUp': '&onwheelup',
          'onWheelDown': '&onwheeldown'
      }
  };
 
  /* On limite la directive aux attributs */
  directive.restrict = 'A';
 
  /* Le code qu'active la directive quand on la pose sur l'élément */
  directive.link = function($scope, element, attributes) {
 
      /* On attache un callback à tous les événements de scrolling */
      element.bind('mousewheel wheel', function(e) {
 
        /* On vérifie si l'utilisateur scroll up ou down */
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
        var isScrollingUp = (e.detail || delta > 0);
 
        /* On appelle le bon callback utilisateur */
        if (isScrollingUp){
          $scope.$apply($scope.onWheelUp());
        } else {
          $scope.$apply($scope.onWheelDown());
        }
 
        /* On évite que la page scrolle */
        e.preventDefault();
      });
  };
 
  return directive;
});

Usage

Comme pour toutes les directives qui impliquent des callbacks, il faut définir des fonctions et les attacher à votre scope dans un controleur (ou un service attaché au controleur) :

app.controller('FooCtrl', function($scope) {
"use strict";
  $scope.votreCallBackPourQuandCaScrollDown = function(){
    // faire un truc par exemple moi je l'utilise pour changer
    // la valeur de l'élément.
  };
  $scope.votreCallBackPourQuandCaScrollDown = function(){
    // faire un autre truc
  };
});

La directive s’utilise en mettant l’attribut wheelable sur l’élément qu’on veut rendre scrollable. Ensuite on déclare dans les attributs onwheeldown et onwheelup le code à exécuter, et zou :

<div ng-controller="FooCtrl">
  ...
  <input type="text" wheelable
         onwheeldown="votreCallBackPourQuandCaScrollDown()"
         onwheelup="votreCallBackPourQuandCaScrollUp()"
         >
  ...
</div>

flattr this!

Aller plus loin avec les hash maps en Python

lundi 30 juin 2014 à 05:28

Les hash map sont souvent sous-utilisés, surtout par les personnes venant d’un autre langage avec implémentation vraiment batarde du concept. Les arrays en PHP et les objets en Javascript étant parmi les pires exemples.

Le point d’entrée pour les hash maps en Python, c’est le dictionnaire. Et la plupart des gens ont pigé le principe de l’association clé / valeur :

>>> d = {}
>>> d['cle'] = 'valeur'
>>> d['cle']
'valeur'
>>> d['pas cle']
Traceback (most recent call last):
  File "<ipython-input-12-eed7cf6f5344>", line 1, in <module>
    d['pas cle']
KeyError: 'pas cle'

L’intérêt du dictionnaire étant qu’accéder à une clé est très rapide (c’est une opération O(1)), tout comme vérifier qu’une clé est présente dans le dico :

>>> 'cle' in d
True

Mais généralement les gens s’arrêtent là.

Itération

Parfois, ils vont plus loin, et tentent l’itération dessus :

>>> scores = {"Joe": 1, "Jonh": 5, "Jack": 3, "Jenny": 7, "Jeanne": 0, "July": 3}
>>> for score in scores:
    print(score)
...
Jenny
Jack
Joe
July
Jonh
Jeanne

Ils s’aperçoivent qu’on peut uniquement récupérer les clés, et essayent de faire ça :

>>> for nom in scores:
    print(nom, scores[nom])
...
Jenny 7
Jack 3
Joe 1
July 3
Jonh 5
Jeanne 0

Rapidement ils sont corrigés par quelques collègues qui leur expliquent qu’on peut faire ceci :

>>> for nom, score in scores.items():
    print(nom, score)
...
Jenny 7
Jack 3
Joe 1
July 3
Jonh 5
Jeanne 0

Sans vraiment expliquer pourquoi. Si vous êtes curieux, cela marche grâce à l’unpacking.

Ensuite ils vont chercher à afficher des choses dans l’ordre, mais un dictionnaire n’est pas ordonné. Là commencent les embrouilles : dans l’ordre des clés, des valeurs ou dans l’ordre d’insertion ?

Dans l’ordre des clés ou des valeurs, il faut se taper le tri à chaque fois :

>>> for nom, score in sorted(scores.items()):
    print(nom, score)
...
Jack 3
Jeanne 0
Jenny 7
Joe 1
Jonh 5
July 3
>>> for nom, score in sorted(scores.items(), key=lambda x: x[1]):
    print(nom, score)
...
Jeanne 0
Joe 1
Jack 3
July 3
Jonh 5
Jenny 7

Dans l’ordre d’insertion par contre, ce n’est pas possible avec le dictionnaire. Mais voilà l’astuce : le hash map en Python, ce n’est pas QUE le type dict.

Pour ce problème, on peut utiliser collections.OrderedDict :

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['Jeanne'] = 3
>>> d['Jack'] = 2
>>> d['July'] = 6
>>> for nom, score in d.items():
        print(nom, score)
...
Jeanne 3
Jack 2
July 6

Après il y a le rare problème, mais tout de même existant, de la très très grosse structure de données que l’on veut itérer dans l’ordre de clés :

>>> import random
>>> l = range(10000000)
>>> random.shuffle(l)

Si on fait un sort dessus, ça prend plusieurs secondes :

>>> l.sort()

Imaginez avec un dico qui contient un million de clés sous forme de texte. La lecture dans l’ordre sera très, très lente. Parfois ce n’est pas grave, et parfois c’est très emmerdant.

La stdlib de Python ne permet pas de répondre à ce problème facilement. On pourrait bricoler quelque chose avec heapq, mais franchement, c’est se casser la tête pour rien.

Le plus simple est d’utiliser une lib externe, par exemple l’excellente sorted_container, qui en plus d’être très rapide, est en pur Python. Du coup, un peu de pip :

pip install sorted_container

Et on est bon.

>>> from sortedcontainers import SortedDict
>>> d = SortedDict()
>>> d['Joe'] = 1
>>> d['Jeanne'] = 6
>>> d['July'] = 3
>>> d['John'] = 3
>>> for nom, score in d.items():
    print(nom, score)
...
Jeanne 6
Joe 1
John 3
July 3

SortedDict s’assure que le dictionnaire reste ordonné à chaque insertion d’un élément, et ainsi, vous évite de devoir faire un tri tout à la fin.

Initialisation

La plupart du temps, on utilise la notation littérale. Mais le constructeur dict trouve son utilité dans le fait qu’il accepte un itérable de tuples en paramètre :

>>> dict([("a", 1), ("b", 2)])
{'a': 1, 'b': 2}

La plupart du temps, les gens n’en voient pas l’utilité. Mais il faut se rappeler que tout le langage Python est organisé autour de l’itération. Je ne cesse de le répéter, en Python, l’itération est tout.

De fait, cette particularité du constructeur du dico vous permet de créer des dictionnaires à partir de structures existantes inattendues…

Prendre deux séquences et les pairer :

>>> personnes = ('Joe', 'John', 'Jean-Michel')
>>> scores = (4, 10, 34)
>>> zip(personnes, scores)
[('Joe', 4), ('John', 10), ('Jean-michel', 34)]
>>> dict(zip(personnes, scores))
{'Jean-michel': 34, 'John': 10, 'Joe': 4}

Pairer les deux derniers champs du résultat d’une commande :

>>> import subprocess
>>> df = subprocess.check_output('df')
>>> print(df)
Sys. de fichiers       blocks de 1K  Utilisé Disponible Uti% Monté sur
/dev/sda7                   7972000  6614840     929156  88% /
none                              4        0          4   0% /sys/fs/cgroup
udev                        1968688        4    1968684   1% /dev
tmpfs                        395896     1112     394784   1% /run
none                           5120        0       5120   0% /run/lock
none                        1979472      160    1979312   1% /run/shm
none                         102400       44     102356   1% /run/user
/dev/sda5                  65438480 57693436    4397852  93% /media/sam/
>>> dict(l.split()[-2:] for l in  list(df.split('\n'))[1:] if l)
{'31%': '/media/truecrypt1', '1%': '/run/user', '93%': '/media/sam', '88%': '/', '0%': '/run/lock'}

Depuis Python 2.7, cette fonctionnalité est partiellement phagocytée par la syntaxe pour les intentions sur les dicos :

>>> from pprint import pprint
>>> pprint( {line: num for num, line in enumerate(open('/etc/fstab'), 1)})
{'#\n': 6,
 '# / was on /dev/sda7 during installation\n': 8,
 '# /etc/fstab: static file system information.\n': 1,
 '# <file system> <mount point>   <type>  <options>       <dump>  <pass>\n': 7,
 "# Use 'blkid' to print the universally unique identifier for a\n": 3,
 '# device; this may be used with UUID= as a more robust way to name devices\n': 4,
 '# swap was on /dev/sda6 during installation\n': 10,
 '# that works even if disks are added and removed. See fstab(5).\n': 5,
 'UUID=4c0455fb-ff57-466a-8d1f-22b575129f4f none            swap    sw              0       0\n': 11,
 'UUID=4f560031-1058-4eb6-a51e-b7991dfc6db7 /               ext4    errors=remount-ro 0       1\n': 9,
 'UUID=b27f7e93-60c0-4efa-bfae-5ac21a8f4e3c /media/sam ext4 auto,user,rw,exec 0 0\n': 12}

Cela dit, on n’a pas toujours besoin de clés ET de valeurs pour créer un dictionnaire. Ainsi, si on a une liste de n’clés qu’on veut toutes initialiser à la même valeur, la très peu connue méthode fromkeys nous rendra bien service :

>>> personnes = ('Joe', 'John', 'Jean-michel')
>>> dict.fromkeys(personnes, 0)
{'Jean-michel': 0, 'John': 0, 'Joe': 0}

De même, on peut ne pas vouloir initialiser un dico, mais vouloir une valeur par défaut pour toutes les clés. collections.defaultdict est fait pour ça. En plus, les valeurs peuvent être dynamiques :

>>> from collections import defaultdict
>>> scores = defaultdict(lambda: 0)
>>> scores['Joe']
0
>>> scores['Joe'] = 1
>>> scores['Joe']
1
>>> scores['July']
0
>>> import datetime
>>> naissances = defaultdict(datetime.datetime.utcnow)
>>> naissances['Joe']
datetime.datetime(2014, 6, 29, 6, 58, 11, 412202)

Enfin, je sais que tous les tutos du monde en Python utilisent le dictionnaire pour montrer une forme ou une aute de compteur. Mais si vous avez VRAIMENT besoin d’un compteur, utilisez collections.Counter qui est un objet avec l’interface d’un dictionnaire mais avec tout ce qu’il faut pour compter :

>>> from collections import Counter
>>> c = Counter('abbbac') # comptage automatique
>>> c
Counter({'b': 3, 'a': 2, 'c': 1})
>>> c['c']
1
>>> c['d'] # pas de KeyError
0
>>> c['z'] += 1 # pas de KeyError
>>> c['z']
>>> c.most_common(2) # et en bonus
[('b', 3), ('a', 2)]

Clé en main

Récupérer une clé si on ne sait pas si elle est présente est une opération courante, et la documentation montre généralement ça :

try:
   val = dico['cle']
except KeyError:
   val = 'valeur par defaut'

Bien que ce soit parfaitement valide, c’est généralement se faire chier pour rien puisqu’on peut faire ça en une ligne :

   val = dico.get('cle', 'valeur par defaut')

Néanmoins la méthode get() est très connue. Moins connue est la méthode setdefault. En effet, parfois on veut faire plutôt ceci :

try:
   val = dico['cle']
except KeyError:
   dico['cle'] = 'valeur par defaut'
   val = 'valeur par defaut'

Et ça peut également se faire en une ligne :

   val = dico.setdefault('cle', valeur par defaut)

J’aimerais aussi en profiter pour rappeler que les clés des dicos peuvent être n’importe quel objet hashable, pas juste une string ou un int. Notamment, les tuples sont des clés valides, et comme l’opérateur tuple est la virgule et non la parenthèse, cette syntaxe est parfaitement valide :

>>> d = {}
>>> d[1, 2] = 'tresor'
>>> d[3, 3] = 'mine'
>>> d
{(1, 2): 'tresor', (3, 3): 'mine'}
>>> d[3, 3]
'mine'

Parmi les objets utilisables comme clés :

Si vous avez un doute, il est facile de savoir si un objet est hashable ou pas :

>>> import collections
>>> isinstance({}, collections.Hashable)
False
>> isinstance(0, collections.Hashable)
True

Mon dico à moi, c’est le meilleur

On peut tout à fait hériter du type dictionnaire pour obtenir un type qui a des fonctionnalités que le type original n’a pas :

>>> class MonDico(dict):
...     def __add__(self, other):
...         new = {}
...         new.update(self)
...         new.update(other)
...         return new
...
>>> d1 = MonDico(a=1, b=2)
>>> d2 = MonDico(b=3, c=3)
>>> d1 + d2
{'a': 1, 'c': 3, 'b': 3}

Mais c’est assez rare. La plupart du temps on veut plutôt rajouter des fonctionnalités de conteneur à un type existant. Dans ce cas, les méthodes magiques viennent à la rescousse. Par exemple :

class Phrase(object):
 
   def __init__(self, string):
      self.words = string.split()
 
   def __getitem__(self, word):
      return [i for i, w in enumerate(self.words) if w == word]
 
>>> p = Phrase("Une petite puce pique plus qu'une grosse puce ne pique")
>>> p['petite']
[1]
>>> p['puce']
[2, 7]

Hey oui, les hash maps en Python, c’est un sujet qui peut aller très, très loin. C’est ce qui est merveilleux avec ce langage, on peut rapidement programmer en effleurant juste la surface, sans se noyer. Et si on a besoin d’aller plus loin, des profondeurs abyssales de features nous attendent.

flattr this!

Annuler les derniers commits avec Git

dimanche 29 juin 2014 à 10:59

(Amélioration de ce dont je parle ici)

Use case typique : on a merdé les derniers commits, et on veut oublier tout ce qu’on a fait et retourner à l’état d’il y a x commits précédents.

Par exemple, là je veux revenir à mon commit 85711ad... :

commit 093bab5aa9d41f580037d51421b7c5d0db73e2ce
Author: sam 
Date:   Sat Jun 28 09:37:55 2014 +0700

    Ok, c'est la merde internationale

commit 0b768f1c0e37a6141e6cd4c472eb3f369f4334d7
Author: sam 
Date:   Sat Jun 28 09:37:36 2014 +0700

    Je commence à merder

commit 85711ad1e8c54f3fd3048d405addef48921e90fd
Author: sam 
Date:   Sat Jun 28 09:37:20 2014 +0700

    J'adore ce commit

Il y a plein de manières de faire, et on voit sur la toile beaucoup de solutions à base de checkout et de reset. La plupart sont dangereuses ou ont un résultat inattendu, présuppose un état de votre repo ou va vous mettre dans une situation que vous ne maîtrisez pas.

Devinez quoi ? Il y a plus simple, et plus propre.

Etape 1: avoir une copie de travail propre

Avant d’inverser des commits, assurez vous que votre copie de travail est nette. Pas de fichiers modifiés en attente d’être commités. Le moins de fichiers non trackés par git possible (idéalement zéro, soit c’est commité, soit c’est dans le .gitignore).

Si vous avez des fichiers modifiés, vous pouvez soit les mettre de côté temporairement avec git stash, soit annuler toutes les modifications avec git reset --hard HEAD. Attention, cette dernière commande n’est pas réversible et va mettre à plat votre copie de travail pour qu’elle soit l’exacte copie du dernier commit de votre histo.

Etape 2

???

Etape 3: profit !

git revert --no-commit 85711ad1..HEAD

Ceci va modifier la copie de travail (donc les fichiers que vous avez sur le disque dur en direct, pas l’histo git) en appliquant des patchs qui contiennent les différences entre HEAD et le commit avec ce hash.

En clair : vos fichiers vont être dans l’état dans lequel ils étaient à ce commit. En prime, l’index est mis à jour.

Vous pouvez alors faire les derniers ajustements que vous le souhaitez. Il faut ensuite finaliser la procédure par un commit avec un message significatif :

git commit -m "Abort ! Abort ! Inversion des 2 derniers commits, retour à 85711a"

Si vous aviez fait un stash, c’est le moment de faire un stash apply derrière.

Maintenant, si vous matez l’histo, vous verrez qu’on n’a pas effacé les commits précédents, on a juste fait un commit qui inverse tout ce qu’ils avaient fait :

commit 03e55de36ad29a26a461874988d4066ebf6fe6be
Author: sam 
Date:   Sat Jun 28 09:43:32 2014 +0700

    Abort ! Abort ! Inversion des 2 derniers commits, retour à 85711a

commit 093bab5aa9d41f580037d51421b7c5d0db73e2ce
Author: sam 
Date:   Sat Jun 28 09:37:55 2014 +0700

    Ok, c'est la merde internationale

commit 0b768f1c0e37a6141e6cd4c472eb3f369f4334d7
Author: sam 
Date:   Sat Jun 28 09:37:36 2014 +0700

    Je commence à merder

commit 85711ad1e8c54f3fd3048d405addef48921e90fd
Author: sam 
Date:   Sat Jun 28 09:37:20 2014 +0700

    J'adore ce commit

Ce qui évite bien des problèmes : pas de réécriture de l’histo, possibilité de récupérer du code dans les commits inversés plus tard, claire indication de ce qui s’est passé…

N’oubliez pas que souvent, revenir à un commit précédent est overkill. Il est généralement beaucoup plus simple de juste récupérer un ou deux fichiers dans l’état de l’époque avec :

git checkout [hash] -- chemin/vers/fichier

flattr this!