3.7 – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 La débâcle de async en 3.7 http://sametmax.com/la-debacle-de-async-en-3-7/ http://sametmax.com/la-debacle-de-async-en-3-7/#comments Tue, 07 Aug 2018 13:14:40 +0000 http://sametmax.com/?p=24891 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.]]> 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.

]]>
http://sametmax.com/la-debacle-de-async-en-3-7/feed/ 27 24891
Python 3.7 sort de sa coquille http://sametmax.com/python-3-7-sort-de-sa-coquille/ http://sametmax.com/python-3-7-sort-de-sa-coquille/#comments Thu, 28 Jun 2018 16:54:41 +0000 http://sametmax.com/?p=24711 La 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.

]]>
http://sametmax.com/python-3-7-sort-de-sa-coquille/feed/ 20 24711