Autoblog de sametmax.comhttp://sametmax.com/http://sametmax.com/ La débâcle de async en 3.7 25http://sametmax.com/?p=24891http://streisand.me.thican.net/sametmax.com/index.php?20180807_151440_La_debacle_de_async_en_3.7_25Tue, 07 Aug 2018 13:14:40 +0000async et await ont été introduits en Python 3.5, tout le monde a trouvé l'idée formidable. D'ailleurs, ça a été intégré à JavaScript. Malheureusement, introduire des mots clés dans un langage est une opération très délicate.]]>Quand les nouveaux mots clés async et await ont été introduits en Python 3.5, tout le monde a trouvé l’idée formidable. D’ailleurs, ça a été intégré à JavaScript.

Malheureusement, introduire des mots clés dans un langage est une opération très délicate.

Limites et contournements des mots clés

En Python les mots clés ont une caractéristique importante : on ne peut pas les utiliser pour quoi que ce soit d’autre.

Par exemple, class est un mot clé, donc je ne peux pas créer une variable, un attribut, ou une fonction appelé class. Ceci lève une erreur:

>>> class = 1
  File "", line 1
    class = 1
          ^
SyntaxError: invalid syntax
>>> class Foo: pass
... 
>>> Foo.class = 1
  File "", line 1
    Foo.class = 1
            ^
SyntaxError: invalid syntax
>>> 

Pour cette raison, quand on veut qu’une variable contienne une classe en Python, on la nomme cls:

>>> class Bar:
...     @classmethod
...     def wololo(cls):
...         print(cls, 'wololo')
... 
>>> 
>>> Bar.wololo()
 wololo
>>> 

C’est aussi pour cela que vous voyez parfois des variables nommées truc_. Souvent from_ par exemple, parce que from est un mot clé.

(pro tip: plutôt que from et to, utilisez src et dest)

Quand en Python 2 on a introduit True et False, un gros problème s’ensuivit: soit on en faisait des mots clés, et on pétait tout le code précédent qui utilisait ces mots, soit on en faisait des variables globales.

Le choix a été de garder la stabilité jusqu’à la prochaine version majeure, et c’est pour cela que:

  • On peut faire True = False en Python 2. Ouch.
  • Python 3 casse ce comportement, et donc ça fait une chose de plus à laquelle il faut penser quand on migre.

Pour la 3.5, on avait donc ce même problème, avec une cerise sur le gâteau: la lib standard utilisait elle-même la fonction asyncio.async.

Le choix a donc de faire de async / await des variables globales, et de les transformer en mot clé en 3.7.

En 3.6, un warning a été ajouté pour rappeler aux gens de migrer leur code.

C’est un sacré taf, et ça comporte des risques comme nous allons le voir plus loin. C’est pour cette raison que l’ajout d’un mot clé dans Python est une des choses les plus difficiles à faire passer sur la mailling list python-idea.

Arrive la 3.7

La 3.7 est sortie avec tout un tas de goodies. Youpi. Mais aussi avec le passage de async/await de variables globales à mots clés, cassant la compatibilité ascendante. Quelque chose de rare en Python, et que personnellement j’aurais réservé pour Python 4, ne serait-ce que pour respecter semver.

Le résultat, tout un tas de systèmes ont pété: des linux en rolling release, des gens qui ont fait l’update de Python à la main, des gens qui maintiennent des libs compatibles 3.5 a 3.7…

D’autant que la 3.5 a asyncio.async, mais 3.7 considère ça une erreur.

Petit exemple avec l’impact sur debian.

Comment on aurait pu éviter ce merdier ?

D’abord, il aurait fallu ne pas introduire asyncio à l’arrache. Dans mon “au revoir” à Guido, je disais que je trouvais que les dernières fonctionnalités majeures de Python avaient été mises en oeuvre de manière précipitée.

Cela se vérifie encore et encore avec asyncio, dont il faudra que je fasse un article pour dire tout ce qui a mal tourné.

Casser la compatibilité ascendante dans une version mineure n’est pas acceptable, même si les dégâts sont limités et qu’on y survivra très bien.

Le fait qu’asyncio soit une API marquée comme “provisional” n’a jamais empêché quelqu’un d’appeler ses variables async. Après tout on utilise les threads depuis bien longtemps.

L’autre problème vient de l’amateurisme qui se glisse de plus en plus dans le dev.

C’est une bonne chose, parce que ça veut dire que la programmation est de plus en plus accessible et accueille de plus en plus de monde.

Mais cela veut dire aussi qu’une grosse part la population de programmeurs est aujourd’hui constituée de personnes qui n’ont ni les connaissances, compétences ou ressources pour faire les choses correctement.

On le voit particulièrement dans le monde JavaScript, ou c’est l’explosion (là encore, ça mérite un nouvel article). Mais l’exemple de la 3.7 nous montre que la communauté Python n’est pas immunisée, et je pense que le problème va s’amplifier.

Que veux-je dire par là ?

Et bien il y a 30 ans, cela ne serait pas venu à l’esprit de la plupart des devs de compiler quelques choses sans mettre les flags en mode parano pour voir ce qui allait péter. Après tout, quand on code en C, on sait que tout peut imploser à tout moment, alors la prudence est une question de culture.

Aujourd’hui par contre, la majorité des devs des langages haut niveau écrivent du code, font quelques tests à la main, et publient ça. D’autres les utilisent. Font des mises à jour en masse. Aucun ne prennent le temps ne serait-ce que d’activer les warnings les plus basiques.

Comme tout est facile à première vue, et c’est quelque chose dont on fait la promotion pédagogiquement parlant, car ça incite les gens à se lancer, on oublie la complexité inhérente à la programmation.

Mais il y a une différence colossale entre avoir un code qui marche une fois sur sa machine, et un code prêt pour la production.

Par exemple en Python, vous pouvez demander l’activation des warning pour chaque appel avec:

python -Wd

En 3.6, ça implique ceci:

>>> def async():
...     pass
... 
:1: DeprecationWarning: 'async' and 'await' will become reserved keywords in Python 3.7

L’info a toujours été là. Prête à être utilisée.

Mais alors pourquoi ne pas afficher tous les warnings, tout le temps ?

Et bien si je le fais:

python -Wa

Voilà ce que ça donne quand je lance juste le shell de python 3.6:

Voir le code sur 0bin.

Vous comprenez donc bien que ce n’est PAS activé par défaut. En fait, originalement le message était dans le corps de l’article, mais j’ai du le mettre sur 0bin parce que ça faisait planter WordPress. Si.

A chaque upgrade, il est important de vérifier les warnings pour préparer ses migrations futures.

Oui, c’est du boulot.

En fait…

La programmation, c’est BEAUCOUP de boulot

Même si on arrive maintenant à extraire une frame vidéo en gif en une ligne de commande.

Surtout maintenant qu’on y arrive en fait, car on multiplie les agencements hétérogènes de boites noires pour créer nos merveilleux programmes qui font le café.

Alors on prend des raccourcis.

Et puis aussi, parce qu’on ne sait pas. Qui parmi les lecteurs du blog, pourtant du coup appartenant à la toute petite bulle des gens très intéressés par la technique, connaissaient le rôle des warnings et comment les activer ?

Mais ce n’est pas le seul problème. Il y a clairement une question d’attentes et de moyen.

L’utilisateur (ou le client) final veut toujours plus, pour moins cher, et plus vite !

Et le programmeur veut se faire chier le moins possible.

Comme la complexité des empilements d’abstractions augmente, cela conduit à ignorer ce sur quoi on se base pour créer ce qui doit combler notre satisfaction immédiate.

J’ai parlé d’amateurs plus haut.

Mais je ne parle pas simplement de mes élèves. De mes lecteurs.

Je parle aussi de moi.

Prenez 0bin par exemple.

Il n’est plus à jour. Il n’a pas de tests unitaires. Il a des bugs ouverts depuis des années.

Ce n’est pas pro du tout.

Sauf que je ne suis pas payé pour m’en occuper, et c’est bien une partie du problème: nous sommes de nombreux bénévoles à faire tourner la machine a produire du logiciel aujourd’hui. Donc si je n’ai pas envie, fuck it !

Vous imaginez si l’industrie du bâtiment ou celle de l’automobile tournaient sur les mêmes principes ?

La moitié des dessins industriels faits par des bloggers, des étudiants, des retraités, des profs de lycées, des géographes, de biologistes et des postes administratifs ?

Des immeubles et des voitures dont des pièces sont fabriquées par des potes qui chattent sur IRC et s’en occupent quand ils ont le temps ? Gratuitement. Y compris le service après-vente.

Alors que les usagers veulent toujours plus: des normes sismiques et de la conduite autonome. Tout le monde le fait, alors la maison de campagne et la fiat punto, c’est mort, personne ne l’utilisera.

Difficile de maintenir la qualité à cette échelle.

Il y a tellement de demandes de dev, jamais assez d’offres, de ressources toujours limitées.

Et ça grossit. Ça grossit !

Aides techniques

Ceci dit, à l’échelle de la PSF, ça aurait dû être évité.

Avant d’aborder les aides techniques, il serait bon d’arrêter les conneries. Je me répète, mais c’était une vaste dauberie de faire passer async/await en mot clé avant Python 4.

J’ai parfaitement conscience du besoin de faire progresser un langage pour ne pas rester coincé dans le passé. Je suis pour async/await, très bonne idée, superbe ajout. Mettre un warning ? Parfait ! Mais on respecte semver s’il vous plait. Si vous avez envie de faciliter la transition, mettre un import __future__, et inciter les linters à faire leur taff.

En attendant, pour la suite, Python va faciliter le debuggage.

Par exemple, depuis la 3.7, les DeprecationWarning sont activés par défaut au moins dans le module __main__. Donc un développeur verra ses conneries bien plus rapidement.

E.G:

Imp est déprécié en 3.6, mais sans -Wd, on ne le voit pas:

$ python3.6
Python 3.6.5 (default, May  3 2018, 10:08:28) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import imp

En 3.7, plein de modules importent imp, mais les DeprecationWarning ne sont pas montrés, car ça arrive dans des codes importés. En revanche, si dans le module principal, vous importez imp:

$ python3.7 
Python 3.7.0+ (default, Jun 28 2018, 14:08:14) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import imp
__main__:1: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses

Ça donne une info importante, sans foutre un mur de warnings à chaque lancement.

Une autre aide est l’apparition, toujours en 3.7, du mode développement de Python avec -X dev qui active tout un tas de comportements aidant au développement:

  • active -Wd
  • appelle PyMem_SetupDebugHooks
  • active faulthandler
  • active le mode debug de asyncio
  • met sys.flags.dev_mode sur True

Évidemment, tout ça ne sert pas à grand-chose si on ne sait pas ce qu’il faut en faire. Et ça demande du temps et du travail, ce que l’amateurisme ne permet pas forcément.

Enfin je dis ça. La plupart des employeurs s’attendent à tout, tout de suite également. Donc au final, n’est-ce pas la culture générale de notre industrie qui est en train de virer dangereusement vers le vite fait mal fait ?

Même si il y a clairement une question de compétence (un prof de maths est généralement compétent en maths, alors que j’attends toujours de rencontrer un prof d’info qui est capable de mettre quelque chose en prod), la pression du marché a créé des attentes impossibles…

L’informatique n’existe comme secteur économique que depuis quelques décennies, contre des siècles pour la plupart des autres disciplines scientifiques. Pourtant on exige d’elle le même niveau de productivité. Il a bien fallut rogner quelque part, et c’est la fiabilité qu’on a choisit.

Quand il y 20 ans, on rigolait en comparant le debuggage de Windows a la réparation d’une voiture, et la punchline sur le redémarrage, ce n’était pas grave: un peu de virtuel dans un monde plein d’encyclopédies papier, de cabines ou bottins téléphoniques et autres cartes routières.

Aujourd’hui que notre monde entier dépend du fonctionnement de nos conneries codées à l’arrache, c’est plus emmerdant. Et ça explique aussi pourquoi le téléphone de ma grand mère fonctionne toujours mieux pour faire des appels que mon putain de smartphone a 600 euros. Mais je peux draguer une meuf par texto en faisant caca à l’aéroport. Tout a un prix.

]]>
Guido van Rossum s’offre “des vacances permanentes” 17http://sametmax.com/?p=24844http://streisand.me.thican.net/sametmax.com/index.php?20180712_225603_Guido_van_Rossum_s___offre____des_vacances_permanentes____17Thu, 12 Jul 2018 20:56:03 +0000BDFL (Dictateur Bénévole À Vie) et créateur du langage Python vient d'annoncer qu'il laissait son bébé à la communauté.]]>Le BDFL (Dictateur Bénévole Bienveillant À Vie) et créateur du langage Python vient d’annoncer qu’il laissait son bébé à la communauté.

Sans quitter son rôle de core dev, et en suggérant qu’il passe plus de temps en tant que mentor de ses collègues sur le projet, il abandonne néanmoins son pouvoir de décision absolu sur le langage et son rôle de validateur ultime de chaque introduction de nouvelles fonctionnalités.

Dans son email, il ne passe pas le pouvoir à qui que ce soit ni n’invite à un mode de direction particulier. Il suggère que l’équipe actuelle des core devs, les développeurs ayant les droits de commit sur le repository du langage, choisisse le format qui leur convienne le mieux pour diriger à présent le projet.

Un moment important dans la vie de Python, mais aussi un événement qui demande beaucoup de remise en contexte.

Python-idea, Python-dev et les PEP

Durant les dernières années, les décisions de modification du langage passaient généralement par plusieurs phases.

D’abord, la proposition faite sur la mailing list python-idea. Cette liste est publique. Tout le monde peut y participer et y donner son avis.

Après débat, si le climat n’est pas hostile (ce qui est une notion TRES subjective), un des core devs suggère généralement à l’auteur de l’idée d’écrire un PEP.

Le PEP (Proposition d’Amélioration de Python) est l’équivalent d’une RFC pour Python. C’est un document formalisant une proposition d’amélioration du langage et de son écosystème. Le plus connu est bien entendu le PEP 8.

S’ensuit encore plus de débats, et des modifications itératives du PEP, jusqu’à, soit l’abandon du projet par l’auteur, soit l’oubli, soit le verdict de Guido.

Ce dernier est définitif: le PEP est approuvé ou rejeté. Dans le premier cas, il sera appliqué. Dans le second, il part à la poubelle.

Quelque part à la fin de ce processus, les implémenteurs du PEP discutent en plus petit comité, sur la liste python-dev, de la réalisation.

Guido a toujours, jusqu’ici, été donc la dernière étape de filtre de cet enchaînement. A la foi gardien de sa philosophie du langage, et goulot d’étranglement.

Il y a eu quelques améliorations, comme la nomination ponctuelle de BDFL délégués pour certains sujets, mais le format reste quand même globalement le même.

Pride and prejudice

Ce système a l’avantage d’être très ouvert. Tout le monde peut participer. Peu de personnes le font pourtant, par rapport à la masse d’utilisateurs de Python, mais c’est déjà beaucoup si on considère l’inertie que ça implique.

Le mail est un format très accessible, décentralisé, ouvert et standard. Son austérité évite l’effet réseau social qu’on voit trop souvent sur les outils plus modernes.

En contrepartie, le processus est très flou. Il n’y a pas de vote a proprement parler. Les threads s’enchaînent un peu n’importe comment. Les nouveaux venus ne sachant pas comment utiliser l’outil le font imparfaitement. Et le media ne permet pas facilement de suivre la continuité à court terme et à long terme de grand-chose, ni en termes de flux, ni en termes de référence, ni en termes de recherche.

Mais c’est aussi l’aspect social qui est problématique. Il n’y a aucune organisation claire dans la liste, et tout le monde semble “au même niveau”. Or ce n’est pas absolument pas le cas. La réputation et les participations passées, ainsi que les caractères des participants et leurs relations jouent énormément sur le processus. Le timing également.

Ça parait équitable sur le papier, mais c’est une illusion, et en prime cela implique des frictions qui pourraient être évitées. Par exemple quand un étudiant demande à Larry Hastings de l’aider à faire ses devoirs ou qu’un auteur de PEP cite le Zen of Python à Tim Peters. Inversement, quand vous proposez une idée, et que tout le monde vous envoie péter, mais que 6 mois plus tard, Brett Cannon dit que c’est intéressant et que tout le monde crie au génie, ça fout les boules.

Ajouté à cela, la qualité du débat qui peut varier du tout au tout : du caprice à la revue technique, en passant par le mail à côté de la plaque, la crise d’égo, l’appel au calme, le récap qui aide tout le monde, les idées brillantes, les suggestions étonnantes, les redites, et le spam.

Et évidement, la réponse par défaut est toujours non.

Se faire entendre là dedans est très difficile. Garder son calme l’est souvent aussi. Mais surtout, persévérer à travers le processus durant le temps nécessaire pour l’aboutissement de celui-ci est une épreuve épuisante, irritante, et ingrate. Surtout quand 9 fois sur 10, elle se finit par un rejet.

Un rejet cependant, n’est pas la mise à mort d’une idée. L’année suivante on peut relancer le débat, et ainsi de suite, comme un politicien à l’assemblée qui veut faire passer son projet de loi. Et comme au Parlement, la même idée présentée par une nouvelle personne, ou avec un autre timing peut avoir un résultat radicalement différent.

Un anus pour les rassembler tôt

GvR, bien qu’au sommet de cette pyramide, n’est pas isolé de tout cela, bien au contraire. Il est une des rares personnes qui doit trancher sur presque tout, et donc se coltiner une bonne partie des débats. Et des réactions quand il rejette l’idée d’autrui.

À ceci se rajoute sa présence sur les bugs trackers, et bien entendu son activité de développeur.

Un rôle difficile, fatigant, et un vrai sacerdoce pour quelqu’un qui pourrait dire “allez tous vous faire foutre, j’ai pondu ce langage utilisé par des millions de personnes tout seul, je sais mieux que vous” au lieu de prendre en considération l’avis du public.

Un rôle, je vous le rappelle, qu’il tient depuis plus de 20 ans.

Depuis la 3.4, on sent le poids commencer à peser sur ses épaules. A 62 ans, et toujours avec un travail à plein temps chez Dropbox (qui heureusement lui laisse le loisir de travailler sur Python), on peut imaginer l’énergie nécessaire pour s’impliquer encore autant.

Cela se ressent particulièrement dans ses mails et ses tickets. Une sorte de lassitude. Et une retenue qu’on devine de plus en plus difficile.

Honnêtement, je ne sais pas comment il a fait pour ne pas exploser sur quelqu’un. Dans certains échanges que j’ai eus avec lui sur la liste ou sur github, si nos rôles avaient été inversés, j’aurais sans aucun doute été bien moins diplomate.

sys.exit() maintenant, avant que la charge ne devienne trop pesante, est à mon sens un acte de grande classe.

Comme un comédien, un musicien, un metteur en scène ou un écrivain qui fait ses adieux au sommet de son art. J’aurais aimé que Lucas, Spielberg et Besson aient su en faire autant.

Mais pour bien comprendre à quel point cette décision de laisser sa place demande du courage, il faut réaliser une chose: il laisse son enfant dans les mains des autres. L’œuvre de sa vie. Une réalisation colossale qui plus est, qui a généré des centaines de milliers d’emplois, fait tourner des milliers de projets.

Moi, j’ai du mal à m’imaginer arrêter d’écrire ce misérable petit blog. Et pourtant j’y ai pensé souvent.

L’étincelle qui a fait déborder la moutarde, c’est bien entendu le débat sans fin sur l’expression d’assignation, et la critique vigoureuse de son acceptation. (C’est vrai que c’était chaud)

Guido en a eu ras le cul, quoi.

So what now ?

Personnellement, j’accueille la nouvelle avec un mélange d’espoir et de mélancolie.

Python n’aurait jamais été ce qu’il est aujourd’hui sans la constante vigilance de son BDFL.

Dire “non” est la partie la plus importante pour sculpter une expérience. C’est ce qui fait la différence entre Gates et Jobs, Van dam et Lee, Hitler et… heu… non.

C’est ce qui fait qu’on n’a des lambda bridées et pas d’opérateur pour ajouter un élément dans une liste. Qu’on le veuille ou pas.

Et c’est ce qui fait qu’un langage né en fucking 1985 (avant Java !) tient toujours la route aujourd’hui. A gardé sa lisibilité. Sa facilité de prise en main. Et continue d’être utile.

C’est un travail de titan !

Néanmoins je ne peux m’empêcher de noter que les dernières fonctionnalités majeures de Python ont été introduites de manière brouillonne. Je pense notamment au type hints et à asyncio, que Guido a bdfl -f sans donner le temps de mûrir leur design. Elles ont mis plusieurs versions à ne serait-ce qu’être utilisables, et méritent encore du travail.

Je pense aussi que Python est prêt pour des ajustements, et espère que passer le flambeau à l’équipe de core dev va apporter du renouveau au langage.

En effet, il est peu probable qu’un nouveau BDFL s’installe, et bien qu’il soit trop tôt pour faire une prédiction, la mise en place d’un quorum semble plus probable. Dans ce cas, il faudra sans doute un vecteur de vote. Et de là, qui sait, naîtra peut-être un complément ou replacement à python-idea.

Je l’espère en tout cas.

Sortir de l’usage de la mailing list pour un outil plus encadré, mettant les propositions en avant, avec un système de sondage, des procédures plus claires, des références plus faciles au passé, et une distinction explicite du statut des personnes qui s’expriment.

Bref, faciliter l’accès au processus, tout en permettant une gestion plus saine. On ne voudrait pas que Victor ou Yuri nous posent leur dem dans les 6 mois pour cause de pétage-de-cablite aigue.

Également, qui sait, peut-être reviendra-t-on – pour le meilleur ou pour le pire ? – sur des BanDFL tel que:

  • le pattern matching
  • les exceptions sous forme d’expression
  • pathlib.Path dans les builtins
  • le mot clé lazy
  • et bien d’autres

Difficile de croire que le débat ne sera pas relancé maintenant que Cerbère ne garde plus les Champs Élysées.

Évidemment, il est logique qu’il y ait un temps de flottement. D’abord par respect pour le père de Python. Ça ferait un peu GoT d’enchaîner quand même. Ensuite parce qu’il va falloir qu’un nouveau mode de fonctionnement émerge, et prouve son efficacité.

Quoi qu’il arrive

Merci à Guido Van Rossum pour ce cadeau immense.

On va essayer de ne pas trop l’abîmer.

]]>
L’expression d’assignation vient d’être acceptée 19http://sametmax.com/?p=24766http://streisand.me.thican.net/sametmax.com/index.php?20180703_111547_L___expression_d___assignation_vient_d___etre_acceptee_19Tue, 03 Jul 2018 09:15:47 +0000python-idea (mailing list sur laquelle, je vous le rappelle, tout le monde a le droit de participer), Guido a validé la PEP 572. (foo := bar) sera donc un code valide en Python 3.8.]]>Après des mois de débats sur python-idea (mailing list sur laquelle, je vous le rappelle, tout le monde a le droit de participer), Guido a validé la PEP 572. (foo := bar) sera donc un code valide en Python 3.8.

Je n’avais pas vu de feature plus controversée depuis les f-strings. Et je gage que, comme les f-strings, après un temps d’adaptation, la communauté va se demander comment on a vécu sans auparavant.

Un peu d’histoire

Il existe deux grandes catégories d’instructions dans les langages de programmation. Les déclarations et les expressions.

La déclaration, en anglais “statement”, est une action indépendante formulée sur une ligne. C’est une unité syntaxique, et une déclaration ne peut être contenue dans une autre déclaration sur la même ligne.

Ex:

import os

est une déclaration. Un import est tout seul sur sa ligne et ne se combine pas avec d’autres instructions.

L’expression, elle, est une combinaison de littéraux, variables, d’opérateurs et d’appels de fonctions qui retournent un résultat. Les expressions peuvent contenir d’autres expressions, et elles peuvent être contenues dans une déclaration.

Ex:

1 + 1 

est une expression. On peut, en effet, assigner le résultat de ce code à une variable, le passer à une fonction en paramètre ou le tester dans une condition.

Les langages, comme le COBOL, qui privilégient le style impératif utilisent majoritairement des déclarations. Les langages, comme LISP, qui privilégient le style fonctionnel, utilisent majoritairement des expressions.

Très souvent néanmoins, les langages populaires font largement usage des deux. L’expression est plus flexible et plus puissante. La déclaration force une opinion sur la structure du programme et évite les divergences de style. Selon la philosophie que l’on souhaite donner à son bébé, un créateur de langage va donc s’orienter plus vers l’une ou l’autre.

Python est multiparadigme et soutient le style impératif, fonctionnel et orienté objet. Il possède donc non seulement des expressions et des déclarations, mais également souvent les deux versions pour une même instruction.

Ex, les déclarations:

squares = []
for x in numbers:
    squares.append(x * x)

def mean(x):
    return x * x / 2

if is_ok:
    print(welcome)
else:
    print(error)

ont des expressions équivalentes:

[x * x for x in number]

lambda x: x * x / 2

print(welcome if is_ok else error)

Parfois Python choisit d’orienter le style du programmeur, non pas évitant de fournir des expressions, mais par le biais de la grammaire imposée. Ainsi il oblige à indenter, et n’autorise pas les lambdas à contenir de déclaration. C’est une autre stratégie pour imposer une philosophie au langage. Pour Python, la philosophie est que la capacité à s’exprimer doit rester riche, mais pas au détriment de la capacité à comprendre le code.

Une particularité de Python, c’est que l’assignation, c’est-à-dire le fait d’associer une valeur à une variable, est une déclaration. Ex:

a = 1

Comme les déclarations ne peuvent contenir d’autres déclarations, cette syntaxe interdit:

if a = 1:  

Ce n’est pas le cas dans de nombreux langages populaires, comme le C, PHP, le JS… Exemple en Ruby:

if (value = Settings.get('test_setting'))
  perform_action(value)
end

Ici, non seulement on assigne le résultat du get() à la variable value, mais en plus, on teste le résultat du get() avec le if.

Actuellement ceci est impossible en Python, et le même code serait:

value = Settings.get('test_setting')
if value:
  perform_action(value)

Ce n’est pas une erreur, c’est un choix de design. En effet une source de bug très courante en programmation est de vouloir faire une comparaison, mais de taper une assignation.

Ainsi, quelqu’un voudrait faire:

while reponse == "oui":
    ...
    reponse = input('Voulez-vous continuer ?')

Mais ferait:

while reponse = "oui": # erreur subtile et difficile à voir
    ...
    reponse = input('Voulez-vous continuer ?')

Ce qui ne compare pas DU TOUT la variable. Et en plus change son contenu. Dans ce cas précis, le résultat est une boucle infinie, mais parfaitement valide et qui ne provoquera pas d’erreur.

Pour éviter ce genre de bug que même les programmeurs aguerris font un jour de fatigue, la syntaxe a tout simplement été interdite en stipulant que l’assignation était toujours une déclaration.

La PEP 572

L’absence de cette fonctionnalité a eu d’excellents bénéfices. Je le vois régulièrement dans mes salles de classe, ce bug. La SyntaxError qui résulte de cette faute en Python permet de l’attraper avant qu’il ne fasse le moindre dégât.

Car c’est tout le problème de cette erreur: si la syntaxe est valide, le bug est silencieux, le code tourne, il ne fait juste pas du tout ce qu’on lui demande. C’est la plus pernicieuse des situations, avec des conséquences qui peuvent ne se déclarer que bien plus loin dans le code et une séance de débuggage des plus irritantes.

A mon sens, c’était une excellente décision de design.

Pourtant la PEP 572 revient dessus, en proposant, comme pour lambda, la boucle for ou la condition, un équivalent sous forme d’expression.

Pourquoi ?

Et bien Python doit aussi satisfaire les programmeurs chevronnés, qui en ont marre de se retrouver avec des situations comme :

match1 = pattern1.match(data)
if match1:
    print(match1.group(1))
else:
    match2 = pattern2.match(data)
    if match2:
        print(match2.group(2))

Certes, ce code est clair et facile à comprendre, mais il est très verbeux. On doit faire avec une indentation artificiellement induite par la limite de la syntaxe, et non par la logique du raisonnement qui est ici parfaitement linéaire.

Comment donc réconcilier le désir d’éviter le fameux bug tout en satisfaisant les besoins d’expressivité, légitimes une fois qu’on quitte le nid des débutants ?

La solution proposée est triple :

  • Ajouter un nouvel opérateur dédié, permettant l’assignation sous forme d’expression.
  • Forcer l’usage des parenthèses pour encadrer cette expression.
  • Rendre les deux opérateurs d’assignation mutuellement exclusifs.

Le nouvel opérateur choisi est := et il ne peut exister qu’entre parenthèses. Il peut être utilisé là où un simple = ne serait pas autorisé. Mais, il ne peut PAS être utilisé là où on peut utiliser =. Le but est de ne jamais mettre ces deux opérateurs en concurrence: une situation permet l’un ou l’autre, jamais les deux.

= ne change pas. La seule différence, c’est qu’à partir de Python 3.8, vous aurez le droit de faire:

if (match1 := pattern1.match(data)):
    print(match1.group(1))
elif (match2 := pattern2.match(data)):
    print(match2.group(2))

L’opérateur := permet donc bien, dans des situations très précises, d’obtenir un code plus cours et élégant, sans introduire pourtant d’ambiguïté et donc de bug potentiel. Il autorise une nouvelle forme d’expressivité, mais sa syntaxe est très marquée: impossible de le confondre avec son frère, et les parenthèses l’isolent du reste du code.

On ne se pose pas non plus la question de quand choisir = et := puisque:

a = 1

est valide, mais pas:

a := 1

Bien que:

(a := 1)

soit valide, personne n’aura envie d’utiliser vainement cette forme plus lourde.

L’usage de := est donc marginal, et cantonné à des cas particuliers.

Une opinion, c’est comme un trou du cul…

Personnellement j’étais mitigé sur l’idée. De plus, j’aurais préféré l’usage du mot clé as, puisqu’on l’utilisait déjà dans les imports, les context managers et la gestion des exceptions.

Chaque ajout d’expression rajoute également la possibilité d’abus. Si vous avez déjà vu ces horreurs à base de lambdas imbriquées ou d’intensions sans fin, vous savez de quoi je parle.

Avec :=, on peut vraiment se lancer dans du grand n’importe quoi:

Pour reprendre l’exemple de la PEP:

while True:
    old = total
    total += term
    if old == total:
        return total
    term *= mx2 / (i*(i+1))
    i += 2

est très clair.

En revanche:

while total != (total := total + term):
    term *= mx2 / (i*(i+1))
    i += 2
return total

Est une abomination qu’il faut purger par le feu.

Mais par expérience, j’ai rarement vu en 15 ans de Python beaucoup d’abus de ses fonctionnalités avancées. Les bénéfices ont toujours dépassé le coût d’une large marge. Pourtant entre les décorateurs, les dunder methods et les meta classes, il y a matière à messe noire.

Par ailleurs j’avoue que je suis ravi de pouvoir enfin faire:

while (data := io.get(x)):

Et:

[bar(x) for z in stuff if (x := foo(z))]

La PEP mentionne aussi un exemple que je n’avais pas prévu, et qui souffle le chaud et le froid:

diff = x - x_base
if diff:
    g = gcd(diff, n)
    if g > 1:
        return g

Peut devenir:

if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
    return g

Comme l’auteur, j’approuve le fait que la première version est inutilement verbeuse, mais contrairement à lui, je trouve que la seconde est bien trop complexe pour être scannée d’un coup d’œil.

En revanche:

diff = x - x_base
if diff and (g := gcd(diff, n)) > 1:
    return g

est tout à fait à mon goût.

Ceci démontre bien qu’il va falloir un temps d’adaptation avant que la communauté trouve l’équilibre entre Perl et BASIC. Quoi qu’il en soit, on n’aura pas à s’en soucier avant l’année prochaine, et même d’ici là, peu de code pourra en faire usage avant que la 3.8 soit largement installée.

De mon côté je m’attends à ce qu’on ignore majoritairement cette fonctionnalité, jusqu’au moment où elle apparaîtra dans un coin de l’esprit le temps d’un besoin ponctuel et local, pour être oubliée à nouveau jusqu’à l’occasion suivante. Comme Dieu Guido l’aura voulu. D’ailleurs, côté enseignement, je ne compte pas introduire l’opérateur dans mes cours, ou alors dans une section bonus, à moins qu’un participant ne pose la question.

Aller, vous pouvez râler en commentaire maintenant :)

]]>
Le don du mois: mobx 7http://sametmax.com/?p=24750http://streisand.me.thican.net/sametmax.com/index.php?20180702_124128_Le_don_du_mois__mobx_7Mon, 02 Jul 2018 10:41:28 +0000Mobx, c'est que le projet déchire. Et il déchire malgré le fait qu'il soit codé en JS.]]>Si vous avez suivi un peu les différents dons du mois au fur et à mesure de la vie du blog, vous avez du vous rendre compte de 2 choses:

  • Le don du mois n’est pas mensuel. Il est juste limité à un par mois. Ça m’évite la pression d’être un bon samaritain. Sauf que du coup, j’oublie parfois pendant trèèèèèèès longtemps :)
  • Il n’y a pas beaucoup de projets JS dans le lot. Ahem.

En fait le seul et unique projet JS supporté a été VueJS. C’est dire que la barre est haute, étant donné la qualité exceptionnelle de ce projet.

Donc quand je vous dis que j’ai donné 50 balles à Mobx, c’est que le projet déchire. Et il déchire malgré le fait qu’il soit codé en JS.

Mobx permet, en gros, de surveiller les modifications à une structure de données. Vous posez un marqueur sur la structure, et un sur les fonctions utilisant la structure, et c’est tout:

class TodoList {
    @observable todos = []; // dire à mobx de surveiller
}

...

@observer // dire à mobx de tenir à jour
class TodoListView extends Component {
    render() {
        return 
    {this.props.todoList.todos.map(todo =>)}
}

C’est là la brillance du système: malgré sa simplicité, mobx va récursivement réagir à toute modifications, même sur des données complexes imbriquées; Et calculer toutes les dépendances de chaque fonction pour ne les appeler qu’au meilleur moment.

C’est facile à utiliser, et étonnamment rapide à exécuter.

Le résultat ? Quand un client me force à utiliser ReactJS, je saute redux, et je met Mobx à la place. Ca donne presque l’impression d’utiliser Vue: le code de manipulation d’état est simple à comprendre, gentil sur le CPU et les mutations d’état restent courtes et élégantes. Le reste est toujours moche, mais ça on y peut rien.

Bref, mobx est ce qui rend react acceptable. Et tout ce qui peut apaiser la douleur du dev en front-end n’a pas de prix.

J’ai regardé le code source, et la popote interne est bien complexe. Mais à chaque fois que je veux l’utiliser je me dis que ça ne pourra pas être si simple… et si.

La page de don, c’est par là.

]]>
Python 3.7 sort de sa coquille 20http://sametmax.com/?p=24711http://streisand.me.thican.net/sametmax.com/index.php?20180628_185441_Python_3.7_sort_de_sa_coquille_20Thu, 28 Jun 2018 16:54:41 +0000La 3.4 était la première version 3 à valoir le coup, et a donc été le déclencheur de la migration 2->3 qui trainait depuis si longtemps.

La 3.5(.3) a rendu asyncio utilisable, incluant async / await et corrigeant le bug abusé de get_event_loop().

La 3.6 est mon chouchou. Sa meilleure intégration de Pathlib et les f-strings en font un plaisir total à utiliser. En plus black ne tourne que dessus. Je suis autant que possible en 3.6, je l’ai même installée sur une vieille centos 7 chez un client.

Alors que vaut cette 3.7, et est-ce qu’il faut migrer ?

Et bien avec des améliorations de perfs partout et une syntaxe simplifiée pour les classes, c’est une belle release. La 3.6 va être bien plus facile à avoir sous linux pendant un bon bout de temps et suffit amplement, donc je ne vais pas forcer le pas. Mais bon je l’ai quand même compilée par acquis de conscience.

Regardons ce qu’il y a au menu.

Les data classes

Clairement la feature phare de la release, les data classes sont une manière plus concise d’écrire des classes, s’inspirant de la bibliothèque attrs dont elles n’implémentent qu’un sous-ensemble.

Une très bonne nouvelle, car je n’installais jamais attrs: dépendre d’une lib juste pour ça, m’embêtait et pour la sérialisation/validation j’utilise marshmallow.

Par exemple:

from dataclasses import dataclass

@dataclass
class Achat:
    produit: str
    prix: float
    quantite: int = 0

Ce qui va générer une classe toute ce qui a de plus normale, mais dont le __init__, __repr__ et __eq__ sont automatiquement créés. Vous pouvez bien entendu ajouter les méthodes que vous voulez, comme d’habitude.

Il ne reste plus qu’à faire:

>>> print(Achat("foo", 2))
Achat(produit='foo', prix=2, quantite=0)

Toute la magie est sélectivement désactivable, et une méthode __post_init__ est ajoutée à la classe qui fait exactement ce que vous pensez que ça fait.

En prime, on a aussi dataclasses.field qui permet de fournir une factory pour un paramètre (typiquement list, tuple, dict…).

Puis, comme un bonheur ne vient jamais seul:

>>> from dataclasses import asdict, astuple
>>> print(asdict(Achat("foo", 2)))
{'produit': 'foo', 'prix': 2, 'quantite': 0}
>>> print(astuple(Achat("foo", 2)))
('foo', 2, 0)

C’est récursif sur les dicts, lists, tuples et dataclasses \o/

Enfin, pour finir:

>>> from dataclasses import replace
>>> a = Achat("foo", 2)
>>> b = replace(a, quantite=3, prix=1)
>>> print(a, id(a))
Achat(produit='foo', prix=2, quantite=0) 140275795603296
>>> print(b, id(b))
Achat(produit='foo', prix=1, quantite=3) 140275775561456

Ouai ça déchire.

P.S: y un backport pour la 3.6

asyncio++

Much love to asyncio dans cette version.

Déjà, un truc qui aurait dû être là dès le début, la nouvelle fonction asyncio.run(), qui masque le setup de l’event loop pour vous.

On passe de :

loop = asyncio.get_event_loop() 
loop.run_until_complete(coro)

à:

asyncio.run(coro)

Juste ça, ça fait vachement moins peur aux gens. Et en prime ça évite qu’ils commencent à chercher la merde avec un setup custom de loop.

asyncio.current_task() retourne la tâche dans laquelle on est. D’ailleurs, un détail, mais on a maintenant l’équivalent de thread local, mais pour la coroutine en cours.

asyncio.get_running_loop() retourne la boucle courante, mais seulement si elle existe. Elle lève une exception plutôt que de créer une loop comme get_event_loop() si aucune loop n’est présente.

StreamWriter.wait_closed() permet d’attendre qu’un stream se ferme. Les gars de aiohttp doivent être contents.

Task.get_loop() retourne la boucle de la tâche. Pour le multi-threading avec plusieurs loops, c’est cool.

loop.create_server() a maintenant un argument start_serving qui contrôle si on veut le lancer immédiatement. J’ai toujours du mal à croire que des dev qui sont capables de participer à la stdlib ont pu commiter un code qui instancie et enchaine sur un effet de bord. Heureusement c’est corrigé.

Les handlers retournés par loop.call_later() retournent leur ETA avec .when() et ont une méthode .cancelled().

Les intensions acceptent maintenant async/await.

Et enfin, les exceptions des tâches annulées ne sont plus loggées. Parce que forcément quand on crashait tout, le log devenait un peu chargé…

Bon, asyncio était déjà très utilisable en 3.6, n’exagérons pas. L’important étant d’utiliser le mode debug, gather() et run_until_complete(), ce qui devrait être écrit en gros, en rouge dans la doc.

Mais toutes ces modifications sont bienvenues.

Ah, oui, les perfs ont été aussi améliorées… Mais c’est le cas partout.

Des perfs

Le focus sur les perfs de Python augmente doucement. La 3.6 avait amorcé la tendance, et ça se confirme. J’attends d’avoir des retours sur des mises en prod un peu serieuses pour savoir si ça a payé.

Le temps de démarrage de Python est d’ailleurs pas mal pointé du doigt. Certes, on est pas au niveau de loutre sclérosée que constitue nodejs au réveil, mais c’est pas une référence. Donc, des choses sont mises en place. Notamment python -X importtime qui va afficher le temps que prend chaque import.

Des aménagements ont aussi été faits pour accélérer le module typing, maintenant que l’usage des type hints pour les annotations est entériné. Un side effect sympa est que les classes que vous allez écrire seront plus rapides à instancier, et les méthodes plus rapides à résoudre.

D’ailleurs, les type hints sont maintenant résolus paresseusement, à la fois pour améliorer la vitesse de chargement et pour faciliter l’auto-référencement.

breakpoint()

breakpoint() est techniquement un alias configurable à import pdb; pdb.set_trace(). Ça a l’air de rien, mais c’est super:

  • C’est dans les builtins, donc plus facile à découvrir. Les débutants utilisent très peu pdb alors que c’est un outil formidable.
  • Plein de gens oublient cette incantation mystique ou font des fautes de frappe.
  • C’est facile à spotter dans le code, on peut l’highlither différement.
  • C’est uniforme. Si on veut utiliser un autre débuggeur, on le plug. Mais on utilise toujours la même fonction. On peut même désactiver cette fonction pour éviter de balancer un break point en prod.
  • C’est clair. Les débutants n’ont aucun mal avec debugger en JS parce que c’est simple de comprendre ce que ça fait au premier coup d’oeil.

Ça vient, bien entendu avec une variable d’environnement et d’un hook dans sys pour custo le comportement.

Quelques détails

Dicts ordonnés

La spec garantit que les clés vont garder leur ordre d’insertion. C’était déjà le cas en 3.6, la 3.7 rend juste la mesure officielle.

Ne jetez pas OrderedDict à la poubelle pour autant, car il préserve l’ordre des clés après suppression également.

async/await sont des mots clés

Et ne peuvent donc plus être écrasés par erreur. C’est juste l’application de l’annonce faite à l’introduction de ces mots clés.

Des sœurs pour __getitem__ et __getattr__

On va pouvoir définir un __getattr__ sur les modules (surtout utile pour le lazy loading) et un __class_getitem__ pour pouvoir faire MaClass[] sans utiliser de metaclass.

Traduction de la doc

Le processus pour avoir des docs dans d’autres langues est maintenant officiel. Pour l’instant le jap, le koréen et le kokorico

DeprecationWarning plus visibles

La connerie de les cacher a été corrigée. Qui a pensé que c’était une bonne idée ? Mais seulement pour le script principal, ce qui va permettre aux dev des libs de les voir sans faire chier les utilisateurs.

Debug mode

python -X dev va devenir votre nouvel ami, activant tout un tas de fonctions de debug coûteuses en production. Notamment plus de warning, asyncio debug mode, le faulthandler qui dump la stacktrace en cas de catastrophe, etc.

Des pyc reproductibles

Un même fichier donnera maintenant toujours un même .pyc. C’est pour les packagers et les amateurs de sécu.

Meilleur ImportError

L’exception va maintenant afficher le nom du module et son __file__ path si from ... import broute. Ça va rendre les imports circulaires, la plaie des gros projets Python, plus faciles à debugger.

packaging

Introduction de importlib.resources, un remplacement pour le détestable pkg_resource qui va rendre sans regret mon article obsolète.

Autre ajout notable: README.rst est maintenant reconnu et ajouté automatiquement quand on fait son paquet cadeau. Puisque maintenant pypi accepte le markdown, ça aurait été cool de le faire avec les .md également.

time est plus précis

Sensible à la nanoseconde. Perso je m’en bats les steaks mais je me suis dit que je ferai passer l’info.

unittest -k

Copieurs.

namedtuple supporte les valeurs par defaut

Rien à ajouter, si ce n’est qu’entre SimpleNamespace et les dataclasses, je crois qu’on a de quoi voir venir. Même si j’aimerais avoir un literal pour les namedtuples sous la forme de (foo=1, bar=2) mais ça a été refusé.

Ajouts à Contexlib

Quelques outils en plus, dont un context manager qui ne fait rien (rigolez pas, c’est super utile !), et des contexts managers async.

lifting pour subprocess

Ok, plutôt bottox. C’est cosmétique, mais c’est bienvenu: des aménagements pour rendre les appels un poil plus courts, notamment dans le cas de la capture des stdx.



Et encore plein d’autres mini trucs.

C’est dispo en DL pour windows et mac. Pour linux, comme d’hab, soit on attend la mise à jour des depôts/ppa/etc, soit on compile à la main (étonnamment facile, si on se rappelle de faire make altinstall et pas make install), soit on utilise l’excellent pyenv et pyenv install 3.7.

]]>
Super article invité sur Trio que l’auteur a oublié de titrer 10http://sametmax.com/?p=24605http://streisand.me.thican.net/sametmax.com/index.php?20180614_093952_Super_article_invite_sur_Trio_que_l___auteur_a_oublie_de_titrer_10Thu, 14 Jun 2018 07:39:52 +0000Ceci est un post invité de touilleMan posté sous licence creative common 3.0 unported.

C’est bon vous avez cédé à la hype ?

Après un n-ème talk sur asyncio vous avez été convaincu que tout vos sites webs doivent être recodé dans cette techno ? Oui, surtout celui de la mairie de Gaudriole-sur-Gironde avec ses 50 visiteurs/jour, Django ça scalera pas et vous aurez sûrement besoin de websockets à l’avenir.

Et puis là pan ! En commençant à utiliser asyncio on se rend compte que ça va pas être aussi marrant que ce que vous a vendu l’enfoiré de hipster dans son talk avec son exemple de crawler web en 20 lignes :

  • la doc de asyncio fait 50 putain de pages, même les plus grands déclarent ne rien y comprendre
  • pdb est aux fraises, un step-over sur un await vous envoi à perpet’ dans l’event loop
  • il faut passer l’event loop en tant qu’argument à chaque fonction, évidemment vous n’allez pas le faire et utiliser get_event_loop() à la place. Et ça va merder sévère à un moment (typiquement quand vous ajouterez des tests non triviaux), et vous allez devoir corriger tout votre code.
  • régulièrement une stacktrace d’une coroutine ayant crashé dégueule de stdout sans que le programme ne bronche, autant de je m’en foutisme on se croirait revenu en PHP !
  • parlons-en de la stacktrace ! Impossible de savoir d’où vient la coroutine, encore une fois on nous renvoi dans les méandres de l’event loop.
  • Et pour initialiser/finaliser proprement votre système alors là c’est la fête totale

Je ne parle même pas des soucis ceinture-noir-2ème-dan du genre high-water mark qui vous tomberons dessus une fois l’appli en prod.

Lourd est le parpaing de la réalité sur la tartelette aux fraises de nos illusions…

1 – Pourquoi c’est (de) la merde ?

Pour faire simple asyncio a été pensé à la base comme une tentative de standardisation de l’écosystème asynchrone Python où chaque framework (Twisted et Tornado principalement) était incompatible avec les autres et devait re-créer son écosystème de zéro.

C’était la bonne chose à faire à l’époque, ça a eu beaucoup de succès (Twisted et Tornado sont maintenant compatible asyncio), ça a donné une killer-feature pour faire taire les rageux au sujet de Python 3 et ça a créé une émulsion formidable concernant la programmation asynchrone en Python.
Mais dans le même temps ça a obligé cette nouvelle lib à hériter des choix historiques des anciennes libs : les callbacks.

Pour faire simple un framework asynchrone c’est deux choses :

  • une grosse boucle infinie (la fameuse « event loop ») qui a configuré les appels d’IO au kernel en mode non-bloquant et qui poll ceux-ci en continu
  • un mécanisme pour garder trace de quel bout de code exécuter quand une IO donnée aura été terminée

Concernant le 2ème point, cela veut dire que si on a une fonction synchrone comme ceci :

def listen_and_answer(sock):
    print('start')
    data = sock.read()
    print('working with %s' % data)
    sock.write('ok')
    print('done')

Il faut trouver un moyen pour la découper en une série de morceaux de codes et d’IO.

Il y la façon « javascript », où on découpe à la main comme un compilo déroulerai une boucle :

def listen_and_answer(sock):
    print('start')

    def on_listen(data):
        print('working with %s' % data)

        def on_write(ret):
            print('done')

        sock.write('ok', on_write)

    sock.read(on_listen)

Et là j’ai fait la version simple sans chercher à gérer les exceptions et autres joyeusetés. Autant dire que quand un vieux dev Twisted vous dit le regard vide et la voix chevrotante qu’il a connu l’enfer, ne prenez pas ses déclarations à la légère.

Sinon la façon async/await si chère à asyncio :

async def listen_and_answer(sock):
    print('start')
    data = await sock.read()
    print('working with %s' % data)
    await sock.write('ok')
    print('done')

C’est clair, c’est propre, la gestion des exceptions est totalement naturelle, bref c’est du Python dans toute sa splendeur.
Sauf que non, tout ça n’est qu’un putain d’écran de fumée : pour être compatible avec Twisted&co sous le capot asyncio fonctionne avec des callbacks.

Vous vous souvenez de cette sensation de détresse mêlée d’hilarité devant une stacktrace d’un projet Javascript lambda d’où vous ne reconnaissez que la première ligne ? C’est ça les callbacks, et c’est ça que vous avez dans asyncio.

Concrètement le soucis vient du fait qu’une callback n’est rien d’autre qu’une fonction passée telle qu’elle sans aucune information quant à d’où elle vient. De fait impossible pour l’event loop asynchrone de reconstruire une callstack complète à partir de cela.
Heureusement async/await permettent à python de conserver ces informations de fonction appelante ce qui limite un peu le problème avec asyncio.
Toutefois en remontant suffisamment haut on finira toujours avec une callback quelque part. Et vous savez qui a l’habitude de remonter aussi haut que nécessaire ? Les exceptions.

import asyncio
import random

async def succeed(client_writer):
    print('Lucky guy...')
    # Googlez "ayncio high water mark" pour comprender pourquoi c'est
    # une idée à la con de ne pas avoir cette methode asynchrone
    client_writer.write(b'Lucky guy...')

async def fail(client_writer):
    raise RuntimeError('Tough shit...')

async def handle_request_russian_roulette_style(client_reader, client_writer):
    handlers = (
        succeed,
        succeed,
        succeed,
        fail,
    )
    await handlers[random.randint(0, 3)](client_writer)
    client_writer.close()

async def start_server():
    server = await asyncio.start_server(
        handle_request_russian_roulette_style,
        host='localhost', port=8080)
    await server.wait_closed()

asyncio.get_event_loop().run_until_complete(start_server())

Maintenant si on lance tout ça et qu’on envoie des curl localhost:8080 on va finir avec:

$ python3 russian_roulette_server.py
Lucky guy...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...
Lucky guy...
Task exception was never retrieved
future:  exception=RuntimeError('Tough shit...',)>
Traceback (most recent call last):
  File "ex.py", line 18, in handle_request_russian_roulette_style
    await handlers[random.randint(0, 3)](client_writer)
  File "ex.py", line 9, in fail
    raise RuntimeError('Tough shit...')
RuntimeError: Tough shit...

Le problème saute aux yeux: asyncio.start_server gère sa tambouille avec des callbacks et se retrouve bien embêté quand notre code remonte une exception. Du coup il fait au mieux en affichant la stacktrace et en faisant comme si de rien n’était. C’est peut-être le comportement qu’on attend d’un serveur web (encore que… si aviez configuré logging pour envoyer dans un fichier vous êtes bien baïzay) mais il existe des tonnes de usecases pour lesquels ça pose problème (et de toute façon on n’a vu que la partie émergée de l’iceberg d’emmerdes qu’est la programmation asynchrone).

Bref, si vous voulez en savoir plus, allez lire ce post, d’ailleurs allez lire tous les posts du blog, ce mec est un génie.

2 – Trio, une façon de faire de l’asynchrone

Ce mec en question, c’est Nathaniel J. Smith et il a eu la très cool idée de créer sa propre lib asynchrone pour Python: Trio

L’objectif est simple: rendre la programmation asynchrone (presque) aussi simple que celle synchrone en s’appuyant sur les nouvelles fonctionnalités offertes par les dernières versions de Python ainsi qu’un paradigme de concurrence innovant. Cette phrase est digne d’un marketeux, vous avez le droit de me cracher à la gueule.

Concrètement ce que ça donne:

# pip install trio asks beautifulsoup4
import trio
import asks
import bs4
import re


# Asks est un grosso modo requests en asynchrone, vu qu'il supporte trio et curio
# (une autre lib asynchrone dans le même style), il faut donc lui dire lequel utiliser
asks.init('trio')


async def recursive_find(url, on_found, depth=0):
    # On fait notre requête HTTP en asynchrone
    rep = await asks.get(url)
    print(f'depth {depth}, try {url}...')

    # On retrouve le corps de l'article grace à beautiful soup
    soup = bs4.BeautifulSoup(rep.text, 'html.parser')
    body = soup.find('div', attrs={"id": 'mw-content-text'})

    # On cherche notre point Godwin
    if re.search(r'(?i)hitler|nazi|adolf', body.text):
        on_found(url, depth)

    else:
        async with trio.open_nursery() as nursery:
            # On retrouve tous les liens de l'article et relance le recherche
            # de manière récursive
            for tag in body.find_all('a'):
                if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
                    child_link = 'https://en.wikipedia.org' + tag.attrs['href']
                    # On créé une nouvelle coroutine par lien à crawler
                    nursery.start_soon(recursive_find, child_link, on_found, depth+1)


async def godwin_find(url):
    results = []

    with trio.move_on_after(10) as cancel_scope:
        def on_found(found_url, depth):
            results.append((found_url, depth))
            cancel_scope.cancel()

        await recursive_find(url, on_found)

    if results:
        found_url, depth = results[0]
        print(f'Found Godwin point in {found_url} (depth: {depth})')
    else:
        print('No point for this article')


trio.run(godwin_find, 'https://en.wikipedia.org/wiki/My_Little_Pony')

L’idée de ce code est, partant d’un article wikipedia, de crawler ses liens récursivement jusqu’à ce qu’on trouve un article contenant des mots clés.

Au niveau des trucs intéressants:

async with trio.open_nursery() as nursery:
    for tag in body.find_all('a'):
        if tag.has_attr('href') and tag.attrs['href'].startswith('/wiki/'):
            child_link = 'https://en.wikipedia.org' + tag.attrs['href']
            nursery.start_soon(recursive_find, child_link, on_found, depth+1)

En trio, une coroutine doit forcément être connectée à une nurserie. Cela permet deux choses:

  • Rattacher la coroutine à sa coroutine parente, de cette façon (et vu que trio est implémenté intégralement en utilisant async/await au lieu de callbacks), on a donc une stacktrace claire et une exception sera toujours propagée jusqu’à la racine du programme si il le faut.
  • Borner la durée de vie de la coroutine. La nurserie est un context manager asynchrone, une fois qu’on arrive à la fin du async with, la nursery bloque tant que toutes les coroutines qu’elle gère n’ont pas terminé. Si une coroutine raise une exception, la nursery va pouvoir cancel les autres coroutines avant de re-raise l’exception en question (cf. le point précédent)

Quel intérêt à borner la durée de vie des coroutines ? Si on avait voulu écrire un truc équivalent en asyncio on aurait sans doute utilisé asyncio.gather:

coroutines = [recursive_find(link) for link in links]
await asyncio.gather(coroutines)

Maintenant on fait tourner ce code avec une connection internet un peu faiblarde (au hasard sur la box Orange de Sam ces temps ci…) les ennuis auraient commencé dès qu’une requête http aurait timeout.
L’exception de timeout aurait été récupérée par asyncio.gather qui l’aurait relancé sans pour autant fermer les autres coroutines qui auraient continué à crawler wikipedia en créant des centaines de coroutines (oui recursive_find est un peu bourrin).
De fait si on se place dans le cas d’un code tournant longtemps (typiquement on a un serveur web qui a lancé notre code dans le cadre du traitement d’une requête entrante) on va avoir bien du mal à retrouver l’état ayant mené à ce bordel.

Du coup en trio la seule solution pour avoir une coroutine qui survit à son parent c’est de lui passer une nursery en paramètre:

async def work(sleep_time, nursery):
    await trio.sleep(sleep_time)
    print('work done !')
    # Je vous ai dit qu'une nurserie contient automatiquement un cancel scope ?
    nursery.cancel_scope.cancel()

async def work_generator(nursery):
    print('bootstrapping...')
    await trio.sleep(1)
    for sleep_time in range(10):
        nursery.start_soon(work, sleep_time, nursery)

async def stop_a_first_work_done():
    async with trio.open_nursery() as nursery:
        await work_generator(nursery)
        print('Waiting for a work to finish...')

Un autre truc cool:

with trio.move_on_after(10) as cancel_scope:
    def on_found(found_url, depth):
        results.append((found_url, depth))
        cancel_scope.cancel()

    await recursive_find(url, on_found)

Vu qu’en trio on se retrouve avec un arbre de coroutines, il est très facile d’appliquer des conditions sur un sous-ensemble de l’arbre. C’est le rôle des cancel scope.
Comme pour les nursery, les cancel scope sont des contexts managers (mais synchrone ceux-ci). On peut les configurer avec un timeout, une deadline, ou bien tout simplement les annuler manuellement via cancel_scope.cancel().

Dans notre exemple, on définit un scope dont on sortira obligatoirement au bout de 10s. Pour éviter d’attendre pour rien, on annule le scope explicitement dans la closure appelée quand un résultat est trouvé.
Vu que les nurseries définies à chaque appel de recursive_find se trouvent englobées par notre cancel scope, elles seront automatiquement détruites (et toutes les coroutines qu’elles gèrent avec).

Pour faire la même chose avec asyncio bonne chance:

  • soit on passe un argument de timeout à notre appel pour récupérer la requête HTTP, mais dans ce cas pour peu que chaque requête soit individuellement plus courte que le timeout on ne s’arrêtera jamais
  • soit on gère à la mano le temps à coup de time.monotonic() en passant le temps restant autorisé aux coroutines filles. Bonjour la gueule du code.

En plus comme en parlait un mec (décidemment !), la gestion du timeout dans une socket tcp est foireuse, il suffit de recevoir un paquet (et une requête entière peut contenir beaucoup de paquets !) pour que le timeout soit remis à zéro. Donc encore une fois pas de garanties fortes quant à quand le code s’arrêtera.

3 – Eeeeet c’est tout !

Au final la doc de l’api de trio pourrait tenir sur l’étiquette de mon slip: pas de promise, de futurs, de tasks, de pattern Protocol/Transport legacy. On se retrouve juste avec la sainte trinité (j’imagine que c’est de là que vient le nom) async/await, nursery, cancel scope.

Et évidemment maintenant, l’enfoiré de hipster qui vous vend une techno à coup de whao effect avec un crawler asynchrone de 20 lignes c’est moi…

Remarquez si vous préférez la version longue je vous conseil cet excellent article de Nathaniel (je vous ai dit que ce mec était un génie ?).

4 – L’écosystème

C’est là où on se rend compte que asyncio est malgré ses lacunes une super idée: il a suffit d’écrire une implémentation de l’event loop asyncio en trio pour pouvoir utiliser tout l’écosystème asyncio (ce qui inclus donc Twisted et Tornado, snif c’est beau !).

Allez pour le plasir un exemple d’utilisation de asyncpg depuis trio:

import trio_asyncio
import asyncpg


class TrioConnProxy:
    # Le décorateur permet de marquer la frontière entre trio et asyncio
    @trio_asyncio.trio2aio
    async def init(self, url):
        # Ici on est donc dans asyncio
        self.conn = await asyncpg.connect(url)

    @trio_asyncio.trio2aio
    async def execute(self, *args):
        return await self.conn.execute(*args)

    @trio_asyncio.trio2aio
    async def fetch(self, *args):
        return await self.conn.fetch(*args)


async def main():
    # Ici on est dans trio, c'est la fête

    conn = TrioConnProxy()
    await conn.init('postgresql:///')

    await conn.execute('CREATE TABLE IF NOT EXISTS users(name text primary key)')

    for name in ('Riri', 'Fifi', 'Loulou'):
        await conn.execute('INSERT INTO users(name) VALUES ($1)', name)

    users = await conn.fetch('SELECT * FROM users')
    print('users:', [user[0] for user in users])


# trio_asyncio s'occupe de configurer l'event loop par défaut de asyncio
# puis lance le trio.run classique trio_asyncio.run(main)

En plus de ça trio vient avec son module pytest (avec gestion des fixtures asynchrones s’il vous plait) et Keneith Reitz a promis que la prochain version de requests supporterait async/await et trio nativement, elle est pas belle la vie !

]]>
Go to (in asyncio) considered harmful 26http://sametmax.com/?p=24534http://streisand.me.thican.net/sametmax.com/index.php?20180607_093129_Go_to__in_asyncio__considered_harmful_26Thu, 07 Jun 2018 07:31:29 +0000Trio, une stack toute neuve concurrente d'asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.]]>Dijkstra était un intellectuel pédant, mais quand il a écrit cette lettre célèbre, il a comme souvent mis le doigt sur un truc fondamental. Et quand l’auteur de Trio, une stack toute neuve concurrente d’asyncio, lui a fait écho 50 ans plus tard, ça a beaucoup discuté sur les mailing lists et les bugs trackers.

Nathaniel J. Smith, le dev susnommé, en a profité pour introduire une nouvelle primitive, actuellement surnommée la nursery, pour répondre au problème. Une idée visiblement tellement bonne que notre Yury préféré a décidé de la porter à asyncio. La boucle d’événements est bouclée, si je puis dire.

Mais une autre chose intéressante en découle : on a mis en lumière la présence d’un goto dans asyncio, et qu’il y a de bonnes pratiques, validées par Guido himself, pour coder avec cette lib pour éviter les douleurs.

What the fuck are you talking about ?

Le problème du goto, c’est que l’instruction permet d’aller de n’importe où à n’importe où. Cela rend le flux du programme très dur à suivre. Pour éviter cela, on a catégorisé les usages clean du goto: répéter une action, changer de comportement en fonction d’un test, sortir d’un algo en cas de problème, etc. Et on en a fait des primitives : les if, les while, les exceptions… Dans les langages les plus modernes, on a carrément viré le goto pour éviter les abus et erreurs. Joie.

Dans asyncio, le “goto” en question se trouve quand on veut lancer des tâches en arrière plan, comme ceci :

import asyncio as aio
loop = aio.get_event_loop()
aio.ensure_future(foo())  # GOTO !
aio.ensure_future(bar())  # GOTO !
loop.run_forever()

Le problème d’ensure_future() est multiple:

  • Comme son nom l’indique, cette fonction retourne un objet… Task. Ca n’a rien à voir, mais je tenais à dire à quel point c’était con de l’avoir nommé ainsi (même si techniquement, Task hérite de Future).
  • Cette ligne ne garantit en aucun cas que foo() ou bar() seront terminées à une zone précise du code. Tout au plus que tuer la boucle tue les taches. Leur flux d’exécution est complètement freestyle et décorrélé de tout le reste du programme, ainsi que de l’une de l’autres. Si ces coroutines font des await, on peut basculer de n’importe où du programme vers elles et inversement à tout moment. goto
  • Cette ligne schedule le démarrage de foo() et bar() dès que la boucle peut les lancer. Ici la boucle ne tourne pas encore. Plus le programme est complexe, plus il va devenir difficile de savoir à quelle étape logique les coroutines vont démarrer.

En prime run_forever() est un piège à con, car les exceptions qui arrivent dans la boucle sont logguées, mais ne font pas crasher le programme, ce qui rend le debuggage super rude, même avec debug mode activé (dont de toute façon personne ne soupçonne l’existence).

La solution asyncio

import asyncio as aio
loop = aio.get_event_loop()
loop.run_until_complete(aio.gather(foo(), bar())

En plus d’être plus court, les exceptions vont faire planter le programme, la loop s’arrêtera quand les coroutines auront fini leur taff, leur flux a un début et une fin encapsulés par le gather(). Ceci est encore plus visible si on met le même code à l’intérieur d’une coroutine à l’intérieur d’une coroutine à l’intérieur d’une coroutine plutôt qu’à la racine du programme. En effet dans un exemple si simple, on se borne au démarrage et à l’arrêt de la boucle. Mais je suis paresseux.

Donc, c’est la bonne pratique, mais tout le monde ne le sait pas.

Pardon, correction.

Tous les devs Python ne connaissent pas asyncio. Parmi ceux qui connaissent asyncio, une petite partie comprend comme ça marche.

Dans ce lot rikiki, un pouillième sait que c’est la bonne pratique.

En fait, gather() est probablement la fonction la plus importante d’asyncio, et pourtant elle apparaît à peine dans la doc. C’est la malédiction d’asyncio, une lib que tout le monde attendait pour propulser Python dans la league des langages avec frameworks modernes, mais qui commence à peine à devenir utilisable par le commun des mortel en 2018. Et encore.

Il ne faut jamais utiliser ensure_future() à moins de vouloir attacher un callback à la main dessus, ce qui n’est probablement jamais ce que vous voulez à cette époque merveilleuse ou existe async/await. ensure_future() est un goto, gather() est un concept de plus haut niveau.

Mais deux problèmes demeurent…

Contrairement au goto banni de Python, ensure_future() est là, et va rester. Donc n’importe quel connard peut dans un code ailleurs vous niquer profond, et en tâche de fond.

ensure_future() (ou son petit frère EventLoop.create_task()) reste le seul moyen valable pour lancer une tâche, faire quelque chose, lancer une autre tâche, puis enfin faire un gather() sur les deux tâches:

async def grrr():
    task1 = aio.ensure_future(foo())
    # faire un truc pendant que task1 tourne
    task2 = aio.ensure_future(bar())
    # faire un truc pendant que task1 et task2 tournent
    # On s'assure que tout se rejoint à la fin:
    await aio.gather(task1, task2)

Et puis, faire une pyramide de gather() dans tout son code pour s’assurer que tout va bien de haut en bas, c’est facile à rater.

La nursery : la solution de trio

Une nursery agit comme un scope qui pose les limites du cycle de vie des tâches qui lui sont attachées. C’est un gather(), sous stéroide, et avec une portée visuellement claire:

async def grrr():
    async with trio.open_nursery() as nursery:
        task1 = nursery.start_soon(foo)
        # faire un truc pendant que task1 tourne
        task2 = nursery.start_soon(bar)
        # faire un truc pendant que task1 et task2 tournent

Les taches sont garanties, à la sortie du with, de se terminer. Le ensure_future() n’a pas d’équivalent en trio, et donc aucun moyen de lancer un truc dans le vent sans explicitement lui passer au moins une nursery à laquelle on souhaite l’attacher.

Résultat, on ne peut plus faire de goto, et le flux du program est clair et explicite.

Notez que, tout comme if et while ne permettaient rien qu’un utilisateur soigneux de goto ne pouvait faire, la nursery ne permet rien qu’un utilisateur soigneux de ensure_future() ne peut faire. Mais ça force un ensemble de bonnes pratiques.

Évidemment, on peut ouvrir une nursery dans un bloc d’une autre nursery, ce qui permet d’imbriquer différentes portées, comme on le ferait avec un begin() de transaction de base de données. Or, une exception à l’intérieur d’une nursery bubble naturellement comme toute exception Python, et stoppe toutes les tâches de la nursery encore en train de tourner. Alors qu’avec asyncio vous l’avez dans le cul.

En définitive, c’était la pièce manquante. La moitié du boulot avait était faite quand on a introduit un moyen de gérer des tâches asynchrones qui dépendent les unes des autres, en remplaçant les callbacks par un truc de haut niveau : async/await. Il restait la gestion des tâches en parallèle qui se faisait encore selon les goûts et compétences de chacun, mais la nursery va remplir ce vide.

Cela devrait être intégré à asyncio en Python 3.8, soit une bonne année et demie pour ceux qui ont la chance de pouvoir faire du bleeding edge.

Comme certains ne voudront pas attendre, je vous ai fait un POC qui vous montre comment ça pourrait marcher. Mais cette version ne sera jamais utilisée. En effet, elle intercepte ensure_future() (en fait le create_task() sous-jacent) pour attacher son résultat à la nursery en cours, évitant tout effet goto, et ça péterait trop de code existant. Mon pognon est plutôt sur un gros warning émis par Python quand on fait une gotise.

Dernier mot: s’il vous plaît, allez voter pour change le nom de nursery. C’est beaucoup trop long à taper pour un truc qu’on va utiliser tout le temps.

]]>
Once you go black, you never go back 18http://sametmax.com/?p=24513http://streisand.me.thican.net/sametmax.com/index.php?20180606_131612_Once_you_go_black__you_never_go_back_18Wed, 06 Jun 2018 11:16:12 +0000PEP8 sont pour moi deux features fondamentales de Python, limitant énormément la quantité de code illisible qu'on trouve dans la communauté.]]>L’indentation obligatoire et l’existence du PEP8 sont pour moi deux features fondamentales de Python, limitant énormément la quantité de code illisible qu’on trouve dans la communauté.

Malgré cela, le reformatage de code reste une tache courante, et nécessaire, mais un gâchis énorme de temps. D’abord il faut décider comment on va formater, ce qui en équipe veut dire débat sur le pire sujet qui soit: le goût. Ensuite il faut mettre en place des configurations de linter (flake8, pylint, etc), et potentiellement l’infra qui va avec (tox, hooks git, CI…).

Pour cette raison, de nombreux outils de formatage automatique ont vu le jour. Le premier a été autopep8, et plus tard yapf de Google.

Mais ces deux outils ont quelques soucis:

  • Pas facile à faire marcher ou à configurer.
  • Ne résout pas l’éternel débat du formatage préféré qui revient dans un meeting chaque année.
  • Parfois ils ne marchent pas.
  • Parfois ils changent le sens du code (arg!).
  • Ils vous font des git diff bien velus.
  • Ne marche pas avec toutes les versions de Python.

Le monde du langage Go a choisi une stratégie différente: la technique du “ta gueule”.

Et aussi: ta gueule

Et aussi: ta gueule

Cette technique subtile et raffinée s’est incarnée dans l’outil Gofmt, qui est fourni par défaut avec go, et n’a AUCUN réglage.

Le résultat, tout le monde a fermé sa gueule et a adopté l’outil.

Est-ce que le formatage est parfait ? Non.

Est-ce qu’il plaît à tout le monde ? Absolument pas.

Est-ce qu’il fait fermer sa putain de gueule à tout le monde afin qu’on puisse enfin retourner à des choses plus importantes comme coder ?

Yes !

Gofmt produit un formatage suffisamment clair et pragmatique, et comme il est fortement ancré dans la communauté, tout le monde est à la même enseigne. Passer d’un code à un autre est facile. Pas de temps perdu à discuter du style ou à tweaker ses linters. Tout le monde lance go fmt (aka go ferme ta …) et on passe à autre chose.

Dernièrement facebook a décidé de faire pareil, et à pondu en open source black (en référence à Henry Ford), un outil de formatage en Python, qui n’a que 2 réglages. Il suit le PEP8, mais évidemment sa propre interprétation, et ne propose rien d’autre.

Black a aussi l’avantage de fournir des diffs assez petits, et surtout, vérifie si l’AST change après un reformatage, et annule le cas échéant, garantissant que le sens de votre code n’est pas altéré.

Est-ce que j’aime toutes les règles de formatages de black ? Non.

Est-ce que regarder sa sortie me donne parfois envie de me bouffer les couilles parce que franchement, qui pense que c’est une bonne idée d’aligner les choses comme ça ? Parfois.

Mais c’est good enough.

Et du coup, l’adoption de black a été très rapide dans la communauté, et il a été appliqué à heroku, requests, tablib, envoy, clint, fabric 2 et pytest. 4000 stars sur github.

Installation

Évidemment, ça se pip install, mais uniquement sur Python 3.6. Black peut checker du code 2.7, mais il lui faut du 3.6 minimum pour exister, donc on l’installe en parallèle. Évidemment, on peut l’intégrer à ST, Vim ou VSCode. Si votre projet utilise un Python different, il faut donc dans les options faire pointer l’exécutable vers l’installation séparée.

Résultat

Dans l’esprit du lien partagé par Seb, créons un générateur de titre de film porno:


import random

subject_qualifiers = ( "shy", "mature", "busty", "hot", "horny", "ebony", "quiet", "excited", "naughty", "bad", "cheating", "beautifull", "gorgeous", "drunk", "emo", "fat", "chubby", "goth", "lingery wearing", "latex enthousiast", "placid", "energic", 'slutty', 'sweaty', 'curvy', )

subjects =(
    'teen',
    'doll',
    'brunette',
    'blonde',
    'midget',
    'milf',
    'bitch',
    'babe',
    'sister',
    'step-mom',
    'vixen',
    'secretary',
    'real estate agent',
    'teacher',
    'student',
    'schoolgirl',
    'cheer leader',
    'asian tourist',
    'babysitter',
    'ex girlfriend',
    'nurse',
    'squirtter',
    'model',
    'granny',
    'furry',
)

actions = (
        "recieves anal",
        "get busted",
        "driven to bukakke",
        "taught double penetration",
        "fucked hard",
        'gently chocked',
        'punished',
        'forced into blow job',
        'pounded',
        'creampied',
        'ass raped',
        "eaten",
        "get her pussy wet",
        "shamed",
        "get an orgasm for the first time",
        'lead to loud climax',
        'offered best sex of her life',
        'worn out',
        'cured from boredom',
        'warmed up',
        'loved in and out',
        'generously oiled',
        'shocked and impressed',
        'decieved into giving it',
        'woke up roughly',
        'get sexy massage',
        'ridden to exhaustion',
        'turned into a lavish slave',
        'never submit to torture',
        'rebels against abuses',
        'taken in every possible way',
        'enjoy the 10 inches provided',
)

actors = (
        "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach"
    "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    'football team',
    'her big brother',
    'security guard',
    'hairy beast',
    'wasted guitard player',
    'hung indian immigrant',
    'a guy twice her size',
    '17 guys in a row',
    'her ideal man',
    'her secret prince charming',
    'weirdo albinos',
    'muscle giant',
    'the worst cook ever',
    'cable man',
    'more men that she can count',
    'two friendly brothers',
    'enrike strongsteel'
)

contexts = (
    "on the beach","in a cheap motel","in the back of a van",
    "in airplane toilets", "for hours", "to pay back her depts",
    "for a stupid mistake", "and it gets better", "and ask for more",
    "because she could", "in exchange for a favor",
    "right next to her boyfriend", "as a reward",
    "hopping to get him back", "caught on security cam", "every monday",
    "in a barn", "but that's not all", 'but she has a secret',
    "and she has a dick too", 'before inviting her friend over',
    'while her father is watching', 'with her ', "while auditing for a role",
    "to get her job back", "for an interview", "in exclusive sex tape",
    "again and again", ", begging to stop", "for a change", "for chrismas",
    "in public", 'in a back alley', "during a concert", 'on her death bed'
)

punctuation = ('','!','!!','...')

def get_title(subject_qualifiers, subjects, actions, actors, contexts) :


    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)

    return f"{qualifier} {subject} {action} by {actor} {context}" .capitalize()


if __name__ == "__main__":
    print(get_title(subject_qualifiers = subject_qualifiers, subjects=subjects,
                    actions=actions, actors=actors, contexts=contexts))

Usage:

$ python3.6 porn_title_generator.py
Chubby model loves bukakke by skinny geek during a concert
$ python3.6 porn_title_generator.py
Busty bitch rebel against abuses by security guard but she has a secret
$ python3.6 porn_title_generator.py
Lingery wearing student creampied by weirdo albinos on the beach
$ python3.6 porn_title_generator.py
Horny bitch offered best sex of her life by hairy beast in airplane toilets
$ python3.6 porn_title_generator.py
Emo blonde punished by airplane pilot on the beach
$ python3.6 porn_title_generator.py
Quiet squirtter lead to loud climax by wasted guitard player while auditing for a role
$ python3.6 porn_title_generator.py
Emo babysitter get her pussy wet by football team caught on security cam
$ python3.6 porn_title_generator.py
Busty asian tourist taken in every possible way by muscle giant and she has a dick too
$ python3.6 porn_title_generator.py
Placid milf ass raped by muscle giant in a back alley
Je soupçonne un coup des frères Markov

Je soupçonne un coup des frères Markov

On applique black, zero réglage, usage simplissime:

$ black . # appel recursif, modification in place par défaut

Le résultat.

import random

subject_qualifiers = (
    "shy",
    "mature",
    "busty",
    "hot",
    "horny",
    "ebony",
    "quiet",
    "excited",
    "naughty",
    "bad",
    "cheating",
    "beautifull",
    "gorgeous",
    "drunk",
    "emo",
    "fat",
    "chubby",
    "goth",
    "lingery wearing",
    "latex enthousiast",
    "placid",
    "energic",
    "slutty",
    "sweaty",
    "curvy",
)

subjects = (
    "teen",
    "doll",
    "brunette",
    "blonde",
    "midget",
    "milf",
    "bitch",
    "babe",
    "sister",
    "step-mom",
    "vixen",
    "secretary",
    "real estate agent",
    "teacher",
    "student",
    "schoolgirl",
    "cheer leader",
    "asian tourist",
    "babysitter",
    "ex girlfriend",
    "nurse",
    "squirtter",
    "model",
    "granny",
    "furry",
)

actions = (
    "recieves anal",
    "get busted",
    "driven to bukakke",
    "taught double penetration",
    "fucked hard",
    "gently chocked",
    "punished",
    "forced into blow job",
    "pounded",
    "creampied",
    "ass raped",
    "eaten",
    "get her pussy wet",
    "shamed",
    "get an orgasm for the first time",
    "lead to loud climax",
    "offered best sex of her life",
    "worn out",
    "cured from boredom",
    "warmed up",
    "loved in and out",
    "generously oiled",
    "shocked and impressed",
    "decieved into giving it",
    "woke up roughly",
    "get sexy massage",
    "ridden to exhaustion",
    "turned into a lavish slave",
    "never submit to torture",
    "rebels against abuses",
    "taken in every possible way",
    "enjoy the 10 inches provided",
)

actors = (
    "pawn shop owner",
    "corrupted cop",
    "dirty plumber",
    "big ass nigga",
    "sport coach" "her boss",
    "twisted psychiatrist",
    "ripped doctor",
    "crispy fire fighter",
    "smug playboy",
    "skinny geek",
    "eccentric millionaire",
    "airplane pilot",
    "movie star",
    "football team",
    "her big brother",
    "security guard",
    "hairy beast",
    "wasted guitard player",
    "hung indian immigrant",
    "a guy twice her size",
    "17 guys in a row",
    "her ideal man",
    "her secret prince charming",
    "weirdo albinos",
    "muscle giant",
    "the worst cook ever",
    "cable man",
    "more men that she can count",
    "two friendly brothers",
    "enrike strongsteel",
)

contexts = (
    "on the beach",
    "in a cheap motel",
    "in the back of a van",
    "in airplane toilets",
    "for hours",
    "to pay back her depts",
    "for a stupid mistake",
    "and it gets better",
    "and ask for more",
    "because she could",
    "in exchange for a favor",
    "right next to her boyfriend",
    "as a reward",
    "hopping to get him back",
    "caught on security cam",
    "every monday",
    "in a barn",
    "but that's not all",
    "but she has a secret",
    "and she has a dick too",
    "before inviting her friend over",
    "while her father is watching",
    "with her ",
    "while auditing for a role",
    "to get her job back",
    "for an interview",
    "in exclusive sex tape",
    "again and again",
    ", begging to stop",
    "for a change",
    "for chrismas",
    "in public",
    "in a back alley",
    "during a concert",
    "on her death bed",
)

punctuation = ("", "!", "!!", "...")


def get_title(subject_qualifiers, subjects, actions, actors, contexts):

    qualifier = random.choice(subject_qualifiers)
    subject = random.choice(subjects)
    action = random.choice(actions)
    actor = random.choice(actors)
    context = random.choice(contexts)

    return f"{qualifier} {subject} {action} by {actor} {context}".capitalize()


if __name__ == "__main__":
    print(
        get_title(
            subject_qualifiers=subject_qualifiers,
            subjects=subjects,
            actions=actions,
            actors=actors,
            contexts=contexts,
        )
    )

L’indentation est revue et normalisée vers 4 espaces, les espacements et sauts de ligne sont rééquilibrés (limite de caractères à 88 ), les quotes deviennent toutes ‘”‘. C’est lisible. Le code marche toujours.

Problem solved.

]]>
Je fais mon coming out 45http://sametmax.com/?p=24447http://streisand.me.thican.net/sametmax.com/index.php?20180529_134823_Je_fais_mon_coming_out_45Tue, 29 May 2018 11:48:23 +0000J’utilise VSCode

J’ai vraiment du mal à m’en remettre, et j’ai des proches qui utilisent Vim et ne sont pas toujours encore à l’aise avec l’idée. J’ai refusé d’utiliser Visual Studio à de nombreuses reprises, alors tester son petit frère était déjà un pas osé. Un truc Microsoft. Un truc écrit en Javascript.

Mais bon, j’aime ça, et il faut pas avoir honte de qui on est.

Le fait que ce soit libre et multiplateforme pour un produit Microsoft est surprenant, néanmoins c’est le maintien continu de l’excellent comportement de la team derrière qui est le plus bluffant: respectueux, proche des utilisateurs, sans bullshit…

Le fait que ce soit facile à installer et utiliser pour un projet javascript est surprenant, néanmoins c’est l’excellente performance du produit qui est le plus bluffant: temps démarrage, réactivité du scroll, gestion de gros projets…

Alors j’ai continué à le garder sous le coude, en parallèle à Sublime Text.

Et quelque chose de subtil a changé, chaque jour, sublime dont j’ai pourtant payé la licence, me faisait de moins en moins bander. Je sollicitais de plus en plus VSCode. Jusqu’à ce que ça devienne mon éditeur par défaut.

Oh, ST et moi on se voit toujours. Pour ouvrir un petit fichier vite fait, taper un article, tester un truc.

En revanche dès que c’est un projet, j’ai un éditeur Electron made in Redmond pour ça, et il me rend heureux.

L’ergonomie de la bestiole

Les auteurs de VSCode ont pompé tous les éditeurs les plus populaires, goulûment. Ils ont optimisé le temps de démarrage à mort, et même si on n’a pas la vitesse d’un ST ou d’un Vim, ça reste moult fois plus rapide que la vaste majorité de la concurrence. Pas de splash screen à rallonge et ce moment de doute où on n’est pas sûr d’avoir vraiment cliqué sur le bouton. J’aime bien Jetbrain mais le startup de PyCharm me fout les boules à chaque fois.

Côté apparence, on retrouve des lignes épurées avec peu de boutons, des tabs, la fameuse bird view du code de ST, un Head Up Display, une statut-bar très riche et le “go to anywhere” que tout le monde adore depuis Mate.

La force de VSCode c’est l’expérience de son équipe : ils ont bien compris ce que les utilisateurs faisaient le plus souvent, et l’ont mis à porter de main. Un clic pour faire un split view ou afficher le terminal intégré. Mais pas de fonction “Imprimer”. Une barre latérale donne l’accès à 4 autres modes, un pour la recherche dans tout le projet, un pour git (et rien d’autre), un pour le debuggeur intégré, et un pour installer des extensions.

Une foule de choses sont configurables, avec une interface qui mélange fichiers de config et aide à la saisie. C’est étrange la première fois qu’on met le nez dans “paramètres de l’utilisateur” ou “ouvrir les raccourcis clavier”. Ni vraiment une fenêtre avec des formulaires. Ni vraiment un JSON à éditer à la main. Un peu des deux. Et c’est super bien fait.

Ceci dit, comme les réglages par défaut sont assez sains, un junior n’aura pas à s’en soucier et pourra tout de suite commencer à introduire des bugs dans votre projet.

L’éditeur

Aucune innovation. Aucune tentative de faire différent de la concurrence. C’est du classique, c’est propre, et ça marche. On peut bien entendu choisir entre plusieurs mode de saisie (mode VI, Emarcs, Sublime, etc), mais perso je reste avec le mode original et quelques raccourcis custo.

Derrière, toutes les fonctionnalités modernes sont là: multi-curseur, sélection/recherche incrémentale, snippets (emmet inclus !), complétion des mots les plus utilisés, navigation par symbole, hot exit. L’avantage, c’est que comme VSCode joue la carte de l’interface minimaliste, on n’a pas besoin de connaitre tout ça, et on peut juste commencer à taper, tout en apprenant chaque feature au fur et à mesure de ses progrès. C’est un excellent éditeur pour débutant en ce sens. Mais les powers users qui aiment malgré tout la souris et les onglets y trouveront leur compte.

La coloration syntaxique est irréprochable (heureusement), mais on voit qu’ils ont du faire des concessions. Par exemple au démarrage, seule la partie de votre viewport est colorée. Il faut attendre une à deux secondes sur les gros projets pour que le reste du fichier le soit, histoire de pas freezer tout le bouzin.

Le bon côté de ça c’est que c’est très fluide. Bon évidemment j’ai 8 coeurs et 32Go de RAM. J’ai tenté l’aventure sur une VM avec 2 de rames et un tout petit coeur, et c’est pénible. Au repos avec quelques tabs ouverts, le truc s’engouffre quand même ses 700Mo de mémoire vive. N’oubliez pas que c’est du V8 derrière.

En comparaison ST en bouffe 300, et Vim, heu, LOL.

Intégration Git

L’intelligence de cette feature, c’est qu’ils se sont limités aux opérations simples et courantes. Permettre de naviguer dans l’espace temps ou de lancer son merge --rebase, c’est dur à faire correctement. Donc VSCode n’essaye pas.

Il affiche juste la liste des fichiers qui sont modifiés et/ou en staging, permet de les bouger de l’un à l’autre ou annuler les modifications, et de faire un commit rapidement. Un clic sur un fichier l’ouvre en mode diff avec HEAD. C’est tout.

C’est pas 1% de ce que permet de faire Git.

Mais c’est facilement 69% de mon usage de Git. Du coup c’est super pratique. Combiné avec le terminal intégré, et vous pouvez gérer presque tout le repo sans sortir de l’éditeur.

Le debuggeur

Je ne l’utilise jamais et je préfère ipdb. Pour le moment, en Python, il est trop lent. Les devs JS en disent du bien, vu qu’apparemment il est capable de se connecter directement au navigateur et comprend TypeScript de manière transparente.

La recherche

Rien à dire. C’est rapide. Ça marche. Ça supporte les trucs les plus importants: case insensitive (activé par défaut), regex, in sélection, dans tous les fichiers, filtrés par extension, et tout le bordel. Cliquer sur le résultat ouvre le fichier à la bonne ligne. Pas de modale qui bloque l’UI.

Pas de surprise, donc. Mais pas de mauvaises surprises.

L’indexage est configurable par projet, ce qui est indispensable dès que vous avez quelque chose d’un peu complexe.

La recherche de fichiers par nom est absente puisque ferait doublons avec “Go To Anywhere”.

Intégration des langages

Là, on attaque la partie intéressante. VSCode est neutre dans son traitement des langages, et toutes les features avancées se font donc via des extensions. L’astuce, c’est que l’équipe supporte officiellement certaines extensions, et elles sont donc d’excellente qualité.

Le support de Python est phénoménal. C’est simplement le meilleur après celui de PyCharm (et de pas beaucoup), ce qui n’est pas peu dire, vu que Jetbrain fait probablement des messes noires et des sacrifices à Quetzalcoatl pour obtenir ce résultat.

Python est notoirement difficile à outiller de par son très grand dynamisme.

Mais là, c’est beau.

Pylint est activé par défaut, et flake8 ainsi que mypy sont optionnellement activables. Leurs préréglages sont de bonne qualité, particulièrement celui de mypy qui est normalement inutilisable out of the box. L’éditeur vous prompte pour l’installation quand il détecte qu’ils sont absents, et lance tout ça pour vous.

Tout est configurable par projet, et donc si vous spécifiez un virtualenv pour votre projet (ce qui vous devriez toujours faire), VSCode va détecter que les outils ne sont pas dedans, vous proposer de les installer, et le faire pour vous.

Du coup, bénéficier des types hints, de la détection des erreurs de syntaxes, des variables non déclarées et des imports manquants ou inutiles est beaucoup plus facile que sur n’importe quel compétiteur. Ok, sauf PyCharm. Mais personnellement je l’appelle PyChiderme.

Si VSCode ne supporte pas nativement un outil, il existe probablement une extension pour ça. Par exemple, il y a une extension pour black, qui est à Python ce que Gofmt est à Go, et que j’installe donc maintenant à chaque nouveau projet.

L’intégration de ces outils est excellente :

  • Les linters de Python sont naturellement lents, mais VSCode les lance en asynchrone et ça ne ralentit pas son UI.
  • L’affichage des erreurs est claire, mais discret. Ça limite l’effet sapin de Noël.
  • à côté du terminal intégré existe une fenêtre qui liste toutes les erreurs par fichier. On peut ainsi parcourir son projet erreur par erreur.

VScode m’a même surpris à détecter mes tests unitaires, m’a proposé d’installer pytest puis de lancer tout ça.

Cependant, pour vraiment parler de l’intégration de Python dans VSCode, il me faut mentionner IntelliSense. C’est un terme marketing inventé par MS pour caractériser toutes les fonctionnalités autour de la compréhension que l’éditeur à du code, et des opérations qu’il propose dessus.

Ok, ok, c’est un mot 100% bullshit.

Mais bordel, ça marche.

La complétion du code est excellente, et marche sans aucun réglage. Avec la lib standard bien entendu, mais aussi avec votre code, et toutes les libs installées dans votre virtualenv (si vous avez précisé le chemin vers ledit env dans les settings du projet, of course, il est pas devin).

VSCode affiche les docstrings, les params et propose d’aller à la définition de n’importe quoi en un clic.

Et si comme moi vous avez passé un temps fou à essayer d’obtenir le même résultat sous ST/Vim/Whatever en chargeant what mille plugins et en changeant 600 valeurs de configs, vous comprendrez que c’est juste, topissime.

Quelques infos

Les réglages de VSCode de base sont bons. C’est vraiment une partie de ce qui fait la force du projet: moins de bordel à faire soi-même. Mais comme il est bien configurable, il ne faut pas s’en priver. Quelques trucs que je fais toujours:

Installer une police avec des ligatures

Genre Fira-Code.

Et activer les settings:

    "editor.fontFamily": "'Fira Code', 'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'",
    "editor.fontLigatures": true,

Exclure plein de fichiers

J’ai pas du tout envie que “Go to anywhere”, la recherche des fichiers ou l’indexage git charge des trucs inutiles. Donc j’ai des settings de base de nazi:

    "files.exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/.DS_Store": true,
        "**/dist": true,
        "**/build": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/node_modules": true,
        "**/bower_components": true,
        "**/vendors": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },
    "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/node_modules/**": true,
        "**/build/**": true,
        "**/dist/**": true,
        "**/env/**": true,
        "**/venv/**": true,
        "**/virtualenv/**": true,
        "**/bower_components/**": true,
        "**/vendors/**": true,
        "**/__pycache__": true,
        "**/**/*.pyc": true
    },

Mes settings par projet sont généralement encore plus restrictifs.

Je change les params de zoom

 "window.zoomLevel": 2,
 "editor.mouseWheelZoom": true,
 "editor.fontSize": 10,

Je vire la télémétrie

Je suis pas sous Windows 10, merde.

    "telemetry.enableCrashReporter": false,
    "telemetry.enableTelemetry": false,

Je mets des barres verticales

    "editor.rulers": [
        79, # PEP8
        88, # Black
        120 # Javascript
    ],

Ergonomie perso

    "editor.renderWhitespace": "none", # overridé par projet
    "editor.renderIndentGuides": true,
    "editor.minimap.enabled": true,
    "editor.minimap.renderCharacters": true,
    "editor.autoIndent": true,
    "window.restoreWindows": "all",
    "window.openFoldersInNewWindow": "on",
    "editor.acceptSuggestionOnEnter": "off",
    "editor.tabCompletion": true,
    "emmet.triggerExpansionOnTab": true,

Pour un Python heureux

    "python.venvPath": "~/.local/share/virtualenvs/",
    "python.linting.mypyEnabled": true,
    "python.linting.enabled": true,
    "python.pythonPath": "/usr/bin/python3.6", # je l'override dans les settings de projet
    "black.path": "/home/user/.local/bin/black", # black a besoin de Python 3.6
    "python.formatting.provider": "none", # pour black
    "editor.formatOnPaste": true,
    "files.associations": {
        ".pylintrc": "ini"
    },
    "python.linting.flake8Enabled": true,
    "python.unitTest.pyTestEnabled": true,
    "python.linting.pylintEnabled": true,

J’installe généralement ces extensions

  • Python. Logique.
  • Color highlight pour que les code hexa soient surlignés avec la couleur qu’ils représentent.
  • Black, parce que je ne veux plus jamais reformater du code manuellement de ma vie.
  • Django template et jinja, pour un meilleur support des templates django et jinja en coloration syntaxique.
  • Editor config parce que j’ai toujours un .editorconfig à la racine de mes projets.
  • gitignore, systemd-init-file, restructured text afin d’avoir la coloration syntaxique pour eux aussi
  • path intellisense, comme ça j’ai la complétion sur les chemins de fichier.
  • Rainbow CSV, qui affiche chaque colonne d’un CSV dans une couleur différente.
  • git history pour afficher l’historique d’un fichier ou d’une ligne.

Astuces utiles

VSCode vient avec les raccourcis traditionnels des éditeurs graphiques: ctrl + F pour rechercher, ctrl + shift + f pour rechercher dans le projet, ctrl + d pour la sélection incrémentale, ctrl + p pour le “go to anywhere”, ctrl + shift + p pour le head up display, ctrl + s pour sauvegarder, ctrl (+shift) pour se balader (sélectioner) de mot en mot, etc.

Il possède aussi quelques trucs sympas dont on parle moins dans les tutos:

  • clic milieu active la sélection verticale
  • Dans un shell, code -r ouvre un fichier dans la fenêtre en court, et code -n dans une nouvelle fenêtre. code --diff foo bar ouvre les deux fichiers en mode comparaison.
  • Dans un projet, le fichier .vscode/tasks.json contient la configuration des taches à lancer. Compilation, tests unitaires, debuggage, serveur… On peut lui faire faire n’importe quoi.
  • “Join line” n’a aucun shortcut par défaut. C’est une opération courante, donc je vous conseille de la mapper. Perso je la mets sur ctrl + j.
  • Ctrl+Shift+ O permet de se déplacer vers n’importe quel symbole. C’est pratique, mais personne ne s’en souvient jamais. Plus facile à retenir: faire un “go to anywhere” (ctrl + P) et commencer la recherche par @. Ou @: pour regrouper les symboles par nature (classes, fonctions, etc). Le “go to symbole” marche sur tout le projet avec Ctrl + T, mais c’est lennnnnnnnnnnnt.
  • De la même manière, on peut aller à un numéro de ligne avec Ctrl+ G. Mais c’est plus facile de faire un “go to anywhere” puis de taper :. Ou depuis un terminal code file.ext:numligne
  • Ctrl+ U, aka “putain nooooooooooooooon, j’ai fais un ctrl + d de trop”.
  • Ctrl + k + ctrl + f appliquer le formateur à la sélection. Pour les gens qui ont des TOC.
  • VSCode a une preview pour le markdown. Facile à lancer depuis le HUD
  • Une fois que vous avez pris l’habitude d’utiliser “go to definition” dans le menu contextuel, sachez que ctrl + clic fait pareil :)

Un snippet perso que je rajoute également (dans $HOME/.config/Code/User/snippets/python.json):

   "wrap_in_try_except": {
        "prefix": "try",
        "body": [
          "try:",
          "\t${0}${TM_SELECTED_TEXT}",
          "except ${1:Exception}:",
          "\t${2:import pdb; pdb.set_trace()}"
        ],
        "description": "Wrap in try/except"
    },

Ça permet de sélectionner un truc, de taper try puis tab et avoir le tout wrappé dans un try/except.

Le futur

Depuis sa sortie, l’éditeur est en constante amélioration. Les mises à jour sont toujours une excellente surprise, avec des tas de goodies, y compris dans les extensions.

Mais là, dans la dernière version instable (qui a la bonne idée de ne pas overrider la stable à l’installation), VSCode vient avec une preview de l’édition collaborative. Genre Google doc, mais pour le code, et dans tout l’éditeur.

La partie chiante, c’est qu’il faut un compte (Microsoft évidemment), et donc que ça passe par leurs serveurs.

La partie amazing par contre, c’est que ça envoie du poney nucléaire. L’ouverture des onglets, l’écriture, le scroll… Tout se synchronise proprement. Si on décide de faire sa vie, VSCode désynchronise la navigation, et permet à tout le monde de travailler en parallèle sur le projet (et même optionnellement donner accès à son terminal). Si on veut de nouveau voir la navigation de l’autre, on peut demander de le suivre à nouveau, et pouf, on suit ce qu’il fait en live.

Testé avec des clients à des milliers de borne. C’est bluffant.

Vous l’avez compris

]]>
Trier un CSV de 5 Go 15http://sametmax.com/?p=24323http://streisand.me.thican.net/sametmax.com/index.php?20180514_102612_Trier_un_CSV_de_5_Go_15Mon, 14 May 2018 08:26:12 +0000un célèbre post de Guido sur la réponse à la blague "comment trier un million d'entiers avec 2M de Ram", j'ai réalisé 2 choses:
  • Le contenu de l'article est génial.
  • Le contenu de l'article est incompréhensible.
]]>
Marrant, j’ai jamais eu autant de RAM, et j’ai jamais eu autant de problèmes de RAM. On est en train de faire un bon dans inefficacité des programmes, et ça va pas aller en s’arrangeant entre docker, electron et nos chers navigateurs. Une grosse app Python peut devenir assez velue aussi niveau mémoire, même quand on n’est pas un boulet comme moi.

Et justement, en relisant un célèbre post de Guido sur la réponse à la blague “comment trier un million d’entiers avec 2M de Ram”, j’ai réalisé 2 choses:

  • Le contenu de l’article est génial.
  • Le contenu de l’article est incompréhensible.

Or c’est un peu ma raison d’être, si vous voulez, de prendre les trucs cools mais imbitables et les rendre utilisables.

Aujourd’hui, donc, on va trier un CSV de 5Go en Python. Ça parle plus qu’un fichier de nombres, et puis ça évite d’expliquer le module array qui est juste une optimisation.

J’ai pris mes petites mimines, et j’ai pondu un CSV qui contient 63Mo de:

A,01/02/2016,2
A,13/07/2011,1
B,24/01/1996,3
C,30/12/1999,1
D,13/07/2011,3
D,01/02/2016,5
E,24/01/1996,4
F,30/12/1999,1
G,13/07/2011,4
H,01/02/2016,4
I,01/02/2016,5
I,13/07/2011,2
A,01/02/2016,2
A,13/07/2011,1

En copier/coller.

Puis j’ai lancé un script pour dupliquer le bébé et jusqu’à atteindre 5,9 Go de taille:

with open('data.csv') as infile:
    with open('data2.csv', 'w') as outfile:
        for x in range(100):
            outfile.write(infile.read())
            infile.seek(0)
Si jamais vous doutiez que je vous aime...

Si jamais vous doutiez que je vous aime…

395366400 lignes. Jusqu’ici tout va bien.

Maintenant, supposons qu’on veuille trier sur la date. Si vos souvenirs en Python sont exacts (ou si vous avez lu notre super article sur Ordonner en Python), vous savez que la solution naïve est de loader le fichier cash pistache, puis d’appeler dessus sorted() avec le paramètre key.

D’abord, il faut choisir le callback à passer à key, c’est à dire la fonction qui va être exécutée pour chaque ligne pour extraire la date de la ligne et permettre ainsi à sorted() de comparer avec les autres dates.

>>> str_date = "A,01/02/2016,2".split(',')[1] # récupère la date uniquement
>>> str_date
'01/02/2016'
>>> ''.join(str_date.split('/')[::-1]) # on inverse la date avoir une valeur ordonnable
'20160201'

On en fait une fonction:

 
def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])

Ensuite on a juste à ouvrir le fichier, trier les lignes, et sauvegarder tout ça dans l’autre fichier:

with open('data2.csv') as infile:
    with open('sorted_data.csv', 'w') as outfile:
        # on fait le tri
        sorted_lines = sorted(infile, key=extract_date)
        # on ecrit les lignes triées dans le nouveau fichier
        outfile.writelines(sorted_lines)

Easy money, double poney.

Enfin avec un fichier de quelques Mo. Parce que si vous faites ça dans un fichier de 5,9 Go, votre RAM va vomir comme une pom pom girl anorexique.

sorted() sur un disque complet, illustré

sorted() sur un disque complet, illustré

Comment résoudre ce problème ?

Et bien en faisant tout pareil, mais avec des petits morceaux !

import heapq

from tempfile import TemporaryFile
from itertools import islice

# On garde notre fonction key
def extract_date(ligne):
    str_date = ligne.split(',')[1]
    return ''.join(str_date.split('/')[::-1])

# Liste de tous les fichiers temporaires avec les lignes triées
sorted_temp_files = []

with open('data2.csv') as infile:
    progress = 0
    while True:
        # On lit seulement 3000000 lignes sur 395366400 à chaque tour de boucle
        lines = list(islice(infile, 3000000))

        if not lines:  # plus de ligne ? On sort
            break

        # On affiche où on en est
        print("{:.2f}%".format(progress))
        progress += (3000000 / 395366400 * 100)

        # On tri les lignes, comme avec sorted() mais sur place. 
        # Ça évite de dupliquer les données en mémoire.
        lines.sort(key=extract_date)

        # On crée un fichier temporaire qui va contenir les 3000000 lignes
        # triées
        f = TemporaryFile(mode="r+")
        f.writelines(lines)

        # On rembobine le fichier pour pouvoir relire le contenu plus tard
        f.seek(0)

        # On balance le fichier dans la liste des fichiers triés
        # L'objet fichier hein. Pas le chemin du fichier. C'est pour ça qu'on
        # a fait .seek(0) avant. On va en avoir besoin plus bas.
        sorted_temp_files.append(f)

    # Toute la magie se fait là.
    # On utilise heapq.merge(), qui prend en paramètre plein de trucs qu'on 
    # vient de trier, et permet de se balader dedans comme si c'était un seul
    # gros truc trié, mais sans tout charger en mémoire.
    # En gros il regarde les premières valeurs de tous les itérables, les compares,
    # puis fait retourne un générateur qui yield tout dans l'ordre 
    with open('sorted_data.csv', 'w') as outfile:
        for ligne in heapq.merge(*sorted_temp_files, key=extract_date):
            outfile.write(ligne)

Au lieu de charger les 5 Go en mémoire, on plafonne à 400 Mo. Une raison de plus d’apprendre à utiliser les générateurs.

Sametmax, c'est de la bombe

Sametmax, c’est de la bombe

Alors évidemment, c’est long. Y a genre 40 minutes de traitement. Moins si on utilise pypy et qu’on accepte de up la conso RAM. On code pas en Rust :)

Si vous avez besoin de perfs et que Python reste votre outil de prédilections, 3 solutions restent à votre disposition:

– Prétraiter le fichier en lui rajoutant une première colonne qui contient un timestamp. Ca c’est super rapide à trier.
– Utiliser un truc de numpy ou pandas comme np.memmap().sort(kind=’merge’). C’est du C, donc ça speed.
Acheter de la ram et tout trier en mémoire avec la solution 1.

EDIT:

Un lecteur m’a alpagué sur twitter pour me dire qu’on peut faire ça plus vite et plus facilement avec sort. Oui, évidement.

L’exercice est académique, il est simplifié pour vous permettre de comprendre heapq.merge(). En réalité, on aura vraiment besoin de ça que pour un cas complexe. Exemple, vous lisez un flux de log une socket et vous checkez les adresses IP d’abord pour filtrer celles de la liste noire, puis pour les faire matcher une zone géographique et vous triez tout ça et vous le passez à la suite du pipeline. Et le tout doit marcher sous Linux et Windows Server avec juste la lib standard parce que diab est de mauvais poil.

Évidement que si vous voulez juste trier un CSV vous n’allez pas coder un script python de 30 lignes.

]]>
13558 Go de rames 25http://sametmax.com/?p=24187http://streisand.me.thican.net/sametmax.com/index.php?20180315_070104_13558_Go_de_rames_25Thu, 15 Mar 2018 06:01:04 +0000J’ai une tache celery qui me génère des données de test. Elle est lancée toutes les 5 minutes pour simuler le crawling d’un site qui popule une base de données, le tout piloté par l’ORM django, puis dumpé dans redis.

Jusqu’ici tout va bien.

Mais après un certain temps, ma machine rame, puis se frise.

Et j’ai du mal à y croire. J’ai 8 coeurs, un 32 Go de mémoire vive, un SSD d’un putain de To. On va pas me la faire à l’envers, c’est pas un def tout moisi qui va me faire trembler les genoux. C’est forcément un de ces cons de wallets que j’ai laissé ouvert, encore codé par un nantais ça !

Mais après une enquête minutieuse, qui a consisté en subtilement killer tous mes processus un par un avec echo "douceur" | sed s/c/l, le constat est là.

Eukekaca

Eukekaca.

Cette fonction est responsable:

def generate_fake_stats(x=1000):

    cur_stats = {
        s.currency.short_code: s
        for s in CurrencyMarketStatsFactory.build_batch(x)
    }
    
    mn_stats: Dict[str, MNMarketStats] = {
        s.masternode.coin.short_code: s
        for s in MNMarketStatsFactory.build_batch(x)
    }

    stats = []

    for code, mns in mn_stats.items():

        cstats: CurrencyMarketStats = cur_stats.get(code)

        if not cstats:
            continue

        dollar_value = float(cstats.dollar_value),
        collateral = mns.masternode.collateral

        stats = {
            'created': int(mns.created.timestamp()),
            'name': cstats.currency.name,
            'code': code,
            'title': f'{cstats.currency.name} ({code})',
            'marketcap': dollar_value * cstats.supply,
            'dollar_value': dollar_value,
            'change_rate': cstats.change_rate,
            'volume': float(cstats.volume),
            'supply': cstats.supply,
            'roi': mns.roi,
            'mn_worth': collateral * dollar_value,
            'node_count': mns.node_count,
            'required_coins': collateral,
        }

    RedisClient.get_instance().jset('marketstats', stats)
    return stats

Il m’a fallu une bonne heure pour trouver ma connerie. J’ai changé plein d’options, mis DEBUG sur False, limité la mémoire de Redis, etc.

Mais non, c’était mon code. Qui générait au bas mot 847390982*1000*16 octets de données, soit 1,355825571×10¹³ pour mes objets Python.

J'ai un nouveau mapping clavier qui permet de n'utiliser qu'un doigt

J’ai un nouveau mapping clavier qui permet de n’utiliser qu’un doigt

Il y en a un peu plus madame, je vous le mets quand même ?

Nan parce que 13 To pour une pov liste de dicos, c’est la boucherie.

Alors, sachant que je vous ai éliminé la recherche des causes parallèles et que vous savez que le bug est de ma (grande et stupide) faute, saurez-vous trouver dans ces lignes le d20 qui tombe sur un 1 à tous les jets ?

Si vous ne trouvez pas, la réponse dans quelques jours.

Explain all the humans !

Explain all the humans !

EDIT pour la réponse:

Comme plusieurs personnes l’ont compris, c’est la virgule sur:

    dollar_value = float(cstats.dollar_value),

Qui est responsable de tout ce malheur.

En effet plus loin on fait:

    'mn_worth': collateral * dollar_value,

Ce qui, au lieu de multiplier un entier par un float, multiplie un entier par un tuple. En python, c’est légal, et ça donne ça:

>>> 10 * (7808979.8989,)
(7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989)

Si collateral est élevé, ce qui est ici mon cas, ça fait de très gros tuples, et le tout dans une boucle.

]]>
Do you rotate ? 2http://sametmax.com/?p=24161http://streisand.me.thican.net/sametmax.com/index.php?20180305_042927_Do_you_rotate___2Mon, 05 Mar 2018 03:29:27 +0000le log de debug du wallet de la coin smartcash, ça peut prendre par surprise.]]>L’explosion de la taille des logs a tué plus d’un serveur. Il est amusant de se dire que c’est du texte qui peut mettre à genoux un serveur de streaming vidéo avec 32 coeurs, 2To de SSD et 64 Go de RAM. Mais un bon access log non contrôlé sous nginx ou même le log de debug du wallet de la coin smartcash, ça peut prendre par surprise.

Le module logging de Python a d’excellentes options pour gérer la taille des logs, notamment le RotatingFileHandler.

Sous Linux, c’est logrotate qui fait le boulot pour le système. C’est performant, ça marche super bien, c’est hyper simple à utiliser, et c’est dispo partout.

Bref, en prod, un petit vi sur /etc/logrotate.d/ton_projet et pouf, on peut demander une rotation en 10 lignes:

/var/www/ton_projet/*.log
{
    size 100K
    daily
    rotate 5
    missingok
    notifempty
}

Y a rien à activer, un cron lance déjà logrotate tous les jours.

Par contre, si votre process utilise systemd pour démarrer, inutile de logger manuellement. Tout ce qui est sur la sortie standard est automatiquement loggué avec rotation et accessible avec un petit journalctl -u ton_process.service --since today.

Bref, attendez encore un peu avant de rajouter logstash à votre stack.

]]>
Introduction aux extensions Python avec CFFI 16http://sametmax.com/?p=24059http://streisand.me.thican.net/sametmax.com/index.php?20180207_225849_Introduction_aux_extensions_Python_avec_CFFI_16Wed, 07 Feb 2018 21:58:49 +0000Ceci est un post invité de Realitix posté sous licence creative common 3.0 unported.

Préambule

Vous avez réalisé une analyse de votre code et vous avez un bottleneck ?
Vous souhaitez utiliser une bibliothèque bas niveau (C/C++/Rust) ?

Pas de problème, dans cet article, je vais vous expliquer les différentes solutions et pénétrer en profondeur dans la plus charmante d’entre elles, son petit nom: CFFI.

Sam&Max me faisant l’honneur d’accepter mon article, je vais suivre la guideline du site avec un langage détendu et beaucoup d’exemples.

Qu’est-ce qu’une extension Python ?

Guido, pendant l’acte créateur, n’a pas oublié une chose importante: les extensions Python !
Une extension Python est un module compilé pouvant être importé dans votre code Python.
Cette fonctionnalité est très puissante: cela vous permet d’utiliser un langage bas niveau (et toutes ses capacités) pour créer un module Python.
Vous utilisez très probablement des extensions Python dans vos projets sans le savoir.
Par exemple, si vous souhaitez embarquer la bibliothèque de calcul physique Bullet, vous pouvez le faire au sein d’une extension Python.

Il y a deux points à différencier:

  • Importer une bibliothèque tierce
  • Améliorer les performances de son code

En y réfléchissant bien, créer un code performant revient à créer une bibliothèque tierce et l’importer au sein de l’interpréteur.

Ça laisse rêveur, alors comment fait-on ?

Plusieurs solutions:

  1. L’API C de CPython. Y’en a qu’ont essayé, ils ont eu des problèmes…
    En utilisant cette méthode, vous aurez accès à tout l’interpréteur Python et vous pourrez tout faire… mais à quel prix ?
    Je ne vous recommande pas cette approche, je me suis cassé les dents pendant 4 mois dessus avec un succès mitigé.
  2. Cython est une bonne solution mais plus orienté sur l’optimisation de code.
  3. CFFI: le saint Graal, alléluia!

CFFI: Première mise en bouche

CFFI va vous permettre de créer des extensions Python mais pas que…
Tout comme le module ctypes, il va permettre d’importer une bibliothèque dynamique au runtime.
Si vous ne savez pas ce qu’est une bibliothèque, c’est par ici.

CFFI va donc vous permettre:

  • D’importer une bibliothèque dynamique au runtime comme ctypes mais avec une meilleure API -> Mode ABI -> Pas de compilation
  • De réaliser une extension Python compilée comme `cython` ou comme avec l’API C de CPYTHON -> Mode API -> Phase de compilation

Par rapport à ctypes, CFFI apporte une API pythonic et légère, l’API de ctypes étant lourde.
Par rapport à l’API C de CPython… Ha non! Je n’en parle même pas de celle-là!

J’ai dit qu’il y aurait beaucoup d’exemples, alors c’est parti !

D’abord, on installe le bouzin, il y a une dépendance système avec libffi, sur Ubuntu:

sudo apt-get install libffi-dev python3-dev

Sur Windows, libffi est embarquée dans CPython donc pas de soucis.
Ensuite, on conserve les bonnes habitudes avec le classique:

pip install cffi

Je vous conseille d’utiliser un virtualenv mais ce n’est pas le sujet!

Les trois modes

Il y a trois moyens d’utiliser CFFI, comprenez bien cela car c’est la partie tricky:

  1. Le mode ABI/Inline
  2. Le mode API/Out-of-line
  3. Le mode ABI/Out-of-line

On a déjà évoqué les modes ABI et API, mais je n’ai pas encore parlé de Inline et Out-of-line.
CFFI utilise une phase de “compilation” pour parser les header C. Ce n’est pas une vraie compilation mais une phase de traitement qui peut être lourde.
Le mode Inline signifie que ce traitement va être effectué à l’import du module alors que Out-of-line met en cache ce traitement à l’installation du module.

Evidemment, le mode API/Inline ne peut pas exister puisque le mode API impose une phase de “vraie” compilation.

Le mode ABI/Inline

# On commence par import le module cffi qui contient la classe de base FFI
from cffi import FFI
 
# 1 - On instancie l'object FFI, cet objet est la base de cffi
ffi = FFI()
 
# 2 - On appelle la méthode cdef.
# Cette méthode attend en paramètre un header C, c'est à dire
# les déclarations des fonctions C qui seront utilisées par la suite.
# CFFI ne connaîtra que ce qui a été déclaré dans le cdef.
# La puissance de CFFI réside dans cette fonction, à partir d'un header C,
# il va automatiquement créer un wrapper léger.
# A noter: le code dans cdef ne doit pas contenir de directive pré-processeur.
# Ici, on déclare la fonction printf appartenant au namespace C
ffi.cdef("""
    int printf(const char *format, ...);
""")
 
# 3 - On charge la bibliothèque dynamique
# dlopen va charger la biliothèque dynamique et la stocker dans la variable nommée cvar.
# L'argument passé est None, cela demande à cffi de charger le namespace C.
# On peut ici spécifier un fichier .so (Linux) ou .dll (Windows).
# Seul ce qui a été déclaré dans cdef sera accessible dans cvar.
cvar = ffi.dlopen(None)

Comme vous pouvez le voir dans ce bout de code, CFFI est très simple d’utilisation, il suffit de copier le header C pour avoir accès aux fonctions de la bibliothèque.
A noter: si les déclarations dans le cdef ne correspondent pas aux déclarations présentes dans la bibliothèque (au niveau ABI), vous obtiendrez une erreur de segmentation.

Le mode API/Out-of-line

Pour bien comprendre ce mode, nous allons implémenter la fonction factorielle.

# Comme pour le mode ABI, FFI est la classe principale
from cffi import FFI
 
# Par convention, en mode API, on appelle l'instance ffibuilder car le compilateur va être appelé
ffibuilder = FFI()
 
# En mode API, on utilise pas dlopen, mais la fonction set_source.
# Le premier argument est le nom du fichier C à générer, le 2e est le code source.
# Ce code source va être passé au compilateur, il peut donc contenir des directives pré-processeur.
# Dans l'exemple, je passe directement le code source mais en général, on va plutôt ouvrir le fichier avec open().
ffibuilder.set_source("_exemple", """
    long factorielle(int n) {
        long r = n;
        while(n > 1) {
            n -= 1;
            r *= n;
        }
        return r;
    }
""")
 
# Comme pour le mode ABI, on déclare notre fonction avec la méthode cdef.
ffibuilder.cdef("""
    long factorielle(int);
""")
 
 
# Enfin, on va appeler la méthode compile() qui génère l'extension en 2 étapes:
# 1 - Génération d'un fichier C contenant la magie CFFI et notre code C
# 2 - Compilation de ce fichier C en extension Python
if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

Après éxécution du script, voici ce que l’on voit dans le terminal:

generating ./_exemple.c  -> Étape 1: Génération du fichier C
the current directory is '/home/realitix/test'
running build_ext
building '_exemple' extension  -> Étape 2: Génération de l'extension
x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -g -fdebug-prefix-map=/build/python3.6-sXpGnM/python3.6-3.6.3=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -I/home/realitix/venv/py36/include -I/usr/include/python3.6m -c _exemple.c -o ./_exemple.o
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -Wl,-Bsymbolic-functions -specs=/usr/share/dpkg/no-pie-link.specs -Wl,-z,relro -g -fdebug-prefix-map=/build/python3.6-sXpGnM/python3.6-3.6.3=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 ./_exemple.o -o ./_exemple.cpython-36m-x86_64-linux-gnu.so

Et vous pouvez trouver l’extension Python `_exemple.cpython-36m-x86_64-linux-gnu.so`.
Étudions le module généré, dans un interpréteur Python:

>>> from _exemple import ffi, lib
>>> dir(ffi)
['CData', 'CType', 'NULL', 'RTLD_DEEPBIND', 'RTLD_GLOBAL', 'RTLD_LAZY', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_NOW', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'addressof', 'alignof', 'buffer', 'callback', 'cast', 'def_extern', 'dlclose', 'dlopen', 'errno', 'error', 'from_buffer', 'from_handle', 'gc', 'getctype', 'init_once', 'integer_const', 'list_types', 'memmove', 'new', 'new_allocator', 'new_handle', 'offsetof', 'sizeof', 'string', 'typeof', 'unpack']
>>> dir(lib)
['factorielle']

Les modules générés par CFFI contiennent 2 objets ffi et lib.

  • ffi: Les fonctions de l’API CFFI ainsi que les typedef et structs
  • lib: Toutes nos fonctions C, ici, il n’y a que factorielle

Ça vous dit d’utiliser notre extension avec un petit test de performance ? Allons y !

import time
from contextlib import contextmanager
 
# On import lib qui contient notre fonction factorielle
from _exemple import lib
 
# On créé l'équivalent de notre fonction C en Python
def py_factorielle(n):
    r = n
    while n > 1:
        n -= 1
        r *= n
    return r
 
# Un petit contextmanager pour mesurer le temps
@contextmanager
def mesure():
    try:
        debut = time.time()
        yield
    finally:
        fin = time.time() - debut
        print(f'Temps écoulé: {fin}')
 
def test():
    # On va réaliser un factorielle 25 un million de fois
    loop = 1000000
    rec = 25
    # Version Python
    with mesure():
        for _ in range(loop):
            r = py_factorielle(rec)
    # Version CFFI
    with mesure():
        for _ in range(loop):
            r = lib.factorielle(rec)
 
if __name__ == '__main__':
    test()

Le résultat sur ma machine:

Temps écoulé: 1.9101519584655762
Temps écoulé: 0.13172173500061035

La version CFFI est 14 fois plus rapide, pas mal !
CFFI permet de faire vraiment beaucoup de choses simplement, je ne vous ai montré que la surface afin de vous donner envie d’aller plus loin.

Où trouver des ressources

  • La doc de CFFI est vraiment bien, un bon readthedocs classique: cffi.readthedocs.io
  • Une de mes présentations à PyConAU, la version EuroPython est moins bonne: ICI
  • Mes projets CFFI:
    1. vulkan: Mode ABI ICI
    2. Pour les curieux, la version en utilisant l’API C de CPython ICI
    3. vulk-bare: Mode API, un module très simple ICI
    4. PyVma: Celui-là est très intéressant, mode API qui étend le module vulkan qui en mode ABI, c’est un très bon exemple

A savoir que CFFI a été créé par Armin Rigo et Maciej Fijalkowski, les deux créateurs de Pypy.
Toutes les extensions créées avec CFFI sont compatibles avec Pypy !

Conclusion

J’espère que cette introduction vous a plu. Si les retours sont bons, je pourrai m’atteler à un tuto plus conséquent.
Vive Python !

Si vous avez des remarques, n’hésitez pas à me le faire savoir: @realitix sur Twitter

]]>
Les critiques des ORM sont à côté de la plaque 38http://sametmax.com/?p=23967http://streisand.me.thican.net/sametmax.com/index.php?20171229_113105_Les_critiques_des_ORM_sont_a_cote_de_la_plaque_38Fri, 29 Dec 2017 10:31:05 +0000En ce moment, y a deux modes. Dire que les cryptomonnaies c’est génial, et dire que les ORM c’est de la merde.

Durant les derniers articles, je pense qu’on vous a assez parlé de crypto, donc on va parler des ORM.

Ou ORMs. Je sais jamais si les acronymes s’accordent.

Rappel: qu’est-ce qu’un ORM ?

Si vous avez lu le titre de l’article et que votre sang n’a fait qu’un tour, vous pouvez passer tout de suite à la partie suivante, puisqu’on va commencer par les révisions.

Un ORM, pour Object-relational Mapping, est une bibliothèque qui permet de décrire son modèle de données, et d’utiliser cette description pour faire le lien entre votre application et une base de données relationnelle. Dans sa forme la plus courante, l’ORM fournit les outils pour générer des requêtes SQL – sans écrire du SQL – et les exécuter, mais présente également les résultats sous forme d’objets du langage dans lequel il est écrit.

Par exemple, en Python, je peux faire de la base de données à la mano (sans ORM):

import sqlite3
 
# Création de la base
with sqlite3.connect('db.sqlite') as conn:
 
    # On se positionne sur la base
    c = conn.cursor()
 
    # Créer la table
    c.execute('''
        CREATE TABLE product (
            name text,
            price real
        )
    ''')
 
    # Insertion de données en base
    c.execute("INSERT INTO product VALUES ('Pizza', 5000)")
    c.execute("INSERT INTO product VALUES ('Love', 150)")
    c.execute("INSERT INTO product VALUES ('Nessie', 3.5)")
 
    # Sauvegarde des changements
    conn.commit()
 
    # Lecture de toute la base:
    for row in c.execute('SELECT * FROM product'):
        print(type(row), ':', *row)

Ce qui va me sortir:

python3 db.py
< class 'tuple' > : Pizza 5000.0
< class 'tuple' > : Love 150.0
< class 'tuple' > : Nessie 3.5

Et voici la même chose avec l’ORM peewee (apres pip install peewee :)):

import peewee as pw
 
# On créé la base
db = pw.SqliteDatabase('product2.db')
 
# Description de la table.
class Product(pw.Model):
 
    name = pw.CharField()
    price = pw.FloatField()
 
    class Meta:
        database = db
 
# Connection et création de la table
db.connect()
db.create_tables([Product])
 
# Insertion de données en base
Product.create(name='Pizza', price=5000)
Product.create(name='Love', price=150)
Product.create(name='Nessie', price=3.5)
 
for p in Product.select():
    print(type(p), ':', p.name, p.price)
 
db.close()

Ce qui sort:

python3 db.py
< class '__main__.Product' > : Pizza 5000.0
< class '__main__.Product' > : Love 150.0
< class '__main__.Product' > : Nessie 3.5

A priori, c’est la même chose, mais avec un style différent. Le premier exemple favorise l’écriture du SQL à la main, fait les requêtes explicitement et récupère les résultats sous forme de tuples. Le second a une surcouche, l’ORM, qui implique que tout est décrit en Python. Le SQL est généré sous le capot, et on récupère les résultats sous forme d’objets Product.

Il existe de nombreux ORM en Python, les plus populaires étant Peewee (pour les petits besoins), SQLAlchemy (pour les gros projets) et l’ORM de Django (ben, si vous utilisez Django ^^). Évidemment le concept des ORM n’est pas particulièrement lié à Python, et on en retrouve dans tous les langages, avec des variations de styles et de fonctionnalités.

Que reproche-t-on aux ORM ?

Avant de répondre aux détracteurs, listons d’abord leurs arguments.

  • C’est un niveau d’indirection de plus, avec de l’implicite et de la magie.
  • L’ORM va générer des requêtes non optimisées, voire lentes.
  • SQL est un excellent DSL spécialisé dans les requêtes. Au lieu d’apprendre une API de plus, autant aller à la source.
  • SQL marche pareil d’un client à l’autre, donc la connaissance est réutilisable, contrairement à celle de son ORM.
  • C’est une béquille pour ne pas apprendre le SQL, et par ailleurs va amener tot ou tard les ignorants à se tirer une balle dans le pied.
  • Les ORMs ne peuvent pas permettre l’usage de toutes les fonctionnalités de sa base de données, et donc limitent la puissance qu’on en tire.
  • Ça ne scale pas. Les gros sites, comme Instagram qui est écrit en Django, on tous finit par virer leurs ORM.

Et vous savez quoi ?

Ils ont raison.

Heu ?

Toutes ces critiques sont parfaitement justifiées.

Sauf qu’elles passent complètement à côté de la problématique.

Les ORM ne servent pas à éviter d’écrire du SQL.

Ça, c’est vaguement un effet de bord.

Ils servent à créer une API unifiée inspectable, une expérience homogène, un point d’entrée unique, un socle de référence explicite et central, pour le modèle de données.

Une fois qu’on a passé pas mal de temps à faire des projets, on note toujours la même chose: certaines parties du logiciel sont réécrites encore et encore. Si un projet commence à parler à une source de données à la main (API, base de données, crawling, fichiers, etc), tôt ou tard, quelqu’un dans l’équipe va commencer à écrire une abstraction. Celle-ci va grossir, et finir par implémenter au bout de nombreux mois, pour l’accès à la base de données, un semi ORM dégueulasse, mal testé / documenté.

Mais, si les requêtes SQL à la main c’est si bien, alors pourquoi ça arrive ?

Simplement parce que tout projet qui grandit a besoin d’une forme de gestion de la complexité. Ca passe par avoir un point, et un seul où sont décrites à quoi ressemblent les données, leurs accès, leurs garanties, leurs contraintes, et leurs validations. Ca passe aussi par permettre aux autres parties du projet d’utiliser automatiquement ces informations pour leur code métier, ainsi que la logique associée.

L’exemple de Django

L’ORM Django n’est pas vraiment le projet Python le plus propre. Il a plein de limitations et bizarreries, et son principal concurrent, SQLAlchemy est probablement une des meilleures libs au monde dans cette spécialité.

Mais !

Il est au coeur de ce qui a fait autant le succès colossal de Django.

Parce que Django est organisé autour de son ORM, il peut proposer:

  • Des validateurs générés automatiquement qui permettent de valider toute saisie de données, et sauvegarder les changements en base de données. Si besoin, il peuvent générer des formulaires HTML afin de proposer une saisie utilisateur automatiquement contrôlée et nettoyée, avec messages d’erreurs pre-traduits.
  • Des vues pour lire, lister, mettre à jour et supprimer les données, générées automatiquement. Y a plus qu’à mettre du HTML autour.
  • Une admin de base de données autogénérée. Un backend gratos et customisable pour votre projet.
  • Une pléthore d’outils pour gérer les données: signals, getters, auto-castage en objet natifs, validation avancées, etc.
  • Des points d’entrées pour étendre la manipulation de ces données de manière générique (fields, managers, etc).
  • De l’outillage pour les migrations.
  • Un worflow d’authentification, d’identification, de session, de cache et de permissions.
  • La normalisation automatique des valeurs: encoding, fuseaux horaires, devises, format de textes et nombres. Et les points d’entrées pour écrire les siens pour plugger ceux de quelqu’un d’autre.

A cela se rajoute le fait que tous les projets Django se ressemblent. Il est très facile de passer d’une équipe à une autre, d’un projet à une autre. La formalisation du schéma devient une documentation, et la seule source de la vérité à propos des données, et pas juste celle de la base. Et qui est commité dans Git. Pour savoir ce que fait un projet Django, il suffit de regarder urls.py, settings.py et models.py, et c’est parti.

Mais ce n’est pas tout. L’ORM ne fait pas que définir un point central inspectable et des outils réutilisables. Il donne aussi une base commune sur laquelle tout le monde peut compter.

Et pour cette raison, l’écosystème Django est très, très riche en modules tierces partis qui se pluggent en 3 coups de cuillère à pot:

La cerise sur le gâteau ? Parce que tout ça utilise l’ORM, tout ça est compatible ensemble. Votre authentification social auth va produire un objet user qui pourra se logger, consulter un dashboard qui contiendra le résultat d’un sondage sur un produit de la boutique.

Et ça marche avec MySQL, Oracle, SQLite ou PosgreSQL, comme l’intégralité du framework, gratos.

Ce n’est pas l’apanage de Django hein, RoR fait pareil.

Maintenant prenez un projet NodeJS. Pour le coup, pas parce que “JS ça pue” mais parce que la culture de l’ORM n’est pas très présente dans cette communauté. Non pas que ça n’existe pas, mais il n’y a pas de Django du monde du Javascript. Même les gros framework type Meteor n’ont rien d’aussi intégré.

Vous allez devoir réapprendre toute la mécanique de gestion de données si vous changez de projet, car ça sera fait différemment à chaque fois. Vous allez devoir former des gens.

Et surtout vous allez devoir réécrire tout ça.

Oh bien sûr, vous aurez une bibliothèque pour chaque chose, mais elle sera écrite différemment. Vous n’aurez pas d’objet User sur qui compter. Votre moyen de traduire le texte ? Pas le même. Vous utilisez Oracle mais l’auteur PostgreSQL ? Pas de bol. Vous voulez générer quelque chose à partir de votre modèle de données ? Ah, pourquoi vous croyez que facebook a créé GraphQL ! Une petite migration ? Vous avez le choix de l’embarras. Bon, maintenant vous allez gérer les dates, et 4 de vos bibliothèques les sérialisent différemment et une utilise l’heure locale au lieu d’UTC.

Évidemment, on sait que votre équipe ne testera pas tout ça, et ne documentera pas tout ça. La suivante non plus.

Donc non, l’ORM, ce n’est pas parce que “mais heu SQL c’est dur”.

C’est parce que ça permet de créer un monde autour.

Objection !

On a des abstractions qui ne sont pas des ORM…

L’important est d’avoir un modèle central, pas un ORM. Mais les ORM font ça particulièrement bien.

Il existe des abstractions (ex: LINQ) qui font un excellent travail pour masquer la source de données. Mais elles ne sont pas un remplacement pour un modèle introspectable central listant nature et contraintes.

Une bonne lib propose les deux.

Par exemple SQLALchemy propose un ORM, mais en vérité l’API de base est fonctionnelle et composable. Pour cette raison, on peut utiliser toutes fonctionnalités avancées de sa base de données avec SQLAlchemy car on a cette alternative à l’ORM à tout moment, et qui est compatible avec l’ORM.

Mais les perfs !

D’abord, on optimise pour les humains. En chemin, on optimise pour la machine. Quand ton ORM arrête de scaler, c’est un BON problème à avoir. Ca veut dire que le projet a atteint un certain succès, et qu’on peut investir dans la séparation avec l’ORM.

De plus, aucune technologie n’est faite pour être utilisée partout, tout le temps, pour tout le projet.

Google a connu ses débuts de succès en Python, et avec sa taille, a réécrit une partie en Java, C puis Go. C’est logique, on ne commence pas avec un langage bas niveau directement, c’est trop lent à écrire. Mais on ne garde pas un langage haut niveau pour tout quand ça monte dans les tours. Enfin les tours… Là on parle de centrifugeuse cosmique hein.

Car gardez en tête que vous n’êtes PAS Google. L’immense majorité des projets deviennent viables, rentables, puis pleins de succès, sans jamais atteindre l’échelle qui amène aux limites de l’ORM.

Quant à l’idée que votre stagiaire peut écrire une boucle avec une requête à chaque step… Il peut tout aussi bien écrire une requête SQL sans se protéger correctement l’injection de code. C’est con un stagiaire, faut le surveiller. C’est le principe, sinon il aurait un CDI.

Mais les fonctionnalités !

Les ORM bien faits n’empêchent pas d’utiliser toutes les fonctionnalités de son système. Et tous permettent d’écrire du SQL à la main si besoin. C’est comme les blocks unsafe de rust: on empêche pas, on isole.

L’idée c’est de garder ça pour le moment où on en a vraiment besoin. SQL est à la base de données ce que l’assembleur est à la machine. On n’écrit pas tout en assembleur de nos jours, ça n’est pas utile.

Root of all evil, tout ça.

Mais on ne change pas de base de données souvent !

L’argument “l’orm supporte plusieurs bases” n’est pas destiné à la migration de données d’un projet existant d’une base de données à une autre.

Pas. Du. Tout.

C’est pas que ça arrive jamais. Ça m’est déjà arrivé 2, 3 de fois.

Mais ce n’est pas le cas courant. Le cas courant c’est la réutilisation du code d’un projet précédent dans un nouveau projet utilisant une base de données différente. C’est la création d’un écosystème de modules qui ne sont pas dépendants de la base de données.

Si vous faites une “app” Django, vous pouvez la publier et elle sera utile pour toutes les bases de données supportées. Et c’est pour ça qu’il y autant d’outils.

Mais on pourrait avoir un modèle central sans ORM !

Oui, mais toutes les formes ne se valent pas.

Par exemple, Doctrine permet d’écrire son modèle dans un fichier YAML, et Hibernate dans un fichier XML.

Au final on écrit son modèle dans un langage moins complet, moins expressif, moins facile à débugger et avec moins de tooling. On perd encore plus en faisant ça qu’en passant de SQL à un ORM, sans autant de gains.

En prime, on peut vouloir de la logique de validation très complexe ou des choses à faire hors validation (signals, génération dynamique de modèle, etc), et là, pouet.

Une alternative, ça serait de se servir d’une lib de pur modèle (ex: l’excellent marshmallow) et de tout dériver de là. Une approche intéressante qui pourrait satisfaire tous les camps, mais que je n’ai jamais vu poussée jusqu’au bout dans aucun framework. Si vous cherchez un projet pour vos week-end :)

Lib VS framework

C’est un peu le vieux débat du découplage VS intégration qu’on retrouve dans la critique des ORM (ou dans vi VS vscode, POO vs fonctionnel, ta femme vs ta mère…).

Et comme d’habitude on retrouve toujours les meilleurs programmeurs du côté de ceux qui veulent le plus de liberté (vive SQL!) parce qu’ils ignorent complètement les problématiques qui vont plus loin que leur fichier de code. Faire fleurir un écosystème, gérer une communauté, favoriser la productivité, facilité l’intégration des membres de ses équipes… Tout ça sont des problématiques moins funs que de faire la requête parfaite avec le tout nouveau champ hyperloglog de PostGres.

Difficile de convaincre des gens qui sont non seulement excellents, mais qui sauront, seuls, être très productifs sans ORM. Surtout quand les gros projets qui atteignent des centaines de millions d’utilisateurs par jour finissent toujours par se séparer de leurs abstractions initiales.

Mais voilà, il ne faut pas perdre de vue que 2 projets sur 3 en informatique, échouent. Quasiment jamais pour des raisons techniques. Généralement la cause est humaine. Et l’ORM est là pour soutenir l’humain en créant un pivot sur lequel il peut compter, quitte à investir plus tard pour s’en passer.

C’est un excellent outil et une très belle réussite de l’informatique moderne. Si on sait l’aborder sans dogmatisme.

]]>
Monter son master node Interzone (ITZ) 21http://sametmax.com/?p=23944http://streisand.me.thican.net/sametmax.com/index.php?20171221_135247_Monter_son_master_node_Interzone__ITZ__21Thu, 21 Dec 2017 12:52:47 +0000Acheter et vendre des cryptomonnaies, tout le monde comprend le principe. J’achète un truc, j’attends, je le revends. Si le prix a monté, je gagne du pognon, s’il est descendu, j’en perds.

Mais il existe d’autres formes d’investissement. L’une d’elles est le master node, et on va apprendre à en monter un.

Attention, ça demande de savoir administrer un serveur linux. Le tutoriel n’explique pas bash, apt, ssh ou make et suppose que vous êtes à l’aise avec ces outils. Si ce n’est pas la cas… pas de bol.

C’est quoi déjà ?

Bitcoin n’est plus la seule crypto, il y en a maintenant des centaines, et certaines essayent de se distinguer technologiquement.

Quelques-unes ont le concept de master node, des machines dont les propriétaires ont acheté un certains nombre de coins, et les bloquent. En échange de quoi, le réseau donne à la machine le droit de faire certaines opérations en plus, opérations qui rendent service au réseau (rendre les exchanges anonymes, accélérer les transactions, etc).

Le but des master nodes est double:

  • Avoir toujours plein de wallets répartis dans le monde et avec plein de propriétaires différents. Ça renforce le réseau.
  • Forcer les gens a acheter plein de coins et donc faire monter le cours, faisant gagner du pognon aux créateurs de la monnaie qui en ont généralement préminé une partie.

Mais pour le propriétaire du master node, qu’est-ce qu’on y gagne ?

Et bien tous les jours, une cagnotte est répartie équitablement entre tous les masternodes en récompense de leur service.

Par exemple, j’ai monté la semaine dernière un master node Vivo. Il a couté a l’achat 3200 euros. Tous les jours, il génère 5 vivos, soit actuellement 20 euros. En plus de cela, le vivo a augmenté de prix depuis l’achat, et le master node vaut maintenant 5000 euros si je décide de revendre les coins qui sont bloquées. Mais ça voudrait dire fermer mon master node.

Choisir un master node

Tous les master nodes ont des coûts et une rentabilité différents. Il existe des listing qui permettent de se faire une idée de la question.

Aujourd’hui je vais vous faire un tuto sur le master node Interzone, car il est très peu cher à l’achat: moins de 300 euros. Évidemment, il ne rapporte que 2 euros par jour.

Mais si vous voulez apprendre à faire des master nodes, c’est plus simple que de débloquer 5000 balles pour du vivo. Ou pire, un million pour un node dash :)

Interzone est aussi un wallet très basique, et donc simple à monter.

Acheter les coins

D’abord, il faut acheter les coins. Le nombre de coins à acheter est différent pour chaque type de node. Pour Interzone, c’est 5000 à bloquer, et donc on va en acheter 5001 pour les frais de transaction.

Pour se faire, il faut d’abord acheter une monnaie plus populaire sur une plateforme d’achat. Par exemple, vous pouvez acheter du Bitcoin sur Bitstamp.

Ensuite, faite un virement (withdrawal) vers une plateforme d’échange.

Tout ça se fait en plusieurs étapes.

D’abord, ouvrir des comptes sur ces sites, et faire les putains de vérifications de sécurité. Ouai, si vous vouliez trader anonymement c’est raté. Et parfois c’est long. Très long. Par exemple pour les achats par carte bancaire, Bitstamp m’avait demandé une photo de moi tenant mon passeport. Je sais pas si ils le font toujours, mais c’est over chiant.

Je vous laisse vous occupez de tous ça, on se revoit dans quelques jours.

Ça y est, de retour ?

Ok.

D’une part on va aller sur un exchange voir le cours de l’Interzone pour savoir combien de BTC acheter.

Par exemple à cet instant je vois que je peux acheter un ITZ pour entre 0.000002699 et 0.000007300 BTC. En regardant les cours, je vois que j’ai suffisamment de vendeurs pour avoir 5001 ITZ à 0.000002990 BTC, donc 0.0149529900 BTC. On rajoute les fees de l’exchange (0.0000373824750 BTC) et on prend en compte les fees de transactions de la block chain bitcoin (0.00265914 BTC).

Total: 0.017649512 BTC, soit 281.57 euros sur Bitstamp.

Parfois c’est plus, parfois c’est moins. Ça fluctue beaucoup.

On achète ça. Ça prend encore du temps…

Quand on a tout, on fait le virement sur son autre wallet, celui sur l’exchange.

Depuis l’exchange, on achète enfin son Interzone.

Monter son master node

En théorie on peut faire ça avec un client, mais en pratique ils ne marchent jamais. En plus il faut que ça reste allumé tout le temps et il faut avoir une IP statique.

Donc on va le faire sur son propre serveur linux.

Le plus pratique est d’avoir un serveur à soi qui tourne quelque part, ou de louer un VPS au prix minimal. En effet un master node, ça consomme que dalle en ressource et vous pouvez le mettre sur un raspberry pi si ça vous chante.

Parce que je suis pas maso, je vais expliquer que pour une distro, car il va falloir compiler. On va donc faire ça pour une Ubuntu 16.04.

D’abord on s’assure d’être toujours à l’heure:

sudo apt-get install ntp ntpdate
sudo update-rc.d ntp enable

On installe la tetrachiée de dépendances:

sudo apt-get install build-essential software-properties-common nano libboost-all-dev libzmq3-dev libminiupnpc-dev libssl-dev libevent-dev   libtool autotools-dev automake pkg-config libssl-dev libevent-dev bsdmainutils libboost-all-dev

Ensuite, on a besoin de downgrader une lib, donc, ppa:

sudo add-apt-repository ppa:bitcoin/bitcoin
sudo apt-get update
sudo apt-get install libdb4.8-dev libdb4.8++-dev

On download les sources:

wget https://github.com/projectinterzone/ITZ/archive/master.zip
unzip master.zip

Et on compile:

cd ITZ-master 
./autogen.sh
./configure
make

Vous pouvez aller boire un coup, pisser, faire la sieste. Ça prend pas mal de RAM, donc si vous serveur en a pas assez, faites les sur un ordi local avec la même version de linux.

Une fois tout ça terminé, le résultat est que dans le dossier ITZ-master/src vous avez deux executable:

  • interzoned (le daemon qui va servir de master node)
  • interzone-cli (le client qui va vous permettre d’envoyer des ordres au master node)

C’est ce qu’on va utiliser. Si vous avez compilé en local, uploadez les n’importe où sur votre serveur.

Maintenant, on se place sur le serveur, dans le dossier qui contient interzoned et interzone-cli, et on va créer un fichier de config.

Pour le fichier de config, il vous faut l’adresse IP du serveur. On peut la choper avec:

$ dig TXT +short o-o.myaddr.l.google.com @ns1.google.com | awk -F'"' '{ print $2}'
xxx.xxx.xxx.xxx

Le port, lui, est toujours 55675 (soyez sur de pas le bloquer).

Puis on choisit un mot de passe pour le client:

$ tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP

On ouvre le fichier:

mkdir .interzone
vi .interzone/interzone.conf

Et on écrit dedans:

bind=:55675
rpcuser=interzone
rpcpassword=BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP

Puis on lance le daemon:

./interzoned -daemon -reindex

Si vous êtes consciencieux, ajoutez un service qui le démarre quand votre machine boot.

Au premier lancement, il va télécharger toute la block chain, mais ici c’est seulement 140Mo car interzone est tout jeune encore. Donnez-lui 30 minutes pour être tranquille.

Au bout de quelques minutes, votre client pourra déjà se connecter au master node et voir le progrès avec:

$ ./interzone-cli getinfo 
{
...
    "blocks" : 92477,
...
}

Il est temps de loader votre master node avec du pognon ! On se fait une adresse:

./interzoned getnewaddress MN1
1MKDs1yNze7VEr4zWHTKbiV3T3fBTwtxZ4

Et là vous allez sur votre exchange où vous avez acheté vos coins, et vous faite un transfert d’EXACTEMENT 5000 coins sur cette adresse (la votre, pas 1MKDs1yNze7VEr4zWHTKbiV3T3fBTwtxZ4, bande de moules). Pas un sou de moins. Pas un sou de plus.

Vous attendez que les coins arrivent. Ça peut prendre quelques minutes. Vérifiez avec:

./interzone-cli getbalance
5000.00

Si ça dit 5000, c’est bon.

Il est temps de démarrer notre master node.

D’abord, on arrête le daemon:

./interzone-cli stop

Ensuite, on se génère une clé privée pour notre master node:

./interzone-cli masternode genkey
EHLKJHoYfNaeziVMAtLs5678G9Em861r3456xtYh1TEotpY1

Ensuite on rouvre notre fichier de config, et on le met à jour pour contenir ça:

bind=:55675
rpcuser=interzone
rpcpassword=BjHQ2T95Og2VNxsVDQ5qcFBU1eVNHP
masternode=1
masternodeprivkey=EHLKJHoYfNaeziVMAtLs5678G9Em861r3456xtYh1TEotpY1

Ne soyez pas con, n’utilisez pas la clé que je poste dans ce tuto.

On va aussi chiffrer son waller, maintenant qu’on a des sous dessus, pour éviter de se le faire tirer. On génère un nouveau mot de passe, et on encrypt:

$ tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
efFMgBGFpVhlQ0j8o8N4TaC5XdASyG
$ ./interzone-cli encryptwallet "efFMgBGFpVhlQ0j8o8N4TaC5XdASyG"

Faites un backup de votre wallet avant et du mot de passe. Si vous perdez ça, vous êtes niqué de chez niqué.

Vous pouvez déverrouiller le wallet avec:

./interzone-cli walletpassphrase efFMgBGFpVhlQ0j8o8N4TaC5XdASyG 120

efFMgBGFpVhlQ0j8o8N4TaC5XdASyG étant à remplacer avec votre mot de passe fraichement généré et 120 est le nombre de secondes pour le garder débloqué.

Il est temps de relancer le daemon:

./interzoned -daemon

Et finalement, le passer en master node:

./interzone-cli masternode start

C’est good.

Vous pouvez vérifier que ça a été accepté par le réseau avec:

./interzone-cli masternodelist | grep PUBLIC_IP

Toutes les 24h, vous pourrez joyeusement contempler le chiffre fourni par ./interzone-cli getbalance qui augmente. Et vous pouvez envoyer vos coins où vous le souhaitez (par exemple pour les revendre) avec un joli:

./interzone-cli walletpassphrase <votre password> 120
./interzone-cli sendtoaddress

Sécurité

Votre master node est ici configuré en mode “hot wallet”, c’est-à-dire que vos coins sont sur le serveur. Il existe une méthode, plus complexe, plus longue, mais plus sure, qui permet de le configurer en “cold wallet”. Vos coins sont alors sur votre machine, et un lien est fait enter votre machine et votre master node.

Si la sécurité vous importe, je vous invite donc à étudier la question.

Renforcer la configuration du firewall aussi est une bonne idée, puisque maintenant vous êtes une cible d’attaque potentielle.

Enfin, n’oubliez pas de vider tout l’historique bash en partant, histoire de pas avoir tous les trucs en clair dedans:

cat /dev/null > ~/.bash_history && history -c && exit

Et backupez comme des porcs aussi.

Profit ?

J’espère pour vous que tout ça va fructifier (et pour moi aussi). Si c’est le cas, vous savez ce qu’il vous reste à faire: envoyez-nous une carte postale de votre ile deserte. Ou des ITZ à 1AChuSLWAHQgmkGa84AHqBRCs1Z1cxHjnU. Ça marche aussi.

Si vous avez tout perdu, sachez que c’est la faute de Max. Je peux vous donner son adresse.

Et pour tous ceux qui n’ont pas la foi de faire tout ça, je pense que je vais monter un service qui permet de cliquer et acheter des masternodes automatiquement.

]]>
Le point sur les crypto-monnaies 36http://sametmax.com/?p=23913http://streisand.me.thican.net/sametmax.com/index.php?20171219_113250_Le_point_sur_les_crypto-monnaies_36Tue, 19 Dec 2017 10:32:50 +0000Max est arrivé, il a posté, puis il est reparti.

Miner du Bitcoin Gold n’a pas été rentable du tout, ni pour vous, ni pour la team de GoldenShower. Et NiceHash s’est fait hacké. Mais au moins c’était fun.

Cela dit ce n’était que la partie émergée de l’iceberg, et avec tout le bordel actuel, vous avez peut-être envie d’un petit topo pour remettre tout en contexte.

Il y a 5 ans, je vous parlais du Bitcoin, un projet qui expérimentait avec l’idée de créer une monnaie décentralisée, non dirigée par les banques ou les états.

Le système Bitcoin a introduit les bases d’un système novateur:

  • Tout le monde est égal, et peut créer un compte Bitcoin sans aucun prérequis. C’est ce qu’on appelle le wallet. Les techniques modernes de chiffrement permettent à chaque wallet de s’identifier, et de déclarer des transactions.
  • Personne ne possède ses propres bitcoins, mais chaque personne dans le réseau peut télécharger tout l’historique des transactions depuis la création du Bitcoin. Cette base de données est ce qu’on appelle la blockchain. Ainsi le système sait exactement qui peut dépenser quoi.
  • Chaque personne peut participer à la création de la monnaie. C’est ce qu’on appelle le minage, et en gros on demande à son ordinateur de résoudre un problème mathématique complexe. On obtient la monnaie quand on peut donner la preuve qu’on a résolu le problème. C’est ce qu’on appelle le “proof of work”.

Bitcoin est un peu le prototype de la roue, taillé dans la pierre: révolutionnaire, mais vraiment super lourd.

  • Tout est public. Les wallets sont anonymes, mais toutes les transactions sont en clair. Les premiers marchés noirs (remember the silk road ?) se sont fait tej assez vite.
  • Les transactions irréversibles et la complexité technique pour sécuriser son portefeuille ont amené de nombreuses attaques.
  • La nature de la production du BTC a amené des gens à se regrouper en grosse communauté afin de miner plus facilement, empêchant les individus seuls de participer de manière significative. C’est ce qu’on appelle les pools. De plus, les gros capitaux ont investi dans de grosses fermes d’ordinateurs pour miner en masse. Une grosse partie est en Chine, et ils ont une énorme influence sur le Bitcoin qui n’est donc plus autant décentralisé.
  • La blochain est devenue énorme. Plus de 150Go et ça va continuer à grossir. Héberger un noeud complet sur son téléphone n’est plus faisable. Du coup les gens utilisent des wallets légers, qui font confiance à un service externe. Bitcoin perd encore plus son bénéfice de décentralisation.
  • Les transactions sont lentes. On plafonne à quelques échanges par seconde. Pour le monde entier !
  • Bitcoin a déjà vu deux forks, le Bitcoin Cash et le Bitcoin Gold, à cause de toutes ses limitations.

Malgré ça, de par le nom célèbre et la robustesse technique du système, Bitcoin reste la référence de la cryptomonnaie. En quelque sort “l’or”, la valeur refuge. Amusant quand on connait sa volatilité. Le cours a explosé (plus de 15000 euros), la bourse de Chicago ouvre les vannes de la spéculation officielle et on peut même en acheter avec sa CB de nos jours (ce qui n’était pas le cas au début).

En gros, Bitcoin est légitimé.

Et c’est ironique, car cette légitimation passe par la négation du projet original. Bitcoin n’est plus du tout une monnaie alternative aux mains du peuple, mais un instrument spéculatif de plus.

Les cryptomonnaies actuelles ont une valeur aberrante, sans rien derrière. C’est du vent total.

Alors qu’est-ce qui se passe maintenant ?

Et bien d’abord les cryptomonnaies alternatives ont fleuri. Il y en a des centaines. La plupart sans aucun projet, avec des teams branlantes et l’espoir de “mooner” (que le cours monte jusqu’à la lune).

Certaines cryptos sont très intéressantes néanmoins:

  • Ethereum a montré la voie des smart contrats, et permet de glisser des programmes dans la blockchain que chacun peut exécuter en dépensant des ETH.
  • Dash a popularisé le concept du master node: certains wallets immobilisent du capital (par exemple 1000 dash), et en échange obtiennent la confiance du réseau pour effectuer des tâches (accélérer les transactions, anonymiser les échanges, etc) contre des prélèvements (fee) qui s’ajoutent à leur pécule.
  • Monero est devenu célèbre parmi les créateurs de virus (et de mineurs JavaScript) car il permet de miner sur le CPU et est anonyme.
  • VTC a promu l’idée d’être ASIC resistant, c’est-à-dire de rendre difficile d’acheter du matériel spécialisé pour le minage pour créer une grosse ferme.
  • Ripple est préminé (toutes les pieces existent déjà) et soutenu par les banques et les GAFAS.
  • Electroneum (Max l’appelle “electroscam”) veut créer du ‘fake mining’ et permettent aux gosses de la cour de récré de générer du cash depuis leur téléphone.
  • duniter mise sur l’aspect social: mineur est un status qu’il faut obtenir et maintenir en convainquant 5 humains de voter pour soi, tous les 2 ans.

Du coup il y a une vraie exploration technique, et ça va donner des choses intéressantes. Même les états s’y mettent.

Néanmoins soyons honnête, l’utilisateur moyen s’en bat les couilles (oui les couilles, l’utilisateur moyen n’est clairement pas une utilisatrice pour le moment) et veut juste avoir de l’argent qui rentre en espérant que plus d’argent sort.

Pour répondre à cette demande, de nombreuses places de marché ont vu le jour. On peut échanger des trucs obscurs tels que le Wyvern, l’ObamaCare, le Satancoin, le CheeseCoin, le Marijuanacoin, le GirlsToken ou le CharlieChaplinCoin.

J’aimerais vraiment vous dire que je plaisante. Mais non.

Et puis tout ça va finir en bulle. Et ça va péter. Et des tas de gens vont tout perdre. Dans 6 mois ? Dans 2 ans ? Dans 10 ans. Aucune idée. Personne ne sait. On sait juste que ça va arriver. On est pas dupe, ces monnaies de singe ne valent rien: pas d’atout technique, pas de projet social, pas de team de ouf, pas de gros backers… Rien pour compenser l’absence de garantie par l’Etat qu’une vraie monnaie a.

Le petit secret, c’est que la plupart des ces cryptos sont des forks des projets principaux (Btc, eth, dash, etc) avec juste un logo différent. Ou pire, juste un smart contract sur ETH et même pas de blockchain, quand on est vraiment paresseux et doué en com, pourquoi se faire chier.

Une minorité, mais bien réelle, deviendront ancrées dans la société après la crise. Après tout la possibilité de stocker une valeur et de l’échanger sans contrôle et rapidement est trop intéressante. Le jeu du chat et de la souris avec les institutions va continuer. Il va y avoir législation. Et au bout d’un temps, acceptation sous certaines conditions.

De plus, les innovations techniques autour de la blockchain vont produire des choses vraiment utiles, et on les utilisera.

Mais la plupart des monnaies actuelles vont disparaitre au son des pleurs. Tout le monde le sait, mais tout le monde s’en fout. On espère tous que ce sera le voisin, et que nous nous on sera riche. Les gens s’identifient beaucoup plus souvent au début du “loup de wall street” qu’à la fin de “the big short”.

Déjà que je regrette la centaine de BTC que j’ai vendu avant que ça monte, alors j’imagine le sentiment quand tout s’écroule après l’achat…

Et comment je fais du pognon moi alors ?

Ben si je savais comment faire de la thune a coup sûr, je le ferais et je ne le vous le dirais pas, bande de guignols.

Mais avec Max, on essaye de profiter de la situation.

Déjà, pour le moment, acheter les cryptos les plus célèbres (Bitcoin, Ethereum, Monero, Dash, Litecoin…), a été une stratégie gagnante puisque ça monte énormément.

Ensuite il y les investissements alternatifs. Par exemple notre bande de potes investis dans des master nodes (particulièrement le Vivo, le CRC, l’interzone, etc). Comme le dit Max “20k en assurance vie m’a rapporté 200 euros en 5 ans. Sur un masternode, 7k me rapporte 40euros par jour”. Évidement…

Enfin il y a la stratégie “pendant la ruée vers l’or, vends des pelles”.

Max essaye de mettre en place un système de bot facile à scripter pour spéculer sur toutes les plateformes de manière uniformisée, et les louer.

Moi je monte un système pour gérer les master nodes des gens en échange d’une part des gains. C’est que c’est chiant à monter et maintenir ces petites bestioles.

Si vous comptez mettre du pognon là dedans, n’oubliez pas de ne jouer dans ce grand casino bordélique et immoral que ce que vous pouvez perdre.

Mais la vérité ?

En tant que dev, on s’était pas autant marré depuis les débuts du Web, quand on bricolait des services pourris en PHP pour se faire une place.

]]>
GoldenShower sera coupé, faute de miners 11http://sametmax.com/?p=23894http://streisand.me.thican.net/sametmax.com/index.php?20171207_154353_GoldenShower_sera_coupe__faute_de_miners_11Thu, 07 Dec 2017 14:43:53 +0000Slt les gens,

 

On va couper le pool Goldenshower, car ça coûte en hébergement et qu’il n’y a plus de miners pour espérer choper un block. C’est la loi du “transvasationnagement” des liquides. Si vous voulez continuer l’aventure essayez les pool btgpool ou suprnova. Je vous encourage d’ailleurs à vous intéresser aux crypto-monnaies dans l’ensemble, ça fait pas de mal ^^

 

On remercie tous ceux qui ont participé à l’aventure en espérant que vous aurez appris au moins autant que nous. Pour info on a rien gagné, les coms de la pool ayant payé l’hébergement du serveur et quelques NiceHash (qui s’est fait hacker au passage…)

Possible qu’on monte un autre pool mais l’expérience nous a appris que c’est pas facile sans une forte concentration de miners.

Je pense vous faire un tuto sur les Masternodes bientôt pour voir les cryptos sous un autre angle.

Passez de bonnes fêtes!

Max,

 

]]>
NiceHash – Pump up your power, à quoi ça sert et comment ça marche ? 8http://sametmax.com/?p=23832http://streisand.me.thican.net/sametmax.com/index.php?20171114_120559_NiceHash_____Pump_up_your_power__a_quoi___a_sert_et_comment___a_marche___8Tue, 14 Nov 2017 11:05:59 +0000Alors c’est quoi ce « gentil hachis » dont vous parlez sur Telegram, c’est un truc de cuisine ?

« Nicehash » c’est un service de location de puissance de « hash », en gros avoir le plaisir de posséder l’équivalent de 25000 cartes graphiques pendant un moment pour miner de la cryptomonnaie.
C’est un peu la Smartbox (vous savez celle où on peut faire 200 bornes pour conduire un R8 pendant ¼ d’heure) de la crypto, mais sans sortir de chez soi, moins de plaisir, mais plus de rendement.

D’un côté Nicehash propose aux mineurs (ceux qui ont des machines qui chauffent leur maison toute la journée) de mettre ces machines en location, et d’un autre on peut participier à des enchères pour louer la puissance (additionnée) de plein de mineurs d’un coup pour devenir un moment un gros mineur bien velu.
On voit bien sur ce graphe ce que ça donne là quand un gars s’est un peu énervé pendant ½ heure

un ptit coup de nicehash ?
Sur le chan de Goldenshower.io on a l’histoire d’un gars qui a réussi à lancer ¼ d’heure de nNicehash au tout départ du BitcoinGold et en a ramassé une bonne grosse poignée à ce moment là, mais sinon de notre expérience avec le service, il est possible de faire un 10-25% de bénéfice (on achète du hashpower en BTC, on mine une monnaie, qu’on revend ensuite, et ça fait 10-25% de BTC en plus pour recommencer ).

Allons-y “gayment”! Rendez-vous sur https://www.nicehash.com/

 

On créer un compte, on valide tout ça et on passe aux choses sérieuses. Le tuto suivant décrit la méthode pour lancer un NiceHash sur la crypto Bitcoin Gold, qui est basé sur l’algo Equihash, pour d’autres cryptos il faudra choisir l’algo approprié mais pour le reste il n’y a rien à changer.

 

  1. On va dans le marketplace (place de marché)
    On sélectionne  market place
  2. On sélectionne l’algo approprié à la crypto que l’on va miner, ici pour BTG (Bitcoin Gold) on sélectionne Equihash.
    Equihash est l'algo que l'on va utiliser pour miner du BTG (Bitcoin Gold)
  3. La partie la plus importante, Il va falloir envoyer notre puissance de calcul louée sur un pool, ici sur goldenshower.io. Pourquoi celui-là et pas un autre ?
    Parce que c’est celui de Sam&Max (et des potes) , qu’on est les plus beaux et qu’on a besoin de tunes pour aller aux putes régulièrement et vous mijoter de jolis articles tous mignons.
    PArtie la plus important, setup du pool Pool Name: Golden Show (le nom de votre pool, mettez ce que vous voulez, c’est pour le reconnaître par la suite dans la liste.)
    Stratum hostname ou IP: btg.goldenshow.io
    port: 3870
    User: là il faut mettre votre addresse de Wallet BTG que vous aurez créé en suivant notre tuto précédent ici

  4. On place son Order (et oui comme à la bourse) qui va consister à acheter de la puissance de Hash à un prix fixé par vous.
    ATTENTION! Avant de placer un order il faut créditer son compte Nicehash avec des BTC (Bitcoins), car tout se paie en BTC.
    Sur la figure suivante j’ai placé un order à 0.01258 BTC.

    PRICE: par défaut un prix est suggéré , il faut regarder dans la liste des ordres à côté que vous êtes dans les bonnes valeurs, un prix trop bas et votre ordre ne passera jamais.
    LIMIT: sert à limiter la puissance de calcul envoyée dans le temps, pour les premiers essais il vaut mieux mettre une faible valeur (attention si vous mettez 0 nicehash va envoyer toute la puissance d’un coup) en cas de mauvaise configuration du pool, ça serait con d’envoyer toute la puissance sur un truc qui mine pas :)
    AMOUNT: le budget que vous voulez allouer, là aussi au départ une petite somme pour tester l’artillerie, une fois que vous êtes sûr que ça marche, vous pouvez placez un ordre plus important.

    on achète des gigoWatts Doc !!

  5. Et C’est tout ! La puissance de calcul louée sur Nicehash va être transmise au pool goldenshow.io et c’est comme si vous aviez une ferme de GPU qui mine. Elle est pas belle la vie ?
    6_MiningStart

Si vous voulez racheter de la puissance de calcul vous éditez votre order dans la liste et vous changez les valeurs appropriées.
8_If_Need_Change_Price_or_refill
Vous l’aurez compris, Nicehash permet d’avoir l’équivalent d’une immense ferme de GPUs sans les inconvenients. Pratique pour miner à la sortie d’une crypto sans avoir à setup grand chose et pour se rendre compte immédiatement de la rentabilité de minage de la monnaie concernée.
A savoir qu’en général quand une monnaie sort la difficulté est faible donc ça vaut le coup de se lancer dessus et le Nicehash est la solution la plus rapide.

Rendez-vous sur goldenshow.io, votre pool préféré pour miner (pas exclu qu’on rajoute d’autres cryptos soon ^^ )

Les précédents Tutos:

http://sametmax.com/de-la-thune-avec-un-mining-pool-de-btg/

http://sametmax.com/tutorial-pour-miner-du-bitcoin-gold-btg/

]]>
Tutorial pour Miner du Bitcoin GOLD – BTG 49http://sametmax.com/?p=23823http://streisand.me.thican.net/sametmax.com/index.php?20171113_123822_Tutorial_pour_Miner_du_Bitcoin_GOLD_____BTG_49Mon, 13 Nov 2017 11:38:22 +0000ça y est le pool est open, tout devrait marcher !

Comment on utilise un mining pool :

Dans peu de temps (ou depuis peu de temps c’est selon), le mainnet (blockchain de prod) de BTG est en ligne, du coup, yahoo on peut commencer à miner. Avec des potes on s’est marrer à monter un pool de minage sur lequel vous pourrez vous connecter. http://goldenshower.io, avec un nom pareil on s’es fait ban du listing du site officiel :) du coup on a pris goldenshow.io mais les 2 ndds marchent pareil.

 

Une bonne douche dorée de BTC ^^

Une bonne douche dorée de BTC ^^

Pour ceux qui ont loupé la 1ère partie c’est ici.

 

Petit tuto rapide, mais j’espère clair :

1) Créer un wallet BTG

https://btgwallet.online/ ou là https://mybtgwallet.com/
Notez bien dans un endroit sécurisé tout ce qu’ils disent de noter (si si c’est important, sinon tous ces beaux Bitcoin Golds seront perdus à jamais). Vous obtiendrez votre adresse que nous nommerons bitcoin-gold-address dans la suite.

2) Installer un logiciel de minage, selon votre système d’exploitation et la marque de votre carte graphique :

Sur ces deux repos se trouvent les versions Linux et Windos (pas encore de miner pour OSX)

Si vous avez une carte graphique NVIDIA:
EWBF v0.3.4b (NVIDIA) –  https://github.com/poolgold/ewbf-miner-btg-edition/releases

Si vous avez une carte graphique AMD:
ClaymoreZcash v12.6 (AMD) – https://github.com/poolgold/ClaymoreBTGMiner/releases

Il est possible que votre antivirus se plaigne, mais ces liens sont légitimes et utilisés par plein plein de gens sans problème.
Ces logiciels contiennent des fonctions cryptographiques (ce qui fait hurler les antivirus) parce que justement c’est leur boulot en tant que mineurs.

3) Configurer le mineur avant de le lancer :

Sous Windows:
* EWBF (les carte nvidia) : miner –server btg.goldenshow.io –user bitcoin-gold-address.worker –pass x –port 3857

* Claymore (les cartes AMD):
Editez le fichier start.bat et mettez ça :

set GPU_FORCE_64BIT_PTR=1
set GPU_MAX_HEAP_SIZE=100
set GPU_USE_SYNC_OBJECTS=1
set GPU_MAX_ALLOC_PERCENT=100
set GPU_SINGLE_ALLOC_PERCENT=100

Editer le fichier config.txt

-zpool stratum+tcp://btg.goldenshow.io:3857
-zwal bitcoin-gold-address.worker
-zpsw x
-allpools 1

et lancer ZecMiner64.exe

4) Pour les plus pointus d’entre vous, il y a quelques variantes :

ports :

3857 = PC standard à quelques GPU
3860 = petite ferme (10-15 GPU ou plus)
3870 = nicehash ou mega fermes ( + 150 gpus)

Pour linux c’est la même chose mais en créant un fichier bash à la place du .bat.

Si vous rencontrez des problèmes (et vous allez en avoir) on a un chan Telegram.

Happy mining !

]]>
Le miner pool BTG est ready: voici comment faire 9http://sametmax.com/?p=23817http://streisand.me.thican.net/sametmax.com/index.php?20171113_010352_Le_miner_pool_BTG_est_ready__voici_comment_faire_9Mon, 13 Nov 2017 00:03:52 +0000ATTENTION !!! Article posté par erreur, BTCGOLD ayant pris un low kick frontal par une DDoS de gniaks, ils ont tout niqué. C’est en train de sync le mainnet, on en est à 50%, je pense que ce sera fini demain matin. Je vous posterai un article/tuto quand ils seront up.

Max s’est bien marré et pour miner du bitcoin gold, nous a pondu le site http://goldenshower.io/.

Qui s’est fait insta banné des listing et donc on a du mettre un alias http://goldenshow.io. On garde l’autre pour la postérité :)

Toutes les instructions sont dessus.

Mais en résumé, vous créez un wallet sur un site type https://btgwallet.online/.

Ca donne un truc comme ça:

Non c'est pas le mien bandes de bananes

Non c’est pas le mien bandes de bananes

Sauvegardez tout ça.

Installez le miner pour votre os et carte graphique tel que listé sur http://goldenshower.io/.

Les windowsiens peuvent avoir un probleme de dll. Dans ce cas y a un truc de plus à installer:

https://superuser.com/questions/1163409/msvcp120-dll-and-msvcr120-dll-are-missing

Si vous avez un doute, essayez en premier https://www.microsoft.com/en-us/download/confirmation.aspx?id=40784

Ensuite, vous votre miner depuis le terminal et on attend :)

]]>
De la thune avec un « Mining Pool » de BTG 25http://sametmax.com/?p=23805http://streisand.me.thican.net/sametmax.com/index.php?20171112_082947_De_la_thune_avec_un____Mining_Pool____de_BTG_25Sun, 12 Nov 2017 07:29:47 +0000ça fait quand même 5 ans qu'on vous parle du Bitcoin sur le blog... Aujourd'hui le Bitcoin est à quelques milliers d'euros, et des centaines de monnaies concurrentes sont arrivées sur le marché. Les opportunités pour faire de l'argent (et en perdre of course) sont décuplées, et Max, ainsi que deux potes à nous, se sont lancés dans un petit montage sympa. Potentiellement, si vous avez une bonne carte graphique, y a moyen de se faire du pognon sans trop d'effort. Plus on a de lecteurs qui participent, plus la thune arrive. Je leur laisse la parole dans un article invité de Tic, où ils vous expliqueront ça mieux que moi ]]>C’est pas parce que vous entendez plus trop parler de Max qu’il est mort. En ce moment, il est à fond sur les cryptomonnaies. Mais si, rappelez-vous, ça fait quand même 5 ans qu’on vous parle du Bitcoin sur le blog… Aujourd’hui le Bitcoin est à quelques milliers d’euros, et des centaines de monnaies concurrentes sont arrivées sur le marché.

Les opportunités pour faire de l’argent (et en perdre of course) sont décuplées, et Max, ainsi que deux potes à nous, se sont lancés dans un petit montage sympa. Potentiellement, si vous avez une bonne carte graphique, y a moyen de se faire du pognon sans trop d’effort. Plus on a de lecteurs qui participent, plus la thune arrive.

Je leur laisse la parole dans un article invité de Tic, où ils vous expliqueront ça mieux que moi


Avec une bande de zozos, on est en train de monter un mining pool sur le Bitcoin Gold. Rien que cette phrase je me demande comment j’ai pu l’écrire.

Je ne suis pas du genre à me baigner dans du charbon, donc je vais préciser les choses :

Certaines cryptomonnaies (Bitcoin, Ether, Monero, mon préféré Electroneum …) reposent sur une blockchain dont la sécurisation (ie gravure des transactions et informations dans le marbre de la blockchain, vérification qu’il n’y a pas de fraude) est réalisée par une armée de machines capable de réaliser des tonnes d’opérations mathématiques : les mineurs.

Pour valider un block de la chaine chaque mineur essaye des foultitudes de combinaisons mathématiques liées à ce block, jusqu’à ce qu’un trouve la bonne combinaison et Bingo il touche la cagnotte, le salaire (versé dans la cryptomonnaie associée) lié à la validation de ce block et ainsi de suite, jusqu’à presque l’infini.

Du coup, soit les mineurs travaillent tout seuls dans leur coin pour choper le gros lot, soit ils s’associent dans un « pool » et mettent leur puissance (et leur chance, car il s’agit bien de chance) en commun pour gagner plus souvent une plus petite partie de la mise.

Un peu comme jouer au loto, soit on fait sa grille tout seul dans son coin pour gagner en moyenne 2 millions avec une probabilité de 1/19 Millions, soit on a 1 000 « amis » avec qui on s’associe pour gagner en moyenne 2000 euros avec une probabilité de 1/19000. On gagne beaucoup moins, mais on a beaucoup plus de chance (au sens probabiliste) que ça arrive au moins dans une vie.

Sauf que pour miner, pas besoin d’acheter un ticket. Il suffit d’avoir une carte graphique sur son ordi.

Et avec un pool, on a un serveur sur lequel chaque mineur se connecte pour lui demander du travail, sa feuille de route au niveau calcul pour les secondes à venir. Chacun fait ses calculs aussi vite qu’il peut et renvoie ses résultats, et si un des mineurs du pool trouve la combinaison gagnante, le pool redistribue les gains selon la quantité de travail (pour la plupart dans le vent, mais c’est le jeu qui veut ça) fournie par chacun. Par chacun, on parle d’un PC (presque) de base à une ferme de minage comprenant plusieurs dizaines (ou centaines) de cartes graphiques.

Sinon j’ai parlé de BTG en titre, c’est le Bitcoin Gold (il y a le silver et surement un jour le copper à venir, mais on va rester sur le Gold). Le BTG c’est une nouvelle cryptomonnaie qui sort officiellement le 12 Novembre à 19h UTC. Donc ce soir. Pour faire court et simple (et pas trop m’embrouiller), le BTG est basé sur la blockchain du BTC (The Bitcoin), c’est un « fork » qui veut redonner le pouvoir au peuple dans la mesure où la sécurisation de la chaine Bitcoin originale est devenue tellement difficile que seule une poignée de gros acteurs basés dans l’Empire du milieu (bref des chinois) détient la quasi-totalité de la puissance. Pour la sécurisation décentralisée de la chose, on repassera. Le bitcoin gold tend à redonner la voix au peuple en proposant une chaine sécurisable par des cartes graphiques et seulement des cartes graphiques, du coup la démocratisation est plus plausible, car il suffit de 1000 PC de gamers disséminés n’importe où pour égaler (et contrebalancer) la puissance d’une ferme entière cachée au fin fond de l’Oural ou de la Mongolie.

Du coup voilà, on est en train de monter un mining pool de BTG.


Maintenant la partie fun.

Quand une nouvelle cryptomonnaie arrive, le début du minage est plus facile. Ce soir, Max et Tic & Tac vont donc ouvrir leur mining pool, et on va poster un tuto sur le blog pour que vous puissiez vous connecter dessus.

Afin qu’on fasse de la thune tous ensemble, dans la joie et la bonne humeur.

]]>
Protégé jusqu’à la mort 14http://sametmax.com/?p=23755http://streisand.me.thican.net/sametmax.com/index.php?20171102_072835_Protege_jusqu___a_la_mort_14Thu, 02 Nov 2017 06:28:35 +0000Ce matin là, comme dans toutes les démos importantes avec un client, rien ne marchait. D’abord la multiprise, puis la prise Ethernet physique, puis la connexion elle-même. Pour faire bonne mesure, j’avais moi aussi un bug dans le code que je n’ai pu résoudre qu’en changeant de bâtiment (ne cherchez pas…).

Un moment merveilleux de communion avec tous les dev du monde qui ont connu cette expérience spirituelle de destruction de moral et de crédibilité.

Après quelques blagues et une utilisation de la partie du service qui marchait qui m’ont permis de garder la face, un nouveau problème fit son apparition.

Un fichier .odt que je générais était corrompu au téléchargement.

God left

And he’s right

Pourtant, ça marchait, j’en étais certain.

Aucune erreur. Rien. Tout était nickel de bout en bout. Sur ma machine, tout va bien. Sur les leurs, plantage direct de Libre Office.

Je change de navigateur sur mon laptop ou leurs tours. Queud.

Je change d’OS. La même.

Avec CURL ? Zob.

Et puis je note un truc étrange : la taille du fichier n’est pas la même sur leur machine. Elle change à chaque putain de téléchargement.

Je reste un instant interloqué. Et par “un instant interloqué” j’entends 2h surcaféine à trafiquer sur 3 machines différentes toutes les hypothèses tordues possibles, frénétiquement, et la bave aux lèvres.

Quand soudain l’idée me vint. La grâce divine.

Jesus saves

Jesus saves. For later.

Je désactive leur antivirus.

Miracle, ça marche.

Karspersky protégeait leurs machines jusqu’à la mort.

Je ne sais pas ce qui se passait dans sa petite tête, mais il lobotomisait de quelques Ko tous les documents, en mode frappe préventive américaine. Peu importe, leur admin n’aurait de toute façon pas été capable de corriger le problème.

La solution est de se passer des forces de l’ordre et faire justice soit même : tout foutre en SSL pour que le petit salopiot arrête de mettre son nez dans mes paquets. Ils sont sur un intranet. Avec un VPN. Mais fuck, ils seront bien protégés.

Something queer this way come

Also, openssl -days 10000000

]]>
pipenv, solution moderne pour remplacer pip et virtualenv 17http://sametmax.com/?p=23691http://streisand.me.thican.net/sametmax.com/index.php?20171008_174416_pipenv__solution_moderne_pour_remplacer_pip_et_virtualenv_17Sun, 08 Oct 2017 15:44:16 +0000pipenv sous le nez, et j'ai donc redonné sa chance au produit. Surprise, l'outil est maintenant très stable (plus de 2000 commits !) et mes propositions avaient même été intégrées. ]]>Kenneth Reitz, l’auteur de requests, tente régulièrement de nous refaire le coup du projet star. Ca n’a malheureusement pas très bien marché, et beaucoup de ses projets comme maya, records, crayon, tablib ou awesome n’ont pas vraiment connu de succès.

Entre alors pipenv, que j’ai testé il y a presque un an, et qui au départ montrait un beau potentiel, mais n’était pas encore très utilisable. J’ai fait quelques suggestions d’amélioration, comme permettre de choisir précisément la version de Python, et je me suis fait envoyé bouler. J’ai donc laissé l’auteur s’enterrer dans sa recherche de gloire passée.

Le hasard de reddit m’a remis pipenv sous le nez, et j’ai donc redonné sa chance au produit. Surprise, l’outil est maintenant très stable (plus de 2000 commits !) et mes propositions avaient même été intégrées.

Après ces 3 paragraphes vous vous demandez sans doute quand est-ce que je vais rentrer dans le vif du sujet, donc:

pipenv reprend les idées de pip, virtualenv, pew et même quelques trucs de npm, yarn, cargo, et essaye d’appliquer tout ça à Python. L’article suppose que vous savez ce que sont ces mots barbares, donc suivez les liens si ce n’est pas le cas.

pipenv permet donc d’installer des packages Python, d’isoler cette installation et de la rendre reproductible. Mais sans effort.

En effet, contrairement à la concurrence:

  • La gestion du virtualenv est automatique et transparente
  • Les paquets installés sont sauvegardés dans des fichiers de config, encore une fois de manière automatique et transparente.
  • Les fichiers de config distinguent les dépendances de prod et de dev, et incluent les versions des sous-dépendances.

Installer pipenv

Contrairement à pip et virtualenv, pipenv n’est pas fourni avec une installation standard de Python, bien que l’outil soit maintenant recommandé par la doc officielle. Il va donc falloir l’installer. Or pipenv se base sur une version récente de pip, donc il faut d’abord être sûr d’avoir pip à jour.

Du coup:

# mise à jour de pip, mais juste au niveau utilisateur pour 
# pas casser le  system
python -m pip install pip --upgrade --user

Puis:

# installation de pipenv
python -m pip install pipenv --user

A moins d’être sous une Debian like type Ubuntu (qui demande un apt install de python-pip avant), tout le monde a pip installé avec une version moderne de Python.

Voilà, vous devriez avoir la commande pipenv disponible, ou pour ceux qui ont un système mal configuré, python -m pipenv.

Usage

Dans le dossier de votre projet:

pipenv install nom_du_package

C’est tout.

Si un virtualenv n’existe pas il sera créé. Sinon il sera utilisé. Les fichiers de configs sont gérés automatiquement, il n’y a rien à faire.

Si vous voulez lancer une commande dans le virtualenv:

pipenv run commande

Exemple:

pipenv run python

Va lancer le Python de votre virtualenv.

Si vous voulez que toutes les commandes soient dans le virtualenv:

pipenv shell

Et vous êtes dans un nouveau shell, dans le virtualenv. Ainsi:

python

Lancera celui de votre virtualenv.

On sort du shell avec Ctrl + D.

Vous pouvez arrêtez de lire l’article ici, c’est l’essentiel de ce qu’il y a à savoir.

Astuces

Si vous lancez pour la première fois dans un dossier pipenv avec:

pipenv --python x.x

Le virtualenv sera créé avec la version de Python x.x, pourvu qu’elle existe sur votre système. Setter la variable d’env PIPENV_DEFAULT_PYTHON_VERSION a le même effet.

Installer un package avec pipenv install --dev le marque comme dépendance de développement uniquement, et permet une installation séparée.

Vous pouvez aussi obtenir quelques infos utiles comme:

  • pipenv --venv: ou est le dossier du virtualenv
  • pipenv graph: un graph de toutes vos dépendances
  • pipenv --py: chemin vers le Python en cours.

Enfin pipenv utilise pew, donc la magie de pew reste dispo, y compris la gestion de projets :)

Usage avancé

Si vous créez un fichier .env dans le dossier de votre projet tels que:

FOO=1
BAR=wololo

pipenv exécutera toutes ses commandes (y compris shell), avec FOO et BAR comme variables d’environnement.

La commande:

pipenv lock

Va créer un lock file. Ce fichier contient toutes les dépendances, et recursivement, les dépendances des dépendances, installées, avec leurs versions. On peut réutiliser ce fichier en prod pour installer une exacte copie de son setup local avec pipenv install. Sans ce fichier, pipenv install se comportera comme pip install.

Il y a plein d’autres trucs mais on va en rester là.

]]>
Regrouper ses fichiers de settings avec stow 15http://sametmax.com/?p=23681http://streisand.me.thican.net/sametmax.com/index.php?20171006_102403_Regrouper_ses_fichiers_de_settings_avec_stow_15Fri, 06 Oct 2017 08:24:03 +0000.machins. Par exemple le .bashrc pour la config du bash, le .mozilla qui contient toutes vos données Firefox, le .ssh avec toutes vos clés privées, le .local/share/virtualenvs avec les envs virtuels Python créés par pew ou .config/sublime-text-3 pour la configuration de Sublime text, etc.]]>Sous Linux, le dossier utilisateur est blindé de fichiers de configuration. Les fameux .machins. Par exemple le .bashrc pour la config du bash, le .mozilla qui contient toutes vos données Firefox, le .ssh avec toutes vos clés privées, le .local/share/virtualenvs avec les envs virtuels Python créés par pew ou .config/sublime-text-3 pour la configuration de Sublime text, etc.

Au final, voici tous les fichiers de conf qui sont importants pour moi de près ou de loin:

├── .autoenv
├── .bashrc
├── .config
│   ├── autostart
│   ├── Code
│   ├── copyq
│   ├── fish
│   ├── gtg
│   ├── liferea
│   ├── pulse
│   ├── stremio
│   ├── sublime-text-3
│   ├── transmission
│   ├── user-dirs.dirs
│   ├── user-dirs.locale
│   ├── variety
│   ├── VeraCrypt
│   ├── Zeal
│   └── zim
├── .django-completion.bash
├── .editorconfig
├── .git-aware-prompt
├── .git-completion.bash
├── .gitconfig
├── .gitignore
├── .git-prompt.sh
├── .git.scmbrc
├── .jupyter
├── .lastpass
├── .liferea_1.8
├── .local
│   └── share
        ├── gtg
        ├── keyrings
        ├── liferea
        ├── omf
        ├── TowerFall
        ├── virtualenvs
        └── Zeal
├── .mozilla
├── .netrc
├── .oh-my-zsh
├── .openambit
├── .pypirc
├── .scmbrc
├── .scm_breeze
├── .sshplus
├── .vscode
│   └── extensions
└── .zshrc

Quand on bidouille, on les change souvent. On les backup aussi, pour pouvoir les porter d’un laptop à un autre, les synchroniser, les uploader sur un serveur ou les récup lors d’une réinstallation. Parce que quand on a tuné ses terminaux et éditeurs aux petits oignons, on a pas envie de recommencer à poil.

Pour bien faciliter les choses, ils sont éparpillés un peu partout, dans des sous-dossiers différents.

Et je sais pas quel vil individu a suggéré une fois que faire une partition séparée pour /home était la solution de Skippy à tous les soucis, mais perso, ça me cause plus de bugs qu’autre chose quand on change de versions d’OS.

Bref, laissez tomber vos vieilles croyances issues de charlatans de sectes. Moi, j’ai vu la lumière (lien de don bitcoin en bas à droite de la page), et elle s’appelle GNU stow.

Stow est un vieil utilitaire (donc sagesse millénaire des anciens, vous pouvez avoir confiance, prenez ce cristal aussi il est en promo), qui est grosso merdo un ln -s récursive. C’est-à-dire que ça fait des symlinks des fichiers et des dossiers que vous lui passez.

On peut l’utiliser pour plein de choses, mais l’usage sacré implique le sacrifice d’une vierge à Max, puis de déplacer tous les fichiers de settings qu’on souhaite gérer dans un seul dossier.

Par exemple, moi j’ai:

/home/user/church/settings/

    ├── .autoenv
    ├── .bashrc
    ├── .config
    │   ├── autostart
    │   ├── Code
    │   ├── copyq
    │   ├── fish
    │   ├── gtg
    ...

Au lieu de les avoir éparpillées partout, toutes les brebis sont maintenant regroupées dans une seule église.

Il est très important de garder l’organisation des dossiers et des sous-dossiers d’origine. Ici vous voyez que j’ai le dossier Code, qui est le dossier de settings de VSCode. Mais il est DANS un dossier .config, car avant mon regroupement il était dans /home/user/.config/.

En revanche, il n’est pas du tout nécessaire que .config contienne tous les dossiers qu’il avait précédemment. Seuls ceux qui vous intéressent. Le reste peut rester à sa place initiale, dans le /home/user/.config/.

Donc je résume:

  • Listez les fichiers et dossiers de settings qui vous intéressent.
  • Déplacez les tous dans un dossier commun en gardant une arborescence similaire.
  • Laissez les fichiers qui ne vous intéressent pas là où ils sont.
  • Priez (une bonne pratique avant toute opération informatique).

Arrive le messie, Stow.

D’abord, il faut l’installer, mais comme c’est un outil vénérable, il est dans les dépôts. Sous Ubuntu, le psaume “apt install stow” fera l’affaire.

Ensuite, on prêche. Je me perds dans mes propres paraboles, mais les voies du seigneur sont impénétrables, contrairement à celles d’Abella Anderson. Bref on demande à stow de traiter récursivement tout le contenu du dossier settings qui est dans /home/user/church afin de le linker vers /home/user/:

stow -d /home/user/church -t /home/user/ settings

Stow va prendre récursivement tous les dossiers qui sont dans /home/user/church/settings, et les comparer à ceux dans /home/user. Si ils existent, il va ne rien faire, mais si ils n’existent pas, il va créer un lien vers chacun de ceux manquants. Pour les fichiers, si ils n’existent pas, il va créer un lien, sinon il va vous afficher une erreur, afin de ne pas écraser quelque chose d’important et vous signalez qu’il y un souci.

Le but de tout ça ?

Pour votre système et tous vos logiciels, ça ne change rien. Ils vont tomber sur les liens et avoir l’impression que tous les fichiers de configs sont à leur place et vont continuer à fonctionner dans la joie et le gospel.

Et pour vous, ben vous avez un seul endroit où tous les fichiers importants sont regroupés. Plus besoin de les chercher. Facile à backuper et à restaurer. On peut même tout foutre sous Git.

Loué soit le sauveur.

Vive moi.

]]>
Jouer l’asticot sous les talons-aiguille, nouvel eldorado érotique 2http://sametmax.com/?p=23629http://streisand.me.thican.net/sametmax.com/index.php?20170917_160909_Jouer_l___asticot_sous_les_talons-aiguille__nouvel_eldorado_erotique_2Sun, 17 Sep 2017 14:09:09 +0000Allongé de tout votre long par terre, encordé, saucissonné, enveloppé dans un drap ou du papier film alimentaire, vous allez découvrir que jouer le vers de terre ou l’asticot, ça fait autant de bien que de se l’astiquer. "ErotiQ LombriQ", un jeu à pratiquer bien sûr sous les ordres de sa chère et tendre. Entre documentaire animalier et art ménagé. ]]>Ceci est un post invité de Emma posté sous licence creative common 3.0 unported.

Préambule

Allongé de tout votre long par terre, encordé, saucissonné, enveloppé dans un drap ou du papier film alimentaire, vous allez découvrir que jouer le vers de terre ou l’asticot, ça fait autant de bien que de se l’astiquer. “ErotiQ LombriQ”, un jeu à pratiquer bien sûr sous les ordres de sa chère et tendre. Entre documentaire animalier et art ménagé.

Avant toute chose : nous sommes tous d’accord pour admettre que le plaisir de l’humiliation fait désormais parti du lifestyle de l’époque. A l’heure des déclarations vexatoires de Trump et des nouilles dans le slip de Hanouna, le sm est devenu un truc de Bisounours dans la mesure où là, tout est consenti.

Quel équipement ?

Pas de panoplie très élaborée. 2 ou 3 cordes achetées chez Bricorama suffiront amplement pour se faire saucissonner dans un premier temps. Vous pouvez aussi utiliser un drap dans lequel il faudra s’enrouler, façon rouleau de printemps. Il y aussi le wrapping, technique sm très en vogue en ce moment chez les dominas pro. Il s’agit de se faire immobiliser, envelopper intégralement de papier film, le même que celui qu’on utilise pour recouvrir un reste de spaghetti bolognaise à mettre au frigo. Seule la tête reste libre. Petit détail qui a son importance, lors de l’enroulement, le sboub doit-il être comprimé vers le haut ou vers le bas ? C’est à votre copine de trancher.

Pour toutes ces techniques, inutile que votre nana maitrise le bondage sur le bout des doigts même s’il faudra qu’elle ait envie d’être un zest directive ce soir là, nous y reviendrons.

Jouer le lombric, quel intérêt pour le mec ?

De prime abord, on s’imagine une pratique pénible, ramper façon entrainement militaire ou Koh Lanta, un mauvais bizutage ou une reconstitution flippante d’un Faite entrer l’accusé avec cadavre retrouvé enroulé dans le tapis persan de la grand-mère… Élargissez votre horizon, la « vers de terre attitude », c’est bien plus profond que ça.

figurine-de-collection-pixi-spirou-enroule-dans-un-tapis-6569-2015L’époque nous a transformé en robots multitâches, pressurisés comme jamais, chaque minute doit être rentable, chaque objectif rempli en temps et en heure, et plus vite que ça ! Il faut réussir sa life dans tous les domaines : être un king dans le taf, avoir pleins d’amis réels et virtuels, s’afficher en papa modèle et enfin assurer comme une bête au pieu, la bite à la main, toujours prêt (le mec n’a pas le droit de dire « non »). Au regard de ces constats, admettez que devenir une larve, une limace dénuée d’énergie, de cerveau, de bras, de jambes, de membres, peut s’avérer séduisant. Si les filles ont le droit de faire l’étoile de mer, les gars eux, ce sera donc le vers de terre.

Immobiliser ou presque, inutile de réfléchir, impossible de décider, d’agir, de contrôler, de faire des choix, c’est le grand soulagement, les vacances forcées, l’immense plaisir de la contrainte qui libère. Alors, vous pouvez philosopher : « je ne suis plus rien, je suis le lombric du monde ». Le lâcher-prise engendre le fameux subspace, cet état de conscience modifié procuré par les endorphines, bref, de quoi économiser quelques joints.

01Au raz du sol, vous ondulez avec vue imprenable sur les Bouboutin de votre meuf, ses jambes, ses bas-couture. Même le grand Bashung l’avait fantasmé dans sa chanson J’ai longtemps contemplé (album Chatterton)

“J’ai longtemps contemplé
Tibias, péronés
Au ras des rez-de-chaussée
Ces cités immenses
Où je ne rutilais pas
J’arpentais des tapis de braise…”

Mais attention, ce n’est pas le Club Med pour autant. Il va falloir essayer de bouger un peu, de se tortiller, avancer centimètre par centimètre, bref ramper tel un nuisible, pour atteindre le ridicule absolu ! Votre douce complice peut vous motiver en semant par terre des lombrics gélifiés trouvés au rayon Haribo, ou disséminer dans toute la pièce ses culottes sales ou encore des lignes de coke. Mais être saucissonné sous cocaïne, c’est un peu comme boire un Pétrus juste après s’être lavé les dents avec un dentifrice mentholé, c’est gâché !

Après vous avoir fait mordre la poussière, la meuf va vous obliger à jouer l’aspirateur à cunni. D’un coup de talon, la miss vous fera rouler sur le dos. Elle ne résistera pas à l’envie sadique de retirer son string histoire de vous faire admirer sa moquette, juste au dessus de votre tête. Toujours impossible de bouger un doigt, vous êtes à sa merci, et on dit merci qui ? « Merci Maîtresse ! » Dans sa grande mansuétude, elle se servira de vous comme d’un vulgaire sextoy en s’asseyant sur votre tronche, pour un facesitting d’anthologie. Attention tout de même à l’asphyxie, car enfoui sous ses fesses, difficile de bafouiller un safe word audible ou de faire un signe de secours, vu que vous n’avez plus de bras. À moins de se tordre comme un appât à deux doigts d’être accroché à l’hameçon de la pécheresse.

ver-une-caricature-stupide-fou-tee-shirts-t-shirt-hommeFaire mordre la poussière à son partenaire, quel intérêt pour la nana ?

Si les mecs ne peuvent pas échapper aux bimbos à plat ventre au détour d’un spam, d’un kiosque à journaux ou d’une pub dans le métro, pour les femmes, profiter de mecs dans le même état d’abandon, c’est beaucoup plus rare. Mais il y a quand même deux professions masculines où les filles ont la chance d’admirer quasi systématiquement un gars à quatre pattes voir à plat ventre, raie du cul apparente : plombier et dépanneur informatique. Ces derniers passent sous le bureau et se retrouvent nez à nez ou plutôt nez à pieds avec les escarpins aux talons parfois vertigineux.

Avoir un homme à ses pieds, lui demander n’importe quoi sans avoir à lui dire merci, ça peut être très excitant. C’est aussi une bonne façon de se foutre de sa gueule, de régler des vieux comptes, lui faire manger les acariens à ce vaurien, depuis le temps qu’il repousse le moment de passer l’aspi. « Les plates excuses, c’est fini ! Tu n’es qu’un insecte rampant. Ce soir, ce ne sera pas « baisons » mais baygon vert ! » (contre insectes rampants, cafards, fourmis, action immédiate et longue durée, comme dit la pub.)

Il peut être très plaisant de faire rouler le vermisseau sous ses pieds, un peu comme pousser un gros boudin, ça rappelle l’émission culte Interville. Et puis, comme expliqué plus haut, la demoiselle ou la dame pourra utiliser l’asticot tel un objet sexuel, en bonne entomologiste zoophile. Elle peut faire un trou dans le drap ou le papier film, juste au niveau du zboub.

353-0Mais votre amour propre est sauf, elle ne pourra pas vous traiter de « bite sur patte », puisque vous n’en avez plus, des papattes. Reste pour elle à s’empaler gaiement sur votre appendice. Concentrez-vous uniquement sur votre érection. Le reste n’a plus aucune importance vu le ridicule dans lequel vous êtes vautré, plus rien à réussir, plus d’objectif à atteindre, le nirvana post productiviste en somme.

 De notre contributrice Emma du blog Paris Derrière, blog sur les frasques du Paris érotique.

logo6

 

]]>
La course du bus de l’innovation sur le chemin de la croissance 6http://sametmax.com/?p=18765http://streisand.me.thican.net/sametmax.com/index.php?20170722_221046_La_course_du_bus_de_l___innovation_sur_le_chemin_de_la_croissance_6Sat, 22 Jul 2017 20:10:46 +0000Ceci est un post invité de 01ivier posté sous licence creative common 3.0 unported.

Bonsjours à toutes et à tous,

Dans un sursaut de patriotisme, je me suis dit qu’il me fallait, moi aussi, participer à l’effort national et promouvoir le savoir-faire technologique français. Partant du constat que, de nos jours, proposer quelque chose de nouveau semble suffisant pour être innovant, j’ai fait comme tout bon startupeur, je ne me suis pas embarrassé à concevoir quelque chose d’utile. À vrai dire, ce n’est même pas nouveau. Mais je suis français et j’ai un compte Twitter, je crois que c’est amplement suffisant pour prétendre au label #CitizenOfTheFrenchTechNation.

 
frenchtech

 

Où le bus l’innovation va tout droit.

Parce que je suis bien conscient de l’influence que cet article va avoir dans le monde du jeu vidéo, je vais détailler les différentes étapes de la réalisation de ce projet bouleversant.

Tout d’abord, j’ai fait en sorte que le bus de l’innovation aille tout droit sur un chemin de la croissance qui est tout droit.

#-*- coding: utf-8 -*-
 
# On importe de quoi dormir
from time import sleep
 
# On définit le graphisme en accord avec l'équipe de designers
chemin = " " 
arbre = "A"
bus = "B"
 
# On définit la qualité de l'image après avoir consulté les experts en GPU du Digital-Agile-Open-Tech-Lab-Responsive.IO
largeur_ecran = 70
 
# On définit l'état initial conjointement avec la team level design.
# (elle-même ayant sollicité au préalable le narrative designer, bien entendu)
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
while 1:
 
    # On place la première portion d'arbres dans le paysage
    paysage = arbre * position_chemin
    # On place le chemin
    paysage += chemin * largeur_chemin
    # On remplit le reste du paysage par la deuxième portion d'arbres
    paysage += arbre * (largeur_ecran - len(paysage))
    # On place le bus sur notre paysage
    paysage = paysage[:position_bus] + bus + paysage[position_bus:]
 
    # On affiche le tout
    print(paysage)
 
    # On dort un peu
    sleep(0.2)

bus_innovation_droit

Oui. Il est effectivement possible d’arriver plus simplement au même résultat, mais, mes qualifications ne m’offrent pas le loisir d’être incompétent. J’anticipe donc les futurs mouvements du bus de l’innovation ainsi que les inéluctables virages du chemin de la croissance.

Ce doit être un peu déroutant pour vous, j’en conviens, mais c’est l’expérience qui parle.

Faites-moi confiance.

 

Où le bus l’innovation va tout droit mais en bien plus beau.

Parce que l’on n’est jamais assez exigent sur l’apparence d’un produit de luxe, j’ai voulu changer le rendu des arbres en remplaçant le “A” par un “█” bien plus raffiné. Voici le résultat que j’ai obtenu :

bus_innovation_droit_arbre

Diable comme le succès est semé d’embûches !

En effet, un petit…

>>> len("█")
3

… m’a appris que le “█” comptait pour trois caractères, faussant mon ingénieuse gestion des positions.

Mon talent m’avait permis d’anticiper les mouvements et autres virages mais pas ça. Damned. Qu’allais-je faire ?

Un petit replace() bien placé et le tour était joué. Sans compter que je pouvais désormais me permettre de mettre de la couleur.

La classe internationale.

#-*- coding: utf-8 -*-
 
from time import sleep
 
chemin = " " 
arbre = "A"
#On définit un arbre classe et vert en utilisant les codes ANSI
arbre_classe_et_vert = "\33[32m█\033[0m"
bus = "B"
 
largeur_ecran = 70
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
while 1:
 
    paysage = arbre * position_chemin
    paysage += chemin* largeur_chemin
    paysage += arbre * (largeur_ecran - len(paysage))
    paysage = paysage[:position_bus] + bus + paysage[position_bus:]
    # PAF ! On remplace l'arbre ridicule par un arbre classe et vert après avoir géré la position des différents éléments.
    paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
    print(paysage)
 
    sleep(0.5)

BIM !

bus_innovation_droit_arbre_vert

 

Où le bus de l’innovation va tout droit mais sur un chemin de la croissance qui tourne.

Pour faire tourner le chemin de la croissance je fais un petit +2/-2 sur sa position avec un randint(), et zou.

Par contre, pour forcer le chemin de la croissance à rester dans l’écran, j’ai été surpris de ne pas trouver de fonction prête à l’emploi pour contraindre un nombre dans un intervalle donné. Je suis donc passé par une “astuce” pécho sur Stack Overflow que j’ai trouvée élégante. On classe par ordre croissant le nombre donné avec les bornes de l’interval et on récupère le deuxième élément.

position = sorted(position_min, position, position_max)[1]

Si vous avez mieux, ça m’intéresse.

#-*- coding: utf-8 -*-
 
# On importe de quoi choisir des nombres au hasard
from random import randint
 
from time import sleep
 
chemin = " " 
arbre = "A"
arbre_classe_et_vert = "\33[32m█\033[0m"
bus = "B"
 
largeur_ecran = 70
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
# On définit une position maximale pour le chemin de la croissance
position_max_chemin = largeur_ecran - largeur_chemin
 
while 1:
 
    # On calcule la nouvelle position du chemin de la croissance
    # Un peu plus à droite, un peu plus à gauche ou un peu plus tout droit...
    position_chemin += randint(-2, 2)
 
    # En s'assurant qu'il ne déborde pas de la largeur de l'écran
    position_chemin = sorted([1, position_chemin, position_max_chemin])[1]
 
    paysage = arbre * position_chemin
    paysage += chemin * largeur_chemin
    paysage += arbre * (largeur_ecran - len(paysage))
    paysage = paysage[:position_bus] + bus + paysage[position_bus:]
    paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
    print(paysage)
 
    sleep(0.5)

bus_innovation_chemin_virage

 

Où le bus de l’innovation crache des flammes.

Avant de me pencher sur les mouvements du bus de l’innovation, j’ai pris le temps de le tuner un peu.

Déjà, direct, je l’ai peint en rouge, rapport au fait que le bus de l’innovation, c’est un peu la Ferrari de l’entrepreneuriat.

Et puis, je me suis dit qu’il fallait qu’il n’y ai vraiment absolument aucun doute sur le fait que c’était bel et bien le bus de l’innovation. Je lui ai donc fait écrire “LE BUS DE L’INNOVATION” sur la route. Je m’en remets à vous pour me dire s’il reste une ambiguïté.

Accessoirement, le “A” utilisé pour placer les arbres est devenu un “a“, pour ne pas être confondus avec le “A” présent dans “INNOVATION”. C’est un détail, mais les générations d’ingénieurs qui liront ça dans deux cent ans seront bien contents de trouver cette explication.

#-*- coding: utf-8 -*-
 
from random import randint
from time import sleep
 
chemin = " " 
arbre = "a"
arbre_classe_et_vert = "\33[32m█\033[0m"
 
largeur_ecran = 70
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
# On définit le texte écrit par le bus de l'innovation
texte_du_bus = "LE BUS DE L'INNOVATION "
# On récupère le nombre de caractères dans le texte écrit par le bus de l'innovation
nb_caractere = len(texte_du_bus)
# On initialise un compteur pour gérer la succession des caractères
compteur = 0
 
position_max_chemin = largeur_ecran - largeur_chemin
 
while 1:
 
    position_chemin += randint(-2, 2)
    position_chemin = sorted([1, position_chemin, position_max_chemin])[1]
 
    paysage = arbre * position_chemin
    paysage += chemin * largeur_chemin
    paysage += arbre * (largeur_ecran - len(paysage))
 
    # Dans le texte écrit par le bus de l'innovation, on prend le caractère 
    # indiqué par le compteur modulo le nombre de caractères possibles
    caractere = texte_du_bus[compteur%nb_caractere]
    # On peint le caractère en rouge Ferrari
    bus = "\33[31m{0}\033[0m".format(caractere)
    # On incrémente le compteur pour avoir le caractère suivant au prochain tour
    compteur += 1
 
    paysage = paysage[:position_bus] + bus + paysage[position_bus:]
    paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
    print(paysage)
 
    sleep(0.5)

Magnifique.

bus_innovation_flamme

 

Où l’on cherche à faire tourner le bus de l’innovation.

Je ne vais pas vous mentir, la course du bus de l’innovation sur le chemin de la croissance n’est pas vraiment une nouveauté vidéoludique. Nous, c’est à dire La Labomedia, l’avons déjà présentée à Toulouse lors du THSF 2014 ainsi qu’au PSESHSF 2016 de Choisy-le-Roi dont il existe même une vidéo de la Master Class.

Sur cette photo spectaculaire, vous pouvez découvrir la pertinence de notre ingénieuse interface biologique imputrescible nourrie aux anti-biotiques NF.

busdelinnovationsurlechemindelacroissance

Mais, non seulement j’avais perdu totalement le code initial mais en plus l’installation s’appuyait sur l’excessivement chère MakeyMakey qui, par ailleurs, telle qu’elle est vendue, impose à l’utilisateur d’être relié à une masse.

Pour sa résurrection, je lui ai donc préférée la Capacitive Touch HAT pour RaspberryPi conçue par Adafruit et qui fonctionne direct au simple touché.

(Il va de soit que les natural chicken flavor interfaces n’ont, elles, pas été remises en question.)

Voici le code minimal pour utiliser le Capacitive Touch HAT une fois la librairie d’Adafruit installée :

import Adafruit_MPR121.MPR121
from time import sleep
 
interface = Adafruit_MPR121.MPR121.MPR121()
interface.begin()
 
while 1:
 
  # Si la patine numéro 0 est touchée
  if interface.is_touched(0):
      print("GAUCHE !")
 
  # Si la patine numéro 10 est touchée
  if interface.is_touched(10):
      print("DROITE !")
 
  sleep(0.1)

Qui devient ceci quand on instaure un seuil de détection pour tenir compte de la conductivité des pattes de poulet.

import Adafruit_MPR121.MPR121
from time import sleep
 
interface = Adafruit_MPR121.MPR121.MPR121()
interface.begin()
 
seuil = 100
 
while 1:
 
  # Si la patte de poulet reliée à la patine numéro 0 est touchée
  if interface.filtered_data(0) < seuil:
      print("GAUCHE !")
 
  # Si la patte de poulet reliée à la patine numéro 0 est touchée
  if interface.filtered_data(10) < seuil:
      print("DROITE !")
 
  sleep(0.1)

On ne peut pas dire que ce soit très compliqué. À noter tout de même la nécessité d’activer l’I2C et de lancer le script en root. Une formalité pour celles et ceux qui ont de l’ambition dans la Vie.

Une fois intégré dans notre formidable simulation de sport mécanique extrême, le script ressemble alors à ça :

#-*- coding: utf-8 -*-
 
# On importe la librairie d'Adafruit
import Adafruit_MPR121.MPR121 as MPR121
 
from random import randint
from time import sleep
 
# On instancie l'interface...
interface = MPR121.MPR121()
# ... et on la démarre.
interface.begin()
 
chemin = " " 
arbre = "a"
arbre_classe_et_vert = "\33[32m█\033[0m"
 
largeur_ecran = 70
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
texte_du_bus = "LE BUS DE L'INNOVATION "
nb_caractere = len(texte_du_bus)
compteur = 0
 
position_max_chemin = largeur_ecran - largeur_chemin
 
seuil = 100
 
while 1:
 
    # En fonction des patines touchées,
    # on déplace le bus de l'innovation vers la droite...
    if interface.filtered_data(0) < seuil:
        position_bus += 1
 
    # ... ou vers la gauche.
    if interface.filtered_data(10) < seuil:
        position_bus -= 1
 
    position_chemin += randint(-2, 2)
    position_chemin = sorted([1, position_chemin, position_max_chemin])[1]
 
    paysage = arbre * position_chemin
    paysage += chemin * largeur_chemin
    paysage += arbre * (largeur_ecran - len(paysage))
 
    caractere = texte_du_bus[compteur%nb_caractere]
    bus = "\33[31m{0}\033[0m".format(caractere)
    compteur += 1
 
    paysage = paysage[:position_bus] + bus + paysage[position_bus:]
    paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
    print(paysage)
 
    sleep(0.5)

bus_innovation_tourne

 

Où le bus de l’innovation est tenu de rester sur le chemin de la croissance.

Écoutez, que les choses soient bien claires, moi aussi je trouve cet article beaucoup trop long, mais c’est du rayonnement de la France dont il est question. Dois-je vous le rappeler ?

Voici donc comment j’ai tenu compte des sorties du chemin de la croissance par notre bus de l’innovation :

#-*- coding: utf-8 -*-
 
import Adafruit_MPR121.MPR121 as MPR121
 
from random import randint
from time import sleep
 
interface = MPR121.MPR121()
interface.begin()
 
chemin = " " 
arbre = "a"
arbre_classe_et_vert = "\33[32m█\033[0m"
 
largeur_ecran = 70
largeur_chemin = 15
position_chemin = 28
position_bus = 35
 
texte_du_bus = "LE BUS DE L'INNOVATION "
nb_caractere = len(texte_du_bus)
compteur = 0
 
position_max_chemin = largeur_ecran - largeur_chemin
 
# On définit un booléen qui rendra compte de l'état du bus de l'innovation
le_bus_roule = True
 
seuil = 100
 
while 1:
 
    if interface.filtered_data(0) < seuil:
        position_bus += 1
 
    if interface.filtered_data(10) < seuil:
        position_bus -= 1
 
    # Si le bus roule...
    if le_bus_roule:
 
        position_chemin += randint(-2, 2)
        position_chemin = sorted([1, position_chemin, position_max_chemin])[1]
 
        paysage = arbre * position_chemin
        paysage += chemin * largeur_chemin
        paysage += arbre * (largeur_ecran - len(paysage))     
 
        # Si le bus sort de la route, à gauche ou à droite...
        if position_bus <= position_chemin or position_bus >= position_chemin + largeur_chemin:
 
            # On change l'apparence du bus (qui devient une croix verte)
            bus = "\33[32mX\033[0m"
            # On change l'état du bus de l'innovation
            le_bus_roule = False
 
        # Sinon, on affiche le bus comme précédement défini
        else:
            caractere = texte_du_bus[compteur%nb_caractere]
            bus = "\33[31m{0}\033[0m".format(caractere)
            compteur += 1
 
        paysage = paysage[:position_bus] + bus + paysage[position_bus:]
        paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
        print(paysage)
 
        # Si, entre temps, le bus de l'innovation s'est arrêté de rouler
        if not le_bus_roule:
 
            # On affiche un message sympathique après avoir sauté une ligne
            print("\nIl n'est pas exclu que le bus de l'innovation se soit pris un arbre...")
 
        sleep(0.5)

bus_innovation_crash

 

Où la course du bus de l’innovation sur le chemin de la croissance va pouvoir enfin commencer.

Afin que le public intègre bien le défi collectif que représente la relance de l’économie par le financement et le développement d’innovants nouveaux projets novateurs avec de la technologie à la pointe de la technologie, j’ai fait en sorte d’afficher la croissance totale que l’ensemble des joueurs auront fait parcourir au bus de l’innovation.

Ainsi, n’y a-t-il jamais un individu qui perd, mais toujours un collectif qui gagne.

Quelque chose entre la victoire éternelle et le succès permanent.

Une véritable leçon de Vie.

#-*- coding: utf-8 -*-
 
import Adafruit_MPR121.MPR121 as MPR121
 
from random import randint
from time import sleep
 
interface = MPR121.MPR121()
interface.begin()
 
chemin = " " 
arbre = "a"
arbre_classe_et_vert = "\33[32m█\033[0m"
 
largeur_ecran = 70
largeur_chemin = 15
 
# On met en place un système d'initialisation des positions
# du chemin de l'innovation et du bus de la croissance
init_position_chemin = 28
init_position_bus = 35
position_chemin = init_position_chemin
position_bus = init_position_bus
 
texte_du_bus = "LE BUS DE L'INNOVATION "
nb_caractere = len(texte_du_bus)
compteur = 0
 
position_max_chemin = largeur_ecran - largeur_chemin
 
seuil = 100
 
le_bus_roule = True
 
# On déclare des variables qui vont nous permettre de rendre compte
# des quantités de croissance parcourue par le bus de l'innovation
croissance_parcourue_totale = 0
croissance_parcourue = 0
 
# On définit un petit texte à formater pour présenter les quantités en question
texte_crash ='''
Bravo !
Tu t'es pris un arbre mais tu as parcouru {0} mètres de croissance !
 
Pour ton information, le Bus de l'Innovation a parcouru {1} mètres
de croissance depuis son départ.
 
Pour le faire redémarrer, appuie sur la cuisse de poulet du milieu.
La France compte sur toi !
'''
 
while 1:
 
    if interface.filtered_data(0) < seuil:
        position_bus += 1
 
    if interface.filtered_data(10) < seuil:
        position_bus -= 1
 
    # On met en place un moyen de reprendre le chemin de la croissance
    # si le bus de l'innovation s'est pris un arbre
    if if interface.filtered_data(5) < seuil and not le_bus_roule:
        le_bus_roule = True
        croissance_parcourue = 0
        position_chemin = init_position_chemin
        position_bus = init_position_bus
        compteur = 0
 
    if le_bus_roule:
 
        position_chemin += randint(-2, 2)
        position_chemin = sorted([1, position_chemin, position_max_chemin])[1]
 
        paysage = arbre * position_chemin
        paysage += chemin * largeur_chemin
        paysage += arbre * (largeur_ecran - len(paysage))     
 
        if position_bus <= position_chemin or position_bus >= position_chemin + largeur_chemin:
 
            bus = "\33[32mX\033[0m"
            le_bus_roule = False
 
        else:
            caractere = texte_du_bus[compteur%nb_caractere]
            bus = "\33[31m{0}\033[0m".format(caractere)
            compteur += 1
            # On incrémente la croissance parcourue à chaque tour
            croissance_parcourue += 5
 
        paysage = paysage[:position_bus] + bus + paysage[position_bus:]
        paysage = paysage.replace(arbre, arbre_classe_et_vert)
 
        print(paysage)
 
        if not le_bus_roule:
 
            # On calcule la croissance totale parcourue
            croissance_parcourue_totale += croissance_parcourue
            # On formate notre petit texte pour informer le joueur de sa 
            # performance individuelle et collective.
            print(texte_crash.format(croissance_parcourue, croissance_parcourue_totale))
 
        sleep(0.5)

bus_innovation_arcade

]]>
Le pattern strategy version gastronomique 9http://sametmax.com/?p=23520http://streisand.me.thican.net/sametmax.com/index.php?20170714_211106_Le_pattern_strategy_version_gastronomique_9Fri, 14 Jul 2017 19:11:06 +0000Allez, un petit article de POO un peu avancée pour faire marcher ses neurones ce WE.

Le design pattern strategy, qui consiste à déléguer une partie du comportement d’un objet à un autre objet, est probablement l’un des motifs de conception les plus utiles en programmation. Trop souvent les gens utilisent l’héritage là où la composition serait plus adaptée, et une injection de dépendance bien faite permet de gagner beaucoup en qualité de code.

Si vous ne vous souvenez plus de ce qu’est le pattern strategy, vous pouvez faire un saut sur le chapitre qui en parle dans le guide de la POO :)

Mais comme un petit rappel ne fait pas de mal, en très court, strategy ressemble à ça :

class MonObjet:
    def __init__(self):
        self.strategie = MaStrategie()
 
    def foo(self):
        return self.strategie.foo()

Ce qui permet à une sous-classe de changer la strategy ou non :

class MonSousObjet(MonObjet):
   def __init__(self):
        self.strategie = MonAutreStrategie()

Ou de changer la strat dynamiquement :

hop = MonObjet()
 
hop.stategie = SuperNewStrat()

Mais si vous vous en tenez à ce design, les utilisateurs de la classe vont très vite rencontrer des limitations.

D’abord, une bonne stratégie peut avoir besoin de contexte. Dans ce cas, donnez lui le choix d’avoir une référence à l’objet parent:

class MonObjet:
    def __init__(self):
        # Passer self permet à la stratégie de connaître son contexte. 
        # Le désavantage est l'introduction potentiel d'un couplage entre 
        # les deux objets, et potentiellement des effets de bords supplémentaires. Cela reste 
        # néanmoins souvent une bonne idée.
        self.strategie = MaStrategie(self)   
    ...

Ensuite, une stratégie devrait pouvoir être passée à la création de l’objet :

class MonObjet:
    def __init__(self, strategie=MaStrategie):
        # On donne la priorité à l'objet passé en paramètre. Si il n'y en a 
        # pas on crée la stratégie par défaut.
        self.strategie = strategie(self)  
    ...

Cela permet d’overrider la stratégie pour les usages plus avancés, tout en permettant aux débutants de ne pas se soucier de cela car il existe quand même une valeur par défaut.

truc = MonObjet(UneStrategieDifferente)

Comme le travail dans un init est souvent assez redondant, avoir un endroit pour permettre aux sous-classes de facilement overrider la stratégie est une bonne pratique. En Python il est courant d’utiliser les variables de classes pour cela :

class MonObjet:
 
    # On appelle souvent cet attribut "strategy_class" ou "strategy_factory"
    strategie_par_default = MaStrategie
 
    def __init__(self, strategie=None):
        self.strategie = strategie(self) if strategie else self.strategie_par_default(self) 
    ...
 
class MonSousObjet(MonObjet):
    # Et boom, overriding de la stratégie par la classe enfant en une ligne.
    # Django fait ça par exemple avec les classes based views et l'attribut 
    # model
    strategie_par_default = MonAutreStrategie

Une fois que vous avez fait tout ça, vous avez déjà fait mieux que 90% des programmeurs. Néanmoins si vous voulez vraiment mettre la petit touche pro à votre API, vous pouvez aussi permettre la création dynamique de la stratégie:

class MonObjet:
 
    strategie_par_default = MaStrategie
 
    def __init__(self, strategie=None):
        self.strategie = strategie(self) if strategie else self.build_strategy() 
 
    def build_strategy(self):
        return self.strategie_par_default(self)
 
    ...

Wow, ça en fait des self et des factories :) En fait, ça fait la même chose qu’avant, c’est à dire que le tout premier exemple de code tout simple qu’on a vu en début d’article marche toujours ! C’est la beauté de la chose.

La différence, c’est que maintenant une classe enfant peut overrider build_strategy() et créer des stratégies à la volée, en fonction du contexte d’exécution. Par exemple créer une stratégie différente en fonction d’une valeur de base de données. C’est rare que ça arrive, et c’est vraiment de l’usage avancé. Mais quand vous avez ça, vous êtes certains que votre code est prêt à être utilisé par autrui. Car si cet autrui n’est pas content, il peut faire une profonde coloscopie à votre code et y insérer ce qu’il veut, quand il veut.

Être dev après tout, c’est être un peu poète.

]]>
Le don du mois: Framasoft (bis) 7http://sametmax.com/?p=23501http://streisand.me.thican.net/sametmax.com/index.php?20170711_091538_Le_don_du_mois__Framasoft__bis__7Tue, 11 Jul 2017 07:15:38 +0000déjà donné à Framasoft il y a 2 ans alors que je n'utilisais plus vraiment le site. Plutôt en remerciement de tout ce qu'ils ont fait pendant que j'étais encore sous Windows à bricoler avec Phoenix, CloneCD et WinAmp. ]]>J’ai déjà donné à Framasoft il y a 2 ans alors que je n’utilisais plus vraiment le site. Plutôt en remerciement de tout ce qu’ils ont fait pendant que j’étais encore sous Windows XP à bricoler avec Phoenix, CloneCD et WinAmp.

Depuis 2001 cet excellent portail a fleuri pour devenir un hébergeur de nombreux services, particulièrement:

Pour l’instant je suis très content de l’agenda qui est une instance nextcloud avec une jolie UI, offrant plusieurs calendriers en parallèle, des partages et exports et tout le tintouin. Donc j’ai thunderbird linké dessus sous Ubuntu, et Solcalendar sous Android (avec pulse sms pour pouvoir taper les sms confortablement sur l’ordi, rien à avoir mais j’en suis content alors je plug).

Bref, cet outil arrive à point pour être bien productif. Reste plus qu’à trouver une alternative à GTG qui est lent comme une grand-mère asthmatique. Si vous avez des suggestions…

Néanmoins, héberger des services comme ça, ça coûte cher. Et framasoft les propose gratuitement.

Donc, don de 30 euros à l’asso. Merci messieurs-dames, vous déchirez.

Pour vous aussi faire un don, c’est par ici.

]]>
Caldigit et USB-C 13http://sametmax.com/?p=23488http://streisand.me.thican.net/sametmax.com/index.php?20170710_134139_Caldigit_et_USB-C_13Mon, 10 Jul 2017 11:41:39 +0000Mon rêve d’avoir un setup tri-écran, ethernet, casque-micro et chargement avec un seul câble est enfin devenu une réalité.

Et ça a pas été de la tarte.

J’ai un Dell XPS 15 qui a un port USB-C avec thunderbolt. J’ai acheté un dock USB-C caldigit qui propose une entrée micro, une sortie casque, un port HDMI, un display port et un port ethernet, le tout avec un seul branchement via USB-C.

Inutile de dire que ça n’a pas marché. L’USB-C est une vraie jungle, vous connaissez aussi mon avis sur les finitions du XPS 15 et puis la vidéo, ça fait partie de ces trucs qui sont toujours galère en 2017.

Mais…

Après avoir:

  • Mis à jour tous les drivers du XPS et flashé le bios et le controller USBC (sic).
  • Téléchargé le driver ethernet linux de caldigit (sympas !).
  • Avoir acheté les bons câbles (USB-C respectant le standard, convertisseurs HDMI actifs, etc).
  • Flashé le dock avec l’outil caldigit en ayant les bons câbles branchés.
  • Et bidouillé avec la config d’affichage d’Ubuntu pendant pas mal de temps.

Ca marche.

Pas plug and play pour deux ronds. Mais j’ai enfin un seul câble qui charge l’ordi, m’envoie un débit fibré et étend mon affichage, automatiquement au boot, sous Linux et Windows.

Joie.

Si j’écris l’article, c’est pour dire que:

  • C’est possible.
  • C’est chiant.
  • Le dock caldigit est le seul que j’ai trouvé qui marche avec le XPS (qui suce un max de Watts). Et j’en ai essayé des bidules.
  • Les câbles et adaptateurs sont le nerf de la guerre. Et ils sont dans le camp ennemi.

Mais surtout pour signaler que le support de caldigit a été fantastique, ce qui est assez rare pour le souligner. Ma première interlocutrice via chat a répondu vite et posé les bonnes questions. Identifiant qu’elle ne pouvait pas résoudre le problème, elle m’a redirigé vers le niveau supérieur du support en faisant suivre correctement tout le dossier. Niveau supérieur très technique qui a permis, mail après mail, détail après détail, à faire marche le bouzin. Pas une question à la noix. Pas besoin de me répéter 15 fois. Pas de remise en cause de ma bonne foi ou de prise pour un gogole. Quand les gens font leur boulot ça étonne toujours.

]]>
Alternative au do…while en Python 21http://sametmax.com/?p=23459http://streisand.me.thican.net/sametmax.com/index.php?20170703_172510_Alternative_au_do___while_en_Python_21Mon, 03 Jul 2017 15:25:10 +0000do..while en python, on fait donc généralement une boucle infinie suivie d'un break sur une condition.]]>De nombreuses instructions ont été volontairement écartées de Python. Le goto bien entendu, mais aussi le switch, unless et le do...while.

Le but est de limiter le nombre de mots clés à connaitre afin de comprendre le langage. Les créateurs ont choisi donc de mettre de côté des mots clés trop souvent mal utilisés, pas assez utilisés, ou qui possèdent des alternatives suffisantes.

La boucle while est rarement utilisée en Python, en tout cas beaucoup, beaucoup moins que sa petite soeur la boucle for. Avoir besoin d’un do...while est encore plus rare, et donc ne peut faire partie du club très fermé des mots clés réservés.

Si l’on souhaite obtenir l’effet du do..while en python, on fait donc généralement une boucle infinie suivie d’un break sur une condition. Exemple:

import random
 
choix = random.randint(0, 100)
 
while True:
    reponse = int(input('Devinez le nombre: '))
    if reponse < choix:
        print('Plus grand')
    elif reponse > choix:
        print('Plus petit')
    else:
        break
print('Bravo')

Le défaut de cette technique est qu’elle ne rend pas clair dès le début la condition de sortie de la boucle. Aujourd’hui en parcourant la mailling list python-idea, je suis tombé sur une idée pas conne:

import random
 
choix = random.randint(0, 100)
 
while "L'utilisateur n'a pas encore deviné le nombre":
    reponse = int(input('Devinez le nombre: '))
    if reponse < choix:
        print('Plus grand')
    elif reponse > choix:
        print('Plus petit')
    else:
        break
print('Bravo')

Ca marche car les chaînes non vides sont toujours vraies en Python, et ça documente le code :)

]]>
Les plus grosses roues du monde 15http://sametmax.com/?p=23443http://streisand.me.thican.net/sametmax.com/index.php?20170630_145808_Les_plus_grosses_roues_du_monde_15Fri, 30 Jun 2017 12:58:08 +0000L’avantage d’avoir quelques années de programmations dans les pattes et un certain nombres de projets à son actif, c’est qu’on arrive à identifier des motifs communs qui se dégagent encore et encore.

Par exemple, quand j’étais en tout début de carrière, j’ai ouvert l’excellent bouquin “Head first design patterns” et je n’en ai pas retiré grand chose car je n’avais pas la matière pour pouvoir identifier l’utilité des solutions proposées. Bien plus tard, en le relisant, je me suis aperçu que j’avais en fait rencontré moult fois chaque chapitre IRL, base de code après base de code.

La vie apprend les design patterns bien plus efficacement que les écrits. Mais ces derniers ont l’avantage de mettre de l’ordre dans ses idées. Ils mettent des mots sur des pensées floues, et tracent des contours qui délimitent pragmatiquement les concepts.

Aujourd’hui néanmoins, nous n’allons pas parler de design pattern, bien que faire un dossier dessus serait une bonne idée. Mais ça fait des mois que je dois finir le dossier tests unitaires, alors je vais pas commencer un nouveau dossier.

Non, aujourd’hui, nous allons parler d’outils dont on a besoin dans quasiment tous les projets importants et qu’on réinvente, ou rapièce, presque à chaque fois.

Dispatching

Que ce soit parce que vous avez un observer, du pub/sub, un système de routing ou des events internes, votre projet finira par avoir besoin d’un système de dispatching. Le dispatching c’est la propagation/distribution de l’information et de son traitement.

Ils ont toujours la même chose en commun:

  • Des points centraux qui prennent les entrées et les dirigent vers les bonnes sorties.
  • Un système d’enregistrement des points d’entrées (hooks) et des points de sorties (endpoints).
  • Une logique de résolution qui permet de faire transiter les données depuis une entrée, vers une ou plusieurs sorties, parfois aller-retour.

Par exemple:

  • Le pub/sub de redis ou crossbar.io.
  • Le routing URL d’un framework Web comme Django ou Rails.
  • Les events d’un UI comme le addEventListener en JS ou connect() en QT.
  • La remontée hiérarchique des messages de logs dans le module Python logging.

Ce sont tous des implémentations spécialisées d’un système de dispatching. Le hello world du dispatching est le design pattern observer, qui minimalement ressemble à ça:

>>> class Dispatcher:
...     def __init__(self):
...         self.registry = {}
...     def on(self, event, callback):
...         self.registry.setdefault(event, []).append(callback)
...     def trigger(self, event):
...         for callback in self.registry[event]:
...                 callback()
... 
... hub = Dispatcher()
... 
... hub.on("j'arrive", lambda: print('coucou'))
... hub.on("j'arrive", lambda: print('salut'))
... 
... hub.trigger("j'arrive")
... 
coucou
salut

En fait, à moins de faire uniquement des scripts, vous avez utilisé plein de systèmes de dispatching sans le savoir.

Bien que pour qu’ils soient utiles il faut des versions spécialisées pour chaque usage, c’est un problème générique et il est ridicule que nous devions réimplementer à chaque fois ce truc. Un bon système de dispatching est utile dans tout gros projet. Vous voulez permettre à quelqu’un de lancer du code quand votre système s’initialise ? Créer une logique de plugin ? Bam, il faut du dispatching.

Il faudrait donc un framework en Python qui permette de fabriquer son propre système de dispatching. Il devra bien entendu inclure des implémentations spécialisées au moins pour les cas les plus courants sinon ça fera comme une lib zope et ça prendra la poussière.

Le but étant qu’au bout de quelques années, tout le monde base son implémentation sur cette brique, robuste et documentée, plutôt que de créer son propre système.

En effet, un bon système de dispatching doit pouvoir gérer les cas suivants :

  • Mono directionnel ou bi-directionnel.
  • Un ou plusieurs endpoints.
  • Middlewares.
  • Algo de routing custo.
  • Passage de paramètres à l’ajout d’un enpoint.
  • Passage de contexte à l’appel de l’endpoint.
  • Appel synchrone ou asynchrone des endpoints.
  • Enpoints locaux ou remotes.
  • Gestion de plusieurs dispatchers en parallèle.
  • Interconnexion de plusieurs dispatchers.
  • Adapters pour les dispatchers courants déjà existants.

Le tout bien entendu avec des backends pour chaque partie qu’on puisse swapper.

Configuration

La conf, c’est l’exemple exacte de la fragmentation dans notre métier. C’est l’usine à roues (carrées) réinventées.

Sérieusement, entre le parsing des arguments de la ligne de commande, les fichiers de config, les services de config (etcd anyone ?), les configs sauvegardées en BDD, les API de conf, les variables d’environnement, etc. c’est un bordel sans nom.

Tout le monde a fait son petit fichier params.(ini|yml|xml|json) ou sa table SQL settings dans un coin. Et la validation. Et la génération. Et les valeurs par défaut. Et l’overriding des envs. Ca change à chaque projet, à chaque framework, à chaque foutue lib.

C’est que le but est simple, mais le problème est complexe. Mais on en a tous besoin, et il n’y a rien, mais alors rien qui existe de générique.

Une bonne lib de conf doit:

  • Offrir une API standardisée pour définir les paramètres qu’attend son programme sous la forme d’un schéma de données.
  • Permettre de générer depuis ce schéma les outils de parsing de la ligne de commande et des variables d’env.
  • Permettre de générer depuis ce schéma des validateurs pour ces schémas.
  • Permettre de générer des API pour modifier la conf.
  • Permettre de générer des UIs pour modifier la conf.
  • Séparer la notion de configuration du programme des paramètres utilisateurs.
  • Pouvoir marquer des settings en lecture seule, ou des permissions sur les settings.
  • Notifier le reste du code (ou des services) qu’une valeur à été modifiée. Dispatching, quand tu nous tiens…
  • Charger les settings depuis une source compatible (bdd, fichier, api, service, etc).
  • Permettre une hiérarchie de confs, avec une conf principale, des enfants, des enfants d’enfants, etc. et la récupération de la valeur qui cascade le long de cette hiérarchie. Un code doit pouvoir plugger sa conf dans une branche de l’arbre à la volée.
  • Fournir un service de settings pour les architectures distribuées.
  • Etre quand même utile et facile pour les tous petits scripts.
  • Auto documentation des settings.

Pas évident non ? Ça change de “ah bah je vais dumper tout ça dans un settings.py et on verra bien” :)

Les bénéfices d’avoir un bon système de settings sont énormes. D’abord, si il est largement adopté, plus besoin de fouiller dans la doc de chaque projet pour savoir comment l’utiliser. Les problèmes difficiles comme les “live settings” sont réglés une fois pour toute. Plus besoin d’écrire pour la millième fois le code de glue entre son schéma marshmallow + ses params clicks + son parser yml qui sera forcément bricolé. Et une expérience utilisateur bien meilleure avec de la doc, des messages standardisés, des checks de sources de données que d’habitude personne ne fait, etc.

Logging

Oui, je sais, je sais, Python a un excellent module de logging, très riche et polyvalent. Et puis ce ne sont pas les projets de logging qui manquent. Il y a celui de twisted, il y a logbook, logpy… En fait j’ai même pondu devpy.

Malgré ça, force est de constater que tous ces projets sont loin d’être une bonne solution pour convenir à tous.

Le module logging Python manque de configuration par défaut, n’a pas de gestion multiprocessing, aucune facilité pour générer des logs structurés ou binaires, etc. logbook et logpy sont des surcouches qui améliorent, mais sans aller assez loin, l’expérience. Twisted comme d’hab fait le café mais est indigeste. Logpy n’est bien que pour les cas simples.

Un bon module de logging devrait:

  • Etre compatible avec la stdlib, et avoir des adaptateurs pour les systèmes les plus courants (comme twisted).
  • Gérer le multiproccessing, particulièrement le locking des fichiers ou avoir un process central dédié aux logs.
  • Proposer une UI user friendly de consommation des logs.
  • Avoir des pré-configurations prête à l’usage hyper simple et rapide à utiliser pour les cas les plus simples, comme le scripting.
  • Gérer des formats avancés: json, binaires, nesting, logs structurés, etc. Avec des recettes toutes faites pour les cas courants.
  • Avoir une bonne intégration avec les systèmes de logs natifs de l’OS sans besoin de trifouiller.
  • Proposer des outils d’analyse de logs.
  • Offrir des solutions pour les cas simples (console, fichier, coloration, post-mortem, etc.) ou complexes mais courants (mail, logstash, sentry, log sur serveur distant, etc).

Au fait, aviez-vous noté que le cœur d’un système de log est un dispatcheur ? :)

Lifecyle

Dès que vous créez un projet, il a un cycle de vie. Il s’initialise, charge les paramètres, load les plugins si il y en a, lance son processus principal, puis finit par s’arrêter, ou foirer, ou les deux.

Si c’est un petit script, ce n’est pas très important, on ne s’en rend même pas compte.

Si c’est un gros projet, vous allez vouloir que le code du reste du monde puisse interagir avec ça. D’ailleurs, tous les gros frameworks vous permettent de réagir au cycle de vie. Django a le fichier appconfig.py par exemple pour lancer du code au démarrage du framework, et des middlewares pour intercepter les requêtes et les réponses. Twisted permet de dire “lance ce code dès que le reactor est en route”. Pour comprendre Angular ou une app Android, la moitié du boulot c’est de piger les différentes phases du cycle de chaque composant.

Le cycle de vie est en fait un système de dispatching (surprise !) couplé à une machine à état fini, et concrétisé dans un processus métier. La bonne nouvelle, c’est que des libs de state machines en Python on en a un max, et des bien fournies. La mauvaise, c’est qu’avec la popularité grandissante d’asyncio, on a de plus en plus besoin de gérer explicitement le cycle de vie de ses projets et qu’on a rien de générique pour ça alors la cyclogénèse envahie la communauté.

En effet, dès qu’on a une boucle d’événement comme avec asyncio/twisted/tornado, on a un cycle de vie complexe mais implicite qui se met en place puisque la loop démarre, s’arrête, est supprimée, est remplacée, est en train d’exécuter une tâche, une tâche qui peut générer des erreurs… Et très vite le cycle dégouline de partout, et on commence à coder ici et là pour gérer tout ça sans se rendre compte qu’on crée petit à petit un énième framework de lifecycle. Pas vrai Gordon ?

C’est l’histoire de la viiiiiiiiiiiiiiiie. C’est le cycle éterneleuuuuuuuuuh. De la roue infinieeeeeeee. Codée à la truelleuuuuuuh.

Structure de projet

Bon, imaginons que vous ayez une lib de life cycle, qui charge vos settings avec votre super lib de conf, logge tout grâce à votre géniale lib de logging, le tout powered par une lib de dispatching que le monde vous envie. Le perfide.

Je dis “imaginez” parce que dans votre projet vous avez plutôt un tas de crottes retenues par un cornet de glace que vous avez codé pour la énième fois à la va vite en utilisant 30% de libs tierce partie, 30% d’outils de votre framework du jour et 40% de roux (du NIH sans âme quoi…).

Donc imaginez ça. Et maintenant vous voulez mettre en place un moyen de diviser votre projet en sous parties. Peut être des apps, ou pourquoi pas des plugins. Mais vous voudriez que tout ça soit gérable par un point d’entrée principal, ou individuellement. Que ça se plug dynamiquement. Que ça joue bien avec votre système de conf et de lifecyle. Diantre, vous voulez qu’un code externe puisse être découvert et pluggé au système. Choisir si ça tourne dans des threads ou des processus séparés. Mais communiquer entre les parties. Et que tout ça soit découplé bien entendu ! Sauf qu’il y a une gestion de dépendances des plugins…

Pas de problème, vous prenez un bus de communication, un système de plugin, un graph de résolution de dépendances, vos super libs ci-dessus et vous gluez tout ça avec de la logique de chez mémé et de la sueur. Une mémé si ronde qu’elle a un pneu autour de la taille. Et un essieu.

Django a ses apps. jQuery a ses plugins. L’app d’un de mes clients avec un hack à base d’importlib et ctype qui loadait une dll pour charger les drivers de leur matos. Ca roule Maurice, ça roule à mort.

Il nous faut une lib de référence qui permette:

  • De diviser son projets en plus petites unités tout en gardant un point central pour faire le lien.
  • De découvrir, charger et décharger ces unités.
  • Permettre de répartir ces unités dans des threads, multiprocess, machines.
  • Les faire communiquer.

Et dans les ténèbres les lier

Une fois qu’on a tout ça, il faut bien entendu un gros framework qui permette de faire le lien entre tout ça et coder un projet automatiquement intégré.

Imaginez… Imaginez pouvoir faire un truc comme ça:

from super_framework import projets, config
 
# Tout est configurable et réassemblable, mais le framework offre des réglages 
# auto pour les usages simples
project, app, conf = projets.SimpleProject('name')
 
# Fichier de conf automatiquement créé, parsé et vérifié. Valeurs exposées en 
# CLI et overridables.
@config.source(file="foo.tml")
class Schema(config.Schema):
    foo = config.CharField(max_len=30, live=True)
    bar = config.DateField(optional=True, local=True)
    baz = config.TextField(
        verbose_name="Basile", 
        default="Je sers la science et c'est ma joie"
    )
 
# Lancé automatiquement à la phase d'init du projet
# Des events comme ça sont lancés pour chaque app, et chaque phase de vie de 
# chacune d'elles.
@project.on('init')
async def main(context):
    # un log sain automatiquement fourni
    app.log('Début du projet. Verbosité:', conf.log.level)
 
# Démarre l'event loop. Parse la ligne de commande et les 
# variables d'env, puis le fichier de conf. Mais seulement si le module n'est 
# pas importé (comme __name__ == "__main__")
# Print le fichier de log automatique dès le démarrage du programme
project.cmd()

Et imaginez que de ce petit script, ça scale sur 20 plugins qui peuvent communiquer, un système de settings live, de gestion d’erreurs et de logs aux petits oignons.

Imaginez que l’api bas niveau soit suffisamment flexible pour que les plus grands frameworks puissent réécrire exactement la même API qu’ils ont déjà en utilisant cette fondation. Imaginez que tous vos projets futurs soient du coup compatibles entre eux.

Vous pouvez imaginez longtemps, car ça n’arrivera jamais. Mais j’avais du temps à l’aéroport alors j’ai écrit cet article.

]]>
Accepter un ID mais retourner un objet pour les liens de Django Rest Framework 13http://sametmax.com/?p=23385http://streisand.me.thican.net/sametmax.com/index.php?20170608_095000_Accepter_un_ID_mais_retourner_un_objet_pour_les_liens_de_Django_Rest_Framework_13Thu, 08 Jun 2017 07:50:00 +0000DRF est une des perles de Django. De Python même. Comme marshmallow, requests, jupyter, pandas, SQLAlchemy ou l’admin Django. Python a tellement d’outils extraordinaires.

Mais aucune n’est parfaite, et une chose qui m’a toujours emmerdé avec celle-ci, c’est que si j’ai un modèle du genre:

class Foo(models.Model):
    name = models.CharField(max_length=64)
    bar = models.ForeignKey(Bar)

Et le serializer:

class FooSerialize(serilizers.ModelSerializer):
 
    class Meta:
        model = Foo

J’ai le choix entre soit avoir que des ID…

En lecture (chiant) :

GET /api/foos/1/

{
    name: "toto",
    bar: 2
}

Et en écriture (pratique) :

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Soit avoir que des objets.

En lecture (pratique):

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}
Et en écriture (chiant) :

POST /api/foos/
{
    name: "tata",
    bar: {
       // tout l'objet bar à se taper à écrire
    }
}

Il y a aussi la version hypermedia où l’id est remplacé par une URL. Mais vous voyez le genre : mon API REST est soit pratique en lecture mais relou à écrire, soit pratique en écriture (je fournis juste une référence), mais relou en lecture, puisque je dois ensuite fetcher chaque référence.

GraphQL répond particulièrement bien à ce problème, mais bon, la techno est encore jeune, et il y a encore plein d’API REST à coder pour les années à venir.

Comment donc résoudre ce casse-tête, Oh Sam! – sauveur de la pythonitude ?

Solution 1, utiliser un serializer à la place du field

class FooSerializer(serilizers.ModelSerializer):
 
    bar = BarSerializer()
 
    class Meta:
        model = Foo

Et là j’ai bien l’objet complet qui m’est retourné. Mais je suis en lecture seule, et il faut que je fasse l’écriture à la main. Youpi.

Pas la bonne solution donc.

Solution 2, écrire deux serializers

Ben ça marche mais il faut 2 routings, ça duplique l’API, la doc, les tests. Moche. Next.

Solution 3, un petit hack

En lisant le code source de DRF (ouais j’ai conscience que tout le monde à pas la foi de faire ça), j’ai noté que ModelSerializer génère automatiquement pour les relations un PrimaryKeyRelatedField, qui lui même fait le lien via l’ID. On a des classes similaires pour la version full de l’objet et celle avec l’hyperlien.

En héritant de cette classe, on peut créer une variante qui fait ce qu’on veut:

from collections import OrderedDict
 
from rest_framework import serializers
 
 
class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):
 
    # en lecture, je veux l'objet complet, pas juste l'id
    def to_representation(self, value):
        # le self.serializer_class.serializer_class est redondant
        # mais obligatoire
        return self.serializer_class.serializer_class(value).data
 
    # petite astuce perso et pas obligatoire pour permettre de taper moins 
    # de code: lui faire prendre le queryset du model du serializer 
    # automatiquement. Je suis lazy
    def get_queryset(self):
        if self.queryset:
            return self.queryset
        return self.serializer_class.serializer_class.Meta.model.objects.all()
 
    # Get choices est utilisé par l'autodoc DRF et s'attend à ce que 
    # to_representation() retourne un ID ce qui fait tout planter. On 
    # réécrit le truc pour utiliser item.pk au lieu de to_representation()
    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}
 
        if cutoff is not None:
            queryset = queryset[:cutoff]
 
        return OrderedDict([
            (
                item.pk,
                self.display_value(item)
            )
            for item in queryset
        ])
 
    # DRF saute certaines validations quand il n'y a que l'id, et comme ce 
    # n'est pas le cas ici, tout plante. On désactive ça.
    def use_pk_only_optimization(self):
        return False
 
    # Un petit constructeur pour générer le field depuis un serializer. lazy,
    # lazy, lazy...
    @classmethod
    def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
        if name is None:
            name = f"{serializer.__class__.__name__}AsymetricAutoField"
 
        return type(name, (cls,), {"serializer_class": serializer})(*args, **kwargs)

Et du coup:

class FooSerializer(serializers.ModelSerializer):
 
    bar = AsymetricRelatedField.from_serializer(BarSerializer)
 
    class Meta:
        model = Foo

Et voilà, on peut maintenant faire:

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Elle est pas belle la vie ?

Ca serait bien cool que ce soit rajouté officiellement dans DRF tout ça. Je crois que je vais ouvrir un ticket

]]>
Dell XPS 15 : une succession d’échecs 53http://sametmax.com/?p=23369http://streisand.me.thican.net/sametmax.com/index.php?20170601_110704_Dell_XPS_15___une_succession_d___echecs_53Thu, 01 Jun 2017 09:07:04 +0000J’aimais beaucoup mon Samsung 9 series, mais quand on utilise à outrance sa machine il faut la remplacer régulièrement. J’ai opté pour un Dell XPS 15, une version 15 pouces du modèle phare de la marque, puisque le XPS 13 a tant d’excellents retours. Ça faisait des années que je traînais sur un 13 pouces, et je voulais voir si coder sur un 15 allait vraiment rajouter au confort.

Le XPS 15 a plusieurs atouts. D’abord un écran superbe, quasiment sans bord, donc plus large que les 15 pouces traditionnels. Ensuite une carte nvidia intégrée. Et surtout, un port USB C thunderbolt avec lequel on peut théoriquement charger le PC, brancher ses écrans, faire passer de l’Ethernet, bref la promesse du monocable sur mon bureau malgré mon setup encombrant de 3 écrans et la fibre.

Marchant très bien avec un dual boot W10/Ubuntu, j’en ai profité pour lui plugger 32Go de Ram et 1To de SSD. La bestiole est donc supposée être un monstre de guerre qui devrait pouvoir avaler du travail de dev dans des conditions féeriques.

Après quelques mois d’utilisation, je peux vous affirmer que le produit n’est pas du tout à la hauteur du prix, à savoir plus de 2000 boules.

Bien que l’écran soit effectivement très confortable, et que les applications de tous les jours tournent sans soucis, l’intérêt du XPS 15 s’arrête là.

D’abord, il est lourd, le chargeur est énorme, et la batterie tient en moyenne 3h, pitoyable de nos jours. Ce n’est donc pas le meilleur ami du voyageur.

Mais surtout, il ne marche pas correctement. Et c’est bien ça le problème.

Pendant un mois les baffles ont tout simplement arrêté de produire un son cohérent, vomissant tel un métalleux bourré sans son micro la moindre note. Puis c’est revenu, sans explication, à la normale.

Un autre mois des écrans bleus incessants m’empêchaient tout simplement d’utiliser Windows jusqu’à ce que je tombe sur un thread de forum de support Dell que la marque a ignoré pendant longtemps. Un des utilisateurs recommande de downgrader le firmware du BIOS pour remédier au problème. Effectivement, ça marche.

Mais la réaction de Dell ? Désolé d’avoir niqué votre machine à un SMIC et demi ? Naaaa. Un mec poste laconiquement une vidéo YouTube d’un utilisateur sur internet d’une solution qui n’a pas marché pour moi. Et c’est tout. Le logiciel d’update DELL continue de suggérer des mises à jour qui refont planter tout le système.

Le port USB-C ne tient aucune de ses promesses. Parfois la charge s’arrête en cours de route sans prévenir. L’Ethernet n’a pas marché pendant plusieurs mois pour soudainement s’activer, mais uniquement sous Windows. Le triple écran qui marchait parfaitement sous Ubuntu et Windows a cessé de fonctionner sans prévenir. Maintenant au mieux je fais du dual screen sous Windows uniquement.

Si vous lancez un jeu, le laptop décide de temps en temps de faire tourner soudainement le CPU en mode éco, tuant les FPS et vous faisant perdre. Il se trouve que la machine chauffe trop, ce que vos doigts auraient pu vous signaler de toute façon. A croire que les ingés qui l’ont conçu n’ont jamais testé l’engin.

La solution ? Beaucoup de recherches (parce que j’ai que ça à faire, pallier aux erreurs de Dell sur une machine que j’ai payé cher pour gagner du temps dans mon travail) pour atterrir sur un autre thread du support Dell. Quelqu’un suggère de désactiver dans le BIOS Intel step. Ça marche, mais bouffe plus encore la maigre autonomie de la batterie.

Ce n’est pas la qualité de produit qu’on peut attendre d’une marque comme Dell, ni d’un modèle à ce prix. Mais surtout, l’absence totale de communication de Dell sur la question, le manque de réaction de leur part laisse à penser qu’ils ont fait une grosse erreur sur ce produit et espèrent que ça passe en faisant le mort.

Sinon j’ai aussi acheté un téléphone One Plus à la même époque, qui lui est toujours formidable pour un prix avantageux. Histoire de finir sur une note positive…

]]>
Vue, j’l’avais pas vu 50http://sametmax.com/?p=23281http://streisand.me.thican.net/sametmax.com/index.php?20170514_151042_Vue__j___l___avais_pas_vu_50Sun, 14 May 2017 13:10:42 +0000(Pour Max: je t’ai mis un exemple de code tout fait en bas de page car je sais que ça va te saouler de tout lire)

Elle était facile, mais ça fait des années que le blog est up, je peux pas avoir de l’inspiration tout le temps.

Dans le dernier article, je vous offrais votre dose maintenant obligatoire de bashing de l’écosystème JS. En l’occurrence en suggérant que React était un bouquet de roses avec plus d’épines que de pétales.

Mais bon, si les gens utilisent React, ça n’est pas QUE parce que c’est la mode. C’est parce que cette techno répond à un besoin de plus en plus impérieux en dev front end : avoir un outil valable pour créer des GUI avancées en JS.

Parce que le combo jQuery + moteur de template, ça nous a mené loin, mais on arrive au stade où ça scale plus.

Pas mal de tentatives ont été faites pour répondre au problème : knockout, angular 1, polymer, etc.

La nouvelle génération (react, riot, vue, angular 2+, etc) est beaucoup plus mature et performante, et offre des fonctionnalités très sympas:

  • DOM virtuel qui permet de faire des mises à jour fréquentes sur de très nombreux objets à l’écran sans freezer le browser.
  • Découplage très élégant des données et de leurs représentations.
  • Facilité de manipulation des éléments HTML, des events et du cycle de vie de tout le bordel.
  • Création de composants graphiques isolés et réutilisables.

En gros, partout où on faisait avant $(selecteur).trucmuche(element), on peut maintenant faire ça plus facilement, plus rapidement et plus proprement. (Par contre Vue/React/etc font pas l’AJAX, tournez-vous vers axios ou gardez jQuery sous le coude).

Bon, si ils offrent TOUS ces éléments là, pourquoi diable est-ce que j’ai une préférence pour Vue.JS plutôt que Riot, Angular ou React ?

Souvenez-vous, je reprochais essentiellement ceci à React:

  • Grosse courbe d’apprentissage. Ce qui impacte la productivité de tout nouveau dans l’équipe.
  • Mauvaise ergonomie du JSX qui du coup s’écrit, se lit et se debug lentement.
  • Mauvaise foi totale de la communauté qui prétend qu’on peut utiliser React sans la tonne de nutella autour, ce qui est comme argumenter qu’on peut faire du Java sans IDE.
  • Nutella sus-nommé qui est blindé d’huile de palme et de vidange.
  • Globalement une API inélégante et peu intuitive.

Mais alors, qu’est-ce que Vue.js fait de mieux ?

Ben tout, mes amis. Tout.

Légèreté

A l’heure où les codeurs hypes de la vibe du flex ont tous un fichier de conf webpack de 3 km, mais déjà obsolète et qui pétera à la prochaine upgade, Vue offre un point de vue rafraîchissant sur la question.

TOUTE, j’ai bien dit TOUTE, la puissance de vue.js est accessible avec une simple inclusion de balise script.

Vous en avez marre des pages de 3Mo ? Minifié et gzippé, Vue.js pèse moins de 30ko.

Dans un monde où il faut soit faire un site Web, soit faire une SPA (aux chiottes l’amélioration progressive !), Vue permet de ne remplacer que quelques bouts de vos pages si vous n’avez pas envie de tout transformer en un monstre de JS.

La doc, très bien faite, vous permettra de prendre l’outil en main en une après-midi. En buvant du thé.

Le tout, sans besoin d’aucun écosystème particulier. Aucun. Rien. Ca marche out of the box.

En gros, là où tout le monde vous vend du scale up, Vue vous permet de scale down.

Puissance

Malgré cela, Vue reste parfaitement adaptée aux gros projets. En fait, elle a été créée pour et par l’équipe du site du géant chinois, alibaba.com. Alors oui, c’est sûr, c’est pas Facebook et son milliard d’utilisateurs, mais comme mes projets ne servent pas 10 millions de pages par jour, contrairement à Alibaba, je pense que je suis ok.

Dans la pratique, qu’est-ce que ça veut dire ?

Et bien d’abord, Vue est plus rapide que React. Ouais ça fait mal au cul de lire ça quand on vient de finir d’optimiser son pipeline de rendu JSX, surtout que la techno a été créée pour la vitesse. Mais voilà, c’est le cas.

Vue supporte aussi parfaitement l’inclusion dans un écosystème plus gros si le besoin s’en fait sentir :

En gros, Vue peut faire absolument tout ce que les autres font. Mais ne vous oblige pas à commencer en sortant le bazooka. Vue est ce que React aurait dû être depuis le début.

Notez qu’on peut remercier les devs de React pour avoir popularisé de nombreux concepts que Vue utilise. Standing on the giants’ shoulders et tout, et tout. Vue n’aurait jamais existé sans React. Mais maintenant que Vue est là, on peut laisser React en paix.

Elégance

De par son design, Vue vous permet donc de commencer doucement, en ajoutant 30ko à votre projet et en l’utilisant un peu ici et là. Puis quand vous en avez besoin, vous pouvez commencer à utiliser des fonctionnalités avancées.

Par exemple, vous pouvez comme avec Angular 1 balancer toutes vos variables et binding dans le HTML de votre page en vrac. Et plus tard tout refactoriser en composant comme avec React. Tranquille.

L’API de Vue est petite, et surtout, facile à comprendre car tout est bien nommé, et bien rangé. Voyez plutôt:

Vue({
  el: "#app", // l'élément sur lequel attacher la vue
  created: function(){
    // code lancé au démarrage
  },
  mounted: function(){
    // code lancé quand la vue est attachée à la page
  },
  updated: function(){
    // code lancé quand la vue est mise à jour
  },
  destroyed: function(){
    // code lancé quand la vue est détruite
  }
  data: {
    // ben ici y a vous données statiques
  },
  methods: {
    // ici les fonctions à rendre disponibles dans le HTML
  },
  computed: {
    // ici on met les données à recalculer à la volée
    // à chaque refresh
  }
})

Voilà vous avez là 50% de l’API de Vue. Very hard indeed.

On sent partout que les devs ont apporté des soins à des petits détails comme:

<!-- Lance truc() juste au premier click.
Appelle preventDefault automatiquement. -->
<a v-on:click.once.prevent="truc()">Foo</a>

D’une manière générale, tout template Vue est du HTML valide, ce qui fait qu’il est très facile d’insérer du Vue sur un site existant.

Un petit bout de vue

Mais alors, pour tous ceux qui en sont restés à jQuery et handlebar.js, à quoi ça ressemble tout ça ?

Ben un todo ressemble à ça en Vue:

<!DOCTYPE html>
<html>
<head>
  <title>Todo</title>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
<!-- Cette partie va être contrôlée par la vue -->
<div id="app">
 
<!-- Quand on soumet le formulaire, on ajoute la tâche -->
<form v-on:submit.prevent="addTask(newTask)">
  <!-- L'input est maintenant lié à la variable newTask -->
  <p><input v-model="newTask"> <button>Ajouter</button></p>
</form>
 
<ul>
  <!-- Créer un li pour chaque tache dans mon array 'tasks' -->
  <li v-for="task in tasks">{{task}}</li>
</ul>
 
</div>
 
<script type="text/javascript">
app = new Vue({
  el: "#app", // j'attache cette vue à l'élément avec l'id App
  data: {
    tasks: [], // mes tâches seront stockées dans l'array 'tasks'
    newTask: "", // ce que contient l'input du formulaire
  },
  methods: {
    addTask: function(task){
      this.tasks.push(task); // on ajoute la tâche dans l'array
      this.newTask = ""; // on vide l'input
    }
  }
})
</script>
 
</body>
</html>

(Vous pouvez littéralement copier-coller ce code dans un fichier todo.html et ça marche. Essayez de faire ça avec React…)

Comme avec Angular/React/Riot on a séparé complètement la logique de manipulation des données de celle de l’affichage qui est mis à jour automatiquement quand les données changent.

Contrairement à React, on n’est pas obligé de créer un arbre complexe de composants qui transforme toute sa page en soupe de JS(X). Mais contrairement à Angular, on n’est pas obligé de rester sur cet exemple simple, et on peut créer une hiérarchie de composants.

Un composant peut complètement isoler le JS, le HTML et le CSS (qui n’affecte alors que le composant):

Et pour que ça marche, pas besoin de préprocesseur, pas besoin de ES6, ES7, typescript, babel, webpack, npm… Vous pouvez les utiliser. Sur un gros projet je me fais chier à setup ce qui vaut le coup. Mais vous n’êtes pas obligé.

La communauté

La communauté est importante pour un projet, et celle de vue est très agréable. Ce sont des gens polis, humbles, et compétents. Ils pensent aux petits détails. Regardez par exemple cette page pleine de jolis schémas.

En comparaison, la communauté React est criarde. Les confs des devs de Facebook sont faites par des présentateurs tout droit sortis de South Park. Quand on critique react, on se fait insulter personnellement sur twitter par les fan boys plutôt que de tenter de discuter, preuve systématique de l’absence d’arguments.

Enfin les seuls éléments donnés pour combattre les critiques sont les raisons des problèmes, un rappel de la taille des utilisateurs et des “moi j’aime” laconiques.

Mais faites un test. Prenez 3 dev, un react, un jquery, un vue. Faites leur faire un agenda CRUD avec recherche et autocompletion qui tape en Ajax dans un backend Django/Rails/Whatever. Donnez leurs des machines vierges. Et sortez le pop corn.

Pour le moment, React possède encore deux avantages: react native, et la masse (fb, les utilisateurs, le nombre de plugins, etc).

Mais honnêtement ? Il y a très peu de bénéfices à utiliser React plutôt que Vue sur la plupart des projets pur Web. Et un coût garanti d’être élevé.

Angular 1 est en train de mourir de l’abandon de Google. Après la sucette de l’incompatibilité d’Angular 2, je n’ai aucune confiance en la techno et ne compte donc pas réinvestir de billes dedans. De plus Angular 2+ souffre du même problème d’obligation d’installer la terre entière avant de commencer à coder.

Riot est une meilleure alternative, mais l’obligation de passer par des composants est un no-go IMO. Créer des composants pour moi est l’exception, pas la règle. Je le fais en dernier, à la phase de refactoring.

Donc ne vous emmerdez pas, prenez quelques heures pour jouer avec Vue, ça coûte pas cher, et c’est chouette.

————-

On me signale dans l’oreillette de linker dans la doc française.

]]>
Réaction à ReactJS 33http://sametmax.com/?p=23244http://streisand.me.thican.net/sametmax.com/index.php?20170510_203923_Reaction_a_ReactJS_33Wed, 10 May 2017 18:39:23 +0000Grand utilisateur de jQuery, formateur Angular et maintenant adorateur de VueJS, vous vous demandez sûrement ce que je pense de React. Si, je le sais, mon avis est pour vous comme un phare dans la nuit sombre du frontend engluée dans le brouillard de JavaScript.

Après tout, Facebook est codé avec cette techno pixel par pixel, et ils servent des milliards de pages chaque jour. Ça ne peut pas être mauvais. Ce ne sont pas de cons quand même.

Et puis tout le monde en parle, les asticots gigotent autour des restes du gigot d’ordre que les devs des GAFAS tentent de mettre au menu dans leur régime sans sel et sans typage.

Alors merde, quoi, react, keskeçavo ?

Je vais passer peut être pour un réact (hashtag lol), mais franchement le gigot au brouillard, c’est pas mon plat préféré.

Taillons un peu JS, ça m’avait manqué.

Mon royaume pour un hello world

React est tellement chiant à setuper que la doc officielle vous propose le hello world dans un code pen histoire de pas vous faire fuir tout de suite.

En fait, si pour vous installer un module avec npm/yarn/bower (plutôt que de copier le truc directement ou d’utiliser un CDN) est déjà un truc qui vous fait grogner, vous êtes loin derrière.

Il va vous falloir un outil de build (gulp, grunt ou l’usine à Vespene Webpack) et la série de recettes obsolètes customisées trouvées sur un coin de github pour connecter tous les bouts de votre pipeline. React bien sûr, mais aussi Babel pour transformer le JSX en JS.

Babel suit la left-pad philosophie, à savoir que tout est configurable et éclaté en centaines de petites dépendances et settings. C’est tellement chiant que des packages existent, appelés “presets”, dont l’unique but est de configurer Babel pour une tâche particulière.

Une fois que vous avez tout ça up, vous vous allez à la pêche au tuto, seulement pour remarquer que tout le monde code en ES6, voir ES7, et vous rajoutez donc un peu plus de plugins à tout ce bordel. Allah vous garde si vous tentez d’utiliser TypeScript.

Et vient alors le moment tant attendu pour faire coucou de la main programmatiquement. Ça foire, bien entendu. Le debugger vous pointe sur un bundle.js de plusieurs Mo, et vous apprenez que le support des sources maps est inégal d’un navigateur à l’autre.

Bienvenue.

Le X c’est pas toujours excitant

Une fois passée la douloureuse expérience de mettre en place votre environnement de travail, vous trouvez un certains confort. L’autoreload de la page est franchement pratique, et pouvoir utiliser l’unpacking (pardon le spread), les valeurs de paramètres par défaut et les arrow functions sans vous soucier de la compat du navigateur c’est quand même cool. Avec un webpack tout bien huilé avec amour (et douleur), vous pouvez faire des imports en JS et on a presque l’impression d’utiliser un langage de programmation.

Et puis le JSX, de loin ça à l’air pas mal ! Du HTML directement dans le JS ça parait un peu bizarre mais on vous le vend comme un avantage : finis les langages de templates limités, vous pouvez utiliser la pleine puissance (hum…) du langage JavaScript pour créer vos balises.

Sauf que non, le JSX, ça pue du cul.

D’abord, il y a le fait que toutes les structures sont à faire en JS. Les conditions, les concaténations et bien entendu, les boucles. Donc vous avez une liste de noms et numéros de téléphone, en VueJS, vous faites:

<ul>
    <li v-for="pers in persons" v-if="pers">
        <strong>{{ pers.nom }} </strong>: {{ pers.tel }} 
    </li>
    <li v-else>
        <em>Nobody's here!</em>
    </li>
</ul>

Mais en React, vous allez faire péter une expression ternaire et une fonction anonyme en plein milieu rien que pour l’occasion:

<ul>
    {
      (this.state.persons)
        ? this.state.persons.map((pers) => (
            <li><strong>{ pers.nom } </strong>: { pers.tel } </li>
        ))
        : (
            <li>
                <em>Nobody's here!</em>
            </li>
        )
    }
</ul>

Vous la sentez bien la puissance de JavaScript là ?

Et attention à ces boucles, car dedans vous guette cette erreur qui va vous poursuivre jusque dans vos cauchemars:

SyntaxError: Adjacent JSX elements must be wrapped in an enclosing tag

Babel vous signale gentiment que ne pouvez pas produire un snippet de JSX qui contienne plus d’un élément à la racine. Donc il faut TOUT wrapper dans un container. TOUT. Vous allez avoir vite des DIVs et des SPANs inutiles partout, juste pour faire plaisir à React. C’est absurde. C’est à vomir. Ca nique votre CSS et remplit vos sessions “examinez un élément” de tendinites dues aux clics pour déplier tout l’arbre des emballages cadeaux de vos balises. Et aussi, nique la sémantique.

Pour éviter ça vous allez tenter d’inliner un maximum de trucs dans une seule expression JSX ou tout foutre dans des arrays. Car je ne l’ai pas précisé ? Les arrays de JSX sont automatiquement et magiquement convertis en sous-éléments. Et du coup vous pourrez profiter au maximum des qualités de lisibilité de JS.

JSX est bourré de petits trucs comme ça, pour faire plaisir. Par exemple, j’ai un bouton, quand je le clique il delete la personne de la ligne de mon agenda. Je mets aussi une classe pour tous et une selon que la personne est importante ou non. Un prevent default pour éviter que le browser recharge la page si je suis dans un form.

En Vue, je passe un objet qui dit quelle classe afficher ou non. Le click handler contient une instruction prevent qui me permet d’appeler preventDefault automatiquement. Et j’appelle deletePerson tout naturellement :

<button class="{'delete-person': true, 'important-person': pers.isImportant}" 
        v-on:click.prevent="deletePerson(pers)">
    Delete
</button>

En react, on ne peut pas passer de paramètre à son handler. Il faut le passer dans une closure. Mais alors la closure va recevoir l’objet event, qu’on doit donc faire suivre à notre handler, afin qu’il puisse appeler preventDefault à la main. A noter aussi que class s’appelle className. Et n’accepte que des strings donc la concaténation se fait à la mano.

// plus haut on appelle preventDefault dans handleDelete
 
<button className={'delete-person ' + (pers.isImportant ? 'important-person': '')}
        onClick={(e) => this.deletePerson(pers, e)}>
    Delete
</button>

Ce n’est pas juste chiant à lire, c’est surtout hyper chiant à trouver, à taper et à debugger.

Un peu de nutella sur votre confiture ?

React, c’est verbeux, il va falloir vous y faire. Il suffit de comparer le code nécessaire pour faire une TODO en react avec les autres frameworks.

Mais ce n’est pas juste ça qui est lourd.

Non, le vrai truc qui est super pesant, c’est que react est un cancer. Quand il est utilisé, il contamine tout.

Par principe, une fois que vous utilisez react, il faut mettre TOUTE VOTRE PAGE en react. Pas juste un petit bout. Par là j’entends que 90% de votre HTML va devoir migrer dans le code JSX. Votre beau code HTML bien propre, converti en cette monstruosité de mélange entre le langage le plus dégueulasse du monde et un monstre mimic qui essaye de se faire passer pour du HTML pour vous bouffer.

Or, toute l’idée c’est de faire des composants, de diviser votre pages en plus petits bouts. Mais voilà, React n’a rien prévu de simple pour la communication. Pour passer des données des composants enfants, il faut les passer via les attributs de balise HTML (appelés les props, pour faciliter la rechercher Google). Si vous avez 5 niveaux d’imbrications, vous vous tapez ça 5 fois.

Plus amusant, il n’y a aucun mécanisme pour passer des infos de l’enfant aux parents. La méthode standard est d’écrire puis passer manuellement un callback du parent à l’enfant, via props, et appeler ce callback dans l’enfant avec la valeur que le parent utilise ensuite. POUR. CHAQUE. PUTAIN. DE. VALEUR.

Passer des callbacks, ça va 5 minutes. Et ça ne résout pas un problème de communication entre composants parallèles, c’est à dire ni parent, ni enfant. Du coup tout le monde finit par utiliser une lib supplémentaire type EventEmitter pour servir de bus de communication.

Quand vous entendez les gens se plaindre de la difficulté d’utiliser l’écosystème de react, les fans répondent souvent que l’écosystème n’est pas obligatoire. On peut très bien s’en passer sur un petit projet.

Sauf que react est absolument inutilisable sans. Sans un bus d’event, un transpiler ES6 + JSX, un builder et un hot reloader au mininum, le projet ne dépasse jamais le stade du prototype.

Mais bon soyons franc, beaucoup de projets react tout court ne dépassent jamais le stade du prototype. On parle de codeurs JS là.

Etat

Les props ne doivent jamais changer. Seul l’état d’un composant react peut changer, ce qui trigger un nouveau rendu du composant.

Mais l’état lui-même est en read-only. On est supposé uniquement créer un nouvel état à chaque fois et remplacer l’ancien.

Au début on le fait à la main, mais JS n’est pas vraiment fait pour faire de l’immutabilité, et ça devient uber chiant très vite. Changer la propriété d’un objet qui est lui même dans un array demande de recréer tout l’array et l’objet. A chaque fois.

On se tourne alors vers des libs (encore une) type immutable.js qui fournit des listes et maps qui sont immutables et permettent de faire ces opérations sans y laisser ses jours de congé.

Une fois de plus, les connards qui vous disent qu’on peut faire du react sans la tonne de boiler plate qu’on voit dans les tutos ne le font jamais eux-mêmes. Parce que oui, on peut faire Paris-Amiens à pied en théorie, mais bon…

Des décisions à la con

Clairement, react a été fait pour créer des UI. Mais au bout d’un moment, les utilisateurs se sont réveillés et ont voulu une encapsulation pour des composants non UI. Mais react ne sait pas faire. Quand vous avez donc un composant non UI, par exemple la déclaration de votre routing, vous le déclarez… en JSX:

  render((
      <Router history={browserHistory}>
        <Route path="/foo" component={FooView}/>
        <Route path="/bar" component={BarViw}/>
      </Router>
  ), document.getElementById('app'))

Votre app va devenir très vite une pyramide de composants, certains qui s’affichent, d’autres non, tous se passant en cascade des données les uns aux autres.

Et au passage, quel est l’abruti qui a décidé des noms des méthodes des cycles de vie comme getInitialState, componentWillMount et componentDidMount ?

Surtout que vos méthodes et les méthodes héritées de la classe du component sont dans le même espace de nom.

Le bonheur des stores

Un truc dont on peut vraiment se passer par contre, ce sont les stores. Flux, redux, vuex, etc. On peut utiliser n’importe quelle solution pour stocker l’état de son projet côté client.

Mais si vous voulez profiter des promesses de react, comme le time travel et la concurrence parfaite, il vous faudra un store.

“store” c’est le nom huppé que react donne à ses modèles. Je ne vais pas rentrer dans les détails, et vous laissez décider par vous même du truc. Voici, comment la doc vous recommande d’ajouter une personne dans une liste d’un agenda avec redux, la store la plus populaire:

// Les imports
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
 
// creation du store
const initialState = {'agenda': []};
 
function reducer(state=initialState, action){
  switch (action.type) {
    case 'ADD_PERSON':
      return {
        'agenda': [..., action.object]
      }
    default:
      return state
  }
}
 
const store = createStore(reducer);
 
 
// creation de l'action d'ajouter un objet dans le store
 
function addPersonAction(obj){
    return {
        'type': 'ADD_Person',
        'object': obj
    };
}
 
 
export function addPerson(Person){
    store.dispatch(addPersonAction(Person));
}
 
 
// composant qui affiche le form de l'agenda et la liste des personnes
var Agenda = React.createClass({
 
  handleAdd: function(){
      addPerson({
        "name": this.refs.name.value,
        "tel": this.refs.tel.value
      })
  },
 
  render: function(){
 
    return <form onClick={this.handleAdd}>
        <p><input ref="name" /><input ref="tel" /><button>Add</button></p>
    </form>
 
    <ul>
        {
            this.store.agenda.map((pers) => (
                <li><strong>{ pers.nom } </strong>: { pers.tel } </li>
            ))
        }
    </ul>
  }
 
 
});
 
 
// adapter qui permet de passer le store à l'agenda
const AgendaContainer = connect(function(state){
    return {'agenda': state.get("agenda")};
})(Agenda);
 
 
// affichage du bouzin
render((
<Provider store={store}>
  <AgendaContainer>
</Provider>
), document.getElementById('app'))

Vous imaginez bien que trouver ça tout seul a été un bonheur. Parce que oui, la doc est absolument à chier, dans la longue tradition des stacks JS modernes.

]]>
Le don du mois : vue.js 16http://sametmax.com/?p=23211http://streisand.me.thican.net/sametmax.com/index.php?20170501_103743_Le_don_du_mois___vue.js_16Mon, 01 May 2017 08:37:43 +0000Le pognon rentre à nouveau et c’est donc le retour du don du mois.

Ça ne fait que quelques temps que j’utilise Vue.js, mais globalement c’est déjà mon outil par défaut pour toute UI en JS.

J’ai encore des demandes de formations et projets pour Angular 1 et React, mais si j’ai le choix il n’y a aucune hésitation.

J’ai essayé quelques alternatives, comme Riot, mais on arrive jamais à cette combinaison parfaite de simplicité, légèreté, puissance et performance. Ce projet est un petit bijoux du monde de l’open source. Un truc rare en Javascript.

Bref, c’est bien beau de cracher toujours sur JS, mais ça résout pas le problème. Il faut aussi aider, et c’est donc pour ça que je fais un don de 50 €, par ici.

Et si vous regrettiez l’époque de la simplicité de jQuery mais que vous avez envie de quelque chose qui automatise bien plus comme un framework moderne, vous savez ce qu’il vous reste à essayer :)

]]>
Comment garder des lignes courtes ? 16   Recently updated !http://sametmax.com/?p=23196http://streisand.me.thican.net/sametmax.com/index.php?20170428_150232_Comment_garder_des_lignes_courtes___16____Recently_updated__Fri, 28 Apr 2017 13:02:32 +0000Arg, j’ai plein de commentaires sans réponse sur le blog, indexerror aussi. Des tickets millénaires sur 0bin et les impôts qui arrivent. Des fois ce qu’il faut c’est une petit victoire pour relativiser.

Du coup, un petit post pour me donner l’impression de ne pas être inutile à l’humanité.

De toutes les recommandations du PEP8, garder des lignes courtes, souvent moins de 80 caractères, est celle qui fait le plus râler.

On rétorque parfois que c’est une règle qui n’a pas de sens de nos jours avec nos grands écrans HD.

Sauf que:

  • L’œil humain fonctionne par micro sauts, et peut donc bien plus efficacement scanner de longues colonnes que de longues lignes.
  • Il est très courant de pouvoir mettre du code côte à côte sur un même écran pour comparer, merger ou avoir plusieurs vues sur un fichier.
  • Vous ne savez pas sur quoi sera lu votre code. Au bureau vous avez peut être un setup à triple écran 4K, mais les autres n’ont peut être pas cette chance. Les chats, les pastebins, les blogs posts, un slide show, un remote ssh, des comments, des tickets, ou un retro projecteur basse résolution sont autant de supports sur lesquels votre code finira probablement. Et ils ne sont pas larges.
  • Un diff sur de longues lignes c’est dégueulasse.
  • Le debug pas à pas qui s’arrête sur une ligne de 3km ça donne des envies de meurtre.

D’une manière générale, garder des lignes courtes est une bonne habitude et incitera à écrire un code sain. Le processus de réduire la taille des lignes passe par l’expressivité, la clarté, le refactoring, etc. Au final, c’est excellent pour la santé du projet.

N’oubliez pas non plus que le PEP8 indique clairement qu’il ne faut jamais suivre une règle de manière rigide. Si votre ligne est absolument plus adéquate sur une ligne, alors mettez la sur une ligne.

Maintenant qu’on a expliqué le pourquoi, parlons du comment. En effet beaucoup de bonnes pratiques ne sont pas appliquées parce qu’on ne sait pas faire. Faire des lignes courtes demande une certaine connaissance, et un peu de pratique.

Voici quelques pistes pour vous aider dans vos prochaines sessions.

Python est fait pour ça

Python force à indenter, et c’est une de ses meilleures caractéristiques. Jamais vous ne vous retrouverez comme dans d’autres langages face au code de votre collègue, ce gros porc, qui aligne ses accolades avec le groin.

Mais pour garder des lignes courtes, l’indentation joue contre vous.

Python vient heureusement avec tout un tas d’outils pour rendre votre code court, rapide et peu gourmand on mémoire. C’est tout bénef.

itertools, all(), any(), filter(), map(), collections, les listes en intension, functools et les générateurs sont tous d’excellents moyens de créer un code plus beau, plus court, et plus efficace. Apprenez à les utilisez ! Vous serez un meilleur programmeur, plus productif, et les lignes seront naturellement plus courtes.

Exemples…

Le module itertools permet de faire un tas de choses, comme remplacer les boucles imbriquées.

Remplaçons :

>>> for lettre in "abc":
...     for chiffre in (1, 2, 3):
...         print(lettre, chiffre)
...
a 1
a 2
a 3
b 1
b 2
b 3
c 1
c 2
c 3

Par :

>>> from itertools import product
>>> products = product("abc", (1, 2, 3))
>>> for lettre, chiffre in products:
...     print(lettre, chiffre)

Les built-ins ont un tas d’outils pour pour manipuler les données.

Si vous avez des collections dont vous voulez vérifier tout le contenu:

>>> from random import randint
>>> data = [randint(0, 10) for x in range(0, randint(0, 10))]
>>> max(data)  # pas besoin de boucler pour trouver le max
9
>>> if any(x > 3 for x in data): # any est un super "or"
...    print('Un élément est supérieur à 3')

Le module collections possède d’ailleurs tout un tas d’outils pour faire le travail à votre place :

>>> from collections import Counter
>>> Counter("aaaaaaaaabbbbbbccccc")
Counter({'a': 9, 'b': 6, 'c': 5})

On peut éviter un tas de try/except avec les helpers qui retournent une valeur par défaut:

>>> {"foo": 1}.get('bar', 3)  # et next, iter, setdefault... 
3

Sans parler de l’unpacking, le slicing, le paramétrage dynamique, etc.

Bref, si votre ligne est trop longue, c’est peut être parce que vous être en train de sous-utiliser Python.

De même, apprenez les libs les plus célèbres en Python.

pytest, pendulum, request, pandas…

Dès qu’un usage particulier intervient, elles seront généralement plus efficaces que la stdlib. Moins de code !

Refactorisez

Un bloc de code qui commence à être long sera avantageusement déplacé dans une méthode ou une fonction. Avec un joli nom bien explicite qui va naturellement rendre votre code mieux documenté, et testable.

Tant qu’on y est, faites usages des variables intermédiaires.

res = [foo for foo in objet.attribut['cle'] if foo > wololo]

Gagnera à devenir:

nom_explicite = objet.attribut['cle']
res = [foo for foo in nom_explicite if foo > wololo]

Les parenthèses pour les sauts de ligne

Les parenthèses en Python, c’est magique.

Avant :

from module_de_la_mort_qui_tue import foo, bar, et, toute, la, clique

Après

from module_de_la_mort_qui_tue import (
	foo, 
	bar, 
	et, 
	toute, 
	la, 
	clique
)

Ca marche partout, dans les appels de fonctions, l’accès des attributs, les chaînes de caractères…

On part de :

queryset = ModelDeDjango.objects.filter(attribut=val).first()

Et bim :

queryset = (ModelDeDjango.objects
                         .filter(attribut=val)
                         .first())

Si des parenthèses sont déjà là, inutile d’en rajouter:

Aller :

raise OverkillError("Arrrrrrrrrrrrrrrrrrrrrrrrrrrrggggggg")

Hop :

raise OverkillError(
	"Arrrrrrrrrrrrrrrrr"
	"rrrrrrrrrrrggggggg"
)

Aidez-vous de l’environnement

Vous outils sont là pour vous faciliter la vie. Votre éditeur par exemple, a certainement un moyen d’afficher un indicateur vertical pour symboliser la limite de 80 caractères.

Les bons IDES et les linters vous signalerons le dépassement. Ça vaut le coup d’investir dedans. Si votre éditeur ne le supporte pas par défaut, il a peut-être un plugin pour utiliser un linter.

VSCode et Sublime Text ont tous deux des plugins pour Python qui permettent cela. Notamment ils peuvent exploiter l’excellent linter flake8, qui sinon peut s’utiliser à la main.

Pour certaines lignes, vous allez dépasser. Ce n’est pas la mer à boire tant que c’est un choix conscient et pas de la flemme.

Par exemple, les commentaires avec des URLs dedans dépassent souvent. Beaucoup de linters ont un réglage pour les autoriser (ou un pattern au choix), activez-le.

Si vous avez une ligne qui dépasse et que le signalement de votre IDE/linter vous casse les couilles, il est souvent possible de le désactiver en ajoutant en fin de ligne le commentaire:

# noqa

Qui signifie tantôt “no question asked” ou “no quality assessment”.

]]>
Sheepit – Le rendering pour tous, accessible sans trop de difficultés et gratos 9http://sametmax.com/?p=23149http://streisand.me.thican.net/sametmax.com/index.php?20170418_122022_Sheepit_____Le_rendering_pour_tous__accessible_sans_trop_de_difficultes_et_gratos_9Tue, 18 Apr 2017 10:20:22 +0000Entre deux bars à putes et les restaux faut bien prendre un peu de temps pour se relaxer, il y a les salons de massage branlette vous me direz, mais pas que.

Dans une galaxie lointaine il y a fort fort longtemps je m’amusais à faire de la 3D, je vous parle d’un temps que les jeunes ne peuvent pas connaître…
A l’époque régnaient en maîtres absolus 3DS MAX, MAYA, CINEMA 4D, etc. et les outils maisons des studios Pixar. Pour les simples mortels comme moi on avait droit à Truespace, Vue D’esprit, Poser et sûrement d’autres dont j’ai oublié le nom. Truespace était mon favori, plutôt sympa, pas compliqué à prendre en main et pas cher (voir gratos quand on se démerdait).

Pour faire mumuse c’était super mais voilà, c’était long, long, longggggg………..

De nos jours on a droit à de supers cartes graphiques comme les Titans X  et pas mal de logiciels de 3D gratuits aussi, certes on peut faire dans le warez et se procurer Maya, 3D Studio, Cinema4D et plein de nouveaux que je ne connais pas mais restons dans le légal pour une fois car ce qui se passe est intéressant.

Donc le monde du gratos a super bien évolué, il y a plein de tutos sur YouTube, des logiciels gratuits concurrencent les plus gros softs de 3D sur le marché et si ses derniers ont du succès c’est surtout parce que les studios en ont fait leur standard.

Je vais vous parler de Blender, c’est un projet open source que je suis de très loin mais qui a attiré mon attention depuis peu avec quelques superbes vidéos sur YouTube sur lesquelles je suis tombé:
https://www.youtube.com/watch?v=-TksegJETqI
https://www.youtube.com/watch?v=kSp3pHA_tRM
https://www.youtube.com/watch?v=7lY9SlQ8gjY
https://www.youtube.com/watch?v=Q1VCLFJY250
https://www.youtube.com/watch?v=143k1fqPukk
https://www.youtube.com/watch?v=LcCQKuWPhXk

Il y a vraiment une bonne communauté.

Des milliers de tutos sur YouTube pour s’en sortir avec les millions d’options que possède le logiciel.

Bref tout ça c’est merveilleux, pour perdre son temps y’a pas mieux. Mais il y a un hic, le temps de rendu justement. Je me souviens de cette époque où j’attendais 1 heure, voire 2 pour rendre une seule image et m’apercevoir que le résultat était moyen.
Et de ce côté ça n’a pas changé ! C’est même plus long avec toutes les nouvelles options de raytracing possibles même avec des cartes graphiques surpuissantes comme cité plus haut. Alors une animation….

Blender possède un plugin de network rendering, à savoir partager le temps de calcul sur plusieurs serveurs du réseau, c’est sympa mais c’est pas tout le monde qui a 10 ordis à la maison…

Une solution a vu le jour il y a quelques années et quelques milliers de membres font vivre une véritable ferme de rendu.

Le principe de la ferme de rendu:
Via un réseau d’ordinateurs on partage le temps cpu entre plusieurs machines, accélérant ainsi le temps de rendu.

Cette solution c’est Sheepit.

Ok on décolle ! Vous installez leur app. java sur un serveur qui traîne au garage ou sur votre ordi et vous avez droit à des points en fonction du temps machine que vous consacrez à l’application. Ces points vont vous permettre “d’acheter” du temps de calcul parmi tous les participants.
Ainsi un projet d’animation qui d’ordinaire me prend 5 jours est rendu en quelques heures, gratos en plus !

Je trouve le concept très sympa, tout est gratuit, il y a une admin avec le nombres de personnes qui partagent vos calculs, on peut former des équipes pour se tirer la bourre ou privilégier les cpu à ses coéquipiers, bref c’est bien pensé et je voulais en parler pour, qui sait, leur ramener un peu de monde.

Aux dernières nouvelles il y a environ 350 machines connectées en permanence, vous imaginez le temps de calcul offert !

Une petite ligne de code pour mettre un serveur dans la pool:

inscrivez-vous sur sheepit : https://www.sheepit-renderfarm.com/getstarted.php
Il y a une version applet java pour le navigateur mais je n’ai pas testé, je préfère laisser tourner leur app. sur un serveur h24:
téléchargez l’app. java: https://www.sheepit-renderfarm.com/media/applet/sheepit-client-5.366.2818.jar

Sur CentOS j’ai créé un user “sheepit” pour pas lancer leur app. en “root”:

useradd sheepit

ensuite dans un screen je lance l’app. avec les paramètres suivants:

sudo -u sheepit java -jar /sheepit/sheepit-client-5.366.2818.jar -ui text -login mon_login_sheepit -password mon_pass_sheepit -cores 3 -compute-method CPU

-ui text : Pour lancer l’app. java en headless (pour les serveur sans GUI)
-login mon_login_sheepit : Le login que vous avez choisi lors de l’inscription à sheepit
-password mon_pass_sheepit : Le mot de passe que vous avez choisi lors de l’inscription à sheepit
-cores 3 : Le nombres de processeurs que vous voulez dédier à Sheepit, plus vous en dédiez plus vous gagnerez des points et plus vos projet passeront en priorité dans la pool de rendu.
-compute-method CPU: N’utilise que le cpu de votre serveur, vous pouvez mettre GPU si vous en avez.

Ce système a beaucoup d’avantages, il vous permet enfin de faire de supers rendus en peu de temps, sans dépenser d’argent, c’est vraiment magique. (je ne vous partagerais pas les merdes que j’ai faites car je suis une quiche en 3D). L’inconvénient c’est qu’il faut ensuite télécharger toutes les frames du projet, là j’ai 24 Go de frames à télécharger, ça fait lourd si on n’a pas la fibre.

Ils ont un forum qui ne demande qu’à grandir.

Il y a des artistes en herbe qui n’ont pas les moyens de s’offrir des cartes graphiques à 2000€ et je trouve ce système juste fantastique pour eux et même pour les gros projets. J’espère que vous ferez passer le mot et si vous avez un serveur ou deux soyez cool, ça prend 5 minutes.

Allez le meilleur pour la fin:

A poil les putes!

]]>
Django, une app à la fois, mis à jourhttp://sametmax.com/?p=22936http://streisand.me.thican.net/sametmax.com/index.php?20170320_092347_Django__une_app_a_la_fois__mis_a_jourMon, 20 Mar 2017 08:23:47 +0000Django, une app à la fois fonctionnait avec une vielle version de Django, donc je l'ai mis à jour pour marcher avec la 1.10.]]>Django, une app à la fois fonctionnait avec une vielle version de Django, donc je l’ai mis à jour pour marcher avec la 1.10.

J’ai trop attendu et le travail a été plus long que prévu car plein de choses avaient du coup changé dans le framework. Des petits détails, mais déjà tout le routing ne marchait plus.

N’oubliez pas que la branche master est en anglais, mais il y a une branche traduite en français. Les deux sont téléchargeables sous forme de zip sur la page d’accueil du projet github si vous ne souhaitez pas utiliser git. Il n’y a rien à installer : unzip, lire, lancer.

D’une manière générale, le projet est toujours très pratique pour enseigner ou apprendre les bases de django en complément des supports traditionnels. Il faudra que je rajoute la partie ORM un de ces 4.

]]>
Dites non aux DSL 13http://sametmax.com/?p=22896http://streisand.me.thican.net/sametmax.com/index.php?20170317_131137_Dites_non_aux_DSL_13Fri, 17 Mar 2017 12:11:37 +0000Un Domain Specific Language est un langage créé pour une tache très particulière. CSS, HTML et SQL sont des bons exemples de DSL populaires. Moins connus: ReactQL, QML, Less, Latex, XPath, Graphviz… Les plus connus sont encore là car ils sont utiles, alors créer le vôtre pourrait être tentant.

Suite à ce tuto sur la fabrication d’un DSL en Python, je réalise que quelques personnes recommandent encore de créer des DSL.

Il faut absolument que je vous empêche de commettre cette erreur irréparable !

Une API n’est pas un DSL

Ruby, qui a des parenthèses optionnelles et la capacité d’intercepter les accès d’attributs à la volée, a lancé la mode du mot DSL à tout va. Sauf que la plupart des DSL en Ruby n’en sont pas. Ce sont juste des API écrites en Ruby.

You keep using that world, I don't think it means what you think it means

You killed my project, prepare to die

Enchaîner les méthodes et opérateurs overloadés dans un langage populaire n’est pas utiliser un DSL. foo.bar().baz() n’est PAS un DSL si ça tourne dans la VM du langage. Quel que soit l’enrobage syntaxique qu’on a rajouté.

Un format de sérialisation générique n’est pas un DSL: XML et YAML n’en sont pas, contrairement à ce qu’on peut lire. Car ils sont généralistes, et donc pas “domain specific” par nature.

Par contre on peut écrire un DSL en JSON, XML ou YAML, et il en existe d’ailleurs pas mal. MathML et SVG sont des exemples de DSL écrits en XML.

Ne créez jamais un DSL

Pourquoi avez vous besoin d’un nouveau langage ? Il existe des centaines de langages et encore plus de formats de sérialisation. Aucun d’entre eux ne peut répondre à votre besoin ? Vraiment ? Vous avez tout testé pendant un mois ?

The world is the problem, the atomic bombs the answer

Choisir le bon outil, Civ style

Créer un DSL , donc un nouveau langage, implique vous avez besoin:

  • d’informations dessus: docs, standards et tutos…
  • d’outils : UI, générateurs, wrapper pour le scripting, coloration syntaxique, snippets, linters, checkers, debuggers…
  • un support du DSL : garantir qu’on pourra le lire, écrire et manipuler dans 5 ans. Pouvoir fournir de nouveaux outils si le besoin évolue. Pouvoir faire évoluer le standard si (pardon “quand”) il se révélera limité. Répondre aux questions des utilisateurs. Fournir des messages d’erreurs et procédures pour résoudre les problèmes. Avoir un bug tracker.

Et il faut tenir tout ça à jour.

Ah. Ah. Ah.

Personne ne le fait jamais, ce qui fait de l’usage de la plupart des DSL un enfer, sans parler de la maintenance des projets qui l’utilisent sur le long terme. Mais les créateurs de DSL s’en effeuillent amoureusement les génitoires car ils seront dans une nouvelle boîte quand ça arrivera. Eux ils se sont bien amusés à créer un parseur pour le fun. D’ailleurs ça fait classe sur le CV.

Fuck everything

Réponse standardisée aux propositions de DSL. Aussi valable pour l’introduction d’une nouvelle stack JavaScript

Créez une API

Votre langage de prédilection est turing complet. Il est donc capable de gérer ce que fait le DSL. Libérez l’anus de cette mouche et traitez votre problème avec un langage supporté, éprouvé, avec un large écosystème et qui résout déjà tout un tas de problèmes.

Ce qu’il vous faut, c’est faire une belle API, pour rendre la tache agréable:

  • Créez des méthodes biens découplées, de l’injection de dépendance et un système d’events.
  • Faites des wrappers plus haut niveau, et des factories pour les cas courants.
  • Utilisez le sucre syntaxique de votre langage pour rendre tout ça tout beau (ex: __getitem__, __enter__, @decorateur, etc. pour python).
Rayon bien rangé

Avec une belle présentation, un truc standard ennuyeux devient soudainement très attractif

Si créer une API n’aide pas assez, par exemple vous faites un outil avec un langage haut niveau et avez besoin d’un langage haut niveau pour le scripting ou la configuration, embarquez un langage haut niveau connu. Par exemple Lua ou Python. Ne faites pas comme varnish ou nginx qui ont des fichiers de configuration dans un langage imbitable, indébuggable et mal documenté.

Quelques bonnes raisons de créer un DSL

Allez, il ne faut jamais dire jamais, et comme toujours en informatique, il y a parfois une bonne raison de faire quelque chose. Voici donc la liste des RARES cas où écrire un DSL a du sens:

  • Vous avez un jeu de règles métier très complexe et des non informaticiens doivent pouvoir les composer. Et une UI ne ferait pas l’affaire pour une raison qui m’échappe. Et vous ne pouvez pas former les non informaticiens parce que fuck you. Les langages de templates types jinja sont dans cette catégorie.
  • Vous avez un jeu de règles métier très complexe, vous voulez pouvoir les serialiser (pour les sauver dans un fichier ou une base de données ou les transmettre à travers le réseau par exemple) et la sérialisation doit pouvoir être accessible depuis plusieurs langages incompatibles (donc pas de dump mémoire, de pickle, etc). Les langages de requêtes comme GraphQL en font partie.
  • Vous créez un langage pour remplacer un autre DSL. Par exemple Sass pour le CSS.
  • Vous voulez des instructions logiques mais sécurisées pour autoriser une source en laquelle vous n’avez pas confiance à manipuler vos données. Et vous ne pouvez pas mettre une couche d’abstraction (ou c’est la couche d’abstraction) type Web API, RPC ou autre et ça va, maintenant, TG, c’est mon projet. Tous les formats des sérialisations pour le RPC justement répondent à ce problème.

Mais dans tous ces cas, ne faites un DSL que si:

  • Une bonne UI n’est pas possible pas pour créer les règles et il faut les sérialiser.
  • Il n’existe pas déjà un standard existant.
  • Vous êtes certains de fournir des outils pour manipuler le DSL.
  • Vous êtes certains de documenter, tester et maintenir le DSL.
  • J’ai dit certains de documenter, tester et maintenir votre DSL. Arrêtez de mentir: vous ne le ferez pas.
  • Lâchez ce DSL tout de suite ! Vous êtes dans une start-up. Vous n’allez jamais faire tout ça. Oust !
Special snowflake award

“Mais moi c’est pas pareil”, le hello word du DSL pour prendre des mauvaises décisions dans la vie

]]>
devpy, outils de dev à l’arrache 4http://sametmax.com/?p=22621http://streisand.me.thican.net/sametmax.com/index.php?20170301_172116_devpy__outils_de_dev_a_l___arrache_4Wed, 01 Mar 2017 16:21:16 +0000devpy, un outil pour se faciliter le développement.]]>Je le publie en coup de vent car je me dis que ça peut servir à des gens, mais j’ai pas l’intention d’y passer des heures… vu que je fais ça pour me faire gagner du temps en mission.

J’ai créé devpy, un outil pour me faciliter le développement.

Dans sa forme la plus simple:

pip install devpy

Ca donne automatiquement:

  • Un logger qui log dans un fichier temporaire et dans la console.
  • Un traceback qui montre les lignes de SON code avec coloration syntaxique.
  • Si le programme crashe, la stack trace est logguée dans le log automatiquement.

En gros quand on commence à bosser on fait:

import devpy.develop as log

Et on peut logguer comme avec les logs python habituels (log.info, log.debug, etc).

On peut facilement retirer devpy une fois qu’on a fini et le remplacer par un log Python traditionnel type:

log = logging.getLogger(__name__)

Donc utiliser devpy ne coûte rien car on peut le retirer facilement une fois qu’on a fini: c’est 100% compat avec le log de la stdlib.

Ça tombe bien, car c’est pas un truc qu’on met en prod en théorie (même si ça peut pas abîmer le serveur).

C’est juste un outil que j’ai fait car j’en avais marre:

  • De galérer à setuper un logging de base à chaque fois. C’est long, verbeux, plein de coups fourrés. Mais 9 fois sur 10 je veux la même chose: la console et un fichier temp. Du coup par flemme je fais un print et je le regrette plus tard.
  • Devoir faire les choses proprement pour le dev. Oui un bon log c’est important, mais quand je commence à bosser je ne veux pas passer mon après-midi à setuper les logs proprement. Je ferai ça plus tard, un truc à l’arrache pour le debug me suffit.
  • Devoir chercher dans la stack trace les lignes qui m’intéressent. Je debugge rarement la stdlib. Montre moi MON code.
  • Avoir des logs avec la stack trace en cas d’erreur est super relou à mettre en place. Mais c’est tellement pratique.

Je sais, il existe une tetra chiées d’outils de logging pour Python. Mais c’est pas ce que je veux. Je ne veux pas choisir une solution ou configurer un truc. Ca je le ferai plus tard, proprement. Je veux juste un log essentiel et à coût zero pour démarrer mes projets et pouvoir switcher plus tard.

Je vais sûrement rajouter des trucs dans devpy au fur et à mesure mais pour le moment ça fait ce que je veux.

Je l’ai mis compatible uniquement 3.6 car j’ai pas envie de me faire chier à tester ailleurs. D’ailleurs j’ai même pas de tests.

Et si vous êtes pas contents, vous pouvez vous mettre ma deadline là où je pense.

]]>
“BlockingIOError: [Errno 11] Resource temporarily unavailable” pour Python 3.6 6http://sametmax.com/?p=22573http://streisand.me.thican.net/sametmax.com/index.php?20170228_103918____BlockingIOError___Errno_11__Resource_temporarily_unavailable____pour_Python_3.6_6Tue, 28 Feb 2017 09:39:18 +0000un bug assez vicieux qui ne se manifestait que sous certaines conditions, généralement dans un daemon sur un serveur, et en important certains modules qui finissent par déclencher par réaction en chaîne l'usage de random. ]]>La toute première version de Python 3.6 avait un bug assez vicieux qui ne se manifestait que sous certaines conditions, généralement dans un daemon sur un serveur, et en important certains modules qui finissent par déclencher par réaction en chaîne l’usage de random.

django est concerné.

On tombait dessus généralement assez tard, à la mise en prod, avec un message cryptique:

 
BlockingIOError: [Errno 11] Resource temporarily unavailable 
  
 The above exception was the direct cause of the following exception: 

Traceback (most recent call last): 
   .... <- des imports de votre code qui ne font rien de mal
   File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 36, in  
     import email.parser 
   File "/usr/local/lib/python3.6/email/parser.py", line 12, in  
     from email.feedparser import FeedParser, BytesFeedParser 
   File "/usr/local/lib/python3.6/email/feedparser.py", line 27, in  
     from email._policybase import compat32 
   File "/usr/local/lib/python3.6/email/_policybase.py", line 9, in  
     from email.utils import _has_surrogates 
   File "/usr/local/lib/python3.6/email/utils.py", line 28, in  
     import random 
   File "/usr/local/lib/python3.6/random.py", line 742, in  
     _inst = Random() 
 SystemError:  returned a result with an error set 

Cela a été corrigé rapidement, et le binaire patché ajoute juste un “+” à sa version:

$ python --version
Python 3.6.0+

En théorie vous ne pouvez pas tomber dessus, tous les liens de téléchargement ont été mis à jour, les distributions ont changé leurs dépôts, etc.

Mais hier je me suis fait bien niqué, et j’ai perdu 1h à debugguer cette surprise qui n’avait aucun sens (puisque mon code allait bien) : les bugs dans les binaires officiels sont rares et c’est le dernier endroit où je cherche.

En effet, certaines sources non-officielles pour installer Python n’ont pas été mises à jour, et c’est le cas du très populaire PPA deadsnakes.

Si vous avez installé Python 3.6 en faisant :

sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install python3.6

vous l’avez dans le cul.

Il existe un PPA plus à jour si vous avez besoin de corriger le tir :

sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update
sudo apt-get install python3.6

Donc si vous avez compilé Python à la main ou utilisé un PPA, assurez-vous bien d’avoir la bonne version, et sinon upgradez. En attendant j’ai un bug report à faire à deadsnakes

]]>
Quelques outils pour gérer les clés secrètes en Django 10http://sametmax.com/?p=22504http://streisand.me.thican.net/sametmax.com/index.php?20170223_160516_Quelques_outils_pour_gerer_les_cles_secretes_en_Django_10Thu, 23 Feb 2017 15:05:16 +0000On ne veut pas mettre sa SECRET_KEY en prod, et utiliser un service pour générer la clé, ça va deux minutes.

Générer une clé secrète:

import random
import string
 
def secret_key(size=50):
    pool = string.ascii_letters + string.digits + string.punctuation
    return "".join(random.SystemRandom().choice(pool) for i in range(size))

Générer une clé secrete avec une commande manage.py:

from django.core.management.base import BaseCommand, CommandError
from polls.models import Question as Poll
 
class Command(BaseCommand):
    help = 'Generate a secret key'
 
    def add_arguments(self, parser):
        parser.add_argument('size', default=50, type=int)
 
    def handle(self, *args, **options):
        self.stdout.write(secret_key(options['size']))

A mettre dans ./votreapp/management/command/generate_secret_key.py :)

Une fonction pour lire la clé depuis un fichier texte ou générer la clé si elle n’existe pas:

import io
import os
 
try:
    import pwd
except ImportError:
    pass
 
try:
    import grp
except ImportError:
    pass
 
 
def secret_key_from_file(
        file_path, 
        create=True, 
        size=50, 
        file_perms=None, # unix uniquement
        file_user=None, # unix uniquement
        file_group=None # unix uniquement
    ):
    try:
        with io.open(file_path) as f:
            return f.read().strip()
    except IOError as e:
        if e.errno == 2 and create:
            with io.open(file_path, 'w') as f:
                key = secret_key(size)
                f.write(key)
 
            if any((file_perms, file_user, file_group)) and not pwd:
                raise ValueError('File chmod and chown are for Unix only')
 
            if file_user:
                os.chown(file_path, uid=pwd.getpwnam(file_user).pw_uid)
 
            if file_group:
                os.chown(file_path, gid=grp.getgrnam(file_group).gr_gid)
 
            if file_perms:
                os.chmod(file_path, int(str(file_perms), 8))
 
            return key
 
        raise

Et une fonction pour récupérer la clé depuis une variable d’environnement ou un fichier:

def get_secret_key(
        file_path=None, 
        create=True, 
        size=50, 
        file_perms=None, 
        file_user=None, 
        file_group=None,
        env_var="DJANGO_SECRET_KEY"
    ):
    try:
        return os.environ[env_var]
    except KeyError:
        if file_path:
            return secret_key_from_file(file_path, create=create, size=size)
        raise

Le but de cette dernière est d’avoir ça dans son fichier de settings:

SECRET_KEY = get_secret_key('secret_key')

Et de foutre ‘secret_key’ dans son .gitignore.

Comme ça:

  • Si on n’a pas de clé secrète, on en génère une.
  • Si on a une, elle est dans un fichier qui n’est PAS dans settings.py.
  • On peut commiter settings.py. Chaque env de dev et prod a sa clé secrète automatiquement.
  • On peut overrider la clé avec une variable d’environnement si on le souhaite.

En attendant, j’ai proposé qu’on ajoute ça a django extensions. Et qui sait, dans le core peut être un jour ?

]]>
Proposition d’un mot clé pour l’évaluation paresseuse en Python 3.7 14http://sametmax.com/?p=22463http://streisand.me.thican.net/sametmax.com/index.php?20170219_181951_Proposition_d___un_mot_cle_pour_l___evaluation_paresseuse_en_Python_3.7_14Sun, 19 Feb 2017 17:19:51 +0000Si il y a bien une mailing-list à suivre, c’est Python-idea. Elle regorge de tout, on est y apprend sans cesse à propos de Python, la programmation en général, la gestion de communautés, etc. Mais c’est accessible pour peu qu’on soit à l’aise en anglais.

Parmi les sujets chauds du moment, il y a l’introduction, pour potentiellement Python 3.7, d’un mot clé pour évaluer paresseusement les expressions.

Je m’explique…

On a déjà plusieurs moyens de faire du lazy loading en python :

  • Le shortcut des opérateurs and et or. La partie droite ne s’exécute que si la gauche ne répond pas déjà à la question.
  • Les générateurs. Il ne sont évalués qu’au premier appel de next().
  • await : évalué seulement quand la boucle d’événement décide de passer par là.

La nouvelle proposition est quelque chose de différent : permettre de déclarer une expression arbitraire, mais qui n’est évaluée que la première fois qu’on la lit.

Ca ressemble à ça:

def somme(a, b):
    print('coucou')
    return a + b
 
truc = lazy somme(a, b)
print("Hello")
print(truc)

Ce qui afficherait:

Hello
coucou
3

On peut mettre ce qu’on veut après le mot clé lazy. Le code n’est exécuté qu’une fois qu’on essaye d’utiliser la variable truc.

L’usage essentiel, c’est de pouvoir déclarer du code sans chichi, comme si on allait l’utiliser maintenant. Le passer à du code qui va l’utiliser sans même avoir besoin de savoir que c’est un truc spécial. Et que tout marche à la dernière minute naturellement.

Par exemple, la traduction d’un texte dans un code Python ressemble souvent à ça :

from gettext import gettext as _
...
print(_('Thing to translate'))

Mais dans Django on déclare un champ de modèle dont on veut pouvoir traduire le nom comme ceci :

from django.utils.translation import ugettext_lazy
 
class Produit(models.Model):
    ...
    price = models.IntegerField(verbose_name=ugettext_lazy("price"))

La raison est qu’on déclare ce texte au démarrage du serveur, et on ne sait pas encore la langue dans laquelle on va le traduire. Cette information n’arrive que bien plus tard, quand un utilisateur arrive sur le site. Mais pour détecter toutes les chaînes à traduire, créer le fichier de traduction, construire le cache, etc., il faut pouvoir marquer la chaîne comme traductible à l’avance.

Django a donc codé ugettext_lazy et tout un procédé pour évaluer cette traduction uniquement quand une requête Web arrive et qu’on sait la langue de l’utilisateur.

Avec la nouvelle fonctionnalité, on pourrait juste faire:

from gettext import gettext as _
 
class Produit(models.Model):
    ...
    price = models.IntegerField(verbose_name=lazy _("price"))

Rien à coder nulle part du côté de Django, rien à savoir de plus pour un utilisateur. Ça marche dans tous les cas, pareil pour tout le monde, dans tous les programmes Python.

Bref, j’aime beaucoup cette idée qui permet de s’affranchir de pas mal de wrappers pour plein de trucs, mais aussi beaucoup aider les débutants. En effet les nouveaux en programmation font généralement des architectures basiques : pas d’injection de dépendances, pas de factories, etc. Avec lazy, même si une fonction n’accepte pas une factory, on peut quand même passer quelque chose qui sera exécuté plus tard.

Évidement ça ne dispense pas les gens de faire ça intelligemment et d’attendre des callables en paramètre. Dans le cas de Django, une meilleure architecture accepterait un callable pour verbose_name par exemple.

Mais c’est un bon palliatif dans plein de situations. Et l’avantage indiscutable, c’est que le code qui utilise la valeur paresseuse n’a pas besoin de savoir qu’elle le fait.

Les participants sont assez enthousiastes, et moi aussi, bien que tout le monde a conscience que ça pose plein de questions sur la gestion des générateurs, de locals(), et du debugging en général.

Plusieurs mots clés sont actuellement en compétition: delayed, defer, lazy. delayed est le plus utilisé, mais j’ai un penchant pour lazy.

Viendez sur la mailing list !

]]>
Le don du mois : nuitka, bis 3http://sametmax.com/?p=22404http://streisand.me.thican.net/sametmax.com/index.php?20170212_172858_Le_don_du_mois___nuitka__bis_3Sun, 12 Feb 2017 16:28:58 +0000Ça faisait un bail que j’avais pas parlé d’un don du mois. Le don du mois n’est pas un don mensuel, mais un don que je fais pour le mois. Des fois j’oublie. Des fois je n’ai pas de thune. Des fois je ne me sens pas généreux, que l’humanité aille crever dans sa crasse !

Mais quand les astres du pognon et de la bonne humeur sont alignés je m’y remets.

J’ai pris des nouvelles de nuitka, un outil qui permet de compiler du code Python en un exe indépendant. Malgré le fait que l’auteur soit visiblement le seul à vraiment travailler dessus, le projet continue d’avancer avec régularité et détermination. Corrections de bugs, optimisation (amélioration de la comptabilité (la 3.5 est supportée, la 3.6 en cours !)).

J’ai été agréablement surpris de voir que l’outil s’était encore amélioré. Le hello world stand alone m’a pris quelques minutes à mettre en œuvre, d’autant que nuitka est dans les dépôts Ubuntu donc l’installation ne demande aucun effort.

Comme pouvoir shipper du code Python sans demander à l’utilisateur final d’installer Python est quelque chose qui est en forte demande en ce moment, j’ai voulu soutenir le projet, et j’ai fait un don de 50 euros.

Puis je me suis souvenu qu’en fait, j’en avais déjà fait un l’année dernière :) Bah, l’auteur mérite qu’on le soutienne. Des mecs comme ça y en a pas des masses.

]]>
Le PEP8 et au delà, par la pratique 14http://sametmax.com/?p=22278http://streisand.me.thican.net/sametmax.com/index.php?20170203_114427_Le_PEP8_et_au_dela__par_la_pratique_14Fri, 03 Feb 2017 10:44:27 +0000PEP8. Pas mal sont en fait dues à un manque de compréhension de son application. Je vais donc vous montrer des codes et les transformer pour qu'ils soient plus propre.]]>Je lis régulièrement des commentaires de batailles d’opinions sur le PEP8. Pas mal sont en fait dues à un manque de compréhension de son application. Je vais donc vous montrer des codes et les transformer pour qu’ils soient plus propres.

Rappelez-vous toujours que des recommandations stylistiques sont essentiellement arbitraires. Elles servent à avoir une base commune, et il n’y en pas de meilleures.

On recommande les espaces au lieu des tabs. Les tabs sont sémantiquement plus adaptés et plus flexibles. Les espaces permettent d’avoir un seul type de caractères non imprimables dans son code et autorise un alignement fin d’une expression divisée en plusieurs lignes. Il n’y a pas de meilleur choix. Juste un choix tranché pour passer à autre chose de plus productif, comme coder.

On recommande par contre une limite de 80 caractères pour les lignes. Cela permet à l’œil, qui scanne par micro-sauts, de parser plus facilement le code. Mais aussi de faciliter le multi-fenêtrage. Néanmoins cette limite peut être brisée ponctuellement si le coût pour la lisibilité du code est trop important. Tout est une question d’équilibre.

Ready ? Oh yeah !

L’espacement

def  foo (bar = 'test'):
   if bar=='test' : # this is a test
      bar={1:2 , 3 : 4*4}

Devient:

def foo(bar='test'):
    if bar == 'test':  # this is a test
        bar = {1: 2, 3: 4*4}

On ne double pas les espaces. On n’a pas d’espace avant le ‘:’ ou le ‘,’ mais un après. Les opérateurs sont entourés d’espaces, sauf le ‘=’ quand il est utilisé dans la signature de la fonction ou pour les opérations mathématiques (pour ces dernières, les deux sont tolérés).

Un commentaire inline est précédé de 2 espaces, une indentation est 4 espaces.

Les sauts de lignes aussi sont importants:

import stuff
 
def foo():
    pass
 
def bar():
    pass

Devient:

import stuff
 
 
def foo():
    pass
 
 
def bar():
    pass

Les déclarations à la racine du fichier sont séparées de 2 lignes. Pas plus, pas moins.

Mais à l’intérieur d’une classe, on sépare les méthodes d’une seule ligne.

class Foo:
 
 
    def bar1():
        pass
 
 
    def bar2():
        pass

Devient:

class Foo:
 
    def bar1():
        pass
 
    def bar2():
        pass

Tout cela contribue à donner un rythme régulier et familier au code. Le cerveau humain adore les motifs, et une fois qu’il est ancré, il est facile à reconnaître et demande peu d’effort à traiter.

Les espaces servent aussi à grouper les choses qui sont liées, et à séparer les choses qui ne le sont pas.

Le style de saut de ligne à l’intérieur d’une fonction ou méthode est libre, mais évitez de sauter deux lignes d’un coup.

Le nom des variables

Si on s’en tient au PEP8, on fait du snake case:

def pasUnTrucCommeCa():
    niCommeCa = True
 
def mais_un_truc_comme_ca():
    ou_comme_ca = True

Sauf pour les classes:

class UnTrucCommeCaEstBon:

Et si on a un acronyme:

def lower_case_lol_ptdr():
    ...
 
class UpperCaseLOLPTDR:
    ...

Malgré la possibilité d’utiliser des caractères non ASCII dans les noms en Python 3, ce n’est pas recommandé. Même si c’est tentant de faire:

>>> Σ = lambda *x: sum(x)
>>> Σ(1, 2, 3)
6

Néanmoins c’est l’arbre qui cache la forêt. Il y a plus important : donner un contexte à son code.

Si on a:

numbers = (random.randint(100) for _ in range(100))
group = lambda x: sum(map(int, str(x)))
numbers = (math.sqrt(x) for x in numbers if group(x) == 9)

Le contexte du code donne peu d’informations et il faut lire toutes les instructions pour bien comprendre ce qui se passe. On peut faire mieux:

def digits_sum(number):
    """ Take a number xyz and return x + y + z """
    return sum(map(int, str(number)))
 
rand_sample = (random.randint(100) for _ in range(100))
sqrt_sample = (math.sqrt(x) for x in rand_sample if digits_sum(x) == 9)

Ici, en utilisant un meilleur nommage et en rajoutant du contexte, on rend son code bien plus facile à lire, même si il est plus long.

Le PEP8 n’est donc que le début. Un bon code est un code qui s’auto-documente.

Notez que certains variables sont longues, et d’autres n’ont qu’une seule lettre. C’est parce qu’il existe une sorte de convention informelle sur certains noms dans la communauté :

i et j sont utilisés dans pour tout ce qui est incrément:

for i, stuff in enumerate(foo):

x, y et z sont utilisés pour contenir les éléments des boucles:

for x in foo:
    for y in bar:

_ est utilisé soit comme alias de gettext:

from gettext import gettext as _

Soit comme variable inutilisée:

(random.randint(100) for _ in range(100))

_ a aussi un sens particulier dans le shell Python : elle contient la dernière chose affichée automatiquement.

f pour un fichier dans un bloc with:

with open(stuff) as f:

Si vous avez deux fichiers, nommez les:

with open(foo) as foo_file, open(bar) as bar_file:

*args et **kwargs pour toute collection d’arguments hétérogènes :

def foo(a, b, **kwarg):

Mais attention, si les arguments sont homogènes, on les nomme:

def merge_files(*paths):

Et bien entendu self pour l’instance en cours, ou cls pour la classe en cours:

class Foo:
 
    def bar(self):
        ...
 
    @classmethod
    def barbar(cls):
        ...

Dans tous les cas, évitez les noms qui sont utilisés par les built-ins :

>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

Les erreurs les plus communes : list, dict, min, max, next, file, id, input et str.

La longueur des lignes

C’est le point le plus sujet à polémique. Quelques astuces pour se faciliter la vie.

L’indentation est votre amie.

dico = {0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}
tpl = ('jaune', 'bleu', 'rouge', 'noir', 'octarine')
res = arcpy.FeatureClassToGeodatabase_conversion(['file1.shp', 'file2.shp' ], '/path/to/file.gdb')

Devient:

dico {
    0: None,
    1: None,
    2: None,
    3: None,
    4: None,
    5: None,
    6: None,
    7: None,
    8: None,
    9: None
}
 
tpl = (
    'jaune',
    'bleu',
    'rouge',
    'noir',
    'octarine'
)
 
res = arcpy.FeatureClassToGeodatabase_conversion([
        'file1.shp',
        'file2.shp'
    ],
    '/path/to/file.gdb'
)

Les parenthèses permettent des choses merveilleuses.

from module import package1, package2, package3, package4, package5, package6, package7
query = db.query(MyTableName).filter_by(MyTableName.the_column_name == the_variable, MyTableName.second_attribute > other_stuff).first())
string = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
from module import (package1, package2, package3, package4,
                    package5, package6, package7)
query = (db.query(MyTableName)
           .filter_by(MyTableName.the_column_name == the_variable,
                      MyTableName.second_attribute > other_stuff)
           .first())
string = ("Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
          "sed do eiusmod tempor incididunt ut labore et dolore magna "
          "aliqua. Ut enim ad minim veniam, quis nostrud exercitation "
          "ullamco laboris nisi ut aliquip ex ea commodo consequat.")

Les variables intermédiaires documentent le code:

sqrt_sample = (math.sqrt(x) for x in (random.randint(100) for _ in range(100)) if sum(map(int, str(number))) == 9)

Devient bien plus clair avec :

def digits_sum(number):
    """ Take a number xyz and return x + y + z """
    return sum(map(int, str(number)))
 
rand_sample = (random.randint(100) for _ in range(100))
sqrt_sample = (math.sqrt(x) for x in rand_sample if digits_sum(x) == 9)

return, break et continue permettent de limiter l’indentation:

def foo():
    if bar:
        rand_sample = (random.randint(100) for _ in range(100))
        return (math.sqrt(x) for x in rand_sample if digits_sum(x) == 9)
 
    return None

Est plus élégant écrit ainsi:

def foo():
    if not bar:
        return None
 
    rand_sample = (random.randint(100) for _ in range(100))
    return (math.sqrt(x) for x in rand_sample if digits_sum(x) == 9)

any, all et itertools.product évident pas mal de blocs:

for x in foo:
    if barbarbarbarbarbarbar(x):
        meh()
        break 
 
for x in foo:
    for y in bar:
        meh(y, y)

Devient:

is_bar = (barbarbarbarbarbarbar(x) for x in foo)
if any(is_bar):
   meh()
 
import itertools
for x, y in itertools.product(foo, bar):
    meh(y, y)

Le fait est que Python est bourré d’outils très expressifs. Oui, il arrivera parfois que vous deviez briser la limite des 80 charactères. Je le fais souvent pour mettre des URLs en commentaire par exemple. Mais ce n’est pas le cas typique si vous utilisez le langage tel qu’il a été prévu.

Passer en mode pro

Le PEP8, c’est un point de départ. Quand on a un script et qu’il grandit sous la forme d’une bibliothèque, il faut plus que le reformatter.

A partir d’un certain point, on voudra coiffer son code et lui mettre un costume. Reprenons :

def digits_sum(number):
    """ Take a number xyz and return x + y + z """
    return sum(map(int, str(number)))
 
rand_sample = (random.randint(100) for _ in range(100))
sqrt_sample = (math.sqrt(x) for x in rand_sample if digits_sum(x) == 9)

On lui fait un relooking pour son entretien d’embauche :

def digits_sum(number):
    """ Take a number xyz and return x + y + z
 
        Arguments:
            number (int): the number with digits to sum.
                            It can't be a float.abs
 
        Returns:
            An int, the sum of all digits of the number.
 
        Example:
            >>> digits_sum(123)
            6
    """
    return sum(map(int, str(number)))
 
def gen_squareroot_sample(size=100, randstart=0, randstop=100, filter_on=9):
    """ Generate a sample of random numbers square root
 
        Take `size` number between `randstart` and `randstop`,
        sum it's digits. If the resulting value is equal to `filter_on`,
        yield it.
 
        Arguments:
            size (int): the size of the pool to draw the numbers from
            randstart (int): the lower boundary to generate a number
            randstop (int): the upper boundary to generate a number
            filter_on (int): the value to compare to the digits_sum
 
        Returns:
            Generator[float]
 
        Example:
            >>> list(gen_squareroot_sample(10, 0, 100, filter_on=5))
            [5.291502622129181, 6.708203932499369, 7.280109889280518]
 
    """
    for x in range(size):
        dsum = digits_sum(random.randint(randstart, randstop))
        if dsum == filter_on:
            yield math.sqrt(x)

Et dans un autre fichier :

sqrt_sample = gen_squareroot_sample()

L’important ici:

  • Le code devient modulable car on en fait des fonctions.
  • Les fonctions ont des paramètres avec des valeurs par défaut pour faciliter la vie.
  • On utilise les docstrings pour documenter le code.
  • Des noms bien choisis complètent cette documentation. Le code devient aussi plus verbeux pour laisser la place à la modularité, mais aussi la facilité d’édition.

Il ne faut pas laisser le PEP8 vous limiter à la vision de la syntaxe. La qualité du code est importante : sa capacité à être lu, compris, utilisé facilement et modifié.

Pour cette même raison, il faut travailler ses APIS.

Si vous avez :

class DataSource:
 
    def open(self):
        ...
 
    def close(self):
        ...
 
    def getTopic(self, topic):
       ...

Et que ça s’utilise comme ça:

ds = DataSource()
ds.open()
data = ds.getTopic('foo')
ds.close()

Vous pouvez raler sur le getTopic. Mais si vous vous limitez à ça, vous ratez l’essentiel : cette API n’est pas idiomatique.

Une version plus proche de la philosophie du langage serait:

class DataSource:
 
    def open(self):
        ...
 
    def close(self):
        ...
 
    def get_topic(self, topic):
       ...
 
    def __getitem__(self, index):
        return self.get_topic(index)
 
    def __enter__(self):
        self.open()
        return self
 
    def __exit__(self, *args, **kwargs):
        self.close()

Et que ça s’utilise comme ça:

with DataSource() as ds:
    data = ds['foo']

Le style d’un langage va bien au-delà des règles de la syntaxe.

Les docstrings

Je suis beaucoup plus relaxe sur le style des docstrings. Il y a pourtant bien le PEP 257 pour elles.

Simplement ne pas le respecter parfaitement n’affecte pas autant leur lisibilité car c’est du texte libre.

Quelques conseils tout de même.

  • Si elle peut tenir sur une ligne, mettez tout sur une ligne, y compris les """.
  • Utilisez """, pas '''. La majorité des gens le font, et ça fera très bizarre de mélanger.
  • Choisissez une convention de documentation et tenez-vous y. Les plus populaires sont celles de sphinx, numpy et Google. J’ai une préférence pour la dernière.
  • N’hésitez pas à la faire longue, avec un exemple. Il n’est pas rare que la docstring soit plus longue que le code qu’elle documente.
  • Une bonne docstring et une fonction gardée courte et avec une signature bien nommée a rarement besoin de commentaires.
  • Les docstests sont un enfer à maintenir.

En bonus

flake8 est un excellent linter qui vérifiera votre style de code. Il existe en ligne de commande ou en plugin pour la plupart des éditeurs.

Dans le même genre, mccabe vérifira la complexité de votre code et vous dira si vous êtes en train de fumer en vous attributant un score. Il existe aussi intégré comme plugin de flake8 et activable via une option.

Tox vous permet d’orchestrer tout ça, en plus de vos tests unittaires. Je ferai un article dessus un de ces 4.

Si vous voyez des commentaires comme # noqa ou # xxx: ignore ou # xxx: disable=YYY, ce sont des commentaires pour ponctuellement dire à ces outils de ne pas prendre en considération ces lignes.

Car souvenez-vous, ces règles sont là pour vous aider. Si à un moment précis elles cessent d’etre utiles, vous avez tous les droits de pouvoir les ignorer.

Mais ces règles communes font de Python un langage à l’écosystème exceptionnel. Elles facilitent énormément le travail en équipe, le partage et la productivité. Une fois habitué à cela, travailler dans d’autres conditions vous paraitra un poids inutile.

]]>
La maison des horreurs de l’encoding 13http://sametmax.com/?p=22198http://streisand.me.thican.net/sametmax.com/index.php?20170129_155851_La_maison_des_horreurs_de_l___encoding_13Sun, 29 Jan 2017 14:58:51 +0000Ah, l’encoding, le truc que tout le monde veut mettre sous le tapis. Il faut dire que c’est dur à gérer. En fait tellement dur que:

  • Les logiciels et pages web continuent parfois d’afficher des ?? en 2017. Tous les langages laxistes laissent des données corrompues plutôt que d’avertir le codeur qui du coup n’apprend jamais à faire les choses correctement.
  • PHP a littéralement abandonné la version 6 car impossible de trouver une solution propre avec leur design.
  • Perl a mis 10 ans à sortir la version 6 qui gère proprement l’unicode.
  • NodeJS ignore juste la question, fout tout en utf8 et vous dit de télécharger une lib externe si vous voulez gérer autre chose.

Tout ce bordel amène les devs à essayer d’ignorer le problème le plus longtemps possible. Ca marche assez bien pour les anglophones car leur environnement est assez homogène, orienté ASCII, et certains peuvent faire une très belle carrière en restant joyeusement ignorant.

Ca marche beaucoup moins bien pour les européens, et pas du tout pour le monde arabe et asiatique. Néanmoins, pas besoin de chercher bien loin pour trouver des échecs critiques.

Naviguez tranquillement sur un site espagnol a priori joli, moderne, utilisant des tildes et tout ce qu’il faut. Maintenant regardez la requête HTTP, vous noterez que le serveur n’indique pas le charset du contenu. Fort heureusement dans le HTML vous trouvez:

<meta http-equiv="Content-Type" content="ISO-8859-1">

Nickel, récupérons le texte du bouton “Ver más ideas”:

>>> import requests
>>> res = requests.get('http://www.airedefiesta.com/76-pinatas-y-chuches.html') 
>>> data = res.content.split(b'http://www.airedefiesta.com/ideas.html?c=76">')[1].split(b'</a>')[0]
>>> data
b'Ver m\xc3\xa1s ideas'

Une suite de bits comme maman les aime. On décode:

>>> data.decode('ISO-8859-1')
'Ver más ideas'
Chenille dans labyrinthe

Et puis on ne voudrait pas que vous arriviez au château trop vite

Enfer et sodomie ! Le charset déclaré n’est pas celui utilisé. Tentons un truc au hasard:

>>> data.decode('utf8')
'Ver más ideas'

Bref, en 2017, on se touche la nouille pour savoir qui a son architecture multi-services load balancée web scale à base de NoSQL, de containers orchestrés et de serveurs asynchrones. Mais pour afficher du texte y a plus personne hein…

Vous croyez que ce ne sont que les amateurs qui font ces erreurs. Naaaaaaaaa. Par exemple le standard pour les fichiers zip a une vision très… hum… personnelle du traitement de l’encoding des noms de fichier.

L’encoding, c’est la raison majeur de l’incompatibilité de Python 2 et 3, mais aussi un signe de la bonne santé de la techno puisque c’est un des rares vieux langages (je rappelle que Python est plus vieux que Java) à gérer la chose correctement. A savoir:

  • Avoir un type haut niveau qui fait abstraction de l’encoding pour le texte.
  • Forcer le développeur à spécifier l’encoding pour les entrées et les sorties.
  • Eviter toute conversion automatique.
  • Avoir de l’utf8 par défaut là où ça a du sens.
  • Lever des erreurs plutôt que de corrompre les données.
  • Avoir une API unifiée autour de la notion de “codec” qui marche pour le FS, le réseau, les chaînes internes, etc.

Python n’est pas parfait pour autant. Par exemple il garantit un accès 0(1) indexing sur les strings, ce qui à mon sens est inutile. Swift a un meilleur design pour ce genre de choses. Mais globalement c’est quand même super bon.

Si ne savez toujours pas comment ça marche, on a évidement un tuto pour ça.

Alors pourquoi l’encoding c’est un truc compliqué ?

Et bien parce que comme pour le temps ou l’i18n, ça touche à la culture, au langage, à la politique, et on a accumulé les problèmes au fil des années.

A solid dick from an iron man

Mais je vous jure ça avait du sens y a 40 ans !

Par exemple, parlons un peu d’UTF.

Vous savez, on vous dit toujours d’utiliser utf8 partout pour être tranquille…

Mais déjà se pose la question : avec ou sans BOM ?

Le BOM, c’est une suite d’octets qui indique en début de fichier qu’il contient de l’UTF. Si ça à l’air pratique, c’est parce que ça l’est. Malheureusement, celui-ci n’est pas obligatoire, certaines applications le requièrent, d’autres l’ignorent, et d’autres plantent face au BOM. D’ailleurs, le standard unicode lui-même ne le recommande pas:

Use of a BOM is neither required nor recommended for UTF-8

Ca aide vachement à faire son choix.

Perso je ne le mets jamais, sauf si je dois mélanger des fichiers de différents encodings et les différencier plus tard.

Mais Powershell et Excel par exemple, fonctionnent parfois mieux si vous leur passez des données avec le BOM :)

Si vous avez un peu creusé la question, vous savez qu’il existe aussi UTF16 (par défaut dans l’API de Windows 7 et 8 et les chaînes de .NET), UTF32 et UTF64. Ils ont des variantes Big et Little Endians, qui ne supportent pas le BOM, et une version neutre qui le supporte, pour faciliter la chose.

Bien, bien, bien.

Mais saviez-vous qu’il existe aussi UTF-1, 5 et 6 ? Si, si. Et UTF9 et UTF18 aussi, mais sauf que eux ce sont des poissons d’avril, parce que les gens qui écrivent les RFC sont des mecs trop funs en soirées.

Que sont devenus ces derniers ? Et bien ils ont été proposés comme encoding pour l’internationalisation des noms de domaine. UTF5 est un encoding en base 32, comme son nom l’indique. Si, 2 puissance 5 ça fait 32. Funs en soirée, tout ça.

Néanmoins quelqu’un est arrivé avec une plus grosse bit(e), punycode, en base 36, et a gagné la partie. J’imagine que les gens se sont dit qu’utiliser base64 était déjà trop fait par tout le monde et qu’on allait pas se priver de cette occasion fabuleuse de rajouter un standard.

Standard qui ne vous dispense pas, dans les URLs, d’encoder DIFFÉREMMENT ce qui n’est pas le nom de domaine avec les bons escaping. Et son lot de trucs fantastiques. Encoding qui est différent pour les valeurs de formulaire.

Python supporte par ailleurs très bien tout ça:

>>> 'Père noël'.encode('punycode')
b'Pre nol-2xa6a'
>>> import urllib
>>> urllib.parse.quote('Père Noël')
'P%C3%A8re%20No%C3%ABl'
>>> urllib.parse.quote_plus('Père Noël')
'P%C3%A8re+No%C3%ABl'

En plus, si Punycode est l’encoding par défaut utilisé dans les noms de domaine, c’est donc aussi celui des adresses email. Ce qui vous permettra de profiter des interprétations diverses de la spec, comme par exemple le retour de la valeur d’un HTML input marqué “email”, qui diffère selon les navigateurs.

Président faisant un fuck dans idiocracy

If you don’t encode in Tarrlytons…fuck you!

Pourquoi je vous parle des adresses emails tout à coup ? Ah ah ah ah ah ah ah !

Mes pauvres amis.

Je ne vous avais jamais parlé d’utf7 ?

Non, je ne me fous pas de votre gueule. Je suis très sérieux.

Figurez-vous que le format email MIME accepte l’utilisation d’utf7 en lieu et place de base64.

Mais ce n’est pas ça le plus drôle.

Y a mieux, je vous jure.

UTF7 est en effet l’encoding par défaut pour IMAP, particulièrement les noms des boîtes aux lettres. Vous savez, “INBOX”, “Spams” et “Messages envoy&AOk-s” ;)

Or comme l’enculerie ne serait pas aussi délicieuse sans un peu de sable…

La version utilisée maintenant (et pour toujourssssssss) par IMAP est une version d’UTF7 non standard et modifiée.

Pourquoi ? Ben parce qu’allez-vous faire foutre.

The choosen one, crying

The choosen one would soon realize that some things survived outside of the vault. Like bad UI and terrible IT standards. And his ‘science’ skills is at 42% and life sucks.

Au final je n’ai fait que parloter d’UTF, mais souvenez-vous que:

>>> import encodings
>>> len(encodings.aliases.aliases)
322

Donc on n’a fait qu’effleurer la surface de l’anus boursouflé de la mouche.

J’espère que la nuit, à 3h du mat, lorsque votre prochaine mise en prod agonisera sur un UnicodeDecodeError, vous penserez à moi et pendant un instant, un sourire se dessinera sous vos larmes.

Ecriture extra-terrestre de the arrival

Militaire : Votre avis ? – Unicode Consortium : Tuez les tous.

]]>
Tout ce qui fait que Python 3 est meilleur que Python 2 31http://sametmax.com/?p=22144http://streisand.me.thican.net/sametmax.com/index.php?20170126_151740_Tout_ce_qui_fait_que_Python_3_est_meilleur_que_Python_2_31Thu, 26 Jan 2017 14:17:40 +0000Avec de nombreuses distros Linux qui viennent avec Python 3 par défaut ainsi que Django et Pyramid qui annoncent bientôt ne plus supporter Python 2, il est temps de faire un point.

Python 3 est aujourd’hui majoritairement utilisé pour tout nouveau projet ou formation que j’ai pu rencontrer. Les plus importantes dépendances ont été portées ou possèdent une alternative. Six et Python-future permettent d’écrire facilement un code compatible avec les deux versions dans le pire des cas.

Nous sommes donc bien arrivés à destination. Il reste quelques bases de code encore coincées, la vie est injuste, mais globalement on est enfin au bout de la migration. Mais ça en a mis du temps !

Il y a de nombreuses raisons qui ont conduit à la lenteur de la migration de la communauté :

  • Python 2 est un très bon langage. On ne répare pas ce qui marche.
  • Il y a beaucoup de code legacy en Python 2 et ça coûte cher de migrer.
  • La PSF a été trop gentille avec la communauté et l’a chouchoutée. En JS et Ruby ils ont dit “migrez ou allez vous faire foutre” et tout le monde a migré très vite.

Mais je pense que la raison principale c’est le manque de motivation pour le faire. Il n’y a pas un gros sticker jaune fluo d’un truc genre “gagnez 30% de perfs en plus” que les devs adorent même si ça n’influence pas tant leur vie que ça. Mais les codeurs ne sont pas rationnels contrairement à ce qu’ils disent. Ils aiment les one-liners, c’est pour dire.

Pourtant, il y a des tas de choses excellentes en Python 3. Simplement:

  • Elles ne sont pas sexy.
  • Elles ne se voient pas instantanément.
  • Personne n’en parle.

Après 2 ans de Python 3 quasi-fulltime, je peux vous le dire, je ne veux plus coder en Python 2. Et on va voir pourquoi.

Unicode, mon bel unicode

On a crié que c’était la raison principale. A mon avis c’est une erreur. Peu de gens peuvent vraiment voir ce que ça implique.

Mais en tant que formateur, voilà ce que je n’ai plus a expliquer:

  • Pourquoi un accent dans un commentaire fait planter le programme. Et # coding:
  • Pourquoi il faut faire from codecs import open et non pas juste open.
  • Pourquoirequest.get().read() + 'é' fait planter le programme. Et encode() et decode().
  • Pourquoi os.listdir()[0] + 'é' fait afficher des trucs chelous.
  • Pourquoi print(sql_row[0]) fait planter le programme ou affiche des trucs chelous. Ou les deux.

Et je peux supprimer de chacun de mes fichiers:

  • Tous les u ou les from __future__/codecs
  • Les en-tête d’encoding.
  • La moitié des encode/decode.

Et toutes les APIS ont un paramètre encoding, qui a pour valeur par défaut ‘UTF8′.

Debuggage for the win

Des centaines d’ajustements ont été faits pour faciliter la gestion des erreurs et le debuggage. Meilleures exceptions, plus de vérifications, meilleurs messages d’erreur, etc.

Quelques exemples…

En Python 2:

>>> [1, 2, 3] < "abc"
True

En Python 3 TypeError: unorderable types: list() < str() of course.

En Python 2, IOError pour tout:

>>> open('/etc/postgresql/9.5/main/pg_hba.conf')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf'
>>> open('/etc/postgresql/9.5/main/')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/'

En Python 3, c'est bien plus facile à gérer dans un try/except ou à debugger:

>>> open('/etc/postgresql/9.5/main/pg_hba.conf')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
PermissionError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf'
>>> open('/etc/postgresql/9.5/main/')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IsADirectoryError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/'

Les cascades d'exceptions en Python 3 sont très claires:

>>> try:
...     open('/') # Erreur, c'est un dossier:
... except IOError:
...     1 / 0 # oui c'est stupide, c'est pour l'exemple
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
IsADirectoryError: [Errno 21] Is a directory: '/' 
 
During handling of the above exception, another exception occurred:
 
  File "<stdin>", line 4, in <module>
ZeroDivisionError: division by zero

La même chose en Python 2:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
ZeroDivisionError: integer division or modulo by zero

Bonne chance pour trouver l'erreur originale.

Plein de duplications ont été retirées.

En Python 2, un dev doit savoir la différence entre:

  • range() et xrange()
  • map/filter et itertools.imap/itertools.ifilter
  • dict.items/keys/values, dict.iteritems/keys/values et dict.viewitems/keys/values
  • open et codecs.open
  • Et md5 vs hashlib.md5
  • getopt, optparse, argparse
  • Cette manipulation de fichier se fait avec sys, os ou shutil ?
  • On hérite de UserDict ou dict ? UserList ou list ?
  • ...

Sous peine de bugs ou de tuer ses perfs.

Certains bugs sont inévitables, et les modules csv et re sont par exemple pour toujours buggés en Python 2.

Good bye boiler plate

Faire un programme correct en Python 2 requière plus de code. Prenons l'exemple d'une suite de fichier qui contient des valeurs à récupérer sur chaque ligne, ou des commentaires (avec potentiellement des accents). Les fichiers font quelques centaines de méga, et je veux itérer sur leurs contenus.

Python 2:

# coding: utf8
 
from __future__ import unicode_literals, division
 
import os
import sys
 
from math import log
from codecs import open
from glob import glob
from itertools import imap  # pour ne pas charger 300 Mo en mémoire d'un coup
 
FS_ENCODING = sys.getfilesystemencoding()
 
SIZE_SUFFIXES = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
 
def file_size(size):
    order = int(log(size, 2) / 10) if size else 0  # potential bug with log2
    size = size / (1 << (order * 10))
    return '{:.4g} {}'.format(size, suffixes[order])
 
def get_data(dir, *patterns, **kwargs):
    """ Charge les données des fichiers  """
 
    # keyword only args
    convert = kwargs.get('convert', int)
    encoding = kwargs.get('encoding', 'utf8')
 
    for p in patterns:
        for path in glob(os.path.join(dir, p)):
            if os.path.isfile(path):
                upath = path.decode(FS_ENCODING, error="replace")
                print 'Trouvé: ', upath, file_size(os.stat(path).st_size)
 
                with open(path, encoding=encoding, error="ignore") as f:
                    # retirer les commentaires
                    lines = (l for l in f if "#" not in l)
                    for value in imap(convert, f):
                        yield value

C'est déjà pas mal. On gère les caractères non ASCII dans les fichiers et le nom des fichiers, on affiche tout proprement sur le terminal, on itère en lazy pour ne pas saturer la RAM... Un code assez chouette, et pour obtenir ce résultat dans d'autres langages vous auriez plus dégueu (ou plus buggé).

Python 3:

# wow, so much space, much less import
from math import log2 # non buggy log2
from pathlib import Path # plus de os.path bullshit
 
SIZE_SUFFIXES = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
 
def file_size(size):
    order = int(log2(size) / 10) if size else 0   
    size = size / (1 << (order * 10))
    return f'{size:.4g} {suffixes[order]}' # fstring
 
def get_data(dir, *patterns, convert=int, encoding="utf8"): # keyword only
    """ Charge les données des fichiers  """
 
    for p in patterns:
        for path in Path(dir).glob(p):
            if path.is_file():
                print('Trouvé: ', path, file_size(path.stat().st_size))
 
                with open(path, encoding=encoding, error="ignore") as f:
                    # retirer les commentaires
                    lines = (l for l in f if "#" not in l)
                    yield from map(convert, lines) # déroulage automatique

Oui, c'est tout.

Et ce n'est pas juste une question de taille du code. Notez tout ce que vous n'avez pas à savoir à l'avance pour que ça marche. Le code est plus lisible. La signature de la fonction mieux documentée. Et il marchera mieux sur une console Windows car le support a été amélioré en Python 3.

Et encore j'ai été sympa, je n'ai pas fait la gestion des erreurs de lecture des fichiers, sinon on en avait encore pour 3 ans.

Il y a des tas d'autres trucs.

L'unpacking généralisé:

>>> a, *rest, c = range(10)  # récupérer la première et dernière valeur
>>> foo(*bar1, *bar2)  # tout passer en arg
>>> {**dico1, **dico2} # fusionner deux dico :)

Ou la POO simplifiée:

class FooFooFoo(object):
    ...
 
class BarBarBar(FooFooFoo):
    def wololo(self):
        return super(BarBarBar, self).wololo()

Devient:

class FooFooFoo:
    ...
 
class BarBarBar(FooFooFoo):
    def wololo(self):
        return super().wololo()

Python 3 est tout simplement plus simple, et plus expressif.

Bonus

Evidement il y a plein de trucs qui n'intéresseront qu'une certaine catégorie de devs:

  • ipaddress.ip_address pour parser les adresses IP.
  • asyncio pour faire de l'IO non blocante sans threads.
  • enum des enum de toutes sortes.
  • functools.lru_cache pour cacher le résultat de ses fonctions.
  • type hints pour vérifier la validité de son code.
  • l'opérateur @ pour multiplier des matrices avec numpy.
  • concurrent.futures pour faire des pools non blocantes propres.
  • statistics stats performantes et correctes.
  • tracemalloc trouver les fuites de mémoire.
  • faulthandler gérer les crash du code C proprement.

Si vous n'êtes pas concernés, ça n'est pas motivant. Mais si ça vous touche personnellement, c'est super cool.

Au passage, depuis la 3.6, Python 3 est enfin plus rapide que Python 2 pour la plupart des opérations :)

Pour finir...

Toutes ces choses là s'accumulent. Un code plus court, plus lisible, plus facile à débugger, plus juste, plus performant. Par un tas de petits détails.

Alors oui, la migration ne va pas restaurer instantanément votre érection et vous faire perdre du poids. Mais sur le long terme, tout ça compte énormément.

]]>
Augmenter son volume de sperme pour des super éjaculations faciales. 18http://sametmax.com/?p=22097http://streisand.me.thican.net/sametmax.com/index.php?20170124_003617_Augmenter_son_volume_de_sperme_pour_des_super_ejaculations_faciales._18Mon, 23 Jan 2017 23:36:17 +0000Un sale titre putaclic comme je les aime :)

Comme tous les 2 mois je parts en mission spéciale pour m’adonner aux pratiques les plus salaces afin d’assouvir mes pulsions DSKniennes.

Et comme 99.99% des mecs j’adore les éjacs faciales mais aussi sur les pieds (fétichisme oblige) ou les fesses ou encore une bonne paire de nichons bien juteux.
Il y a quelques années je m’étais renseigné sur des pilules pour augmenter le volume de sperme mais c’était du pipeau, j’ai donc regardé la composition du sperme et grosso modo la voici:

Le sperme contient de nombreux éléments nourriciers pour le spermatozoïde :
vitamines C et B12, sels minéraux comme le calcium, le magnésium, le phosphore, le potassium et le zinc, des sucres (fructose et sorbitol).

Je me suis penché sur les éléments les plus courants à trouver à savoir la vitamine C, B12, magnésium, zinc et fructose.

vitamine C = orange
B12 = banane
magnesium et zinc = cacao
fructose = les fruits

Je me suis donc mis en tête d’ingurgiter quotidiennement ces ingrédients afin de tester une éventuelle augmentation du volume spermique.
Mon test n’est basé sur absolument aucun étude scientifique, mais de toutes façons c’est que des produits naturels, et en plus ça file la pêche.

Bien évidement vous n’allez pas jouer au pompier du jour au lendemain mais j’ai pu noter une certaine augmentation du volume sans que ce soit non plus les chutes du niagara (le titre c’est juste pour être en premier dans les forums d’ados) mais je les trouves supérieures tout de même avec un certain avantage, une femme m’a dit que mon liquide séminal avait le goût de fruits, elle avait l’air d’en être plutôt contente.

 

Passons au cocktail !

2 fois par jour, en général 10h du math et 16/17h:

Dans un mixer versez:

  • 1 banane
  • 1 orange
  • une tête de menthe fraîche
  • un peu de lait d’amande
  • un mélange d’amandes pilées et de coco râpée (ça se trouve rayon patisserie)
  • du cacao en poudre
  • du miel
  • noix de cajou
  • cannelle
  • quelques glaçons (pour le frais mais aussi pour l’apport d’eau)

Mixez le tout pendant 5 minutes et servez frais !

Faut faire ce “régime” pendant au moins quelques semaines en buvant pas mal d’eau mais ça c’est la base, au moins 1 à 2 litres d’eau par jour pour que le corps élimine bien sinon il s’acidifie. Vous devriez voir les premiers résultats, madame sera ravie.
Du 100% Bio ! Bon pour le teint.

ban

chériiiiiiie, tes vitamines arrivent…

Aller cadeau, la HD ça rend tout de suite mieux.  

PS: j’ai fait plusieurs tests et c’est celui qui me donne le plus de satisfaction, si vous avez des variantes ou des suggestions allez-y. A côté de ça j’ai une alimentation qui exclue tout produit surgelé ou transformé par l’industrie agro alimentaire, paraît qu’on appelle ça le régime paléo, perso je fais pas de régime, je bouffe comme ma grand-mère et mes parents, pas de junk food et je me sens bien.
Je me gène pas me défoncer un plateau de fruits de mer à l’occas (au passage les huîtres sont blindées de zinc, B12, omega3 et ça a une texture de chatte alors pourquoi s’en priver ?!)

 

Oui je sais c’est très réducteur pour la dignité des femmes, ça rappelle les heures les plus sombres de notre histoire, la météorite qui a fait disparaître les dinosaures et ça prédit même l’arrivé de Nibiru. Pour les féministes défoulez-vous, mais n’oubliez pas vos cachets et le rendez-vous de demain chez le psy.

 

A poil les putes !

]]>
Je peux faire un clone de Twitter en un week end 16http://sametmax.com/?p=22032http://streisand.me.thican.net/sametmax.com/index.php?20170120_160149_Je_peux_faire_un_clone_de_Twitter_en_un_week_end_16Fri, 20 Jan 2017 15:01:49 +0000clones pour le prouver.]]>Il ne se passe pas un mois sans que je ne lise le commentaire d’un abruti qui annonce être capable de réécrire service X en quelques jours. Des projets et des projets de clones pour le prouver.

Twitter, Uber, Imgur, whatever.

Une variante est de juger la stack d’un service en prétendant qu’on pourrait faire beaucoup mieux avec moins.

Je ne sais pas si c’est de la bêtise, de l’ignorance ou de l’arrogance. Probablement des trois.

Prenons par exemple Twitter. Easy non ? Des messages de 140 caractères, quelques tags.

Mais, mais, mais les amis. Ca c’était Twitter le premier week-end de sa sortie aussi.

Maintenant Twitter c’est beaucoup plus que ça:

  • Suivi des messages en temps réel ou par historique complet.
  • Suivi par personne ou tag.
  • Possibilité de faire une recherche complexe sur des gens, tags ou du texte libre. Avec filtrage par settings comme la langue ou la date. Et obtenir les résultats en temps réel.
  • API complète pour accéder au service.
  • Analytics sur les tweets, personnes ou tags. Et l’auth, le bouton twitter, etc.
  • Auto-complétion partout.
  • UI responsive design, et compatible avec des dizaines de navigateurs.
  • Et une app mobile qui fonctionne sur 2 OS majeurs.
  • Upload de médias.
  • Minification d’URLs.
  • Instruments de modération et anti-spam.
  • Intégration de miniatures de contenus liés.
  • Intégrations avec services externes tels que Youtube, Slideshare, etc.
  • Comptes avec fonctionnalités professionnelles.

Bien entendu avec son succès Twitter doit maintenant scaler et assurer:

  • La connexion de centaines de millions d’utilisateurs chaque jour.
  • Qui postent et lisent des messages.
  • Qui peuvent être reçus par des millions d’utilisateurs. Et oui, c’est pas du chat. Un message ne va pas à une dizaine de destinataires.
  • Qui peuvent être reçus par des tags lus par des centaines de services en ligne. Et oui, en plus des abonnés, les messages sont aussi distribués par tag. Des millions de tags. Combinables arbitrairement.
  • L’historique de tous les tweets doit être accessible. Des milliards de tweet.
  • Gérer tout ce bordel alors que les messages sont un graphe. Et oui, entre les utilisateurs, les tags, les RT récursifs, les réponses et les likes, twitter n’est pas une timeline linéaire. C’est un graphe. Traité en quasi temps réel.
  • Se défendre contre les attaques, les abus du service, supporter le trending topics dûs à l’actualité.
  • Fournir le service dans des centaines de pays avec des langues, cultures, lois et situations politiques différentes.
  • Gérer la partie commerciale (faire de la thune) et marketing (préserver son image).
  • Gérer la boite : l’équipe, la thune, les locaux, les projets.
  • Gérer les incidents incongrus. Ben oui, si un truc à une chance de 0.0000000001% de se produire, sur un service qui a des centaines de millions de users qui s’activent comme les chimpanzés hystériques, ce truc arrive TOUS LES JOURS.

Aujourd’hui ce service, c’est des millions de lignes de code.

En prime, un service à succès n’est pas que du code, c’est aussi une énorme infra. Une logistique de dingue. Et un effort marketing colossal.

Personne ne peut reproduire ce que fait X actuellement en un an de travail. Je ne parle pas d’un week-end.

Les projets que l’on voit sont des “représentation du concept clé de X”.

Ca ne veut pas dire qu’il ne faut pas essayer de copier, concurrencer, proposer des alternatives, etc. Faut juste pas prendre les gens pour des cons.

Bref, à tous les prétendants au “c’est super simple” présents, passés et futurs :

]]>
Le formatage des strings en long et en large 17http://sametmax.com/?p=21963http://streisand.me.thican.net/sametmax.com/index.php?20170116_155304_Le_formatage_des_strings_en_long_et_en_large_17Mon, 16 Jan 2017 14:53:04 +0000Un bon article bien long. Je sens que ça vous avait manqué :) Musique ?

Un problème qui se retrouve souvent, c’est le besoin d’afficher un message qui contient des valeurs de variables. Or, si en Python on privilégie généralement “il y a une seule manière de faire quelque chose”, cela ne s’applique malheureusement pas au formatage de chaînes qui a accumulé bien des outils au fil des années.

TL;DR

Si c’est juste pour afficher 2, 3 bricoles dans le terminal, utilisez print() directement:

>>> print("J'ai", 3, "ans")
J'ai 3 ans
>>> print(3, 2, 1, sep='-')
3-2-1

Si vous avez besoin d’un formatage plus complexe ou que le texte n’est pas que pour afficher dans le terminal…

Python 3.6+, utilisez les f-strings:

>>> produit = "nipple clamps"
>>> prix = 13
>>> print(f"Les {produit} coûtent {prix:.2f} euros")
Les nipple clamps coûtent 13.00 euros

Sinon utilisez format():

>>> produit = "nipple clamps"
>>> prix = 13
>>> print("Les {} coûtent {:.2f} euros".format(produit, prix))
Les nipple clamps coûtent 13.00 euros

Si vous êtes dans le shell, que vous voulez aller vite, ou que vous manipulez des bytes, vous pouvez utiliser “%”, mais si ça ne vous arrive jamais, personne ne vous en voudra:

>>> produit = "nipple clamps"
>>> prix = 13
>>> print("Les %s coûtent %.2f euros" % (produit, prix))
Les nipple clamps coûtent 13.00 euros

N’utilisez jamais string.Template.

Si vous avez un gros morceau de texte ou besoin de logique avancée, utilisez un moteur de template comme jinja2 ou mako. Pour l’i18n et la l10n, choisissez une lib comme babel.

Avec print()

Par exemple, si j’ai :

produit = "nipple clamps"
prix = 13

Et je veux afficher :

"Les nipple clamps coûtent 13 euros"

La manière la plus simple de faire cela est d’utiliser print():

>>> print("Les", produit, "coûtent", prix, "euros")
Les nipple clamps coûtent 13 euros

Mais déjà un problème se pose : cette fonction insère des espaces entre chaque argument qu’elle affiche. Cela est ennuyeux si par exemple je veux utiliser le signe et le coller pour obtenir :

Les nipple clamps coûtent 13

print() possède un paramètre spécial pour cela : sep. Il contient le séparateur, c’est à dire le caractère qui va être utilisé pour séparer les différents arguments affichés. Par défaut, sep est égal à un espace.

Si je change ma phrase et que j’ai besoin d’espaces à certains endroits et pas à d’autres, il me faut définir un séparateur – ici une chaîne vide – et jouer un peu avec le texte :

>>> print("Les ", produit, " coûtent ", prix, "€", sep="")
Les nipple clamps coûtent 13

C’est mieux. Mais, ça commence à devenir moins lisible.

Maintenant que se passe-t-il si je veux utiliser une valeur numérique mais que j’ai besoin de la formater ?

Par exemple :

produit = "nipple clamps"
prix = 13
exo_taxe = 0.011

Et je veux tronquer le prix au centime de telle sorte que j’obtienne :

Les nipple clamps coûtent 13.01

Arf, ça va demander un peu plus de travail.

>>> total = round(prix + exo_taxe, 2)
>>> print("Les ", produit, " coûtent ", total, "€", sep="")

Bon, mais admettons que je veuille sauvegarder ce texte dans une variable ? Par exemple pour le passer à une fonction qui vérifie l’orthographe ou met la phrase en jaune fluo…

Dans ce cas ça devient burlesque, il faut intercepter stdout et récupérer le résultat :

>>> faux_terminal = io.StringIO()
>>> print("Les ", produit, " coûtent ", total, "€", sep="", file=faux_terminal)
>>> faux_terminal.seek(0)
>>> msg = faux_terminal.read()
>>> print(msg)
Les nipple clamps coûtent 13.01

Vous l’avez compris, print() est fantastique pour les cas simples, mais devient rapidement peu pratique pour les cas complexes : son rôle est d’être bon à afficher, pas à formater.

Avec +

A ce stade, un débutant va généralement taper “concaténation string python” sur son moteur de recherche et tomber sur l’opérateur +. Il essaye alors ça :

>>> "Les " + produit + " coûtent " + total + "€"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-126b156e23bd> in <module>()
----> 1 "Les " + produit + " coûtent " + total + "€"
 
TypeError: Can't convert 'float' object to str implicitly

Et il apprend par la même occasion que Python est fortement typé. On ne peut pas additionner des choux et des carottes disait ma prof de CE1, et donc on ne peut pas additionner "coûtent" (type str) et total (type float).

Il faut donc convertir total :

>>> "Les " + produit + " coûtent " + str(total) + "€"
'Les nipple clamps coûtent 13.01€'

C’est mieux que notre version avec print(), d’autant qu’on peut sauvegarder facilement tout ça dans une variable :

>>> total = round(prix + exo_taxe, 2)
>>> msg = "Les " + produit + " coûtent " + str(total) + "€"
>>> print(msg)
Les nipple clamps coûtent 13.01

Mais ça reste chiant à taper, et encore plus à modifier. Si je veux insérer quelque chose là dedans, il me faut faire très attention en déplaçant mes + et mes " sans compter calculer ma gestion des espaces.

La raison est simple : il est difficile de voir la phrase que j’essaye d’afficher sans bien étudier mon expression.

Par ailleurs, je suis toujours obligé de faire mon arrondi.

Pour cette raison, je recommande de ne pas utiliser + pour formater son texte, car il existe de bien meilleurs outils en Python.

Avec %

Là, on arrive à quelque chose de plus sympa !

L’opérateur % appliqué aux chaînes de caractères permet de définir un texte à trous, et ensuite de dire quoi mettre dans les trous. C’est une logique de template.

Elle est courte et pratique : c’est la méthode que j’utilise le plus actuellement dans un shell ou sur les chaînes courtes.

Par exemple, si je veux créer la chaîne:

Les nipple clamps coûtent 13€

Alors mon texte à trou va ressembler à :

Les [insérer ici le nom du produit] coûtent [insérer ici le prix du produit]€

Avec l’opérateur %, le texte à trous s’écrit :

Les %s coûtent %s€

%s marque les trous.

Pour remplir, on met les variables à droite, dans l’ordre des trous à remplir :

>>> total = round(prix + exo_taxe, 2)
>>> "Les %s coûtent %s€" % (produit, total)
'Les nipple clamps coûtent 13.01€'

Pas besoin de convertir total en str, et la phrase qu’on souhaite obtenir est facile à deviner en lisant l’expression.

%s veut dire “met moi ici la conversion en str de cet objet”. C’est comme si on appelait str(total).

Il existe d’autres marqueurs :

  • %d est comme si faisait int() sur la valeur.
  • %f est comme si faisait float() sur la valeur.
  • %x est comme si on faisait hex()[2:]
  • etc

Ex:

>>> "%d" % 28.01
    '28'
>>> "%f" % 28
    '28.000000'
>>> "%x" % 28
    '1c'

La liste des marqueurs est disponible sur cette page de la doc.

En plus des marqueurs qui permettent de savoir où insérer la valeur et quel format lui donner, on peut aussi donner des précisions sur l’opération de formatage. On peut ainsi décider combien de chiffres après la virgule on souhaite, ou obliger la valeur à avoir une certaine taille :

>>> "%4d" % 28 # au moins 4 caractères
    '  28'
>>> "%04d" % 28 # au moins 4 chiffres
    '0028'
>>> "%.2f" % 28 # 2 chiffres après la virgule
    '28.00'

Ainsi notre exemple:

>>> total = round(prix + exo_taxe, 2)
>>> "Les %s coûtent %s€" % (produit, total)
    'Les nipple clamps coûtent 13.01€'

peut maintenant être réduit à :

>>> total = prix + exo_taxe
>>> "Les %s coûtent %.2f€" % (produit, total)
    'Les nipple clamps coûtent 13.01€'

Néanmoins un des défauts de % est qu’il n’accepte qu’un tuple, ou une valeur seule. Impossible de passer un itérable arbitraire :

>>> "Les %s coûtent %.2f€" % [produit, total]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-bb9b7acda392> in <module>()
----> 1 "Les %s coûtent %.2f€" % [produit, total]
 
TypeError: not enough arguments for format string

Et si vous voulez formater un tuple, il faut le mettre dans un tuple d’un seul élément, source de plantage :

>>> "Les données sont %s" % data
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-4e21e97b8a3f> in <module>()
----> 1 "Les données sont %s" % data
 
TypeError: not all arguments converted during string formatting
 
>>> "Les données sont %s" % (data, )
    "Les données sont ('nipple clamps', 13.01)"

Tout codeur Python s’est retrouvé un jour devant ce cas et s’est gratté la tête.

Par ailleurs, dès qu’il y a beaucoup de trous à combler dans le texte, ça devient vite difficile de savoir ce qui va où :

 "[%s] %s%s %s(%s) - %s%s%s"  % (
        datetime.datetime.now(),
        res,
        unit,
        type,
        variant,
        testers[0],
        testers[1],
        testers[2]
  )

Pour pallier ce problème, % peut accepter aussi un dictionnaire et avoir des trous nommés :

 "[%(date)s] %(value)s%(unit)s %(type)s(%(variant)s) - %(tester1)s%(tester2)s%(tester3)s"  % {
        "date": datetime.datetime.now(),
        "value": res,
        "unit": "m",
        "type": "3",
        "variant": "beta",
        "tester1": testers[0],
        "tester2": testers[1],
        "tester3": testers[2]
  }

On peut voir néanmoins que le pari n’est pas tout à fait gagné. Et on ne gagne pas tant que ça en lisibilité. Pour cette raison, les formatages complexes sont plus intéressants à faire avec format() que nous verrons plus loin.

Rappelez-vous néanmoins que depuis Python 3, format() ne fonctionne plus sur les bytes. % reste donc la seule option pour formater des paquets réseaux, des headers de jpeg et tout autre format binaire.

Formater les dates

Même si il est toujours recommandé d’utiliser une bonne lib pour manipuler les dates, Python permet déjà de faire pas mal de choses avec la lib standard.

En effet, certaines notions, comme le temps, ont une forme très différente entre celle utilisée pour les manipuler, et celles utilisées pour les représenter.

Pour cette raison, l’objet date de Python propose deux méthodes, strptime et strftime, pour gérer le format des dates.

La procédure pour gérer les dates se fait donc toujours en 3 parties, un peu comme l’encoding d’un texte :

  1. Créer une nouvelle date, soit à la main, soit à partir de données existantes.
  2. Manipuler les dates pour obtenir ce qu’on souhaite (un autre date, un durée, un intervalle, etc.
  3. Formater le résultat pour le présenter à l’utilisateur ou le sauvegarder à nouveau.

Pour récupérer une date existante, on va utiliser strptime (“str” pour string, “p” pour parse) :

>>> from datetime import datetime
>>> date = datetime.strptime("1/4/2017", "%d/%m/%Y")
>>> date.year
    2017
>>> date.day
    1

Le deuxième paramètre contient le motif à extraire de la chaîne de gauche : c’est l’inverse d’un texte à trous ! On dit “dans la chaîne de gauche, j’ai le jour là, le mois là et l’année là, maintenant extrais les”.

Pour formater une date, c’est la même chose, mais dans l’autre sens, avec strftime (“f” pour format) :

>>> date = datetime.now()
>>> date.strftime('%m-%d-%y')
    '04-01-17'

Le mini-langage pour formater les dates est documenté ici, et vous pouvez en apprendre plus sur les dates sur une petite intro dédiée.

Avec format()

% a ses limites. C’est un opérateur pratique pour les petites chaînes et les cas de tous les jours, mais si on a beaucoup de valeurs à formater, cela peut devenir vite un problème. Il possède aussi quelques cas d’utilisation qui causent des erreurs inattendues puisqu’il n’accepte que les tuples.

format() a été créé pour remédier à cela. Dans sa forme la plus simple, il s’utilise presque comme %, mais les marqueurs sont des {} et non des %s :

>>> "Les {} coûtent {}€".format(produit, prix)
    'Les nipple clamps coûtent 13€'

Mais déjà, format() se distingue du lot car il permet de choisir l’ordre d’insertion :

>>> "Les {1} coûtent {0}€".format(prix, produit)
    'Les nipple clamps coûtent 13€'

La méthode accepte également n’importe quel itérable grâce à l’unpacking :

>>> "Les {} coûtent {}€".format(*[produit, prix])
    'Les nipple clamps coûtent 13€'

Formater un tuple seul est aussi très simple :

>>> "Les données sont {}".format(data)
    "Les données sont ('nipple clamps', 13.01)"

Mais là où format() est bien plus pratique, c’est quand on a beaucoup de données et qu’on veut nommer ses trous :

 "[{date}] {value}{unit} {type}({variant}) - {testers[0]}{testers[1]}{testers[2]}".format(
        date=datetime.datetime.now(),
        value=res,
        unit="m",
        type="3",
        variant="beta",
        testers=testers
  )

Le texte à trous est plus clair, et on peut utiliser l’index d’une liste directement dedans.

Un autre avantage non négligeable, est que format() n’utilise pas de nombreux marqueurs différents comme %f, %d, %s

A la place, il n’y a que {}, et format() appelle en fait pour chaque valeur sa méthode … __format__, ou en l’absence de celle-ci appelle str().

>>> a = 42
>>> a.__format__("")
    '42'

Chaque objet peut définir __format__, et accepter ses propres options:

>>> a.__format__(".2f")
    '42.00'
>>> from datetime import datetime
>>> datetime.now().__format__('%d %h')  # pas besoin de strftime !
    '20 Sep'

Et format() utilise tout ce qui est après : dans un trou pour le passer à __format__:

>>> "{foo:.2f} {bar:%d %h}".format(foo=42, bar=datetime.now())

Cela permet des formatages très poussés.

Les f-strings

Les f-strings sont une nouvelle fonctionnalité de Python 3.6, et elles sont merveilleuses, combinant les avantages de .format() et %, sans les inconvénients :

>>> produit = "nipple clamps"
>>> prix = 13
>>> print(f"Les {produit} coûtent {prix:.2f} euros")
Les nipple clamps coûtent 13.00 euros

En gros, c’est la syntaxe de format(), mais sans sa verbosité.

En prime, on peut utiliser des expressions arbitraires dedans:

>>> print(f"Les {produit.upper()} coûtent {prix:.2f} euros")
Les NIPPLE CLAMPS coûtent 13.00 euros

A première vue, ça ressemble à du exec, et donc à un parsing lent, doublé d’une une grosse faille de sécurité.

Et bien non !

C’est en fait du sucre syntaxique, et au parsing du code, Python va transformer l’expression en un truc du genre:

"Les " + "{}".format(produit.upper()) + " coûtent " + "{:.2f}".format(prix) + " euros"

Mais en bytecode. Pas d’injection de code Python possible, et en prime, les f-strings sont aujourd’hui la méthode de formatage la plus performante.

En clair, si vous êtes en 3.6+, vous pouvez oublier toutes les autres.

Méthodes de l’objet str

Parfois, on ne veut pas remplir un texte à trous. Parfois on a déjà le texte et on veut le transformer. Pour cela, l’objet str possède de nombreuses méthodes qui permettent de créer une nouvelle chaîne, qui possède des traits différents :

>>> "    strip() retire les caractères en bouts de chaîne   ".strip() # espace par défaut
    'strip() retire les caractères en bouts de chaîne'
>>> "##strip() retire les caractères en bouts de chaîne##".strip("#")
    'strip() retire les caractères en bouts de chaîne'
>>> "##strip() retire les caractères en bouts de chaîne##".lstrip("#")
    'strip() retire les caractères en bouts de chaîne##'
>>> "##strip() retire les caractères en bouts de chaîne##".rstrip("#")
    '##strip() retire les caractères en bouts de chaîne'
>>> "wololo".replace('o', 'i') # remplacer des lettres
    'wilili'
>>> "WOLOLO".lower() # changer la casse
    'wololo'
>>> "wololo".upper()
    'WOLOLO'
>>> "wololo".title()
    'Wololo'

Notez bien que ces méthodes créent de nouvelles chaînes. L’objet initial n’est pas modifié, puisque les strings sont immutables en Python.

Parmi les plus intéressantes, il y a split() et join(), qui ont une caractéristique particulière : elles ne transforment pas une chaîne en une autre.

split() prend une chaîne, et retourne… une liste !

>>> "split() découpe une chaîne en petits bouts".split() # défaut sur espaces
    ['split()', 'découpe', 'une', 'chaîne', 'en', 'petits', 'bouts']
>>> "split() découpe une chaîne en petits bouts".split("e")
    ['split() découp', ' un', ' chaîn', ' ', 'n p', 'tits bouts']

join() fait l’inverse, et prend un itérable (comme une liste), pour retourner… une chaîne :)

>>> "-".join(['join()', 'recolle', 'une', 'chaîne', 'DEPUIS', 'des', 'petits', 'bouts'])
    'join()-recolle-une-chaîne-DEPUIS-des-petits-bouts'

Avec juste ces méthodes, on peut s’autoriser pas mal de fantaisies avec le texte, et le nombre de méthodes disponibles est assez large :

>>> dir(str)
    [...
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

Donc référez-vous à la doc.

Caractères spéciaux

On vous a menti !

Quand on écrit "" en Python on ne crée pas une chaîne. En fait, on écrit une instruction qui dit à Python comment créer une chaîne.

La différence est subtile, mais importante. "" n’est PAS la chaîne, "" est une instruction, une indication pour Python de comment il doit procéder pour créer une chaîne.

Et on peut donner des instructions plus précises à Python. Par exemple on peut dire, “insère moi ici un saut de ligne”. Cela se fait avec le marqueur "\n".

>>> print('un saut\n de ligne')
un saut
 de ligne

\n n’est PAS un saut de ligne. C’est juste une indication donnée à Python pour lui dire qu’ici, il doit insérer un saut de ligne quand il créera la chaîne en mémoire.

Il existe plusieurs marqueurs de ce genre, les plus importants étant \n (saut de ligne) et \t (tabulation).

Pour rentrer les caratères \t\n, il faut donc dire à Python explicitement qu’on ne veut pas qu’il insère un saut de ligne ou une tabulation, mais plutôt ces caractères.

Cela peut se faire, soit avec le caractère d’échappement \ :

>>> print('pas un saut\\n de ligne')
pas un saut\n de ligne

Soit en désactivant cette fonctionalité avec le préfixe r, pour raw string:

>>> print(r'pas un saut\n de ligne')
pas un saut\n de ligne

Cette fonctionalité est très utilisée pour les noms de fichiers Windows et les expressions rationnelles car ils contiennent souvent \t\n.

Bytes, strings et encoding

Python fait une distinction très forte entre les octets (type bytes) et le texte (type str). La raison est qu’il n’existe pas de texte brut dans la vraie vie, et que tout ce que vous lisez : fichiers, base de données, socket réseau, et même votre code source (!) est un flux d’octets encodés dans un certain ordre pour représenter du texte.

En Python, on a donc le type str pour représenter du texte, une forme d’abstraction de toute forme d’encodage qui permet de manipuler ses données textuelles sans se soucier de comment il est représenté en mémoire.

En revanche, quand on importe du texte (lire, télécharger, parser, etc) ou qu’on exporte du texte (écrire, afficher, uploader, etc), il faut explicitement convertir son texte vers le type bytes, qui lui a un encoding en particulier.

Ce principe mérite un article à lui tout seul, et je vous renvoie donc à la page dédiée du blog.

Templating

Parfois on a beaucoup de texte à gérer. Par exemple, si vous faites un site Web, vous aurez beaucoup de HTML. Dans ce cas, faire tout le formatage dans son fichier Python n’est pas du tout pragmatique.

Pour cet usage particulier, on utilise ce qu’on appelle un moteur de template, c’est à dire une bibliothèque qui va vous permettre de mettre votre texte à trous dans un fichier à part. Les moteurs de templates sophistiqués vous permettent de faire quelques opérations logiques comme des boucles ou des conditions dans votre texte.

La première chose à savoir, c’est de ne PAS utiliser string.Template. Cette classe ne permet d’utiliser aucune logique, et n’a aucun avantage par rapport à .format().

Pour le templating, il vaut mieux se pencher vers une lib tièrce partie. Les deux principaux concurrents sont Jinja2, le moteur de templating le plus populaire en Python, créé par l’auteur de Flask. Et le moteur de Django, fourni par défaut par le framework.

Depuis Django 1.10, le framework supporte aussi jinja2, donc je vais vous donner un exemple avec ce dernier. Sachez qu’il existe bien d’autres moteurs (mako, cheetah, templite, TAL…) mais jinja2 a plus ou moins gagné la guerre.

Un coup de pip :

pip install jinja2

On fait son template dans un fichier à part, par exemple wololo.txt:

Regardez je sais compter :
  {% for number in numbers %}
    - {{number}}
  {% endfor %}

Puis en Python:

import jinja2
 
# On définit où trouver les fichiers de template. Ex. le dossier courant:
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('.'))
 
# On dit à jinja de charger le template à partir de son chemin relatif
template = jinja_env.get_template('wololo.txt')
 
# On crée un contexte, c'est à dire une collection d'objects qu'on veut rendre
# accessibles dans le template. Généralement, c'est un dictionnaire dont les
# clés sont les noms des variables telles qu'elle apparaîtront dans le template
# et les valeurs ce que contiendront ces variables.
ctx = {"numbers": [1, 2, 3]}
 
# On demande le "rendu" du template, c'est à dire le mélange du template
# et du contexte.
resultat = template.render(ctx)
 
print(resultat)
 
# Ce qui donne :
# - 1
# - 2
# - 3

i18n et l10n

L’i18n, pour ‘internationalisation’ (soit 18 lettres entre le i et le n) est le fait d’organiser votre code de telle sorte que son interface puisse s’adapter à plusieurs cultures. La l10n, pour ‘localisation’ (soit 10 lettres entre le l et le n), est le fait de fournir avec son code les données nécessaires pour une culture en particulier.

Par exemple, marquer toutes vos chaînes de caractères comme étant traductibles et fournir un mécanisme de substitution de la chaîne est de l’i18n. Fournir un fichier de traduction pour l’espagnol pour ces chaînes est de la l10n.

La combinaison des deux est parfois nommée g11n pour “globalization”.

La g11n peut inclure:

  • La gestion de l’UI (traduction, sens de la lecture, formatage des nombres, devise…).
  • La gestion des dates (formatage, différences de types de calendriers, événements locaux, ordres des jours…).
  • La gestion du temps (zones horaires, heure d’été…).
  • La gestion de la géolocation (fournir des informations autour de soi, filtrer par la distance…).
  • La gestion politique et culturelle (symbolisme des couleurs, adaptation du contenu aux moeurs…).
  • La gestion légale (services et contenus selon la loi en vigueur, warnings obligatoires…).

Plus qu’un article, c’est un dossier qu’il faudrait faire sur ces sujets car c’est très, très vaste.

La traduction de texte peut être faite directement avec le module gettext fourni en Python. Certains formatages de nombres et de dates sont aussi faisables avec la stdlib grâce au module locale.

Néanmoins dès que vous voulez faire quelque chose de plus gros avec la g11n, je vous invite à vous tourner vers des libs externes.

Babel est la référence en Python pour le formatage du texte et des nombres, et il existe des extensions pour les moteurs de template les plus populaires. La lib inclut une base de données aussi à jour que possible sur les devises, les noms de pays, les langues…

pendulum est idéal pour la manipulation des dates en général, et des fuseaux horaires en particulier, y compris pour le formatage. Et ça évite de manipuler pytz à la main.

Et pour le reste… bonne chance !

Programmation orientée objet

Souvenez-vous, Python a des méthodes magiques. 3 sont dédiées au formatage.

__repr__ est utilisée quand on appelle repr() sur un objet. Typiquement, c’est ce qui s’affiche dans le shell si on utilise pas print(). C’est aussi ce qui détermine la représentation d’un objet quand on affiche une collection qui le contient.

__str__ est utilisée quand on appelle str() sur un objet. Quand on fait print() dessus par exemple. Si __str__ n’existe pas, __repr__ est appelée.

__format__ est utilisée quand on passe cet objet à format(), ou que cet objet est utilisé dans une f-string.

Ex :

class Foo:
    def __repr__(self):
        return "<Everybody's kung foo fighting>"
    def __str__(self):
        return "C'est l'histoire d'un foo qui rentre dans un bar"
    def __format__(self, age):
        if int(age or 0) > 18:
            return "On s'en bat les couilles avec une tarte tatin. Tiède."
        return "On s'en foo"

Ce qui donne :

>>> Foo()
<Everybody's kung foo fighting>
>>> print(Foo())
C'est l'histoire d'un foo qui rentre dans un bar
>>> print([Foo(), Foo()])
[<Everybody's kung foo fighting>, <Everybody's kung foo fighting>]
>>> "J'ai envie de dire: {}".format(Foo())
"J'ai envie de dire: On s'en foo"
>>> f"J'ai envie de dire: {Foo():19}"
"J'ai envie de dire: On s'en bat les couilles avec une tarte tatin. Tiède."

Astuce de dernière minute

Enfin pour conclure cet article dont la longueur n’a d’égale que celle de la période entre deux publications sur le blog, une petite remarque.

S’il est certes courant de formater une string, il est aussi possible de déformer un string. Ce sont des pièces plus résistantes qu’il n’y parait, et en cas d’empressement, le retrait total n’est pas nécessaire :

String en levrette

Ne pas porter de strings du tout évite aussi tout une classe de bugs

Assurez-vous juste que la partie ficelle soit suffisament éloignée pour éviter les frictions fort désagréables quand on entame un algo avec une grosse boucle.

Sinon, moins intéressant, mais toujours utile, les strings en Python peuvent êtres écrites sur plusieurs lignes de plusieurs manières:

>>> s = ("Ceci est une chaine qui n'a pas " 
...      "de saut de ligne mais qui est "
...      "écrite sur plusieurs lignes")
>>> print(s)
Ceci est une chaine qui n'a pas de saut de ligne mais qui est écrite sur plusieurs lignes

Cela fonctionne car deux chaînes littérales côte à côte en Python sont automatiquement concaténées au démarrage du programme. Cela évite les + \ à chaque fin de ligne, pourvu qu’on ait des parenthèses de chaque côté de la chaîne.

L’alternative des triples quotes est assez connue:

>>> s = """
...    Ceci est une chaine avec des sauts de lignes
...    écrite sur plusieurs lignes.   
... """
>>> print(s)
 
    Ceci est une chaine avec des sauts de lignes
    écrite sur plusieurs lignes.

Pour éviter l’indentation et les espaces inutiles:

>>> from textwrap import dedent
>>> print(dedent(s).strip())
Ceci est une chaine avec des sauts de lignes
écrite sur plusieurs lignes.

Perso j’ai un wrapper pour ça.

]]>
Un tools d’itertour, ou l’inverse 21http://sametmax.com/?p=20922http://streisand.me.thican.net/sametmax.com/index.php?20161029_222622_Un_tools_d___itertour__ou_l___inverse_21Sat, 29 Oct 2016 20:26:22 +0000itertools. ]]>Ah, ça faisait longtemps que je ne vous avais pas fait un petit article bien long. Ca manquait d’opportunité de partager de la musique. Allez hop !

Python est un langage avec un “for” penchant pour l’itération si je puis dire. Et ce n’est que justice qu’il ait donc un module dédié pour les opérations d’itération : itertools.

itertools est un peu impénétrable pour quelqu’un qui n’a pas l’habitude, alors je vais vous faire un petit tour du propriétaire.

Principes de base

Itertools a pour but de manipuler les itérables, et si vous voulez en savoir plus sur le concept d’itérabilité, je vous renvoie à l’article sur les trucmuchables. Pour faire court, les itérables sont tout ce sur quoi on peut appliquer une boucle for.

Le problème, c’est que tous les itérables ne se comportent pas de la même façon. En effet, certains ont une taille définie:

>>> len("abcd")
4
>>> len(range(3))
3

D’autres non:

>>> len(open('/etc/fstab'))
Traceback (most recent call last):
  File "<ipython-input-7-baed595d06b3>", line 1, in <module>
    len(open('/etc/fstab'))
TypeError: object of type '_io.TextIOWrapper' has no len()

Certains, se terminent:

>>> a = list(open('/etc/fstab'))
>>>

D’autres sont infinis:

>>> def infinite_generator():
...     while True:
...         yield 1
...
>>> list(infinite_generator) # bloque la VM jusqu'à crasher sur un MemoryError

Du coup, si vous voulez faire des opérations qui marchent sur tous les itérables, il faut écrire des algos spéciaux dit “paresseux” (en anglais, “lazy”), c’est à dire qui font le travail minimum. Ils se lancent à la dernière minute et ne lisent rien de plus que nécessaire.

Et c’est ce que contient itertools. Les fonctions de ce module marchent sur les fichiers, les générateurs, les sockets réseaux comme les listes, les tuples et les chaînes de caractères.

chain

Combine plusieurs itérables en un seul:

>>> from itertools import chain
>>> generator = chain("abc", range(3), [True, False, None])
>>> generator
<itertools.chain object at 0x7f2ea903d550>
>>> list(generator)
['a', 'b', 'c', 0, 1, 2, True, False, None]

Très utile pour boucler sur des itérables hétérogènes comme si c’était une seule et même structure. Chaîner des fichiers, des générateurs, etc.

islice

Comme la syntaxe de slicing [::]:

>>> carres_de_nombres_pairs = [x * x  for x in range(100) if x % 2 == 0]
>>> carres_de_nombres_pairs[3:11]
[36, 64, 100, 144, 196, 256, 324, 400]

Mais marche sur tous les itérables, même sur les générateurs:

>>> carres_de_nombres_pairs = (x * x  for x in range(100) if x % 2 == 0)
>>> carres_de_nombres_pairs
<generator object <genexpr> at 0x7f2eab171a98>
>>> from itertools import islice
>>> generator = islice(carres_de_nombres_pairs, 3, 11)
>>> generator
<itertools.islice object at 0x7f2eab1a26d8>
>>> list(generator)
[36, 64, 100, 144, 196, 256, 324, 400]

cycle

Boucler de manière infinie sur un itérable. Quand on arrive à la fin, on revient au début.

>>> generator = cycle('abc')
>>> generator
<itertools.cycle object at 0x7f2eab59a588>
>>> next(generator)
'a'
>>> next(generator)
'b'
>>> next(generator)
'c'
>>> next(generator)
'a'

C’est très pratique, mais assez dangereux donc faites des tests avant de l’utiliser. En effet si l’itérable génère les données à la volée, elles sont stockées dans un buffer, donc ça charge tout en mémoire. De plus, c’est une boucle infinie. Ne castez pas ça avec un tuple ou une liste :)

repeat

Prend n’importe quel élément, et le retourne encore et encore.

>>> generator = repeat("plop") # répète infiniment
>>> next(generator)
'plop'
>>> next(generator)
'plop'
>>> next(generator)
'plop'
>>> generator = repeat("plop", 2) # répète 2 fois
>>> for x in generator:
...     print(x)
...
plop
plop

On ne peut pas passer un callable, mais iter() le fait déjà. La signature normale de cette dernière prend un iterable, mais si on lui passe deux paramètres, un callable et un sentinel, on obtient un générateur qui va appeler le callable jusqu’à ce qu’il retourne une valeur égale au sentinel.

Exemple, vous voulez lire un fichier 50 caractères à la fois. On passe une fonction qui retourne les 50 caractères suivants (le callable) et une chaîne de caractères vide (le sentinel):

>>> f = open('/etc/fstab')
>>>
>>> def read_50_chars():
...     return f.read(50)
...
>>> generator = iter(read_50_chars, '') # lit 50 caractères à la fois jusqu'à tomber sur ''
>>> generator
<callable_iterator object at 0x7f2eab5abef0>
>>> next(generator)
'# /etc/fstab: static file system information.\n#\n# '
>>> next(generator)
"Use 'blkid' to print the universally unique identi"
>>> next(generator)
'fier for a\n# device; this may be used with UUID= a'

iter() est un built-in, pas dans itertools, mais ça aurait été con de pas en parler.

zip_longest

Comme zip(), mais au lieu de s’arrêter quand le premier itérable est fini:

>>> list(zip('abc', range(5)))
[('a', 0), ('b', 1), ('c', 2)]

On remplit les valeurs manquantes:

>>> list(zip_longest('abc', range(5)))
[('a', 0), ('b', 1), ('c', 2), (None, 3), (None, 4)]
>>> list(zip_longest('abc', range(5), fillvalue="Tada !"))
[('a', 0), ('b', 1), ('c', 2), ('Tada !', 3), ('Tada !', 4)]

starmap

Comme map(), mais applique l’opérateur splat sur les arguments avant.

Si map() ressemble (en mieux) à:

def map_en_moins_bien(callable, iterable):
    for x in iterable:
       yield callable(x)

starmap() fait:

def map_en_moins_bien(callable, iterable):
    for x in iterable:
       yield callable(*x)

Exemple:

>>> nombres = [(1, 2), (3, 4), (5, 6)]
>>> def ajouter(a, b):
...     return a + b
...
>>> from itertools import starmap
>>> list(starmap(ajouter, nombres))
[3, 7, 11]

Ces fonctions sont moins utilisées à cause des listes en intension. Mais il y a des trucs rigolos comme:

>>> list(starmap(print, zip(range(3), map(int, "123"))))
0 1
1 2
2 3
[None, None, None]

Ouais, vous allez pas utiliser ça tous les jours :)

filterfalse

Le contraire de filter(), garde les résultats négatifs au lieu des résultats positifs:

>>> list(filter(est_pair, range(10)))
[0, 2, 4, 6, 8]
>>> from itertools import filterfalse
>>> list(filterfalse(est_pair, range(10)))
[1, 3, 5, 7, 9]

Comme précédemment, aujourd’hui on utilise peu ces fonctions car on a les listes en intension. Mais certaines astuces sont sympas comme:

>>> list(filter(bool, [True, False, 1, 0, 'foo', '']))
[True, 1, 'foo']
>>> list(filterfalse(bool, [True, False, 1, 0, 'foo', '']))
[False, 0, '']

groupby

Ah, groupby(), la fonction la moins facile à comprendre. D’abord, personne ne lit la doc, et la doc dit clairement qu’il faut trier les éléments de l’itérable AVANT d’appliquer groupby, sinon les résultats sont incohérents.

Ensuite, groupby() peut prendre un callback pour un usage avancé, et toutes les fonctions qui prennent un callback sont plus dures à comprendre. Mais pour que ça ait du sens, il faut que le tri préalable s’applique sur le même callback. Personne ne pige jamais ça.

Enfin, groupby() ne renvoie pas les éléments directement, mais des objets “groupers”, qui sont eux mêmes des générateurs. Tout est bien fait pour embrouiller le chaland, surtout pour une fonction qu’on utilise pas souvent.

Pour comprendre groupby(), le mieux est de commencer par l’entrée et la sortie. Vous avez un truc comme ça:

>>> noms = ['Zebulon', 'Léo', 'Alice', 'Bob', 'Anaïs', 'Adam', 'Bernardo', 'Io']

Et vous voulez les grouper par un critère, un peu comme un GROUP BY en SQL. Ca peut être n’importe quoi:

  • La valeur de la première lettre.
  • La position de la première voyelle.
  • Deux catégories: ceux qui contiennent des caractères non ASCII et les autres.

Prenons un exemple simple, et groupons les par le nombre de lettres qu’ils contiennent. Ca donnerait quelque chose comme ça:

[(2, ('Io',)),
 (3, ('Léo', 'Bob')),
 (4, ('Adam',)),
 (5, ('Alice', 'Anaïs')),
 (7, ('Zebulon',)),
 (8, ('Bernardo',))]

Pour obtenir ce résultat avec groupby(), il faut d’abord trier l’itérable (ici notre liste) par nombre de lettres:

>>> noms.sort(key=len)
>>> noms
['Io', 'Léo', 'Bob', 'Adam', 'Alice', 'Anaïs', 'Zebulon', 'Bernardo']

Si vous ne vous souvenez plus comment fonctionne le tri en Python, particulièrement le paramètre key, allez vite lire l’article dédié du lien précédent. En effet le mécanisme est le même pour groupby() donc il faut comprendre ce fonctionnement de toute manière.

Ensuite on applique groupby(), avec la même fonction key:

>>> noms = groupby(noms, key=len)
>>> noms
<itertools.groupby object at 0x7f19c4461778>

A ce stade, on a un générateur qu’on peut parcourir, mais il ne contient pas le résultat tel que je vous l’ai montré. A la place, il yield des paires (group, grouper):

>>> next(noms)
(2, <itertools._grouper object at 0x7f19c48c0080>)

Le premier élément, le groupe, est ce que votre fonction key retourne, c’est à dire ce sur quoi vous vouliez grouper. Ici la taille du mot.

En second, un objet grouper, qui est un générateur yieldant les mots qui correspondent à ce groupe.

Donc pour vraiment voir le contenu de tout ça, il faut se taper une double boucle imbriquée:

>>> from itertools import groupby
>>> for groupe, groupeur in groupby(noms, key=len):
...     print(groupe, ":")
...     for mot in groupeur:
...         print('-', mot)
2 :
- Io
3 :
- Léo
- Bob
4 :
- Adam
5 :
- Alice
- Anaïs
7 :
- Zebulon
8 :
- Bernardo

La raison de ce manque d’intuitivité, c’est que théoriquement groupby() peut travailler de manière paresseuse et donc les groupeurs sont des générateurs qui traitent l’arrivée des données au fur et à mesure. Mais c’est rare d’avoir des données lazy qui arrivent déjà pré-ordonnées. Ensuite ça suppose que vous voulez lire ces données au fur et à mesure, mais généralement le jeu de données groupées n’est pas suffisamment grand pour que ça soit un vrai problème et on caste directement le groupe en tuple ou en liste.

Dans la pratique donc, groupby() fait parti de ces fonctions un peu overkill, car un usage typique ne bénéficiera pas du côté paresseux. En tout cas ça ne m’est jamais arrivé.

dropwhile

dropwhile() prend un callable et un itérable. Le callable est appelé sur chaque élément, et tant qu’il retourne quelque chose de vrai, l’élément est ignoré. C’est un peu tordu car ça demande d’écrire sa fonction de filtre à l’inverse de ce qu’on veut.

Par exemple, si vous voulez commencer à lire un fichier à partir de la première ligne qui commence par un commentaire, il faut que votre fonction retourne True… si la ligne ligne ne commence PAS par un commentaire.

>>> def ne_commence_pas_par_un_commentaire(element):
...     return not element.startswith('#')
...
>>> generator = dropwhile(ne_commence_pas_par_un_commentaire, open('/etc/fstab'))
>>> generator
<itertools.dropwhile object at 0x7f19c450d688>
>>> next(generator)
'# /etc/fstab: static file system information.\n'

dropwhile veut dire “ignorer tant que”. Donc ici on ignore tant que la ligne ne commence pas par un commentaire.

takewhile

takewhile() est le contraire de dropwhile(), on prend les éléments tant qu’une condition est vraie, et on s’arrête dès que la condition n’est plus vraie. On les utilise souvent en combinaison d’ailleurs, pour slicer les itérables en utilisant des conditions plutôt que des indexes.

Comme pour dropwhile, il faut penser à l’envers. Vous voulez vous arrêter quand un nombre dépasse 10000 ? Alors il faut faire une fonction qui retourne True tant que le nombre ne dépasse PAS 10000. Jusqu’ici j’ai utilisé des fonctions traditionnelles, mais bien entendu, une lambda marche aussi:

>>> nombres = (x * x for x in range(1000000000000))
>>> from itertools import takewhile
>>> generator = takewhile(lambda x: x < 10000, nombres)
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
4
>>> list(generator)[-1]
9801

count

count() est une relique du passé, qui fait ce que fait range() maintenant, mais en moins bien. Vous pouvez l’ignorer.

tee

tee() est une fonction très intéressante qui duplique votre itérable en autant de clones que vous le souhaitez. Pour ce faire, tee() garde en mémoire les derniers éléments générés, ce qui fait que si vous lisez les clones les uns après les autres, ça n’a aucun intérêt. Autant caster en liste et lire la liste plusieurs fois.

Non, l’interêt de tee() est si vous lisez les clones en parallèle. Car là, tee() ne garde en mémoire que les éléments qui n’ont pas été lus par le dernier clone utilisé.

Par exemple, ceci est inutile:

>>> from itertools import tee
>>> clone1, clone2, clone3 = tee((x for x in range(100)), 3)
>>> list(clone1)[:3]
[0, 1, 2]
>>> list(clone2)[:3]
[0, 1, 2]

Par contre ceci économise pas mal de mémoire:

>>> clone1, clone2, clone3 = tee((x for x in range(100)), 3)
>>> next(clone1)
0
>>> next(clone2)
0
>>> next(clone3)
0
>>> next(clone1)
1
>>> next(clone2)
1
>>> next(clone3) # la valeur 0 n'est plus en mémoire après ça
1

compress

compress() est une curiosité qui va surtout parler aux data-scientists et matheux, et d’une manière générale aux adeptes de numpy.

Cette fonction prend deux itérables, un avec des valeurs à filtrer, et un avec des valeurs qui indiquent s’il faut filtrer l’élément ou non:

>>> list(compress(['toto', 'tata', 'titi'], [True, False, True]))
['toto', 'titi']

Je n’ai pas trouvé de use case pour cette fonction dans ma vie de tous les jours. Si quelqu’un est inspiré en commentaire, je mettrai l’article à jour.

accumulate

accumulate() est un pur produit de la programmation fonctionnelle. C’est un peu comme un map(), mais au lieu de passer chaque élément au callable, on lui passe l’élément, et le résultat du dernier appel.

Par exemple, vous voulez multiplier tout une liste : [1, 2, 3, 4]

Vous allez faire:

(((1 * 2) * 3) * 4)

C’est typiquement pour ce genre de chose qu’on utiliserait la fonction reduce():

>>> reduce(lambda x, y: x * y, [1, 2, 3, 4])
24

accumulate() fait cela, mais en plus vous yield tous les résultats intermédiaires:

>>> list(accumulate([1, 2, 3, 4], lambda x, y: x * y))
[1, 2, 6, 24]

La signature est inversée par rapport à reduce(), c’est ballot. Dans d’autres langages vous trouverez ce genre de fonction sous le nom fold() ou scan(), mais c’est le même principe.

product

product() prend des itérables, et vous balance toutes les combinaisons des éléments de ces itérables. On s’en sert surtout pour éviter les boucles for imbriquées. Par exemple au lieu de faire:

>>> lettres = "abcd"
>>> chiffres = range(3)
>>> for lettre in lettres:
...     for chiffre in chiffres:
...         print(lettre, chiffre)
...
a 0
a 1
a 2
b 0
b 1
b 2
c 0
c 1
c 2
d 0
d 1
d 2

On va faire:

>>> from itertools import product
>>> for lettre, chiffre in product(lettres, chiffres):
...     print(lettre, chiffre)
...
a 0
a 1
a 2
b 0
b 1
b 2
c 0
c 1
c 2
d 0
d 1
d 2

combinations

Génère tous les combinaisons possibles des éléments d’un itérable:

>>> from itertools import combinations
>>> list(combinations('abcd', 2))
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')]
>>> list(combinations('abcd', 3))
[('a', 'b', 'c'), ('a', 'b', 'd'), ('a', 'c', 'd'), ('b', 'c', 'd')]

combinations_with_replacement

Pareil, mais autorise les doublons:

>>> from itertools import combinations_with_replacement
>>> list(combinations_with_replacement('abcd', 3))
[('a', 'a', 'a'), ('a', 'a', 'b'), ('a', 'a', 'c'), ('a', 'a', 'd'), ('a', 'b', 'b'), ('a', 'b', 'c'), ('a', 'b', 'd'), ('a', 'c', 'c'), ('a', 'c', 'd'), ('a', 'd', 'd'), ('b', 'b', 'b'), ('b', 'b', 'c'), ('b', 'b', 'd'), ('b', 'c', 'c'), ('b', 'c', 'd'), ('b', 'd', 'd'), ('c', 'c', 'c'), ('c', 'c', 'd'), ('c', 'd', 'd'), ('d', 'd', 'd')]

permutations

Comme combinations, mais en générant aussi les mêmes combinaisons dans un ordre différent:

>>> list(permutations('abc', 3))
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]
]]>
Puis-je copier/coller mon brave ? 6http://sametmax.com/?p=20808http://streisand.me.thican.net/sametmax.com/index.php?20161023_094148_Puis-je_copier_coller_mon_brave___6Sun, 23 Oct 2016 07:41:48 +0000un snippet pour utiliser l'API du clipboard en JS. Et j'ai donc voulu savoir comment détecter que cette functionalité est implémentée par le navivateur en cours. ]]>Dans 0bin on utilise encore flash pour faire le copier/coller, et je pense qu’on va le virer. Plus de raison de supporter les navigateurs trop vieux et après tout c’est pas grave de se voir refuser un raccourci pour copier/coller : le reste est utilisable.

Pourquoi je vous dis ça ?

Et bien parce que sebsauvage a partagé un snippet pour utiliser l’API du clipboard en JS. Et j’ai donc voulu savoir comment détecter que cette functionalité est implémentée par le navigateur en cours.

Je suis ainsi allé voir les sources de modernizr, et il implémente en fait une combinaisons de 2 techniques.

D’abord, vérifier si l’objet window a un attribut ClipboardEvent. Si oui, c’est réglé. Si non, il crée un div, on check s’il a un attribut paste. Le reste sont les hacks de compatibilité avec les très vieux navs que je ne vais pas retranscrire ici.

Donc en gros, avant de faire un copier/coller en JS, vérifiez:

function implementClipboardAPI(){
   try {
     return (!!window.ClipboardEvent || 'onpaste' in document.createElement('div'));
   } catch(e) {
     return false;
   }
}
]]>
Script Python qui installe ses dépendances 25http://sametmax.com/?p=20305http://streisand.me.thican.net/sametmax.com/index.php?20160920_095016_Script_Python_qui_installe_ses_dependances_25Tue, 20 Sep 2016 07:50:16 +0000pip installer toutes les dépendances, ou faire un setup.py complet.]]>Quand on a un petit script, on ne veut pas forcément dire aux gens avec lesquels on le partage qu’il faut pip installer toutes les dépendances, ou faire un setup.py complet.

On peut évidemment se tourner vers des solutions de compilation type nuitka, mais si on veut être KISS, on peut juste utiliser pip depuis le code Python:

import pip, sys
 
# On essaye d'importer les dépendances, et si elles sont pas dispos on les installe 
# pour l'utilisateur courant.
def require(*packages):
    for package in packages:
        try:
            if not isinstance(package, str):
                import_name, install_name = package
            else:
                import_name = install_name = package
            __import__(import_name)
        except ImportError:
 
            cmd = ['install', install_name]
            if not hasattr(sys, 'real_prefix'):
                cmd.append('--user')
            pip.main(cmd)
 
require('arrow', 'requests', 'minibelt', ('path', 'path.py'))
 
# Et votre script peut joyeusement utiliser ses dépendances
import arrow
import requests
 
from path import Path
from minibelt import window
 
print(Path('.').realpath())
print(arrow.get(), len(list(window(requests.get('http://sametmax.com')))))

Bien entendu, ça marche que pour les dépendances en pur Python, mais ça fait déjà un paquet de trucs.

N’oubliez pas qu’on reste dans une solution quick and dirty, ne commencez pas à me mettre ça partout.

]]>
Mise à jour de l’article sur les middlewares Djangohttp://sametmax.com/?p=20288http://streisand.me.thican.net/sametmax.com/index.php?20160919_120438_Mise_a_jour_de_l___article_sur_les_middlewares_DjangoMon, 19 Sep 2016 10:04:38 +0000Ça faisait un bout de temps que je n’avais pas mis à jour un article en Python 3.

Là je fais un Pierre deux couilles puisque les middlewares ont changé avec Django 1.10 et ça me permet de rafraîchir cette notion assez floue du framework.

Allez hop, on va lire l’article.

]]>
C’est pas pour tout de suite 10http://sametmax.com/?p=20252http://streisand.me.thican.net/sametmax.com/index.php?20160916_094452_C___est_pas_pour_tout_de_suite_10Fri, 16 Sep 2016 07:44:52 +0000J’ai envie de vous écrire un article sur les types hints, mais le tooling est encore incomplet. On vient juste d’avoir le support de async / await et le typage structurel n’est pas d’actualité. Ennuyeux pour un langage qui aime autant les canards.

J’ai envie de vous écrire un article sur asyncio, mais je ne veux pas faire un article qui survole le sujet comme tout ce qu’il y a sur la toile. Je veux parler de cas concrets, et amener des solutions pour le debuggage, l’architecture, et du code pour les actions les plus communes. Et ça fait des mois que j’y travaille, mais comme vous pouvez le voir y a encore du taff.

J’ai envie de vous parler de la 3.6, mais tout est toujours en beta et la mailing list de Python est en ébullition, difficile de savoir comment ça va se finir, même avec un feature freeze pas loin. Et c’est pas faute de participer. je préfère attendre que ça se soit calmé.

Bref, y a des trucs sympas à écrire, mais des trucs pas prêts.

Du coup j’ai sous le coude des petits trucs comme un petit middleware django, le test coverage, etc.

Je dis ça parce que je me fais en ce moment tout le temps twitter pour que j’écrive sur ces sujets, mais c’est pas encore le bon moment.

]]>
Vérifier qu’un uuid est valide en Python 9http://sametmax.com/?p=20169http://streisand.me.thican.net/sametmax.com/index.php?20160906_132350_Verifier_qu___un_uuid_est_valide_en_Python_9Tue, 06 Sep 2016 11:23:50 +0000uuid, on veut vérifier qu'on récupère bien des uuid valides là où on les attend.]]>Si on teste du code qui utilise des uuid, on veut vérifier qu’on récupère bien des uuid valides là où on les attend.

Le plus simple est de déléguer cette vérification au module uuid qui est déjà celui qu’on utilise pour les produire.

from uuid import UUID
 
def is_uuid(uuid_string, version=4):
    try:
        # Si uuid_string est un code hex valide mais pas un uuid valid,
        # UUID() va quand même le convertir en uuid valide. Pour se prémunir
        # de ce problème, on check la version original (sans les tirets) avec
        # le code hex généré qui doivent être les mêmes.
        uid = UUID(uuid_string, version=version)
        return uid.hex == uuid_string.replace('-', '')
    except ValueError:
        return False

Et pouf:

>>> import uuid
 
>>> print(uuid.uuid4())
1049d03f-6f45-4637-89ff-f57567f6d022
 
>>> is_uuid('1049d03f-6f45-4637-89ff-f57567f6d022')
True
 
>>> is_uuid('1049d03f6f45463789fff57567f6d022')
True
 
>>> is_uuid('1049d03f6f45')
False

À chaque fois que j’utilise un uuid, j’ai un peu l’impression d’user la fabrique de l’univers, comme si je fabriquais quelque chose d’unique pour le mettre la poubelle, et qui n’existera plus jamais. Le gâchis d’un flocon de neige virtuel :)

]]>
C’est le futur ! 28http://sametmax.com/?p=20052http://streisand.me.thican.net/sametmax.com/index.php?20160901_182616_C___est_le_futur___28Thu, 01 Sep 2016 16:26:16 +0000It's the future. Mais j'ai bien conscience que plein de gens ne lisent pas l'anglais et ne peuvent profiter de cette perle. Comme on a ouvert le blog S&M en français car on voulait justement créé des bonnes ressources dans notre langue, je traduis de temps en temps ces trucs fantastiques, comme par exemple Le bonheur des frameworks. Donc voici la traduction de celui-ci. Faites vous plaiz' ]]>J’ai twitté il y a quelque temps l’article hilarant It’s the future. Mais j’ai bien conscience que plein de gens ne lisent pas l’anglais et ne peuvent profiter de cette perle. Comme on a ouvert le blog S&M en français, car on voulait justement créer des bonnes ressources dans notre langue, je traduis de temps en temps ces trucs fantastiques, comme par exemple Le bonheur des frameworks.

Donc voici la traduction de celui-ci. Faites-vous plaiz’

Hey, mon boss m’a dit de venir te voir. Il paraît que tu gères niveau web apps ?

– Ouais, je suis plus un spécialiste des systèmes distribués maintenant. Je reviens du ContainerCamp et de la Gluecon et je vais à la Dockercon la semaine prochaine. Je suis emballé par la direction que prend l’industrie – tout est plus simple et fiable. C’est le futur !

Cool. Je veux juste faire une simple Web app pour le moment – un CRUD normal avec Rails, que je vais déployer sur Heroku. C’est toujours d’actu ?

– Oh non. C’est old school. Heroku, c’est mort – plus personne ne l’utilise. Il te faut Docker maintenant. C’est le futur.

Oh, OK. Qu’est-ce que c’est ?

– Docker est la nouvelle façon de faire de la conteneurisation. Comme LXC, mais c’est aussi un format de package, une plateforme de distribution et des outils pour rendre la création de systèmes distribués très facile.

Conteneuri.. – Hein ? C’est quoi LXE ?

– LCX. C’est comme un chroot aux stéroïdes.

C’est quoi cheroot ?

– Ok, regarde. Docker. Conteneurisation. C’est le futur. C’est comme la virtualisation, mais plus rapide et moins coûteux.

Oh, comme Vagrant.

– No, Vagrant c’est mort. Tout va être conteneurisé maintenant, c’est le futur.

Ok, donc je n’ai pas besoin de savoir quoi que ce soit sur la virtualisation ?

– Non, tu as quand même besoin de la virtualisation, parce que les conteneurs ne fournissent pas toutes les couches de sécurité pour le moment. Donc ce que tu veux, c’est faire tout tourner dans un environnement multitiers de telle sorte qu’on ne puisse s’échapper de la sandbox.

Ok, je suis un peu perdu là. On rembobine. Donc il y a un truc comme la virtualisation qu’on appelle les conteneurs. Et je peux l’utiliser sur Heroku ?

– Et bien, Heroku a un support pour docker, mais je viens de le dire : Heroku c’est mort. Ce que tu veux c’est faire tourner tes conteneurs sur CoreOS.

Ok, c’est quoi ?

– C’est un OS hôte très cool qu’on peut utiliser avec Docker. Tu n’as même pas besoin de Docker, tu peux utiliser rkt.

Rocket ?

– No, rkt.

Ouais, Rocket.

– Non, ça s’appelle rkt maintenant. Rien à voir. C’est un format de conteneurisation alternatif qui n’est pas aussi intégré que Docker par défaut, et donc plus composable.

C’est une bonne chose.

– Bien entendu. La composabilité c’est le futur.

Ok, ça s’utilise comment ?

– Je ne sais pas. Je ne pense pas que qui que ce soit l’utilise.

Arf. Tu disais quoi à propos de CoreOS ?

– Ah oui, c’est un OS hôte pour Docker.

C’est quoi un OS hôte ?

– Un OS hôte fait tourner tous tes conteneurs.

Tourner mes conteneurs ?

– Ouais, tu dois avoir quelque chose pour faire tourner tes conteneurs. Donc tu te crées genre une instance EC2, tu mets CoreOS dessus, tu lances le daemon Docker, puis tu déploies tes images Docker dessus.

Quelle partie est le conteneur ?

– Tout. Écoute, tu prends ton app, écris un Dockerfile, tu en fais une image locale, puis tu la push sur n’importe quel hôte Docker.

Ah, comme Heroku ?

– Non, pas Heroku. Je te l’ai dit. Heroku c’est mort. Tu gères ton propre cloud en utilisant Docker.

Hein ?

– Ouais, c’est tout simple. Check #gifee.

Gify?

Google’s infrastructure for everyone else. Tu prends les outils que tu veux, avec des conteneurs, et tu as la même infra que Google.

Pourquoi pas juste utiliser celles de Google ?

Tu crois que ça sera encore là dans 6 mois ?

OK, mais est-ce que quelqu’un fait du hosting pour ça ? J’ai pas la foi d’héberger mon bordel moi-même.

– Et bien, Amazon a ECS, mais il faut écrire du XML et 2, 3 merdes.

Et OpenStack ?

– Beurk.

Beurk ?

– Beurk.

Écoute, j’ai vraiment pas envie d’héberger tout ça.

– Na, c’est super simple. Tu te setup juste un cluster Kubernetes.

J’ai besoin d’un cluster ?

– Un cluster Kubernetes. Ça gère le déploiement de tous tes services.

J’ai seulement un service.

– Qu’est-ce que tu racontes. Tu as une app non? donc tu as bien au moins 8-12 services ?

Quoi ? Non. Juste une app. Service, on s’en branle. Juste un truc.

– Non, jette un coup d’oeil à la notion de microservices. C’est le futur. C’est comme ça qu’on fait tout de nos jours. Tu prends ton app monolithique et tu la divises en quelque chose comme 12 services, un pour chaque tâche.

Ça semble excessif.

– C’est le seul moyen de s’assurer que c’est fiable. Ainsi si ton service authentification tombe…

Service authentification ? J’allais juste utiliser une gem que j’avais déjà mis en place plusieurs fois avant.

– Super. Utilise la gem. Mets la dans son propre projet. Fous une API RESTful par dessus. Et fait en sorte que les autres services utilisent cette API et gèrent gracieusement les échecs. Mets ça dans un conteneur et push le bordel en déploiement continu.

Ok, donc là j’ai une douzaine de services ingérables, et maintenant ?

– Ouais, d’où Kubernetes. Il orchestre tes services.

Orchestre ?

– Ouais, donc tu as ces services, ils doivent être fiables donc il t’en faut plusieurs copies. Donc Kubernetes s’assure que tu en as assez, et qu’ils sont distribués à travers de multiples hôtes dans ta flotte de serveurs, afin qu’ils soient toujours disponibles.

Ah parce qu’il me faut une flotte maintenant ?

– Ouais, pour la fiabilité. Mais Kubernetes la gère pour toi. Et tu sais que Kubernetes ça marche vu que Google l’a fabriqué et que ça tourne sur etcd.

C’est quoi etcd ?

– Une implémentation de Raft.

OK, c’est quoi Raft.

– C’est comme Paxos.

Putain, jusqu’où ça va le terrier du lapin blanc là ? Je veux juste lancer une app. Arf. Bordel, Ok, on respire. Fiou. Ok, donc Paxos c’est quoi ?

– Paxos c’est ce très vieux protocole de consensus distribué des années 70 que personne ne comprend ou n’utilise.

Super, merci de m’en avoir parlé. Et donc Raft ?

– Comme personne ne pige Paxos, y a ce mec, Diego

Oh, tu le connais ?

– Non, il travaille a CoreOS. Bref, Diego a créé Raft pour sa thèse de doctorat, car Paxos était trop compliqué. Futé le gars. Et il a écrit etcd comme implémentation, et Aphyr a dit que c’était pas de la merde.

C’est quoi Aphyr ?

– Aphyr est le mec qui a écrit Call Me Maybe. Tu vois, le mec spécialiste des systèmes distribués et du BDSM ?

WUT ? BDSM ?

– Ouais, BDSM. C’est San Francisco hein, tout le monde est à fond dans les systèmes distribués et le BDSM

Hum, OK. Donc il a écrit cette chanson de Katy Perry ?

– Nan, il a écrit des posts sur son blog comme quoi toutes les databases n’atteignaient jamais le CAP.

C’est quoi le CAP ?

– Le théorème CAP. Ça dit que tu peux avoir des données consistantes, disponibles et tolérantes au partitionnement, mais qu’il faut choisir 2 de ces caractéristiques sur les 3.

Ok, et donc toutes les bases de données foirent CAP. C’est à dire ?

– Ça veut dire qu’elles sont à chier. Comme Mongo.

Je pensais que Mongo scalait ?

– Ben personne d’autre ne le pensait.

Ok, et etcd.

– Ouais, etcd c’est un système de stockage de clé/valeur distribué.

Oh, comme Redis.

– Non, rien à voir. etcd est distribué. Redis perd la moitié de ce que tu écris en cas d’échec réseau.

Ok, donc c’est un système de stockage de clé/valeur distribué. Pourquoi c’est utile ?

– Kubernetes setup par défaut un cluster de 5 nodes et utilise etcd comme bus de transport de messages. En combinaison avec quelques services de Kubernetes lui-même, ça fournit un système d’orchestration assez résilient.

5 nodes ? J’ai une app. Combien de machines je vais devoir prendre pour tout ça ?

– Et bien tu vas avoir environ 12 services, et bien sûr il te faut des copies de chaque pour la redondance, quelques load balancers, le cluster etcd, ta base de données, et le cluster kubernetes. Je dirais peut-être 50 conteneurs.

WTF ?!

– Aucun problème ! Les conteneurs sont vraiment efficaces, donc tu peux les distribuer sur genre, 8 machines. C’est pas incroyable ?

C’est une manière de voir les choses. Et avec tout ça, je vais être capable de simplement déployer mon app ?

– Yep. Je veux dire, la question du stockage est toujours en suspend avec Docker et Kubernetes, et la partie réseau va demander un peu de travail, mais techniquement, tu es à 2m du bol de sangria.

Je vois. Ok, je pense que j’ai pigé.

– Super !

Merci pour l’explication.

– No problem.

Laisse-moi faire un résumé pour voir si j’ai bien tout compris.

– Bien sûr !

Donc j’ai besoin de diviser mon app CRUD toute simple, en 12 microservices, chacun avec leur propre API qui s’appellent les uns et les autres, mais qui gèrent les erreurs de manière résiliente, les mettre dans des conteneurs Docker, lancer une flotte de 8 machines qui sont des hôtes Docker tournant sous CoreOS, les orchestrer avec un petit cluster Kubernetes qui tourne avec etcd, me démerder avec la question du réseau et du stockage, et je fais tout ça en déploiement continu pour de multiples copies redondantes des microservices de ma flotte. J’ai bon ?

– Yep ! C’est pas fabuleux ?

Je retourne sous Heroku.

En relisant l’article, je réalise deux choses :

  • Putain il en faut des références rien que pour comprendre la blague. Plein de devs ne savent même pas ce qu’est Heroku.
  • Et franchement cette parodie est vraiment gentille, dans la réalité cette stack pose beaucoup d’autres problèmes : il y a plus de trucs que ça, ça s’emboîte mal, les versions changent, la doc est merdique, faut former les nouveaux arrivants car personne ne connaît ces technos, y a plein de roues à réinventer, il faut un temps fou pour choisir les briques technos, etc. Mais on peut aussi utiliser Docker très simplement, avec un image, à la main, sous Ubuntu. C’est presque simple et pratique.

 

]]>
Des astuces avec pytest 9http://sametmax.com/?p=20037http://streisand.me.thican.net/sametmax.com/index.php?20160830_113639_Des_astuces_avec_pytest_9Tue, 30 Aug 2016 09:36:39 +0000Pytest est fantastique. En fait si la lib n'est pas dispo je n'ai même plus la volonté d'écrire des tests unitaires tellement je suis habitué aux facilités qu'elle offre. Au fur et à mesure de son usage, j'ai noté quelques astuces qui, je le sais, vous serviront bien sur le long terme.]]>Pytest est fantastique. En fait si la lib n’est pas dispo je n’ai même plus la volonté d’écrire des tests unitaires tellement je suis habitué aux facilités qu’elle offre.

Au fur et à mesure de son usage, j’ai noté quelques astuces qui, je le sais, vous serviront bien sur le long terme.

Choper une exception avec un message particulier

On peut vérifier qu’une fonction lève bien une exception avec la fonction raises():

import pytest
 
def test_truc():
    with pytest.raises(MachinError):
        foo()

Ce test réussira si foo() lève bien de manière consistante MachinError. Mais parfois on a plein d’erreurs similaires, et on en veut une avec un message particulier. Dans ce cas, on peut matcher le message sur une regex:

def test_truc():
    with pytest.raises(MachinError) as excinfo:
        foo()
     excinfo.match(r"votre regex du message d'erreur")

Configurez, il en restera toujours quelque chose

On peut mettre la config de votre projet dans un fichier pytest.ini (mais le fichier tox.ini ou setup.cfg marche aussi \o/). Parmi les options les plus pratiques:

addopts, qui permet de forcer des options à la ligne de commande pytest pour ne pas les passer à chaque fois à la main.

Celles que j’active toujours:

--exitfirst : le premier échec arrête les tests. Pas la peine de me mettre 22 mille erreurs.
--capture=no : affiche les print() de mon code.
--ignore="virtualenv" : permet de ne pas cherche les tests dans un dossier. Par exemple le virtualenv.
-vv : bien verbeux. Je veux de l’info sur mes tests.
--showlocals : montre les variables locales sur les tests qui foirent.

Par exemple:

[pytest]
addopts = --exitfirst --capture=no --ignore="virtualenv" -vv --showlocals

Mais bon comme j’ai toujours ces trucs-là activés, je force ces options dans mon .bashrc via la variable d’env:

export PYTEST_ADDOPTS="--exitfirst --capture=no -ignore="virtualenv" -vv --showlocals"

Autres options sympas:

testpaths qui permet de choisir les chemins dans lesquels chercher les tests:

Ex:

[pytest]
testpaths =
    tests
    foo
    bar

Si à l’intérieur de ces dossiers il a un truc à ne pas choper, norecursedirs est là pour ça :

[pytest]
norecursedirs =
    tests/media
    tests/scripts

Les options pratiques de tous les jours

Il y a des options qu’on ne veut pas tout le temps, mais qui sont super utiles ponctuellement:

--failed-first : relance tous les tests, mais ceux qui ont foiré en premier.
--pdb : lance pdb juste après le premier échec.
-k : lance uniquement les tests dont le nom matche cette regex.

Et souvenez-vous que vous pouvez lancer un seul test en spécifiant son chemin avec la syntaxe relative/file/path.py::test_func. Par exemple:

pytest test/test_foo.py::test_bar

Vive les plugins

Pytest a une grosse communauté de plugins, il suffit de chercher sur google “pytest + techno” pour avoir tout de suite des helpers qui popent.

Ceux que j’utilise très souvent:

pytest-pythonpath :

Permet d’avoir dans son conf file python_paths = chemins qui seront ajoutés au PYTHON PATH:

Ex:

[pytest]
python_paths =
    .
    libs

pytest-flake8 permet de lancer le linter flake8 pendant les tests et de considérer son échec comme un test qui foire. En plus flake8 peut mettre sa config dans les mêmes fichiers que pytest donc j’ai souvent ça:

[flake8]
exclude = doc,build,.tox,.git,__pycache__,build # ignorer les dossiers inutiles
max-complexity = 10 # verifier que le code n'est pas trop complexe
max-line-length = 80 # pep8 for ever, mais noqa peut servir parfois

Après j’utilise souvent tox, donc ce plugin n’est plus aussi utile qu’avant.

pytest-django qui ajoute des setup et tear down avec la base de données Django, fourni des fixtures pour le client de test django et permet de configurer DJANGO_SETTINGS_MODULE dans son fichier ini.

pytest-asyncio qui permet de gérer la loop, utiliser await dans les tests, etc.

Gérer ses fixtures

Le système de fixtures est une des choses qui rend pytest si cool, particulièrement parce qu’on peut être très précis dans ce qu’on charge. Mais des fois on veut les fixtures pour tout le monde, ou tout un module. autouse fait exactement ça:

@pytest.fixture(autouse=True, scope="module")
def ma_fixture():
    return foo()

Et cette fixture sera instanciée une seule fois pour tout le module. On peut aussi avoir un scope de session (une seule fois par lancement de pytest) ou de class (une fois par classe).

Si vous avez besoin de lancer un code avant toute session de tests, par exemple pour vous définir des fixtures qui sont dispo dans tous les tests, il suffit de le mettre dans un fichier conftest.py à la racine de vos tests. Ce fichier est automatiquement détecté par pytest, et lancé avant toute chose. Il permet également de faire des hooks complexes, créer ses propres plugins, etc. Mais je l’utilise surtout pour faire des fixtures globales et m’éviter de les importer.

]]>
L’ordre des choses 21http://sametmax.com/?p=19982http://streisand.me.thican.net/sametmax.com/index.php?20160816_164523_L___ordre_des_choses_21Tue, 16 Aug 2016 14:45:23 +0000Des mois et des mois que je n’ai pas écrit :) Je ne compte même pas Max, qui à l’heure où je vous parle est en train découvrir les joies de la sidérurgie et qui ne se souvient du blog que quand on en parle dans le jacuzzi d’un FKK.

Ça va faire 4 ans qu’on a ce truc.

Entre temps lui et moi avons déménagé plusieurs fois, séparément, puis réaménagé ensemble, on a pris des responsabilités comme l’achat d’une machine à glaçons et la culture de plantes carnivores (la mienne est morte, mais il a la main verte apparemment).

Je vais être franc avec vous, l’écriture ne m’a pas manqué. Pas du tout.

Et puis aujourd’hui, j’ai eu envie. Ça ne m’avait pas pris depuis pas mal de temps, mais c’est une bonne chose : le sens du devoir avait depuis longtemps perdu son influence sur ma motivation éditoriale.

Est-ce que ça va durer un article ? 10 ? Un an ?

On va voir.

Mais ce ne sont pas les sujets qui manquent. En fait Python 3.6 est droit devant nous, avec plein de trucs chouettes à la clé. J’ai aussi beaucoup joué avec asyncio, jusqu’à trouver des bugs dedans et les reporter aux core-dev direct.

Pour le cul par contre, je ne sais pas encore, je vais laisser traîner jusqu’à ce que, tel Rocco dans Omar et Fred, ma bite me parle.

Python pour commencer donc.

Plein de changements de perfs, en vitesse, occupation mémoire, etc, prévus pour la prochaine version. Parmi eux, un changement majeur sur l’implémentation des dictionnaires inspiré directement de Pypy, qui les rendront plus compacts et plus rapides.

Et surtout, ordonnés par défaut.

Yep, plus besoin de collections.OrderedDict, qui restera malgré tout pour des raisons de compatibilité.

Enfin s’ils décident que ça fait maintenant partie de la specs et n’est pas juste un détail d’implémentation.

Ca veut dire aussi que **kwargs et __dict__ deviendront ordonnés, ce qui va arranger beaucoup de monde.

Mais l’article, en plus de prêcher la bonne nouvelle, est aussi là pour vous parler d’un petit hack sympa qui peut être pratique dans votre PYTHONSTARTUP pour des sessions shellifiantes : donner l’impression d’avoir un OrderedDict builtin par défaut.

from textwrap import dedent
from collections import OrderedDict
 
class OrderedDictFactory(object):
 
    # __getitem__ est la méthode magique appelée quand on fait objet[trucs]
    # et keys contient la liste des trucs
    def __getitem__(self, keys):
 
        for key in keys:
 
            # slice est un objet builtin fabriqué par Python quand on fait 
            # objet[debut:fin], qui contient le debut et la fin
            if not isinstance(key, slice):
 
                # on check qu’on a bien une liste de slices
                raise SyntaxError(dedent("""
                    One element of the dict is not a key/value pair: {!r}.
 
                    The syntax is d["key1": "value", "key2": "value", ...].
                    Check if you haven't missed a semicolon somewhere.
                """.format(key)))
 
        # et on retourne juste un ordered dict classique
        return OrderedDict([(k.start, k.stop) for k in keys])
 
# instance de référence pour notre factory abrégé pour des raisons de 
# facilité
d = OrderedDictFactory()

Et hop, on abuse la notation du slicing :

>>> menu = d[
   "Samurai pizza cats" : "fruits de mer", # un slice
   "Tortue ninja" : "4 saisons", # un autre slice 
   'Max': "paysanne" # do you wanna slice ?
]
 
>>> menu
OrderedDict([('Samurai pizza cats', 'fruits de mer'), ('Tortues ninjsa', '4 saisons'), ('Max', 'paysanne')])

Rien à voir avec la choucroute, mais certaines personnes m’ont parlé de faire un miroir SFW du blog. Je vous rappelle que le blog est sous creative common, alors faites-vous plaiz, vous pouvez tout réutiliser.

]]>
Récupérer le premier et le dernier élément d’un OrderedDict 15http://sametmax.com/?p=18724http://streisand.me.thican.net/sametmax.com/index.php?20160322_191811_Recuperer_le_premier_et_le_dernier_element_d___un_OrderedDict_15Tue, 22 Mar 2016 18:18:11 +0000collections.OrderedDict est une structure de données que j'utilise de plus en plus, surtout que sa réécriture en C en 3.5 lui donne des performances décentes.]]>collections.OrderedDict est une structure de données que j’utilise de plus en plus, surtout que sa réécriture en C en 3.5 lui donne des performances décentes.

Néanmoins, il n’y a pas dans l’API de moyen de récupérer le premier ou le dernier élément inséré dans dico. Il y a bien popitem(), mais ça retire l’élément du dictionnaire, et c’est pas forcément ce qu’on veut.

Heureusement OrderedDict est un itérable, et implémente __reversed__, et on peut donc utiliser les outils suivant our récupérer les extrémités avec une perf 0(1):

>>> from collections import OrderedDict
>>> d = OrderedDict.fromkeys('azerty')
>>> next(iter(d.items())) # premier élément
'a'
>>> next(reversed(d.items())) # dernier élément
'y'

Après l’implémentation de OrderedDict reste une liste doublement chainée, et on ne peut donc pas récupérer un élément à un index arbitraire sans le parcourir à la main…

]]>
Nouvel alias pour Python et .bashrc 7http://sametmax.com/?p=18674http://streisand.me.thican.net/sametmax.com/index.php?20160317_162741_Nouvel_alias_pour_Python_et_.bashrc_7Thu, 17 Mar 2016 15:27:41 +0000J’en avais marre de taper Python en entier. Et surtout, je voulais lancer Python3.5 si il est dispo, et si possible ptpython, ou ipython. Sauf si je passe des arguments. Et que ça pete pas tout dans un virtualenv.

Bref:

function p {
    local SUFFIX="$@"
    if [[ "$VIRTUAL_ENV" != "" ]]
    then
      local PREFIX="$VIRTUAL_ENV"/bin
      COMMANDS=("python")
    else
      local PREFIX=/usr/bin
      COMMANDS=("python3.5" "python")
    fi
 
    if [[ "$#" -eq 0 ]]; then
        SUFFIX=""
        local COMMANDS=("python3.5 -m ptpython"
                        "python3.5 -m ipython"
                        "python -m ptpython"
                        "python -m ipython"
                        "python")
    fi
 
    for i in "${COMMANDS[@]}"
    do
       $PREFIX'/'$i $SUFFIX;
       [ "$?" -eq 0 ] && return 0
    done
 
}
]]>
Ce ne sont pas les services pirates qui ont une jambe de bois 30http://sametmax.com/?p=18627http://streisand.me.thican.net/sametmax.com/index.php?20160314_172142_Ce_ne_sont_pas_les_services_pirates_qui_ont_une_jambe_de_bois_30Mon, 14 Mar 2016 16:21:42 +0000Quand le Divx est sorti, il y a eu tout un débat sur la valeur du DVD. Le truc qui prend de la place, qui s’abime, qui est une plaie à sauvegarder, qui est zoné… En prime, quand tu payais un DVD, tu avais ça:

Dvd pirate vs légal

Attendez : il fallait aussi se déplacer pour acheter physiquement ce putain de DVD


Mais les industriels ont tapé du pied sur le piratage depuis les K7 audio. Jamais, au grand jamais, ils ne se sont remis en question.

Et est venu l’ère du streaming.

Et on a eu le droit à une redite.

Limite par pays, peu de sous-titre, catalogue limité et gros délais entre certaines sortis selon les médias et/ou les zones. Or the pirate bay et pop corn time n’ont pas tous ces problèmes.

Mais alors qu’en est-il du livre numérique ?

Le livre, c’est fait par des gens qui utilisent leur cerveau, non ?

Je veux dire, ils lisent, en théorie…

Et bien, après avoir galéré pour trouvé certains titres légalement et perdu ma collection suite au bricage de ma première tablette, je n’étais déjà pas très convaincu. J’aime l’outil pour le voyage, mais le service laisse à désirer. Nénamoins, je suis un geek, peut etre que les gens moins attachés à la cause étaient moins sensibles à ces problématiques.

Il faut croire que non, puisque mon frère m’a écrit dernièrement ce mail bien rageux:

ALORS j´ai acheté mon livre sur un site.

DRM ADOBE donc j´installe tout etc..1h30 à faire les inscriptions les mots de passe,
les confirmations. Et je peux même pas lire mon livre…
Les mecs sont les plus nazes de la terre ils se tirent un balle dans le pieds.

Du coup je vais devoir pirater mon propre livre que j´ai acheté légalement

Youpi.

Et du coup bah qu´ils aillent bien se faire mettre pour la suite ce sera le premier et le dernier livre avec DRM que achète et si je trouve pas mon livre en Epub sans DRM bah je le piraterai.

Dés que j´ai mon livre sans DRM je te l´envois.

Bisoux

Notez que lui sait ce qu’est un DRM. Je n’ose pas imaginer Mme Michu.

]]>
Look it up 7http://sametmax.com/?p=18545http://streisand.me.thican.net/sametmax.com/index.php?20160308_095849_Look_it_up_7Tue, 08 Mar 2016 08:58:49 +0000un projet magique. Chaque jour est une nouvelle découverte, une aventure !]]>Je travaille sur un projet magique. Chaque jour est une nouvelle découverte, une aventure !

Par exemple, c’est un projet utilisant l’excellent Django Rest Framework, une app Django très puissante qui permet de créer des API succulentes.

DRF est très flexible, et permet de régler tout un tas de paramètres à l’aide de classes de configuration. Par exemple, il extrait automatiquement tout champ lookup_field sur ses classes Serializer afin de choisir sur quel champ filtrer les données.

L’auteur du code que j’ai sous les yeux, je crois, a voulu être vraiment, mais alors vraiment sûr d’avoir un look up:

class FooViewSet(ModelViewSet):
 
    class Meta:
        model = Foo
        lookup_field = 'pk'
        lookup_fields = ('pk', 'data_id')
        extra_lookup_fields = None

En soi, c’est très drôle.

Et je pourrais arrêter l’article ici.

Mais non.

En effet, y a pas un truc qui vous choque ?

Je veux dire, autre que la sainte trinité des lookup fields…

Allez, relisez l’article depuis le début, je vous laisse une chance.


J’ai dit que DRF extrayait un champ lookup_field sur les classes Serializer, et comme vous pouvez le constater, l’auteur ici hérite joyeusement de ModelViewSet, mais pas du tout de Serializer.

Oui, parce qu’on est en pleine exploration de Fistland (Au fond du fun !™), ces 3 champs ne sont en aucun cas exploités automatiquement par DRF… car sur les Viewset, lookup_field est utilisé pour générer des URLs, et mes prédécesseurs ont créé un router custo qui override ceci. Mais si on retire les champs, ça pète tout car il y a des bouts de leur code qui supposent l’existence de ce champ.

Néanmoins, ne soyons pas complètement négatif, certaines classes héritent bien de Serialiser, et définissent aussi lookup_field. D’ailleurs une part de mon job est de migrer tout ça. Car la petite touche humoristique finale, c’est que lookup_field est deprecated depuis 3 releases dans DRF \o/ Mais deprecated sur les Serializer uniquement hein, pas les Viewset. Enfin je dis ça…

]]>
Se faciliter la vie quand on utilise asyncio dans le shell 19http://sametmax.com/?p=18526http://streisand.me.thican.net/sametmax.com/index.php?20160307_150308_Se_faciliter_la_vie_quand_on_utilise_asyncio_dans_le_shell_19Mon, 07 Mar 2016 14:03:08 +0000Dernièrement j’ai mis les fonctions suivantes dans mon script PYTHONSTARTUP

Toujours choper une event loop toute fraiche, et ouverte:

import asyncio
 
def aloop(*args, **kargs):
    """ Ensure there is an opened event loop available and return it"""
    loop = asyncio.get_event_loop()
    if loop.is_closed():
        policy = asyncio.get_event_loop_policy()
        loop = policy.new_event_loop(*args, **kargs)
        policy.set_event_loop(loop)
    return loop

Lancer une coroutine dans une event loop jusqu’à ce qu’elle se termine:

import inspect
 
def arun(coro):
    """ Run this in a event loop """
    loop = aloop()
    if not inspect.isawaitable(coro):
        if not inspect.iscoroutinefunction(coro):
            coro = asyncio.coroutine(coro)
        coro = coro()
    future = asyncio.ensure_future(coro)
    return loop.run_until_complete(future)

Et ça s’utilise comme ça:

async def foo():
    await asyncio.sleep(1)
    print('bar')
 
arun(foo)

Ça me fait gagner pas mal de temps.

J’utilise aussi cette commande magique dans iPython qui permet de balancer des await en plein milieu du shell. Je suis en train de voir pour ajouter un truc similaire à ptpython.

Je me suis fait aussi un petit script pour lancer vite fait n’importe quel script dans une boucle asyncio de manière transparente, mais j’ai pas le temps de poster ça aujourd’hui donc ça sera pour la prochaine fois.

]]>
Comment tu as appelé ton utilisateur ? 6http://sametmax.com/?p=18505http://streisand.me.thican.net/sametmax.com/index.php?20160304_111459_Comment_tu_as_appele_ton_utilisateur___6Fri, 04 Mar 2016 10:14:59 +0000Je travaillais pour un client dont l’infrastructure m’avait pris quelques jours à prendre en main. Avec l’aide de toute l’équipe et de la patience, victoire, j’avais l’env de prod sur ma machine, et je pouvais faire tourner le produit…

Previouly, on Sam and Max

Je me rends sur la page d’accueil, qui me redirige instantanément sur l’admin du site Django.

Ceci n’avait aucun sens. Je regarde les requêtes HTTP, j’ai une 302 qui sort de nulle part. Je suis authentifié, ma conf nginx est pas déconnante, et dans le routing défini du urls.py, le pattern pour l’admin est le classique http://domain.tld/admin.

Rien de fou.

Alors après m’être arraché les cheveux, je vais voir un techos et lui demande si à tout hasard il a une idée.

- Il me dit “tu as appelé ton user comment ?”

– Oh, j’ai fait un ./manage.py createsuperuser vite fait pour tester en local avec username:admin, password:admin.

– Ben c’est ça, on peut pas créer un user qui s’appelle “admin”.

– Ah ?

– Oui on a un url pattern pour la page du profile utilisateur qui fait:

http://domain.tld/<username>

Quand tu arrives sur /, on redirige dessus automatiquement.

Du coup si tu l’appelles admin, tu as plus accès à ton profile…

And now…

Aujourd’hui, en explorant la base de code, je tombe sur ça:

class RegistrationForm(forms.Form):
 
    _reserved_usernames = [
        'accounts',
        'about',
        'admin',
        'clients',
        'data',
        'forms',
        'maps',
        ... + 50
    ]

J’ai de quoi écrire un bouquin avec le contenu des fichiers que je vois, je sens que ça va me faire quelques articles :)

]]>
Les interpréteurs alternatifs de python 20http://sametmax.com/?p=18144http://streisand.me.thican.net/sametmax.com/index.php?20160303_091741_Les_interpreteurs_alternatifs_de_python_20Thu, 03 Mar 2016 08:17:41 +0000

Ceci est un post invité de deronnax posté sous licence creative common 3.0 unported.

Bonjour, tous.
Vu qu’un des derniers posts de Sam&Max parlait de vitesse et des implémentations alternatives de Python, je propose un article sur l’état de ces différentes implémentations justement, et leur rapport à la vitesse.

Je commence par un petit rappel, pour nos lecteurs les moins aguerris avec l’écosystème Python : Python est un langage et aussi un interpréteur de référence, dont le vrai nom est CPython, écrit en C, qui livre également la bibliothèque standard Python (les modules datetime, urllib, collections, etc). CPython a des performances typiques de langage de script comme PHP ou Ruby, c’est à dire pas terribles, et a une conception assez simple, qui utilise des solutions simples pour répondre aux problèmes de langages de script. Celles qui font le plus parler d’elles (en mal évidement) étant la gestion de la mémoire par comptage de références (references counting), considérée moins performant qu’un vrai ramasse-miette (garbage collector) et assez limitée, et un verrou global de l’interpréteur, dont vous avez déjà dû entendre parler, le très mal-aimé GIL (Global Interpreter Lock), qui empêche entre autre à CPython d’avoir de vrai threads, c’est à dire des portions de code s’exécutant réellement en même temps, concurremment, sur des cœurs processeur différents. Néanmoins, rappelons-le encore : ces solutions permettent d’avoir un interpréteur simple et clair, pour lequel écrire des modules d’extensions en C est simple, ce qui est considéré actuellement comme le plus important. De plus, CPython lui-même n’ayant pas des performances significatives, du vrai multithread n’est pas un besoin pertinent. Si vous avez besoin de faire des traitements lourds, 9 fois sur 10 une bibliothèque spécialisée en C s’en occupe et fait elle même le traitement lourd en C dans ses threads, tel qu’OpenCV ou NumPy. En général vous avez très rarement besoin de vrai thread Python, et donc le GIL est relativement un faux problème.
La communauté Python s’est adaptée à cet absence de vrai concurrence, et différentes solutions sont apparues pour contourner le problème, je pense par exemple à l’excellent module multiprocessing, qui vous permet d’instancier différents interpréteurs et de les faire communiquer entre eux de manière transparente comme si c’était des threads.
D’une manière générale, quand il sera question d’un interpréteur alternatif, quatre points intéresseront la communauté :

  • a-t-il de meilleures performances que CPython ?
  • a-t-il un GIL/des vrais threads ?
  • a-t-il un vrai ramasse-miette ?
  • supporte-il les modules en C codés pour CPython ?

Je ne parlerai ici que des interpréteurs se voulant concurrents de CPython et remplissant la même niche. Les interpréteurs remplissant une tache un peu différente, et/ou nécessitant des modifications significatives de la manière d’écrire du code Python, ou n’ayant pas un niveau d’aboutissement significatif ne seront pas abordés, comme Cython ou Stackless Python. Voici une liste complète des implémentations de python.

Déjà, niveau implémentations alternatives, parlons des historiques Jython et IronPython ; ces deux projets sont des interpréteurs python tournant dans la VM existante d’un autre langage, respectivement la JVM Java d’Oracle (Sun, avant son rachat) et le CLR/.Net C# de Microsoft, apportant ainsi différents bénéfices, entre autre un vrai ramasse-miette et aussi des vrai threads qui carburent. Ils ont aussi en commun d’avoir été peu adoptés (très peu dans le cas d’IronPython) et qu’ils ne sont plus très vivants (très peu dans le cas d’IronPython, déjà lâché par Microsoft en 2010) : dernière release de Jython: mai 2015 ; IronPython: juin 2014. Ça fait mal. De plus, si à leur sortie ils donnaient par moment de meilleures performances que CPython, ce dernier s’est beaucoup amélioré et maintenant ils se valent au mieux. De plus, tous les deux sont très lourds, avec une empreinte mémoire beaucoup plus grosse et un temps de démarrage sans comparaison avec CPython. À déployer, c’est complètement la mort aussi, rien à voir avec apt-get install python/pypy. Très beaucoup la mort pour obtenir un IronPython qui tourne sous Mono sous Ubuntu. Ces projets sont bien partis pour disparaître s’ils ne changent pas. Et le support de python3 est inexistant pour tous les deux. Et vos modules codés en C, vous pouvez vous asseoir dessus.

Pour la petite histoire (anecdote à recaser en soirée pour briller), Jim Hugunin, le créateur de Jython, a commencé à écrire IronPython pour démontrer aux gens que la plateforme .Net était mauvaise et biens moins bonne que la JVM pour écrire une implémentation de langage. Jim a commencé à écrire une implémentation de Python en .Net, et à sa grande surprise a réussi à faire une implémentation très aboutie en peu de temps et a trouvé la plateforme .Net tellement géniale qu’il a quitté le projet Jython pour lancer IronPython.

Ensuite vient Pypy, l’interpréteur Python en python orienté vitesse. Pypy existe depuis une dizaine d’année et vise à être un drop-in remplacement de CPython, c’est à dire qu’ils suffit de l’installer et de changer les commandes “python truc” par “pypy truc” et hop ça marche. Ça fonctionne, à condition de ne pas utiliser de modules compilés en C. Si c’est néanmoins le cas, Pypy peut utiliser les modules en C de CPython moyennant recompilation, mais attention, les perfs sont moins bonne qu’avec CPython, dû au fait que l’API vient de CPython et impose à Pypy, un interfaçage/fonctionnement qui n’est pas le sien et ne supporte pas toute l’API. Pypy délivre actuellement des performances en moyenne neuf fois supérieures à CPython sur leur suite de tests, ce qui est assez bluffant. Après, à chacun de considérer si la suite de test de Pypy est représentative d’une utilisation réelle, ce qui est le problème existentielle de chaque implémentation Python alternative qui vous met sous le nez un graphe montrant qu’elle est X fois plus rapide que CPython sur sa suite de tests. Pypy utilise également un GIL, donc ne fourni pas de vrais threads, mais a dans un coin un projet de passer à un modèle de concurrence qui permettrait de s’affranchir du GIL, et dispose par contre d’un vrai ramasse-miette. Le support de Python 3 est minimum, en juin 2014 est sorti Pypy 2.4 supportant python version 3.2, l’équipe informant qu’il est néanmoins plus lent que Pypy visant python 2.7, autant vous dire qu’on est pas en avance.

Vous avez pu tomber sur des noms genre Unladden Swallow et Stackless Python : Unladen Swallow était un Pypy-like antérieur par des ingénieurs de Google qui voulait générer du JIT avec LLVM en promettant des performances 5 fois supérieurs à CPython. Le projet a échoué à tenir ses promesses et a fini par mourir doucement lors que Google a retiré son financement. Ce qui a pu être sauvé a été intégré dans Pypy (des améliorations au module Pickle). Stackless  est un interpréteur modifié qui intègre les coroutines de base et d’autres trucs parallèles/asynchrone, il n’est pas un drop-in remplacement et demande d’écrire du python qui ne tournera que sur et pour lui.

Ensuite est venu il y a un an le “Pypy” de Dropbox, Pyston. Tout le monde a tapé sur Dropbox pour avoir réinventé sa roue carrée avec Pyston au lieu de contribuer à Pypy, Dropbox a répondu qu’ils utilisent une approche différente de celle de Pypy, ils utilisent un method-at-a-time JIT au lieu d’un tracing JIT, méthode qui a donné de si bons résultats avec le moteur javascript de Google, V8, en s’appuyant sur la célèbre LLVM, la “machine virtuelle pour le bas-niveau”. LLVM étant par ailleurs soutenue par des grands noms comme Apple et Intel, si Dropbox ne merde pas, ça pourrait donner un truc intéressant. De plus, contrairement à Pypy qui a choisi de peu supportter l’API C de CPython pour les modules, Pyston vise une compatibilité absolue avec l’API C, c’est une de ses priorité fondamentale . Nous pouvons en déduire que Dropbox a beaucoup de modules d’extension en C auxquels ils tiennent beaucoup. Pypy utilise un ramasse miette et un GIL. Un point fait néanmoins tousser avec Pyston, c’est que les créateurs déclarent ne vouloir que viser Python 2.7 et ne pas envisager de supporter python3. On a du mal à croire que Guido van Rossum travaille pour cette boite. Toutes ces informations viennent de la FAQ de Pyston. Le blog de Pyston, intéressant, explique leurs choix technique et rend compte de leur avancée : http://blog.pyston.org/.

Pyston a fait plusieurs chose intelligemment, notamment au lieu de réécrire un interpréteur, ils ont forké CPython et branché leurs systèmes de JIT dedans, ce qui leur a permis de sortir en moins d’un an une release de Pyston plutôt viable qui fait tourner beaucoup de choses et passe beaucoup de tests. Leur interpréteur est déjà 25% plus rapide que CPython sur leur suite de test, face à un Pypy 50% plus performant. C’est un petit tour de force.

Un énorme écueil d’écrire un interpréteur alternatif à CPython étaient les modules en C de la bibliothèque standard ; une partie significative des modules de la bibliothèque standard étaient écrits en C, donc si vous faisiez un interpréteur alternatif, il fallait aussi vous recoder dans le langage de votre interpréteur tous les modules de la bibliothèque standard que vous ne pouviez utiliser, ce qui implique de faire 10.000 tests pour s’assurer qu’ils ont exactement le même comportement que ceux de la bibliothèque standard, ce qui est extrêmement fastidieux et pénible. Depuis Python 3.3 en septembre 2012 , CPython fourni un recode en Python de tous ses module en C qui passent exactement la même suite de test et qui sont donc “garantis” (modulo erreur humaine, c’est une vaste et dure tâche) de se comporter exactement comme leurs congénères en C. C’est une énorme charge de travail en moins pour les développeurs d’interpréteurs alternatifs. Également, depuis Python 3.1 , l’import de modules, qui auparavant était une machinerie interne un peu obscure de l’interpréteur CPython, et donc qu’il fallait recoder en croisant les doigts pour que ça reproduise exactement le comportement de CPython sous peine de tout voir exploser, a été recodé en Python et est livré avec CPython, donc fini de redevoir recoder un système d’import, vous pouvez juste le reprendre et l’utiliser, ouf.

De tous, Pypy est le seul qui s’approche actuellement vaguement d’un “concurrent” sérieux à CPython, talonné par Pyston pour un futur proche. Et encore, ce n’est pas encore le drop-in remplacement parfait. Et tous ont un support de Python3 inexistant, sauf Pypy qui en a un insuffisant :). Et tous, en tant que VM complète (donc lourde) ou interpréteur-optimiseur visant à optimiser le code (et donc analyser), ont des temps de démarrage et d’atteinte de l’efficacité sans commune mesure avec CPython. Ils ne sont intéressants qu’en cas de long-running (genre, vos sites Django). Et tous utilisent un ramasse-miette, ce qui donne un comportement différents sur les objets “finalisés”. Alors que CPython a un comportement déterministe et détruit immédiatement les objets qui ne sont plus utilisés, les ramasse-miettes reportent leur destruction à un moment où il sera plus opportun de les détruire, potentiellement très longtemps ou jamais au pire des cas. C’est pour ça que par hygiène, il faut toujours libérer explicitement les ressources des objets représentants des ressources tel que les files-like objets représentants fichiers ou socket en utilisant leur méthode “close()” ou le manager “with” même si ça fonctionne très bien sans sous CPython. Pour les objets représentant des ressources limitées comme les fichiers, les sockets ou n’importe quel lien vers une ressource limitée procurée par l’OS, ne pas le faire signifie se prendre rapidement un stop de l’OS. D’une manière générale, CPython applique des solutions simples et déterministes qui offrent pas mal de garanties sympathiques (gestion des ressources, atomicité, alignement mémoire, adresse des objets en mémoire fixe, etc) qui sont maintenant considérées comme acquise et immuables par beaucoup de programmes python, malheureusement gênantes pour faire un interpréteur optimisant vraiment agressivement (là j’ai pas de sources mais c’est une complainte qui revient régulièrement dans les discussions des autres interpréteurs, surtout Pypy).

Parmi les news relatives à la vitesse de Python que l’article de S&M ne mentionne pas, la nouvelle release de la JVM vient avec un meilleur découplage des éléments la composant, et du coup il devient facile de l’utiliser pour un autre langage. IBM a fait un proof-of-concept en faisant un interpréteur python, qu’ils vont open-sourcer (et faire pareil avec Ruby). Pas d’altruisme là-dedans, c’est pour montrer au monde que leur VM est bien fichue. Ça pourrait être le nouveau Jython : http://www.infoworld.com/article/3014128/open-source-tools/ibms-open-source-jvm-project-could-also-speed-ruby-python.html.

Aussi, Microsoft s’étant visiblement, dernièrement, un peu amouraché de Python, on peut s’attendre que leur coopération à Python aille un peu plus loin. Ou pas.

Après, la triste vérité est que les langages dynamiques sont très durs à optimiser, parce qu’ils sont justement dynamiques, surtout Python où tout est modifiable, et donc interpréteurs/VMs ne peuvent être sûrs de rien et ne peuvent pas supprimer pleins de tests/résolutions/recherches, et que Python, malgré toutes ses implémentations pleines de bonne volonté, ne sera jamais aussi rapide que le C, ni même que Java. La dernière fois que j’ai regardé (~3 ans), le benchmark game donnait python 20x plus lent que le C, contre 2x pour le Java. Ça fait réfléchir. Tous les efforts poussés à fond, on tapera peut être le 5x plus lent que le C :)

Actuellement, un core-contributeur de CPython (français, cocorico !) travaille sur différents sujets visant à améliorer significativement les performances de CPython. C’est la première fois qu’un chantier aussi vaste est lancé visant à sérieusement améliorer les performances de CPython. Le site sur le sujet : http://faster-cpython.readthedocs.org/

Ces différents projets acceptent les dons. Je vous encourage évidemment vivement à donner. La vitesse de Python est l’un dernier écueil qui pourraient faire choisir à un décideur technique pressé un autre langage plutôt que Python. Des meilleurs interpréteurs Python, c’est plus de tâches accomplies en Python, donc plus de postes pour les développeurs Python, donc plus de demande, et donc de meilleurs salaires. Quand vous donnez à des projets Python, vous vous versez de l’argent à vous-même dans le futur. De plus, ça aide à faire reculer les parts de marché de cette horreur qu’est PHP et de la concurrence de Ruby et Go, et ça, c’est toujours ça de pris :)
Même une petite somme est significative. Avec l’euro fort, vous avez un grand pouvoir. Et un grand pouvoir implique de grandes responsabilités.
Donner à Cpython : https://www.python.org/psf/donations/
Donner à Pypy : http://pypy.org/

Voila, vous savez tout sur l’histoire des différents interpréteurs Python. Avec ça, vous pourrez vous la pétez en soirée et niquer des tonnes de meufs. Ou pas.

]]>
Il n’y a pas de mauvais script… 11http://sametmax.com/?p=18200http://streisand.me.thican.net/sametmax.com/index.php?20160301_122225_Il_n___y_a_pas_de_mauvais_script____11Tue, 01 Mar 2016 11:22:25 +0000Learning Python et Programming Python de Mark Lutz (à l'époque la seconde édition "couvre python 2 !").]]>

Ceci est un post invité de atrament posté sous licence creative common 3.0 unported.

… il n’y  a que des scripts en passe de devenir bons. Avis aux débutants, croyez-y, vous ne le resterez pas.

C’est l’histoire d’un des premiers “vrais scripts” que j’ai écrits. Je venais de finir Learning Python et Programming Python de Mark Lutz (à l’époque la seconde édition “couvre python 2 !”).

J’étais même pas un script kiddie, et je ne programmais que pour m’amuser. J’étais fan de Sinfest, un web comic gavé de gros blasphèmes qui tâchent.

Je me suis mis en tête de télécharger tout ces comics d’un coup. Voilà ce que ce script était, et ce qu’il est devenu.

Script originel avec formatage encore moins lisible, sur 0bin.

#~ Script to DL all sinfest webcomics
#~ one can edit the START and END parameters to DL a subset
STARTDAY = 17  # 17
STARTMONTH = 01  # 01
STARTYEAR = 2011  # 2000
ENDDAY = 31
ENDMONTH = 01
ENDYEAR = 2012
ROOTDIR = 'Sinfest'
ROOTURL = "http://www.sinfest.net/comikaze/comics/"
IMGEXT = ".gif"
#~ Code start - no edition from here unless you know what you're doing
#~ --------------------
from distutils.file_util import copy_file
from urllib import urlopen
import os.path
import os
#~ ROOTDIR=os.getcwd()+ROOTDIR
#~ Check if date is out of range
 
 
def checkdate(d, m, y, ed, em, ey, sd, sm, sy):  # end date is ed, em, ey
    if y &gt; ey or (y == ey and m &gt; em) or (y == ey and m == em and d &gt; ed):
        return 0  # date over date range
    elif y &lt; sy or (y == sy and m &lt; sm) or (y == sy and m == sm and d &lt; sd):
        return 0
    else:
        return 1  # date below end date and above start date
#~ make the file name teh sae format than sinfest server
 
 
def getfilename(y, m, d, IMGEXT):
    M =`m`
    D =`d`
    Y =`y`
    if len(M) &lt; 2:
        M = '0' + M
    if len(D) &lt; 2:
        D = '0' + D
    filename = Y + '-' + M + '-' + D + IMGEXT
    return filename
#~ run the loop
if not os.path.isdir(ROOTDIR):
    os.mkdir(ROOTDIR)
for y in range(STARTYEAR, ENDYEAR + 1):
    for m in range(1, 13):
        for d in range(1, 32):
            filename = getfilename(y, m, d, IMGEXT)
            if checkdate(d, m, y, ENDDAY, ENDMONTH, ENDYEAR,
                         STARTDAY, STARTMONTH, STARTYEAR):
                if os.path.isfile(ROOTDIR + '/' + filename):
                    print '[' + filename + '] already exists'
                else:
                    src = urlopen(ROOTURL + filename)
                    if src.info().gettype() == 'image/gif':
                        dst = open(ROOTDIR + '/' + filename, 'wb')
                        dst.write(src.read())
                        dst.close()
                        print '[' + filename + ']' + ' copied'
                    else:
                        print "MIMETYPE ERROR (probably ERROR 404) ignored"
                    src.close()

On remarquera l’arrogance du jeune codeur fier de lui dans certains commentaires. Ils étaient à mon intention, en vrai, et montraient surtout que je ne savais pas trop ce que je faisais.

En vrac, parmi les choses qui me font sourire aujourd’hui :

  • la fonction checkdate qui prend 9 paramètres pour vérifier si une date est entre deux autres,
  • les grosses constantes au début
  • la portabilité nulle du script qui doit être lancé au bon endroit
  • la date codée avec 3 boucles pour l’année, le mois, le jour.
  • Je ne parle même pas de la pep008, elle n’avait même pas un an, d’abord, et puis visiblement on était loin des questions de style…

Bon, il y a prescription, et puis je suis pas mal autodidacte, c’était pas si mal : ça fonctionnait ! Ça m’a quand même servi, de temps en temps, jusque 2012, pour remettre à jour mes dossiers d’images.


Et puis je l’ai relu et j’ai décidé de le reprendre avec ce que je savais de nouveau :

  • le module datetime,
  • la construction if __name__ == '__main__',
  • les exceptions,

et ça a donné ça :

# -*- coding: utf-8 -*-
#! /usr/bin/python
# Script to DL all sinfest webcomics
# one can edit the START and END parameters to DL a subset
#-------------------------------------------------------------------------------
# Name:        getSinfest
# Purpose:
#
# Author:      Atrament
#
# Created:     12/04/2013
# Copyright:   (c) Atrament 2013
# Licence:     
#-------------------------------------------------------------------------------
 
import datetime
from urllib.request import urlopen
import os.path
import os
from urllib.error import HTTPError
 
 
def main():
    debut=datetime.date(2000,1,17)
    curGif=debut
    if not os.path.isdir("Sinfest"):
        os.mkdir("Sinfest")
    while curGif&lt;=datetime.date.today():
        if not os.path.isfile("Sinfest/"+curGif.isoformat()+".gif"):
            try:
                src=urlopen("http://www.sinfest.net/comikaze/comics/"+curGif.isoformat()+".gif")
                dst=open("Sinfest/"+curGif.isoformat()+".gif",'wb')
                dst.write(src.read())
                dst.close()
                print("    "+curGif.isoformat()+".gif : fetched.")
                src.close()
            except HTTPError as e:
                print("on "+curGif.isoformat()+" error happened. So is life.")
        else:
            print(curGif.isoformat()+" : ok")
        curGif+=datetime.timedelta(days=1)
 
if __name__ == '__main__':
    main()

Et honnêtement ça s’améliore (un peu): un shebang (pas tout à fait en haut du fichier, mais j’apprendrai plus tard), bien que je code à l’époque uniquement sous windows avec pyscripter, qui me fournit les commentaires de début de fichier, qui ne servent à rien, mais à l’époque m’éclatent. Ben oui, à ce moment là, je suis à peine adulte, et avec les heures que j’ai passé sur IDLE à tâtonnner, c’est une victoire pour moi.


C’est un peu plus tard encore que j’apprends que les requêtes internet qui prennent des heures, c’est pas obligatoire, parce qu’on peut faire du multi thread, et continuer de tourner pendant qu’on attend qu’une autre tâche s’accomplisse. Voilà de quoi faire rêver un jeune programmeur : la puissance du multi-coeurs à la portée de mon code ! (non, je ne savais pas que c’est faux). En conséquence, mon script évolue, pour devenir… ça. Attention, ça pique les yeux.

# -*- coding: UTF-8 -*-
# ---------------------------------------------------------------------
# Author:      Atrament
# Licence:     CC-BY-SA https://creativecommons.org/licenses/by-sa/4.0/
# ---------------------------------------------------------------------
# All libs are part of standard distribution for python 3.4
import imghdr
import os
import queue
from threading import Thread
import datetime
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
import zipfile
import sys
 
 
# Useful functions
def make_cbz(directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for gif_file in [x for x in os.listdir(directory) if x.split("-")[0] == str(year)]:
                archive.write(directory + '/' + gif_file, arcname=gif_file)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def confirm(prompt):
    if input(prompt + " (y/n)") in "yY":
        return True
    else:
        return False
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url):
    if file_needs_download(filename):
        try:
            src = urlopen(base_url + filename)
            dst = open(filename, 'wb')
            dst.write(src.read())
            # gracefully close theses accesses.
            dst.close()
            src.close()
            print("\t" + filename + " : fetched.")
        except HTTPError:
            # many days do not have a comic published.
            # no need to flood the console for this.
            pass
        except URLError:
            pass
            print("network error on " + filename)
        finally:
            # clean garbage on disk, useful if failure occurred.
            file_needs_download(filename)
 
 
class ThreadedWorker():
    def __init__(self, function=None, number_of_threads=8):
        self.queue = queue.Queue()
 
        def func():
            while True:
                item = self.queue.get()
                if function:
                    function(item)
                else:
                    print(item, "is being processed.")
                self.queue.task_done()
 
        self.function = func
 
        for i in range(number_of_threads):
            t = Thread(target=self.function, name="Thread-{:03}".format(i))
            t.daemon = True
            t.start()
 
    def put(self, object_to_queue):
        self.queue.put(object_to_queue)
 
    def join(self):
        self.queue.join()
 
    def feed(self, iterator):
        for task in iterator:
            self.queue.put(task)
 
 
def download_sinfest(target_folder):
    """
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    f = lambda filename: conditional_download(filename, "http://www.sinfest.net/btphp/comics/")
    # Make a worker with this function and run it
    t = ThreadedWorker(function=f, number_of_threads=20)
    # structure of comprehended list is a bit complex to generate all file names
    files = [(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat() + ".gif"
             for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)]
    t.feed(files)
    t.join()
 
 
if __name__ == "__main__":
    if any(("y", "Y", "-y", "-Y" in sys.argv)):
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        print("\nproceeding to download...")
        download_sinfest(folder)
        print("\ngenerating comic book files (.cbz)...")
        os.chdir(folder)
        os.chdir("..")
        make_cbz(folder)
    else:
        folder = os.path.expanduser("~/Pictures/Sinfest/").replace("\\", "/")
        while not confirm("Target to downloads is {} ?".format(folder)):
            folder = input("Please enter new folder (N to abort) :")
            if folder in "nN":
                exit(0)
        if confirm("Proceed to download ?"):
            download_sinfest(folder)
        if confirm("Do you want to generate cbz (comic books) files ?"):
            os.chdir(folder)
            os.chdir("..")
            make_cbz(folder)
        input("Finished. Please press Enter")

C’est une horreur. Si vous êtes de ces perfectionnistes qui lisent le code et font la code review par habitude, je suis désolé, vous avez du pleurer.  Le commentaire sur la lib standard “en intro” montre que à ce moment là, j’ai un peu conscience qu’on peut accéder à d’autres modules, mais on est loin de pip pour moi. Je tente maladroitement d’exploiter sys.argv (avec des erreurs qui pourraient être catastrophiques),  et l’input à défaut. Une fonctionnalité neuve est apparue : faire des archives comic book en zip. Je suis fier.

Mais le côté comique de ce code, c’est la classe ThreadedWorker. C’est ptêt bien ma première ‘vraie’ classe, mais j’implémente moi-même un dispatcheur de jobs sur des threads. Il y a de quoi être fier, mais on n’est pas du tout dans du python propre, là. C’est ballot, la version précédente était pas si mal, niveau clarté.

Et dire que la lib standard en fournit un, de dispacheur de jobs…


Je passe quelques itérations sur des détails, aujourd’hui il en est là, ce script.

#! /usr/bin/env python3.5
import imghdr
import os
import datetime
import zipfile
from concurrent.futures import ThreadPoolExecutor
 
import requests
import begin
 
 
# Useful functions
def make_cbz(dst_directory, src_directory):
    for year in range(2000, datetime.date.today().year + 1):
        with zipfile.ZipFile("Sinfest-{}.cbz".format(year), "w") as archive:
            for filename in os.listdir(src_directory):
                if filename.startswith(str(year)):
                    archive.write(src_directory + "/" + filename, arcname=filename)
        print("Sinfest-{}.cbz has been generated".format(year))
 
 
def file_needs_download(filename):
    """Checks whether a file exists, is corrupt, so has to be downloaded again
    also cleans garbage if detected"""
    if not os.path.isfile(filename):
        # many comics are *supposed* to be missing,
        # no need to output for these (uncomment for debug)
        # print("IS NOT FILE :", filename)
        return True
    elif os.path.getsize(filename) == 0:
        print("WRONG SIZE for", filename)
        return True
    elif filename.split(".")[-1] != "gif":
        print(filename, "IS NOT GIF")
        return True
    elif filename.split(".")[-1] in {"jpg", "gif", "png"} and imghdr.what(filename) is None:
        # Encoding error...
        print("WRONG FILE STRUCTURE for", filename)
        return True
    else:
        return False
 
 
def conditional_download(filename, base_url, caller=None):
    if file_needs_download(filename):
        src = requests.get(base_url + filename)
        # manage failure to download
        if src.status_code == 404:
            src.close()
            return  # ignore it, that file is simply missing.
        if src.status_code != 200:  # an error other than 404 occurred
            # print("Error {} on {}".format(src.status_code, filename))
            src.close()
            if caller:  # retry that file later
                caller.submit(conditional_download, filename, base_url, caller)
        # actually copy that file
        dst = open(filename, 'wb')
        dst.write(src.content)
        # gracefully close theses accesses.
        dst.close()
        src.close()
        print("\t{} : fetched.".format(filename))
 
 
def download_sinfest(target_folder):
    """
    Source function for the process
    Creates a directory and fetches Sinfest comics to populate it in full.
    """
    if not os.path.isdir(target_folder):
        os.makedirs(target_folder)
    os.chdir(target_folder)
 
    with ThreadPoolExecutor(max_workers=64) as executor:
        for file in ("".join([(datetime.date(2000, 1, 17) + datetime.timedelta(days=x)).isoformat(), ".gif"])
                     for x in range((datetime.date.today() - datetime.date(2000, 1, 17)).days + 1)):
            executor.submit(conditional_download, file, "http://www.sinfest.net/btphp/comics/", executor)
 
 
@begin.start
def run(path: "folder in which the comics must be downloaded" = os.path.expanduser("~/Sinfest/"),
        makecbz: "Compile CBZ comic book archives" = False):
    """Download the Sinfest WebComics"""
    download_sinfest(path)
    if makecbz:
        dst = path[:-1] if path.endswith('/') else path
        dst = "/".join(dst.split("/")[:-1])
        make_cbz(dst, path)
    print("Finished. Goodbye.")
    exit()

Enfin je m’autorise à utiliser des modules tierces. Alors, ça marche, même plutôt vite et bien. Mais c’est largement perfectible : si c’est mieux que par le passé, ça ne correspond pas à ce que je code aujourd’hui : il y a beaucoup de code hérité des anciennes versions et la prochaine évolution serait de le réécrire entièrement. Mais je ne lis plus vraiment Sinfest.


 

Mais à écrire ce texte, j’ai pris les nerfs, et j’ai refait tout ça. J’ai fait un effort, et j’ai tout jeté à github.

Encore un pas de mieux : les fichiers ont du sens, il y a une classe abstraite pour faire une pseudo-API, c’est devenu pratiquement évolutif.

 

La conclusion à l’attention du débutant : chacun voit et juge son code et celui des autres selon son niveau de compétence à l’instant T. On a tous été fiers comme Artaban de bouts de code laids à nos yeux d’aujourd’hui. Débutant qui m’a lu (merci, t’as bien du courage), garde tes scripts à travers les années, reprends les, tu te rendras compte de tous les progrès que tu fais au fil du temps. Quand un type te dis sur internet que ce que tu fais n’est pas “pythonique”, il a sûrement raison, de son point de vue, et il y a sans doute un autre gars qui lui dira la même chose demain. La courbe d’apprentissage est longue comme la vie.

]]>
Open bar sur asyncio 5http://sametmax.com/?p=18443http://streisand.me.thican.net/sametmax.com/index.php?20160229_140441_Open_bar_sur_asyncio_5Mon, 29 Feb 2016 13:04:41 +0000Plus je fais joujou avec asyncio, plus j’apprécie la lib. Mais je tombe aussi sur des tas de petits trucs qui me font dire qu’il va falloir créer quelques couches d’abstraction pour rendre tout ça plus miam.

Par exemple, lire de manière asynchrone les données pipées sur stdin:

async def main(loop):
 
    reader = asyncio.StreamReader()
    def get_reader():
        return asyncio.StreamReaderProtocol(reader)
 
    await loop.connect_read_pipe(get_reader, sys.stdin)
 
    while True:
        line = await reader.readline()
        if not line:
            break
        print(line)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))

C’est assez verbeux, pas très Pythonique (une factory qui instancie un classe a qui on passe une autre instance, c’est très java), ça n’exploite pas toute la force de async/await (le while qui pourrait être un async for), et il faut se soucier de lancer la boucle.

En prime, c’est pas encore évident de trouver qu’il faut faire ça dans la doc.

Pareil, pour lire de manière asynchrone les données écrites par un utilisateur sur stdin:

def on_stdin(*args):
    print("Somebody wrote:", sys.stdin.readline())
 
loop = asyncio.get_event_loop()
loop.add_reader(sys.stdin.fileno(), on_stdin)
loop.run_forever()

Bon, là on a du callback, clairement on peut faire mieux.

La bonne nouvelle, c’est que ça veut dire qu’on a un champ entier où on peut être le premier à écrire une lib, et donc devenir une implémentation de référence, un outil connu, etc. Si vous avez envie de faire votre trou, c’est une opportunité, et en plus, c’est rigolo :)

En effet, avec un peu d’enrobage, on peut rapidement faire des trucs choupinets:

class AioStdinPipe:
 
    def __init__(self, loop=None):
 
        self.loop = loop or asyncio.get_event_loop()
        self.reader = asyncio.StreamReader()
 
    def get_reader(self):
        return asyncio.StreamReaderProtocol(self.reader)
 
    async def __aiter__(self):
        await self.loop.connect_read_pipe(self.get_reader, sys.stdin)
        return self
 
    async def __anext__(self):
        while True:
            val = await self.reader.readline()
            if val == b'':
                raise StopAsyncIteration
            return val
 
 
def run_in_loop(coro):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(coro())

Et ainsi réduire le premier code à un truc très simple, très clair, très Python:

@run_in_loop
async def main():
    async for line in AioStdinPipe():
        print(line)

J’aime 2016, c’est une année pleine de possibilités.

]]>
Mais qui donc set ce signal handler ? 6http://sametmax.com/?p=18388http://streisand.me.thican.net/sametmax.com/index.php?20160225_085958_Mais_qui_donc_set_ce_signal_handler___6Thu, 25 Feb 2016 07:59:58 +0000signal permet de réagir aux signaux envoyés l'OS, par exemple celui qui vous annonce qu'il va vous défoncer la gueule (SIGTERM) ou que vous votre mémoire n'est plus ce qu'elle était (SIGSEGV).]]>Le module signal permet de réagir aux signaux envoyés par l’OS, par exemple celui qui vous annonce qu’il va vous défoncer la gueule (SIGTERM) ou que vous votre mémoire n’est plus ce qu’elle était (SIGSEGV).

Mais le problème, c’est que rien n’annonce qu’un signal est attrapé. Pire, quand on enregistre un handler pour un signal, ça écrase le précédent. Yep, c’est très con.

ç’aurait été plus malin d’avoir un truc comme propose le DOM, genre addEventListener, qui accumule les handlers pour un event donné, mais non, on override.

Donc parfois, une lib tierce partie se permet un petit handling de signal sur le pouce, comme ça, sans demander la permission, provoquant chaos et moult jurons. Car des signaux, y en a pas mal:

>>> import signal
>>> [s for s in dir(signal) if s.isupper()]
['ITIMER_PROF', 'ITIMER_REAL', 'ITIMER_VIRTUAL', 'NSIG', 'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD', 'SIGCLD', 'SIGCONT', 'SIGFPE', 'SIGHUP', 'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT', 'SIGKILL', 'SIGPIPE', 'SIGPOLL', 'SIGPROF', 'SIGPWR', 'SIGQUIT', 'SIGRTMAX', 'SIGRTMIN', 'SIGSEGV', 'SIGSTOP', 'SIGSYS', 'SIGTERM', 'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU', 'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM', 'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIG_BLOCK', 'SIG_DFL', 'SIG_IGN', 'SIG_SETMASK', 'SIG_UNBLOCK']

En plus, certains sont Windows only, d’autres Unix only…

Et pour faciliter les choses, les handlers peuvent être des entiers (bon, des enums dont __eq__ accepte les entiers…):

>>> type(signal.getsignal(signal.SIGKILL))
<enum 'Handlers'>
 
>>> type(signal.getsignal(signal.SIGINT) )
<class 'builtin_function_or_method'>

Voici un petit snippet pour checker si un sacripant n’est pas en train de vous brouter le signal:

>>> import signal
>>> handlers = {}
>>> for s in dir(signal):
...     if s.isupper(): # on chope uniquement les constantes
...         try:
...             handler = getattr(signal, s) # quel handler pour ce signal ?
...             if callable(handler): # on ignore SIG_DFL, SIG_IGN, etc.
...                 handlers[s] = handler # ok, on a un truc gégé !
...         except ValueError:
...            pass
>>> print(handlers[s])

On s’en sert pas souvent, mais je me suis dit que je ferai passer le mot.

]]>
L’internationalisation, c’est long et dur (et ça racle le fond) 22http://sametmax.com/?p=18253http://streisand.me.thican.net/sametmax.com/index.php?20160219_143028_L___internationalisation__c___est_long_et_dur__et___a_racle_le_fond__22Fri, 19 Feb 2016 13:30:28 +0000Je veux tradure mon site en Anglais, c'est combien ? Je tique toujours un peu.]]>L’internationalisation, abrégé i18n car il y a 18 lettres entre le i et le n, est le processus qui consiste à proposer différentes versions d’un contenu ou d’une interface afin de coller au plus proche des attentes culturelles de différents groupes d’humains.

Quand je discute avec un client et qu’il me dit :

Je veux traduire mon site en Anglais, c’est combien ?

Je tique toujours un peu.

C’est que l’i18n, ce n’est pas juste traduire des mots vers des autres. C’est beaucoup, beaucoup plus compliqué que ça. Et il faut avant tout identifier qui on vise, ce qu’on veut leur apporter, et ce qu’on est prêt à investir comme énergie pour le leur offrir.

En générale, les populations anglophones sont assez proches culturellement de nous, donc c’est plus facile de faire la transition que vers des cultures arabes, russes ou chinoises. Et si c’est imparfait, ce n’est pas trop grave, car l’utilisateur s’adaptera assez bien.

Néanmoins, il ne faut pas croire que prendre le texte et le coller dans Google translate va suffire, loin de là.

Et si le travail vise de gens culturellement plus éloignés de vous, alors le travail peu vite de venir énorme.

Le texte

Bien entendu, on pense tout de suite à la traduction, et c’est déjà un gros boulot.

Mais déjà, il faut s’adapter au contexte culturel que l’on vise. En Japonais, les niveaux de respect ne seront pas les mêmes selon les clients. Vous traduisez vers de l’espagnol, mais visez-vous l’Amérique du Sud et ses ustedes partout, ou l’Espagne et son vosotros ? Vous voulez proposer de louer un cab aux Londoniens ou un taxi aux New-Yorkais ?

Au passage, prenez un bon traducteur, car en plus de devoir traduire le sens du texte (et pas juste les mots), il faut qu’il adapte la ponctuation. Ainsi, on colle “:”, “!” et “!” au mot précédent en anglais, mais pas en français.

On a à peine commencé l’article et c’est déjà la merde, mais attendez !

Il y a le sens de lecture aussi !

De gauche à droite, de droite à gauche, de haut en bas. Et si vous croyez que ce n’est que le texte, lol… Car ça influence également toute la mise en page, avec notamment le placement des éléments importants : titres, boutons, etc. afin qu’ils soient mis en évidence.

Après, il y a l’espacement. Vous savez, on peut se permettre de se la jouer cool sur les espacements en français, parce que’on a plein de ponctuation. Donc un bloc de texte est clair, facile à lire en diagonale. En Thaïlandais par contre, il n’y a pas de virgule, de point, ni même d’espace entre les mots et les phrases. Donc si vous avez un truc à bien séparer du reste, il va falloir trouver une astuce visuelle.

Et puis il y a mon favoris, particulièrement parce que Python est très sévère (mais juste:)) avec : l’encodage.

Car dans le meilleur des mondes, on a de l’UTF8 partout. Mais on est pas dans le meilleur des mondes, et si vous visez certains terminaux, il faudra encoder dans un truc couleur locale. Par exemple, si vous envoyez des SMS sur des vieux 3310 en Afrique.

Les nombres

Ah, les nombres. Qu’est-ce qu’il pourrait être plus simple ? C’est universel pas vrai ?

Pas vrai ?

Et bien disons que comme d’habitude, la donnée est universelle, et la représentation ne l’est pas.

Déjà, il a les bases. On aime beaucoup la base 10, et quand c’est du technique parfois un peu de binaire, d’octal ou d’hexa.

Mais certaines populations ont des manières de compter étonnantes. Par exemple, si vous faites un système à Bamako (ne rigolez pas, c’est un exemple de ma carrière), sachez qu’en Bambara 1000 se dit 2 fois 500.

À cela se rajoutent les séparateurs et devises. Mille dollars et quarante cents pour un américain, c’est $1,000.40 ou $1000.40, alors qu’en français ce sera 1000,40 $ ou 1 000,40 $.

Et chaque format à ses règles de groupages, ses séparateurs, ses positions de préfixes… Les numéros de téléphone, les numéros de sécurité sociale, les codes postaux…

Attendez, ne fuyez pas, on n’en est pas encore à la moitié !

Les unités

Même la NASA a niqué une mission parce qu’ils se sont embrouillés entre les unités impériales et le système métrique.

Alors vous allez me dire, tant qu’on n’utilise pas des unités pifométriques, il suffit de convertir. Oui, mais encore faut-il savoir de quoi on parle. Il faut donc toujours demander à l’utilisateur en quoi il saisit ses données, et être très précis.

Très précis parce que figurez-vous que:

  • Le Gallon est utilisé aux UK, en Ireland, au Canada, aux Iles caïman, A la Dominique et Myanmar.
  • Les Émirats Arabes Unis, Panama et la Guyane utilisaient le Gallon jusqu’en 2013.
  • Mais le Gallon américain et le Gallon britannique ne sont pas équivalent. L’un fait 3.785l, l’autre 4.54609l, ce qui a causé la mort de quelques pilotes en Mediterranée qui on fait un plein pas assez Charlie.

Les dates

Je ne vous apprends rien en vous disant que gérer les fuseaux horaires c’est chiant. Mettez tout en UTC, et utiliser une bonne lib parce que le temps c’est relou : des zones géographiques alignées n’ont pas les mêmes heures (France et UK), des pays très grands ont plusieurs fuseaux (USA) ou un seul malgré leur gigantisme (Chine), et certains on les heures d’été/hiver, mais pas tous. Et il y a les années bissextiles, et la fameuse leap second. Et le fait que certains pays décident de changer de fuseau horaire comme ça, pouf, parce que ça les arrange.

Du coup il faut bien demander aux utilisateurs où ils sont pour récupérer la date dans le bon fuseau (l’ordinateur peut être réglé pour envoyer la mauvaise info, il peut sortir de l’avion, il peut ne pas vouloir utiliser la timezone actuelle, etc), convertir tout en UTC, et l’afficher dans le bon.

Après, hop, on a le formage des dates. Mais si, vous savez, ce truc qui a 40 000 règles qui changent d’un pays à l’autre, d’un standard à l’autre. 02/03/2015: dd/mm/yyyy ou mm/dd/yyyy ? Timestamp en seconde ou en millisecondes ?

Allez, vous croyez que vous vous en sortirez comme ça ?

N’oubliez pas, tout le monde n’a pas le même calendrier…

Nous utilisons un calendrier basé sur le chrétien, mais vous avez des calendriers bouddhistes, musulmans, des trucs basés sur les phases de la lune, et même il y a quelque temps en France le très rigolo calendrier républicain. Donc maintenant, il faut se soucier de pour où, de pour qui, mais aussi de pour quand on formate. Je plains les gens qui font des apps pour les musées.

Je plains aussi tous les gens qui font des plannings internationaux, qui doivent décider si lundi ou dimanche est le premier jour à afficher et traduire les notations naturelles comme “il y a 3 minutes(s)”.

La loi

Oh, si, oh si !

La loi change d’un pays à l’autre ! Nudité, le copyright, l’accessibilité…

Le DMCA aux USA, la censure en Chine, l’Hadopi en France. Nan pour le dernier je déconne hein, ils ont condamné 5 personnes depuis le début du programme.

Et allez prendre en compte les lecteurs d’écran, mettre du ARIA comme il faut, tester le bouzin pour les personnes en déficience visuelle (problèmes de couleur, aveugles, malvoyants…) ou motrice.

Bon alors là je ne vais pas rentrer dans les détails, car c’est trop, trop compliqué pour ma petite tête.

La culture

Le symbolisme diffère d’un groupe culturel à l’autre.

Vous allez faire un site pour le mariage, en beau blanc !

Mais en Chine, il faut du rouge.

Et dans certaines cultures, c’est la couleur du deuil.

Ce qui rend crédible commercialement aussi est différent. Un beau site bien épuré, avec plein d’espace négatif, c’est classe non ? Mais en Asie, avoir une page saturée est la norme.

En fait toute l’infrastructure, et même le système entier change la manière de voir les choses.

Quand on écrit du contenu pour un pays, il faut se rappeler des choses comme le climat, la monnaie différente, conduite à droite ou à gauche, voltage du courant, jours chômés, situation politique…

Si on fait une application avec beaucoup de carto, on ne mettra pas les mêmes choses en avant. Dans certains pays, les pistes sont importantes, dans d’autres les autoroutes le sont. Le truc le plus marquant qui me vient à l’esprit, c’est le fait que les cartes japonaises définissent un adresse en labélisant les blocs entre les rues, tandis que nous labélisons les rues entre les blocs.

Et je ne parle même pas du markéting

Qui est bien entendu radicalement différent selon la population visée, et non juste en termes d’i18n, mais aussi selon l’appartenance ethnique, sociale, culturelle, démographique, les handicape…

Il serait vraiment intéressant d’avoir un espèce de wiki open source qui, pour chaque groupe visé, liste les choses auxquelles il faut penser.

Finalement tout ça est trop chiant, z’ont qu’à se démerder ces estrangers de merde !

Arg, putain, on est 70 millions seulement, c’est nous les estrangers.

Après, ça ne veut pas dire qu’il faut tout faire, et trouver l’équilibre entre les ressources disponibles et le résultat qu’on vise. Mais il est bon de savoir ce qu’on décide de ne pas faire.

]]>
Comment foirer sa communication avec son client en une leçon 23http://sametmax.com/?p=18191http://streisand.me.thican.net/sametmax.com/index.php?20160216_162127_Comment_foirer_sa_communication_avec_son_client_en_une_le__on_23Tue, 16 Feb 2016 15:21:27 +0000article qui expliquait la difficulté à faire predre aux gens leur habitude de penser en tables. Malheureusement, si l'auteur a parfaitement réussi à faire passer son message aux lecteurs, il a complètement échoué à communiquer avec son client.]]>S.Lott est un excellent informaticien, très connu dans le monde de Python. J’ai régulièrement eu affaire à lui sur stackoverflow, et il sait de quoi il parle.

Je lisais son (excellent) article qui expliquait la difficulté à faire perdre aux gens leur habitude de penser en tables.

Malheureusement, si l’auteur a parfaitement réussi à faire passer son message aux lecteurs, il a complètement échoué à communiquer avec son client.

Voyez-vous, S.Lott fait preuve ici d’un péché auquel de nombreuses personnes intelligentes s’adonnent : il a raison.

Mais le client s’en branle.

Le client se fout complètement de savoir pourquoi on ne peut pas dumper ses données MongoDB telles-quel dans un tableau Excel. Il se fout complètement d’avoir tort.

Le client a un problème, et il souhaite une solution.

Notre taf n’est pas d’avoir raison. Notre taf est de résoudre des problèmes. On ne nous paie pas pour donner des leçons de justesse.

Donc, quand un client arrive avec une demande impossible à satisfaire, la bonne réaction à avoir n’est pas de lui expliquer que ce n’est pas possible. C’est inutile, improductif, et ça n’aide personne.

La bonne réaction à avoir, c’est de redéfinir le problème :

  • Quel objectif souhaite-t-il atteindre ?
  • Pourquoi il a défini cette solution ?
  • Quelles sont ses contraintes ?

Vous pouvez, bien entendu, dans le cadre de la démarche vers la mise en place de la bonne solution, lui expliquer pourquoi c’est la manière la plus appropriée. Pourquoi sa solution initiale ne va pas lui résoudre son problème.

Par exemple, dans le cadre de l’article, des gens demandent un dump de données hiérarchiques (document Mongo) vers un format tabulaire (tableau, Excel, csv…).

La mauvaise réponse est de dire que c’est impossible. Zéro intérêt pour les gens en face, et peut-être un léger sentiment d’humiliation. Ce n’est ni le lieu ni le moment pour l’enseignement, il y a d’autres opportunités bien plus appropriées pour ça.

Il y a pourtant une réponse très simple:

Je vois. Donnez-moi le schéma de vos données et à quoi doit ressembler la feuille Excel.

Il faut que vous sachiez qu’une base Mongo a un format de stockage très différent d’une feuille Excel, donc il est possible que ça demande des aménagements, où peut être choisir une solution différente. Il risque d’y avoir de la perte d’informations, ou pas mal de travail manuel pour dénormaliser les données. Il faut donc qu’on passe un peu de temps ensemble pour définir clairement le cadre de tout ça. J’aurais besoin de faire un tête-à-tête avec une personne qui sait à quoi va servir la feuille Excel en détail, savoir qui va la lire, dans quel but, etc. On peut prendre rendez-vous ?

Bien entendu, le client peut être votre patron. Ou votre collègue de bureau. Auquel cas le format de la conversation va changer, mais pas le sens général.

Le but est de comprendre ce que la personne veut faire.

Parfois (souvent même), en définissant clairement le problème, la personne voit que sa solution ne lui convient pas et vous demande par elle-même une alternative.

Une fois sur deux, vous n’aurez pas besoin de prendre rendez-vous, car la personne sent que quelque chose se passe et va se lancer dans la conversation : son problème simple à l’air plus compliqué que prévu. Elle va se rebeller : « c’est simple, tu me fais juste un dump de la base dans un bête fichier Excel, c’est tout ».

Les mots-clés sont « juste », « bête », « c’est tout ». On peut avoir aussi « simple », « évident », « c’est pas bien compliqué »…

Il suffit de rester calme, et avoir un comportement respectueux, mais d’expert. « J’ai déjà travaillé sur un problème similaire, je sais que je ne saurais pas résoudre ce problème correctement sans ces informations. Vous avez 2 minutes pour griffonner à quoi doit ressembler le fichier Excel sur un papier ? »

Vous la prenez à son propre jeu. Ça ne durera pas deux minutes. Car pour faire ça il faut avoir le schéma des données, les bonnes personnes en jeu, et une idée claire de l’usage final, ce qui rend de plus en plus clair, et sans argumenter, que le problème demande plus de travail que « juste », « bête » et « c’est tout ». C’est l’occasion de trouver la solution ensemble.

Bref, « laisse-moi te démontrer que c’est con ce que tu dis » n’est jamais une bonne phrase à dire, même enrobée dans des termes polis et une explication technique. Il faut rester factuel : voilà comment je comprends le problème, voilà ce qu’il me faut pour le résoudre.

En plus, le meeting de définition du problème, vous pouvez le facturer. Hey :)

]]>
Signes qu’il faut extraire une fonction. 18http://sametmax.com/?p=18179http://streisand.me.thican.net/sametmax.com/index.php?20160215_124132_Signes_qu___il_faut_extraire_une_fonction._18Mon, 15 Feb 2016 11:41:32 +0000Organiser son code est tout un art. Ça se peaufine au fur et à mesure d’une carrière. J’ai découvert au fil du temps des règles qui formalisent quelque chose que je faisais sans vraiment m’en rendre compte. Vous savez comme ces règles de français à la noix que vous utilisez quotidiennement sans savoir qu’il y a règle.

DRY au bout de 3

Je commence par la plus facile.

Le célèbre Don’t Repeat Yourself n’a pas besoin d’être poussée à l’extrême. Copier/coller du code une fois est tout à fait acceptable, et souvent bien plus productif que de refactoriser son code. On commence donc à faire une fonction si on a 3 copies de son code.

Dans ce code par exemple, j’utilise 3 fois mon trigger, il est mur pour la cueillette :

async def setup(self, cwd=None):
 
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    self.state = "init"
    await self.trigger('init')
 
    self.state = "ready"
    await self.trigger('ready')
 
    self.state = "running"
    return self.trigger('running')

Pouf:

def change_state(self, value):
    """ Change the inner state of the app, and call all state observers. """
    self.state = value
    return self.trigger(value)
 
async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    await self.change_state('init')
    await self.change_state('ready')
    return self.change_state('running')

Transformez vos commentaires en fonctions

C’est un conseil qui a apparemment été rendu populaire pas le courant Extreme Programming. Leur idée c’est que les noms de fonctions sont une forme de documentation. Chaque fonction devant être testée et documentée, si vous avez un commentaire qui se balade tout seul au milieu du code, c’est le signe qu’il faut en faire une fonction.

Dans le code qu’on vient de voir, j’ai un gros commentaire en plein milieu expliquant un bout de code un peu compliqué. Hop, on coupe :

async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    # Tell all component to hook them self to the events they want to react
    # to
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    await asyncio.gather(*futures)
 
    await self.change_state('init')
    await self.change_state('ready')
    return self.change_state('running')

Pouf:

async def setup_components(self):
    """ Give all components a chance to hook to life cycle events """
    futures = [ensure_awaitable(c.setup) for c in self.components.values()]
    return await asyncio.gather(*futures)
 
 
async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    await self.setup_components()
 
    await self.change_state('init')
    await self.change_state('ready')
    return self.change_state('running')

On voit ici que la présence du commentaire signalait que le code était assez complexe, et donc méritait d’être isolé. Cela permet de :

  • Rendre la fonction setup() plus facile à comprendre.
  • Isoler la complexité en une unité simple, plus facile à lire et à expliquer donc les limites et dépendances sont claires.
  • Et donc permettre un beau test unittaire juste pour cette petite fonction.

Une partie qui n’a rien à voir avec la choucroute mérite sa propre choucroute

Dans de grosses fonctions, on a souvent des bouts qui sont des étapes, mais qui ne sont pas liés sémantiquement aux autres étapes. Elles doivent juste arriver chronologiquement avant ou après celle-ci.

Toujours dans le même code, on a un bout qui initialise le project_dir de l’app. Ce snippet, c’est comme l’inspecteur Karamazov, fils unique, aucun lien. En tout cas aucun avec le setup de components ou le changement du state. Il est là car il doit arriver en premier. Aucune, aucune, aucune hésitation ! Amputation, SO-LU-TION !

async def setup(self, cwd=None):
 
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
    await self.setup_components()
 
    await self.change_state('init')
    await self.change_state('ready')
    return self.change_state('running')

Pouf :

def setup_environment(self, cwd=None):
    """ Set project dir for the current app """
    if cwd is None:
        cwd = get_project_dir()
    self.project_dir = Path(cwd)
 
async def setup(self, cwd=None):
 
    self.setup_environment(cwd)
 
    await self.setup_components()
 
    await self.change_state('init')
    await self.change_state('ready')
    return self.change_state('running')

On gagne en testabilité.

Et maintenant notre méthode setup() est très, très facile à comprendre. Son rôle est d’organiser 5 étapes. Pas de faire les étapes elle-même, ça c’est délégué. Le cycle de vie de l’application saute maintenant aux yeux. S’il y a un bug, on cherchera uniquement dans la fonction qui est responsable.

Sans le savoir, et sans vraiment le chercher, nous avons mis en oeuvre le principe de séparation des responsabilités. Seulement au lieu d’avoir cette notion floue et abstraite de responsabilité, on a maintenant quelques règles qui peuvent être facilement appliquées.

Diantre, je faisais donc de la prose, depuis toujours dans le savoir…

Pas de paramètre on/off

Le code de l’article d’hier était un cas d’école

Ma première version ressemblait à :

def resource_stream(resource, locations, encoding="utf8", ..., binary=False):
    """ Return a resource from this path or package """
    # du code, du code... puis:
    if binary:
        return stream
    return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)

On voit clairement ici que binary est un paramètre on/off, un truc qui déclenche un mode auquel on va passer un booléen.

Ça va s’utiliser comme ça:

txtstream =  resource_stream(...)
bstream =  resource_stream(..., True)

Quand on a des constantes magiques, il faut sortir la hache :

def binary_resource_stream(resource, locations):
    """ Return a resource from this path or package """
    ...
    return stream
 
 
def text_resource_stream(path, locations, encoding="utf8", ...):
    stream = binary_resource_stream(path, locations)
    return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)

La seconde fonction utilise la première. A l’usage le code est beaucoup plus clair :

txtstream =  text_resource_stream(...)
bstream =  binary_resource_stream(...)

Encore une fois, c’est plus facile à tester, plus facile à comprendre, et on diminue le nombre de branches dans le code.

Pour les gens particulièrement intéressés par la lisibilité du code, il existe l’indicateur de McCabe qui se base notamment sur le nombre de branches logiques. Le max recommandé est en général de 10, personnellement mes codes dépassent rarement 5. flake8 a l’option –max-complexity pour vérifier que votre code s’y tient.

]]>
Embarquer un fichier non Python proprement 8http://sametmax.com/?p=18160http://streisand.me.thican.net/sametmax.com/index.php?20160212_133851_Embarquer_un_fichier_non_Python_proprement_8Fri, 12 Feb 2016 12:38:51 +0000Ah, le packaging Python, c’est toujours pas fun.

Parmi les nombreux problèmes, la question suivante se pose souvent : comment je livre proprement un fichier de données fourni avec mon package Python ?

Le problème est double:

  • Comment s’assurer que setup.py l’inclut bien ?
  • Comment trouver et lire le fichier proprement ?

On pourrait croire qu’un truc aussi basique serait simple, mais non.

Ca ne l’est pas car un package Python peut être livré sous forme de code source, ou d’un binaire, ou d’une archive eggs/whl/pyz… Et en prime il peut être transformé par les packageurs des repos debian, centos, homebrew, etc.

Mais skippy a la solution à tous vos soucis !

Include les fichiers avec votre code

A la racine de votre projet, il faut un fichier MANIFEST.IN bien rempli et include_package_data sur True dans setup.py. On a un article là dessus, ça tombe bien.

N’utilisez pas package_data, ça ne marche pas à tous les coups.

Charger les fichiers dans votre code

Si vous êtes pressés, copiez ça dans un fichier utils.py:

import os
import io
 
from types import ModuleType
 
 
class RessourceError(EnvironmentError):
 
    def __init__(self, msg, errors):
        super().__init__(msg)
        self.errors = errors
 
 
def binary_resource_stream(resource, locations):
    """ Return a resource from this path or package """
 
    # convert
    errors = []
 
    # If location is a string or a module, we wrap it in a tuple
    if isinstance(locations, (str, ModuleType)):
        locations = (locations,)
 
    for location in locations:
 
        # Assume location is a module and try to load it using pkg_resource
        try:
            import pkg_resources
            module_name = getattr(location, '__name__', location)
            return pkg_resources.resource_stream(module_name, resource)
        except (ImportError, EnvironmentError) as e:
            errors.append(e)
 
            # Falling back to load it from path.
            try:
                # location is a module
                module_path = __import__(location).__file__
                parent_dir = os.path_dirname(module_path)
            except (AttributeError, ImportError):
                # location is a dir path
                parent_dir = os.path.realpath(location)
 
            # Now we got a resource full path. Just open it as a regular file.
            canonical_path = os.path.join(parent_dir, resource)
            try:
                return open(os.path.join(canonical_path), mode="rb")
            except EnvironmentError as e:
                errors.append(e)
 
    msg = ('Unable to find resource "%s" in "%s". '
           'Inspect RessourceError.errors for list of encountered erros.')
    raise RessourceError(msg % (resource, locations), errors)
 
 
def text_resource_stream(path, locations, encoding="utf8", errors=None,
                    newline=None, line_buffering=False):
    """ Return a resource from this path or package. Transparently decode the stream. """
    stream = binary_resource_stream(path, locations)
    return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)

Et faites:

from utils import binary_resource_stream, text_resource_stream 
data_stream = binary_resource_stream('./chemin/relatif', package) # pour du binaire 
data = data_stream.read() 
txt_stream = text_resource_stream('./chemin/relatif', package) # pour du texte text = txt_stream.read()

Par exemple:

image_data = binary_resource_stream('./static/img/logo.png', "super_package").read() 
text = text_resource_stream('./config/default.ini', "super_package", encoding="cp850").read()

Si vous n’êtes pas pressés, voilà toute l’histoire…

A la base, on fait généralement un truc du genre:

PROJECT_DIR = os.path.dirname(os.path.realpath(__file__)) # ou pathlib/path.py 
data = open(os.path.join(PROJECT_DIR, chemin_relatif