PROJET AUTOBLOG


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

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

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Revue de code publique 17

mardi 7 avril 2015 à 10:13

En faisant ma revue quotidienne, je suis tombé sur un code Python assez tarabiscoté que twitter s’est empressé de massacrer.

Néanmoins je ne pense pas que mettre l’affiche à l’auteur soit le meilleur moyen de l’amener à passer plus de temps à coder en Python et à participer à la communauté.

En effet, il me semble qu’il est prof de bio (si j’ai bien suivi l’histo), et personnellement, si on devait me demander de faire un schéma d’une méiose, je pense que le résultat serait du même niveau.

D’un autre côté laisser circuler une code dans ce format, surtout si il a une vocation pédagogique serait contre productif. Améliorons donc ce code pas à pas en justifiant des modifications.

D’abord, voici la fonction en question :

def tripositions(Ligne): #Tri a bulle ?
    l = [k for k in range(len(Ligne))]
    q = [Ligne, l]
 
    for i in range(len(Ligne)-1):
        for j in range(len(Ligne)-i-1):
            if q[0][j]>q[0][j+1]:
                for k in range(2):
                    q[k][j], q[k][j+1]=q[k][j+1], q[k][j]
 
    while q[0][0]==0:
        q=[[q[i][j] for j in range(1, len(q[0]))] for i in range(2)]
 
    return(q)

Elle effectue un bubble sort (un tri des éléments en faisant remonter les plus grands en fin de liste, comme des bulles), puis retire les zéros, et retourne deux ensembles : une liste des éléments triés, et une liste de la position des ces éléments dans la liste originale. Par exemple:

>>> tripositions([4, 0, 9, 0,  5, 17, 7])
[[4, 5, 7, 9, 17], [0, 4, 6, 2, 5]]

D’abord, essayons de comprendre le fonctionnement de l’algo :

def tripositions(Ligne): #Tri a bulle ?
 
    # Ici on récupère une liste [0, 1, 2, 3...] qui contient les positions
    # des éléments dans la liste passée en paramètre
    l = [k for k in range(len(Ligne))]
    # Là on groupe la liste initiale et les positions en une liste
    q = [Ligne, l]
 
    # On fait un tour de boucle pour chaque élément de la liste
    for i in range(len(Ligne)-1):
        # Et à chaque tout de boucle, on refait une boucle qui fait elle-même
        # un tour de boucle pour tous les éléments de la liste, sauf le dernier
        for j in range(len(Ligne)-i-1):
            # Si l'élément de la liste à la position actuelle est plus grand
            # que l'élément de la liste à la prochaine position
            if q[0][j]>q[0][j+1]:
                # On change la place de cet élément dans la liste initiale,
                # la place de la position de cet élément dans la liste des
                # positions
                for k in range(2):
                    q[k][j], q[k][j+1]=q[k][j+1], q[k][j]
 
    # Maintenant que tout est trié, si il y a des zéro, il sont tous en début
    # de liste. Tant qu'il y a un zéro en début de liste, on retire le premier
    # élément de la liste initiale et de la liste des positions.
    while q[0][0]==0:
        q=[[q[i][j] for j in range(1, len(q[0]))] for i in range(2)]
 
    return(q)

Le style d’écriture en Python étant codifié par un document appelé le PEP8, reformatons le code afin qu’il soit plus facile à lire :

# Les noms des variables ne commencent pas par une majuscule. On réservera
# les majuscules aux noms de classe.
# On donne aussi un nom un peu plus explicite à la fonction
def elements_tries_et_positions(ligne):
    """ Tri a bulle d'une liste retournant les valeurs triés et leurs positions"""
    # On met une doctstring pour documenter la fonction plutôt qu'un commentaire
    # On pourra ainsi appeler help(elements_tries_et_positions)
 
    l = [k for k in range(len(ligne))]
    q = [ligne, l]
 
    # On aère un peu le code en rajoutant des espaces autour des signes = et <
    for i in range(len(ligne)-1):
        for j in range(len(ligne)-i-1):
            if q[0][j] > q[0][j+1]:
                for k in range(2):
                    q[k][j], q[k][j+1] = q[k][j+1], q[k][j]
 
    while q[0][0] == 0:
        q = [[q[i][j] for j in range(1, len(q[0]))] for i in range(2)]
 
    # "return" est un mot clé, pas une fonction, et n'a pas besoin de
    # parenthèses
    return q

Le code utilise massivement des ranges() et len(), ce qui me laisse à penser que notre auteur a voulu reproduire un schéma qu’il a connu lors d’une expérience dans un autre langage tel que le C ou le Fortran. C’est très courant quand on passe à une nouvelle technologie, car on essaye de trouver ses repères.

Premier passage pour retirer quelques appels qui ne sont pas idiomatiques :

def elements_tries_et_positions(ligne):
    """ Tri a bulle d'une liste retournant les valeurs triés et leurs positions"""
    # range() va déjà retourner un itérable contenant les valeurs voulues donc
    # pas besoin de faire une liste en intension dessus
    q = [ligne, range(len(ligne))]
 
    # Il n'y a pas d'avantage de performance à itérer sur range() plutôt que
    # sur "ligne" directement : c'est toujours une itération 
    # de même taille et seul le nombre de tours nous intéresse
    # puisqu'on utilise pas l'index dans notre code
    for e1 in ligne:
        # enumerate() retourne automatiquement un itérable [(index, element),
        # (index, element), ...], et permet de se passer du calcule manuel
        # de la position des éléments
        # ligne[:-1] est ce qu'on appelle un slicing, qui nous permet de
        # créer une copie de la liste sans le dernier élément.
        for j, e2 in enumerate(ligne[:-1]):
            # q[0] est la même chose que "ligne", donc pas besoin de faire
            # un indexing.
            if ligne[j] > ligne[j+1]:
                for k in (0, 1):
                    q[k][j], q[k][j+1] = q[k][j+1], q[k][j]
 
    # Même chose puisque q[0] == ligne
    while ligne[0] == 0:
        q = [[q[i][j] for j in range(1, len(q[0]))] for i in range(2)]
 
    return q

Le fait de manipuler la liste initiale et la liste d’indexes groupés dans une 3eme liste rend le code plus difficile à lire. Nous allons éviter ça en créant la liste finale à la fin.

def elements_tries_et_positions(ligne):
    """ Tri a bulle d'une liste retournant les valeurs triés et leurs positions"""
 
    # On supprime la création de la liste finale et on manipule
    # deux variables pendant toute la fonction ce qui rend plus clair sur
    # quelle partie des données on travaille.
    positions = range(len(ligne))
 
    for e1 in ligne:
        for j, e2 in enumerate(ligne[:-1]):
            # On a déjà e2 gratuitement, pas besoin d'utiliser un index
            if e2 > ligne[j+1]:
                # Ici, on manipules les listes par leur nom, c'est
                # plus facile à comprendre que des indexes
                ligne[j], ligne[j+1] = ligne[j+1], ligne[j]
                positions[j], positions[j+1] = positions[j+1], positions[j]
 
    # Si ligne[0] == 0, alors bool(ligne(0)) == False. Python n'a pas besoin
    # de la comparaison numérique dans une condition, juste du contexte booléen.
    # En l'écrivant ainsi, on se rapproche du langage parlé.
    while not ligne[0]:
        # Encore une fois, inutile de grouper les deux variables dans une
        # liste. Les séparer rend l'action plus lisible.
        ligne = [ligne[i] for i in range(1, len(ligne))]
        positions = [positions[i] for i in range(1, len(ligne))]
 
    # On retourne les deux variables sous forme de liste à la fin
    return [ligne, positions]

Enfin, et meme si les listes en intension sont une fonctionalité du langage qu’il faut toujours garder sous le code, ce sont des boucles et il est bon de se passer de boucles non nécessaires. A l’inverse, la compatibilité avec Python 3 va nous obliger à rajouter une boucle (implicite) de plus.

def elements_tries_et_positions(ligne):
    """ Tri a bulle d'une liste retournant les valeurs triés et leurs positions"""
 
    # range() retourne un générateur en Python 3, il faut donc le convertir
    # en liste pour que ça marche dans les 2 versions
    positions = list(range(len(ligne)))
 
    for e1 in ligne:
        for j, e2 in enumerate(ligne[:-1]):
            if e2 > ligne[j+1]:
                ligne[j], ligne[j+1] = ligne[j+1], ligne[j]
                positions[j], positions[j+1] = positions[j+1], positions[j]
 
    while not ligne[0]:
        # Il s'agit juste de retirer tous les éléments nuls de gauche, il
        # n'est pas nécessaire de parcourir toute la liste pour ça : pop(0)
        # retire l'élément en début de liste
        ligne.pop(0)
        positions.pop(0)
 
    return [ligne, positions]

Rajoutons des commentaires techniques, car en première lecture, ce n’est pas évident de savoir quel bloc fait quoi (le retrait des zéros m’a demandé un certains temps avant que je le pige) :

def elements_tries_et_positions(ligne):
    """ Tri a bulle d'une liste retournant les valeurs triés et leurs positions"""
 
    # Liste des positions initiales des éléments
    positions = list(range(len(ligne)))
 
    # Bubble sort qui traite en parallèle les valeurs et leurs positions
    for e1 in ligne:
        for j, e2 in enumerate(ligne[:-1]):
            if e2 > ligne[j+1]:
                ligne[j], ligne[j+1] = ligne[j+1], ligne[j]
                positions[j], positions[j+1] = positions[j+1], positions[j]
 
    # Retrait des zéro (qui après le tri sont tous en début de liste)
    while not ligne[0]:
        ligne.pop(0)
        positions.pop(0)
 
    return [ligne, positions]

En tout nous avons fait plusieurs choses, qui sont presque toujours les mêmes étapes pour toute revue de code :

Python est un langage de haut niveau, qui favorise la productivité et met l’accent sur la lisibilité. Aussi il est bon de tirer parti le maximum du langage pour avoir le code le plus facile à lire possible. Ne pas hésiter à avoir des noms longs, des opérations sur deux lignes, des usages d’outils tout faits, etc.

J’imagine que le but de l’exercice était de faire coder un tri à bulle, donc j’ai modifié le code précédent. Néanmoins Python propose déjà des fonctions qui font tout ça automatiquement, et voici ce pourrait donner la fonction si on devait l’écrire en tirant parti du toute la puissance du langage :

# On met tout en anglais et on change le nom de paramètre pour montrer
# qu'on accepte n'importe quel itérable.
# On rajoute aussi deux paramètres avec des valeurs saines par défaut mais
# qui permettent d'orienter le tri différemment si on le souhaite.
def get_sorted_items_and_positions(iterable, key=lambda x: x[1], reverse=False):
    """ Sort an iterable and return a tuple (sorted_items, old_positions)"""
 
    # On récupère la liste de tous les éléments non nuls et leur position
    indexed_non_null_elements = [(i, e) for i, e in enumerate(iterable) if e]
    # On les tri avec un tim sort plutôt qu'un bubble sort
    indexed_non_null_elements.sort(key=key, reverse=reverse)
    # On sépare les indexes des élements avec une astuce de sioux
    indexes, elements = zip(*indexed_non_null_elements)
 
    # On retourne généralement un tuple quand on a peu de valeurs à retourner
    return elements, indexes

La fonction va alors retourner un tuple de tuple au lieu d’une liste de liste, mais sinon fera les mêmes opérations :

>>> get_sorted_items_and_positions([4, 0, 9, 0,  5, 17, 7])
((4, 5, 7, 9, 17), (0, 4, 6, 2, 5))

Néanmoins elle aura le double avantage de fonctionner avec n’importe quel itérable (get_sorted_items_and_positions acceptera alors en paramètre une liste, mais aussi un tuple, un set ou même un fichier) mais aussi d’être beaucoup plus rapide puisque sort() est codé en C sous le capot.

Comme ai-je pondu cette fonction ?

D’abord, j’ai observé l’algorithme, et j’ai noté que les zéros étaient retirés. Il est souvent plus facile de filtrer ses données avant le traitement, donc je retire les 0 tout au début. Python est un fantastique langage pour tout ce qui est filtre, et on peut non seulement le faire en une ligne, mais indexer les éléments restant dans la foulée.

Ensuite, j’utilise la fonction de tri intégrée au langage, qui est un algo non seulement plus efficace que le bubble sort mais en prime est codé en C par les auteurs de Python, donc qui sera beaucoup plus performant. La manière dont fonctionne le tri en Python permet de trier mes paires (position, valeur) comme un seul élément, mais en fonction de la valeur uniquement. Un autre avantage, c’est que cette fonction permet de changer le type de tri (par exemple trier du plus grand au plus petit), et donc je peux exposer ces paramètres supplémentaires dans ma fonction pour laisser plus de control à l’utilisateur. Grâce aux valeurs par défaut, ce contrôle supplémentaire ne rend pas l’usage plus complexe, puisque l’utilisateur peu juste ignorer ces paramètres.

Enfin, j’utilise une combinaison de slicing, d’unpacking (via l’opéateur splat l’opérateur splat) et une particularité de la fonction zip() qui permet de transposer un itérable de paires en deux itérables d’éléments seuls :

>>> paires = [(1, "a"), (2, "b"), (3, "c")]
>>> zip(*paires)
[(1, 2, 3), ('a', 'b', 'c')]
>>> nombres, lettres = zip(*paires)
>>> nombres
(1, 2, 3)
>>> lettres
('a', 'b', 'c')

Notez bien que personne ne demande à quelqu’un qui commence à toucher du Python de trouver ce code. C’est juste comme ça qu’on finit par le faire, à l’usage.

Comment installer des libs Python externes dans QGIS ? 5

mardi 31 mars 2015 à 11:16

QGIS vient avec son Python perso, séparé du reste du système. Du coup si vous faites un pip install une_lib, elle s’installera sur le Python du système, et non de QGIS. Si vous voulez utiliser requests ou arrow pour vos scripts, c’est relou, il faut les télécharger, les extraire, et les mettre à la main sur le PYTHONPATH.

Il est pourtant possible de manipuler interpréteur Python de QGIS, c’est juste que ça saute pas aux yeux :) Ca va nous permettre d’installer pip (si ça ne vous parle par, on a un article pour ça), à la mano, à l’ancienne, comme un homme qui fait ses cartes à la main sur des calques avec des lames de rasoirs en guise de gomme.

D’abord, trouver où il est niché. Sur ma machine, il est dans “C:\Program Files\QGIS Wien\bin\python.exe”.

Si vous le démarrer, vous noterez qu’il plante en prétextant qu’il ne trouve pas le module site. Il faut donc lui dire où sont installées les libs en faisant:

set PYTHONHOME=chemin vers l'installation de Python de QGIS.

Chez moi ça donne :

set PYTHONHOME=C:\Program Files\QGIS Wien\apps\Python27\

Ainsi vous pouvez lancer C:\Program Files\QGIS Wien\bin\python.exe dans la console et obtenir le shell Python de QGIS.

Ensuite, il faut télécharger ez_tools.py qui va nous servir à installer setuptools dans le Python voulu. C’est un script Python (meta meta ! \^O^\), qu’on lance avec l’interpréteur qui nous intéresse, dans mon cas:

"C:\Program Files\QGIS Wien\bin\python.exe" C:\Users\sam\Downloads\ez_setup.py

Il va télécharger depuis internet tout un tas de trucs, et installer tout un tas de machins. Parfois, ça va râler parce que vous n’avez pas les droits. Dans ce cas, lancez la console en mode admin, et faites un cd sur le bureau avant de lancer la commande.

Bien, on a setuptools, qui a du rajouter la commande easy_install. Chez moi elle est dans “C:\Program Files\QGIS Wien\apps\Python27\Scripts”.

On va s’en servir pour installer pip.

Que de bordel !

C:\Program Files\QGIS Wien\apps\Python27\Scripts\easy_install.exe pip

Ca remouline, ça re-dl.

Ce qui va nous mettre la comamnde pip dans le même dossier qu’easy_install.

Et pouf, en utilisant le chemin canonique, on peut utiliser pip :

C:\Program Files\QGIS Wien\apps\Python27\Scripts\pip install ipython pyrealine

Faites attention néanmoins, utiliser cet interpréteur hors de QGIS ne vous donne pas accès à des objets comme iface qui n’existent que quand l’UI de QGIS est chargée. Ca reste pratique pour installer des libs externes, faire des tests python rapides dans le même environnement que QGIS sans lancer tout le bouzin et se la péter face à ses collègues géomaticiens qui ont l’air d’une poule avec un canif devant une console.

Vrac Python (encore) 5

lundi 30 mars 2015 à 21:01

Les billets en vrac sont au blogging ce que les pâtes de fond de frigo sont à la cuisine: le dernier recours en cas de manque de ressources ou d’inspiration.

Mais bizarrement, c’est aussi quelque chose qui plait beaucoup. C’est d’ailleurs les posts que je préférais sur le standblog, ou les trucs qui énervaient sebsauvage avant qu’il shaarlise tout ça.

Bref, quelques trucs en Python qui peuvent être passés sous le radar.

Min et Max ont un param “key”

Si vous avez lu l’article sur le tri, vous savez que sorted() et sort() peuvent prendre une fonction de callback via l’argument key, permettant de choisir comment extraire l’information qui va servir à déterminer la place de chaque élément.

min et max marchent de la même manière :

>>> scores = {"allemagne": 1,"montagne": 0}
>>> scores.items()
dict_items([('allemagne', 1), ('montagne', 0)])
>>> max(scores.items())
('montagne', 0)
>>> max(scores.items(), key=lambda x: x[1])
('allemagne', 1)

next possède un second paramètre

Vous savez, sur les dicos, on peut choper une valeur par défaut si une clé n’existe pas :

>>> scores.get("allemagne")
1
>>> scores.get("kamoulox", 1j)
1j

Mais ceci n’existe pas pour les listes. Je pensais que c’était un oubli, mais en fait c’est comblé par le deuxième argument de next :

>>> l = list(range(1))
>>> l
[0]
>>> g = iter(l)
>>> next(g, 1j)
0
>>> next(g, 1j)
1j

__future__ n’est pas bullet proof

Certains comportements backportés en Python 2.7 ne peuvent pas être parfaitement implémentés. Ce sont “ce qu’on peut avoir de plus proche”. C’est déjà pas mal, et je les active tout le temps, mais ça peut vous rattraper à un moment inattendu.

Par exemple, les caractères d’échappements unicodes.

En Python 3:

>>> r"\u"
'\\u'

En Python 2.7, par contre, utiliser u et r entraine une exception :

>>> ur"\u"
  File "<stdin>", line 1
SyntaxError: (unicode error) 'rawunicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX

Et donc si on active les littéraux unicodes (ce qui est une bonne pratique) :

>>> r"\u"
  File "<stdin>", line 1
SyntaxError: (unicode error) 'rawunicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX

De quoi se gratter la tête sur des regexs ou des chemins de fichiers Windows.

Reportez, il en restera toujours quelque chose 4

samedi 28 mars 2015 à 10:54

L’année dernière j’ai reporté un bug sur nuitka, un compilateur Python.

Mon rapport était incomplet, et on m’a demandé plus d’informations. J’ai pris du temps, mais j’ai répondu.

Malheureusement à la deuxième demande d’informations, j’ai trainé la patte. Je l’ai mis dans ma todo list, et ai regardé en chien de fusil l’entrée pendant des mois.

Il fallait réinstaller nuitka, me replonger dans ce que je voulais faire à l’époque, lancer la compilation, répondre sur le bug tracker. Une heure dépensée au moins.

C’est ça dont les gens ne se rendent pas compte : participer à l’open source, ou à un quelconque effort collectif, ça prend énormément de temps. Entre les interruptions que ça génère, l’historique qu’il faut remonter à chaque fois, le changement de contexte, et bien entendu l’activité elle-même.

Donc, quand on vous rapporte un bug, même de manière énervée, bénissez le Dieu de la procrastination que la personne ait pris le temps de le faire. Vous ne vous devez rien l’un à l’autre, et c’est un beau moment de synergie humaine.

En l’occurrence, donc, mon interlocuteur a fini par marquer le bug comme “lack of feedback”.

D’un certain côté, ça m’a soulagé, je n’avais plus du tout envie de m’y remettre.

D’un autre côté, j’ai culpabilisé, me disant que quand même, j’utilise du FOSS, faudrait contribuer plus, et que j’avais fais perdre du temps à l’auteur.

Au final, cette semaine, je reçois une notification comme quoi une autre personne a continué le fil : il a le même problème.

J’avais oublié ce détail : en prenant le temps d’ouvrir un fil de discussion, je lui avais donné vie. Je lui avait donné une légitimité. Et il se trouve qu’avec les infos fournies par le nouvel arrivant, le ticket a été marqué en bug, et sera corrigé.

C’est un effort collectif, et même si je n’ai pas fais autant que ce que j’aurais voulu, j’ai joué mon rôle. Un rôle imparfait, mais utile.

Donc souvenez-vous : reportez vos bugs, il en restera toujours quelque chose.

Views VS generators 6

mercredi 25 mars 2015 à 19:57

Avec Python 2.7, un outil appelé les “views” (les “vues”) est apparu. Une vue est juste un enrobage qui permet de voir un objet d’une certaine façon, et de le manipuler d’une certaine façon (avec une autre API), sans changer cet objet.

Les vues ont surtout été notables pour leur apparition dans les dictionnaires avec Python 2.7:

    >>> scores = {"sam": 1, "max": 0}
    >>> scores.items() # retourne une lsite
    [('max', 0), ('sam', 1)]
    >>> scores.iteritems() # retourne un générateur
    <dictionary-itemiterator object at 0x7f8782a26628>
    >>> list(scores.iteritems()) # sans views
    [('max', 0), ('sam', 1)]
    >>> scores.viewsitems() # avec views
    Traceback (most recent call last):
      File "<ipython-input-12-dc0b08011047>", line 1, in <module>
        scores.viewsitems() # avec views
    AttributeError: 'dict' object has no attribute 'viewsitems'
 
    >>> scores.viewitems() # retourne une vue
    dict_items([('max', 0), ('sam', 1)])

Néanmoins personne ne les a vraiment utilisé, et c’est un tort. Elles sont en effet très performantes, et pour cette raison sont retournées par défaut avec items() en Python 3.

En effet, les vues ne sont qu’un enrobage : elles ne contiennent rien, et donc ne prennent pas beaucoup mémoire, tout comme les générateurs.

Mais contrairement aux générateurs, les vues ne se vident pas et peuvent exposer une API plus complète que les générateurs, comme par exemple déclarer une taille :

>>> items = scores.iteritems()
>>> list(items)
[('max', 0), ('sam', 1)]
>>> list(items) # woops
[]
>>> items = scores.viewitems()
>>> list(items)
[('max', 0), ('sam', 1)]
>>> list(items)
[('max', 0), ('sam', 1)]
>>> len(scores.iteritems()) # nope
Traceback (most recent call last):
  File "<ipython-input-21-9c7f250da51d>", line 1, in <module>
    len(scores.iteritems())
TypeError: object of type 'dictionary-itemiterator' has no len()
 
>>> len(scores.viewitems())
2

Alors certes, on ne peut pas mettre des vues partout, et les générateurs restent utiles. Mais quand il est possible de les utiliser, et à moins d’avoir besoin d’une liste afin de modifier les valeurs in place, il n’y pas de raison de ne pas le faire : c’est le meilleur des deux mondes.

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?74 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}