Batbelt, la lib des petits outils Python qui vont bien


A force de coder plein de projets, il y a des opérations qui reviennent très souvent. Ces traitements sont petits et complètement sans relation, difficile d’en faire quelque chose. J’ai tout de même finit par en faire un lib, batbelt, qui au final n’est qu’une grosse collections de snippets que j’utilise régulièrement. Il y a aussi des trucs que j’utilise moins ou des astuces / hacks un peu crades, c’est toujours pratique pour geeker à l’arrache vite fait. Vous allez d’ailleurs retrouver des bouts de code dont j’ai déjà parlé sur le site

pip install batbelt

Et la plupart des fonctions sont accessible avec un from batbelt import...

Voici les choses qui pourraient vous intéresser le plus dans batbelt…

To timestamp

Mais combien de fois j’ai du la coder celle-là ? En plus l’inverse existe, alors pourquoi, mon Dieu, pourquoi ?

>>> from datetime import datetime
>>> to_timestamp(datetime(2000, 1, 1, 2, 1, 1))
946692061
>>> datetime.fromtimestamp(946688461) # tu as codé celle là et pas l'autre connard !
datetime.datetime(2000, 1, 1, 2, 1, 1)

Récupérer une valeur dans une structure de données imbriquée

Au lieu de faire :

try:
    res = data['cle'][0]['autre cle'][1]
except (KeyError, IndexError):
    res = "valeur"

On peut faire :

get(data, 'cle', 0, 'autre cle', 1, default="valeur")

Récupérer la valeur d’un attribut dans un attribut dans un attribut…

Pareil, mais pour les attributs.

try:
    devise = voiture.moteur.prix.devise
except AttributeError:
    devise = "euro"

On peut faire :

devise = attr(voiture, 'moteur', 'prix', 'devise', default='euro')

Itérons, mon bon

Ces fonctions retournent des générateurs qui permettent d’itérer par morceau ou par fenêtre glissante.

>>> for chunk in chunks(l, 3):
...     print list(chunk)
...
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
>>> for slide in window(l, 3):
...     print list(slide)
...
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5, 6]
[5, 6, 7]
[6, 7, 8]
[7, 8, 9]

Ça devrait être en standard dans Python.

Parfois on veut juste le premier élément d’une collection. Ou juste le premier à être vrai:

>>> first(xrange(10))
0
>>> first_true(xrange(10))
1

Marche avec n’importe quel itérable, contrairement à [0] qui ne marche que sur les indexables. Et en prime on peut spécifier une valeur par défaut:

>>> first([], default="What the one thing we say to the God of Death ?")
'What the one thing we say to the God of Death ?'

Set ordonné

On a des dicts ordonnés dans la lib standard, mais pas de set ordonné. On en a pas besoin souvent, mais ça peut être TRES pratique, et TRES chiant à implémenter soi-même.

Donc acte.

>>> for x in set((3, 2, 2, 2, 1, 2)): # booooooo
...     print x
...
1
2
3
>>> for x in sset((3, 2, 2, 2, 1, 2)): # clap clap !
...     print x
...
3
2
1

Attention, c’est pas la structure de données la plus rapide du monde…

Je suis une feignasse et j’aime les one-liners sur les dicos

Je ne comprends pas pourquoi + ne fonctionne pas sur les dico.

>>> dmerge({"a": 1, "b": 2}, {"b": 2, "c": 3})
{'a': 1, 'c': 3, 'b': 2}

Ne modifie pas les dictionnaires originaux.

>>> from batbelt.structs import rename
>>> rename({"a": 1, "b": 2})
>>> rename({"a": 1, "b": 2}, 'b', 'z')
{u'a': 1, u'z': 2}

Modifie le dictionnaire original et n’est PAS thread safe.

Et le cas tordu mais tellement satisfaisant :

>>> from batbelt.structs import unpack
>>> dct = {'a': 2, 'b': 4, 'z': 42}
>>> a, b, c = unpack(dct, 'a', 'b', 'c', default=1)
>>> a
2
>>> b
4
>>> c
1

Slugifier

>>> slugify(u"Hélo Whorde")
helo-whorde

Il y a pas mal de réglages possibles avec slugify(), mais je vous laisse les découvrir :-) Cette fonction fait partie du sous-module strings, qui contient d’autres utilitaires comme escape_html/unescape_html (qui transforme les caractères spéciaux en HTML entities et inversement) ou json_dumps/json_loads (qui fait un dump / load du JSON en prenant en compte le type datetime).

Importer une classe ou une fonction depuis une string

Dès que vous faites un fichier de config vous avez besoin de ce genre de truc, mais la fonction __import__ a une signature uber-zarb. Voici une version beaucoup plus simple:

TaClasse = import_from_path('foo.bar.TaClasse')
ton_obj = TaClasse()

Capturer les prints

Parfois on a une lib qui print plutôt que de retourner une valeur. C’est très chiant. J’ai donc fait un context manager qui permet de récupérer tout ce qui est printé dans le block du with.

>>> with capture_ouput() as (stdout, stderr):
...    print "hello",
...
>>> print stdout.read()
hello

Créer un décorateur qui accepte des arguments

Même dans le cas où vous avez parfaitement compris les décorateurs grâce à un très bon tuto (^^), se souvenir de comment faire un décorateur qui attend des arguments en paramètre, c’est mission impossible. Voici donc un décorateur… pour créer un décorateur.

Étape un, écrire votre décorateur :

# tout les arguments après 'func' sont ceux que votre décorateur acceptera
@decorator_with_args()
def votre_decorateur(func, arg1, arg2=None):
 
    if arg1:
        # faire un truc
 
    # ici on fait juste le truc habituel des décorateurs
    # wrapper, appel de la fonction wrappée et retour du wrapper...
    def wrapper():
        # arg2 est dans une closure, on peut donc l'utiliser dans
        # la fonction appelée
        return func(arg2)
 
 
    return wrapper

Et on peut utiliser son décorateur tranquile-bilou :

@votre_decorateur(False, 1)
def hop(un_arg):
    # faire un truc dans la fonction décorée

Les processus parallèles finissent toujours par se rencontrer à l’infini et corrompre leurs données

Mais en attendant on en a quand même besoin. Parfois un petit worker, c’est sympa, pas besoin de faire compliqué et de sortir des libs de task queue complètes:

 
from batbelt.parallel import worker
 
@worker()
def une_tache(arg):
    # faire un truc avec arg
    arg = arg + 10
    return arg
 
 
# on demarre le worker
process = une_tache.start()
 
# on balance des tâches au worker
for x in range(10):
    process.put(x)
 
# on récupère les résultats (optionnel)
# ca peut être dans un fichier différent
for x in range(10):
    print process.get()
 
## 10
## 11
## 12
## 13
## 14
## 15
## 16
## 17
## 18
## 19
 
# on arrête le worker
process.stop()

Le worker est un subprocess par défaut, mais vous pouvez en faire un thread avec @worker(method=”tread”). Toujours utile, par exemple pour avec un processeur de mails qui envoit tous les mails hors du cycle requête / réponse de votre site Web. Par contre si votre process meurt la queue est perdue.

Template du pauvre

Avec format(), on a déjà un mini-langage de template intégré. Pas de boucle, mais pour des tâches simples ça suffit. Du coup j’ai une fonction render() qui prend un fichier de template au format string Python et qui écrit le résultat dans un autre. Pratique pour faire des fichiers de conf configurable.

from batbelt.strings import render
 
render('truc.conf.tpl', {"var": "value"}, "/etc/truc.conf")

Il y a aussi des implémentations de Singleton, du Null Pattern, etc. Mais ça s’utilise moins souvent alors je vais pas faire une tartine.

15 thoughts on “Batbelt, la lib des petits outils Python qui vont bien

  • kontre

    Y’a des astuces sympa. J’aime bien l’implémentation de first() par exemple, il fallait y penser.

    J’ai un commentaire, ça ne serait pas plus simple pour l’utilisateur de rajouter des arguments avec des valeurs par défaut (genre unidecode=True/False et ascii_only=True/False) plutôt que d’avoir 3 fonctions slugify ?

  • Etienne

    Personnellement, je trouve get, attr “syntactiquement étranges” (si j’ose dire). Tous les paramètres sont sur le même plan: la structure de données, les éléments du “chemin” dans la structure et la valeur par défault.

    Un truc comment ça me paraît déjà moins étrange:

    chemin = ['cle', 0, 'autre cle', 1]
    get(data, chemin , default="valeur")

    idem pour unpack: je passerais un tuple contenant les clés à unpacker.

  • Etienne

    Je ne comprends pas pourquoi + ne fonctionne pas sur les dico.

    Je suppose que t’as cherché pourquoi. En ce qui me concerne, je trouve cette raison convaincante:

    what would be the result of {“a”: 1, “b”: 2} + {“a”: 2, “b”: 1}?
    Should it be {“a”: 1, “b”: 2} or {“a”: 2, “b”: 1}?

    or {“a”:[1,2], “b”:[1,2]}

    As I remember, Guido rejected because of this ambiguity.

  • Sam Post author

    @kontre:

    Il n’y a que vraiment qu’une fonction slugify, et elle s’appelle “slugify()”.

    Elle est très facile à utiliser car un help dessus te montre qu’elle n’a que deux arguments, et tu sais ce que tu peux faire avec sans regarder la doc de plus prêt.

    Les 3 sous fonctions slugify ne sont pas là pour l’usage courant.

    unicodedata_slugify et unidecode_slugify ne seront normalement jamais appelée directement. La meilleure est automatiquement aliasée comme “slugify()” selon l’existence de la lib unidecode ou non, et si elle existe, il n’y a pas de raison de ne pas l’utiliser. Au cas où, l’implémentation sans unidecode est laissée utilisable, mais mettre une condition n’a pas de sens pour l’utilisateur sauf exception improbable.

    Reste unicode_slugify, qui est aussi un cas particulier. On voudra rarement l’utiliser, donc je ne voulais pas ajouter une argument pour ça : les utilisateurs regarderait la signature et se demanderait “à quoi ça sert” ? J’ai donc choisi de simplifier au max l’utilisation de l’API le plus usité, et j’ai planqué dans les source les cas particuliers. C’est très important pour l’appropriation d’un API.

    @Etienne:

    tu peux:

    chemin = ['cle', 0, 'autre cle', 1]
    get(data, *chemin , default="valeur")

    C’est la beauté de Python.

    Quand au plus du dictionnaire, on a déjà dict.update() qui a un comportement bien défini. Pourquoi le critique s’applique à + et pas à update(). Et si update est parfaitement ok, alors il n’y a pas de raison que les gens ne s’attendent pas à la même chose avec plus. Franchement, tu t’attends à autre chose toi ? C’est le résultat qui me parait le plus naturel, et je ne vois pas comment ça peut introduire un bug. Additionner des dicos arrive pas souvent le jour où le mec en a besoin, il va de toute façon faire un test dans le shell et voir ce que ça donne.

  • Etienne

    @Sam
    J’y avais jamais réfléchi jusque il y a 10 minutes, mais il me semble que .update() exprime bien ce qui se passe: mise à jour du dictionnaire depuis un autre dictionnaire: celui qui met à jour a la priorité sur celui qui est mis à jour. Comme je le comprend, les clés existantes sont mise à jour et créées si elles n’existent pas.

    Dans le cas du + tu ajoutes quelque chose. D’ailleurs, dans le cas d’une liste, le + se traduit par .extend(), qui est plus proche de quelque chose comme la concaténation que de la mise à jour.

    Sinon:

    chemin = ['cle', 0, 'autre cle', 1]
    get(data, *chemin , default="valeur")

    Oui, évidemment, mais ce que je disais concerne la signature de la fonction. Mais bon, c’est un détail.

  • kontre

    @Sam OK, ça se comprend même si perso j’aime moins. Des goûts et des couleurs…

    Par contre ils sont où les from __future__ import ... ? Allez, python3 et qu’ça saute !

  • Etienne

    PL = ? (Parfaitement Loufoques? Petits Loulous? Parfois Lourds? Premiers Largués? Pisses Lent? …)

  • Sam Post author

    Pull request. Quand tu fork un repo git, tu peux proposer à l’autre de merger ton code avec un pull de sa part, cette requête est très facilitée sous Github car on peut le faire en un clic.

  • glickind

    PL ?
    j’ai bien pensé à picolitre, mais avec ca on va pas picoler grand’chose ;)

  • kontre

    Uhu, j’ai survolé le bug du timestamp, ça bashe bien ! On dirait que l’opensource manque parfois de décideurs. Mais que fait BDFL ?

    J’avais deviné que tu parlais de pull request, mais pourquoi PL et pas PR ? Je ferai peut-être quelques trucs pour le fun (et pour apprendre, j’ai encore jamais fait de pull request), mais comme je ne pense pas que j’utiliserai votre lib (ça ne correspond pas à ce que je fais en ce moment) ça limite la motivation…

    Je l’ai d’ailleurs pas encore dit, mais c’est cool de partager du code comme ça !

    Question : il sort d’où le nom de la lib ?

  • Sam Post author

    Ahahahaha. Je voulais dire PR. Je sais pas pourquoi j’ai dis PL, en insistant en plus. Je suis un boulet.

    Le nom de la lib: Bat belt. Nananananananna !

  • G-rom

    Gaffe au timestamp, je me suis fait avoir une fois.

    >>> datetime.fromtimestamp(0)
    datetime.datetime(1970, 1, 1, 1, 0)
    >>> datetime.utcfromtimestamp(0)
    datetime.datetime(1970, 1, 1, 0, 0)

    Oui les dates ça fait toujours chier ><

    De la même manière j'utilise plutôt

    calendar.timegm(dt.utctimetuple())

    Pour convertir en timestamp, j’ai déjà eu des cas foireux à cause du tz.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.