Journal Découvrir Docker, Python, LLVM et Emscripten

Posté par  . Licence CC By‑SA.
Étiquettes :
33
23
fév.
2021

Sommaire

Alors que l'année 2020 se terminait, je me suis mis à faire un petit tour des outils qui sont rentrés dans mon quotidien pendant cette période. Ça t'intéresse ? Allez, regardons ensemble.

Docker

Je ne connaissais Docker que de nom et j'avais entendu ici et là que c'était chouette pour isoler des trucs. C'est vrai que c'est chouette.

Si tu développes une application sous Linux, c'est un vrai gain d'avoir une image Docker avec toutes les dépendances de ton application. D'une part cela te permet de retrouver très facilement la liste de ses dépendances ainsi que leurs versions, d'autre part cela t'autorise à lancer des builds dans un environnement exempt de parasitage. Fini le problème du build qui tire libfoo 2.4 quand t'as besoin de la 4.2.

Sur la CI c'est bien pratique. Tu lances ton build dans une image Docker qui ne contient que ce qui est nécessaire pour compiler. Si tu oublies des dépendances, tu le vois tout de suite ; si tu mets trop de dépendances, euh…, tu ne le vois toujours pas… Si tu as un projet qui nécessite awesome-tool 2.3 et un autre qui requiert awesome-tool 3.2, tu fais une image avec chaque version et ça roule.

Après pour le cas d'un projet ayant des dépendances spécifiques, en général ça se gère aussi facilement sans Docker en configurant correctement son build et en installant les dépendances dans le build et non pas au niveau système. Mais il y a quand même quelques outils qui n'aiment pas trop être dupliqués. Par exemple Python ou Ruby. Avec Python on s'en sort un peu avec virtualenv, même si ça m'a l'air un poil fragile du fait que cela crée des liens vers le Python du système. Avec Ruby, si je me souviens bien, c'est hyper galère, voire impossible, d'avoir plusieurs installations de Ruby Version Manager. Cela dit ça fait deux ans que j'ai regardé ça, cela a peut-être changé depuis.

Néanmoins, je n'irais pas utiliser une image Docker pour le développement au quotidien. Déjà parce que ça prend une éternité de télécharger les images, ensuite parce que c'est inconfortable au possible de compiler dans l'image. À la rigueur tu peux partager ton dossier de dev pour coder sur l'hôte et compiler dans l'image, mais alors tous les fichiers deviennent la propriété de root et ça devient vite crado.

Du coup il faut quand même quelque chose d'autre pour gérer les dépendances proprement.

Enfin le plus gros problème de Docker est que ça n'existe pas sous OSX et à peine sous Windows. Quand tu cibles toutes ces plates-formes ça devient vite limitant.

Au final j'utilise volontiers Docker sur la CI pour partir d'un truc frais à chaque build, et pour pouvoir relancer d'anciens builds, mais je crois que pour ce qui est des dépendances je préfère encore utiliser quelque chose au niveau du projet.

Python

J'ai fait très peu de Python durant ma carrière ; il faut dire que j'en avait une assez mauvaise image. Déjà c'est de l'interprété, donc tu ne peux rien faire de sérieux avec, et en plus c'est lent.

Finalement c'est pas si mal.

Venant du C++ j'apprécie en particulier les facilités de formatage de chaînes de caractères, de manipulations de listes, etc. Il y a un côté concis et direct de certaines opérations qui rendent l'écriture de code assez confortable.

Venant du C++ je n'apprécie guère le duck typing et le côté interprété du langage. Lors du premier jet c'est assez agréable, le script est propre et on a tout en tête, mais lorsqu'il faut y revenir plus tard par exemple pour ajouter un paramètre à une fonction, alors ça devient galère. On se met à la recherche de tous les appelants pour les modifier, puis les appelants des appelants. Et si tu en oublies tu ne le sauras pas tant que le chemin d'exécution ne passera pas par l'appel erroné. Quelle galère.

Alors tu vas me dire, ouiiiiii, tests unitaiiiiiiires, gna gna gna, ça oblige les devs à bien couvrir leur code. Oui, mais non. C'est quand même pénible.

De mon point de vue, un code doit être correct syntaxiquement, sémantiquement, et algorithmiquement. Les tests écrits par le développeur couvrent le dernier point. Pour les deux autres un outil peut parfaitement le faire. Valider la sémantique au niveau des tests c'est mélanger les problèmes, c'est pas très single responsibility principle.

Un truc qui me perturbe un peu en Python, c'est qu'il y a une certaine résistance à l'algorithmique — le côté négatif de la concision. Genre si t'écris une boucle for à l'ancienne, avec initialisation, condition d'arrêt et incrément, tu te sens sale. Par exemple, pour extraire les éléments d'une liste selon deux propriétés indépendantes, en Python on préférera « filtrer » deux fois la liste avec un prédicat pour chaque propriété, plutôt que de boucler une seule fois et tester les deux propriétés à chaque itération. Ce que je trouve discutable puisque le deuxième filtre va retester des éléments sélectionnés par le premier.

Enfin bon, c'est juste l'avis d'un dev C++. En dehors de ces soucis c'est franchement pas mal. J'en viens même à apprécier d'écrire mes scripts en Python plutôt qu'en Bash.

LLVM

Aaaah LLVM. Quel bel outil. C'est la base de Clang, un des meilleurs compilateurs C++ du moment. En plus de l'outil je me suis mis à travailler sur les composants de LLVM lui-même, c'est à dire l'API du langage intermédiaire.

Ces dernières années LLVM est monté à toute vitesse en popularité. Une base de code jeune et accessible, en C++ et orienté objet, ça rafraîchit. Et puis ils ont bien amélioré l'état des messages d'erreurs affichés par le compilateur par rapport à ce qu'on pouvait avoir avec GCC. Entre temps, du côté du vieux GCC, les devs se plaignaient de la complexité du code depuis des années. LLVM a offert un vent de renouveau, et tout le monde est parti vers lui, délaissant l'ancêtre qui, bien que fort de 30 années d'expérience, se voit mis au placard au profit du jeunot fraîchement sorti de l'œuf.

La grande force de LLVM c'est aussi l'ensemble des outils à disposition pour transformer et manipuler du code, notamment clang-tidy, pour vérifier certaines propriétés du code, et clang-format, pour formater le code selon certaines propriétés.

Savais-tu que LLVM était lui-même codé en C++ ? À coup sûr ça doit être hyper clean.

Argh. Et bien non, ce n'est pas hyper clean du tout. LLVM est l'incarnation du cordonnier mal chaussé. On y trouve des fichiers de dizaines de milliers de lignes de code, et de l'objet à foison, même quand ça ne colle pas du tout. Dans quel modèle objet est-il acceptable pour une instance de se downcaster ? La documentation est d'un côté excellente (cf. la référence du langage par exemple) et de l'autre côté complètement inutile (cf. une bonne vieille doc Doxygen pleine de « diagrammes de collaboration » inutiles, et qui ne fait que lister les méthodes sans expliquer l'intention).

À côté de ça on a des templates à foison et des compilations interminables. J'ai vaguement espéré contourner ça en utilisant des builds unitaires mais évidemment, entre les using namespace au niveau global et les #define qui ne sont jamais #undef, ça ne fonctionne pas.

Ajoute à tout ça des fichiers de plusieurs dizaines de milliers de lignes et une structure de dépôt bien chaotique, et c'est complet.

Bref, les outils fournis par LLVM sont vraiment top mais en interne c'est très décevant.

Emscripten

Emscripten est un compilateur C ou C++ vers JavaScript basé sur LLVM qui se veut être un remplaçant direct de GCC ou Clang pour compiler à peu près n'importe quel projet vers WebAssembly.

On en est à la troisième version : la première que je ne connais pas, la deuxième nommée fastcomp qui était un fork de LLVM, et la version actuelle qui s'appuie sur la version officielle de LLVM, sans modifications supplémentaires. Ce qui est assez chouette.

J'avais déjà tenté d'utiliser Emscripten il y a 8 ans pour faire une version web d'un jeu en C++ qui utilisait la SDL, OpenGL, Boost. Je ne me souviens pas du résultat mais je pense que ça n'avait pas donné grand chose. Il y a encore la trace des galères de compilation de Boost sur StackOverflow. Il faut dire qu'associer le pénible BJam avec le tout jeune Emscripten c'était un peu chercher les problèmes.

À titre personnel, je trouve que combiner du C et du JavaScript c'est un peu unir deux mondes complètement opposés ; et en dehors du fun de la technique j'ai quelques doutes sur l'utilité de l'outil. Ça permet de mon point de vue de ne pas avoir la performance du natif tout en n'ayant pas la simplicité de JavaScript, et en ne bénéficiant ni les outils développeurs du natif, ni de ceux du navigateur.

J'ai pu retoucher un peu à Emscripten récemment et j'ai découvert au passage l'existence de WASI, qui offre notamment un moyen de lancer les programmes WebAssembly en dehors du navigateur. Et là, franchement, l'idée de pouvoir écrire mon programme en C++, le compiler en WebAssembly, l'intégrer à une app Electron, installée via Flatpak, pour ensuite la lancer dans un Docker qui tourne dans une VM… Toutes ces indirections, ça me fait rêver.

Allez allez, j'arrête de blaguer là dessus.

Donc, j'ai refait un peu de WebAssembly pour le travail, et franchement c'est assez accessible. Effectivement ça remplace assez simplement un autre compilateur, et il y a de plus de nombreux points de customisation. On peut facilement, par exemple, remplacer la version de LLVM utilisée en back-end par une autre version. Facilement avec quelques limites quand même car Emscripten utilise des options de compilation en dur, qui doivent être comprises par le compilateur.

Pour ce qui est de la conversion C++ vers WebAssembly, on peut quasiment tout convertir tant qu'il n'y a pas de threads, pas d'exceptions, et autres subtilités.

Au final, il y a un truc que j'ai adoré en utilisant Emscripten, c'est l'accueil des développeurs. J'ai envoyé quelques petits patchs et ouvert quelques rapports de bug, et à chaque fois les retours étaient clairs, encourageants et constructifs. Un projet qui prend soin des nouveaux arrivants comme ça, fraaaaaaaanchement. C'est top.

C'est tout

Ces quatre projets sont les plus gros que j'ai découvert cette année. Peut-être que mon jugement est un peu à côté de la plaque ? Il faut dire que, de fait, je n'ai pas beaucoup d'expérience sur chacun.

Si tu en sais plus ou si tu vois des points remarquables à côté desquels je serai passé, n'hésite pas à me corriger dans les commentaires !

  • # Docker Desktop

    Posté par  (site Web personnel) . Évalué à 8 (+6/-0).

    Enfin le plus gros problème de Docker est que ça n'existe pas sous OSX et à peine sous Windows. Quand tu cibles toutes ces plates-formes ça devient vite limitant.

    Mouais, c'est pas totalement vrai. Docker Desktop te fait tourner Docker sous OSX et sous Windows, où d'ailleurs tu as le choix de faire tourner des conteneurs windows ou des conteneurs linux. Les dernières versions utilisent WSL2 et ça marche quand même vachement bien.

    Certains logiciels sont d'ailleurs packagés que comme des images docker, utilisables sous windows et mac genre GitPitch.

    Bref, tu peux avoir un soft packagé sous forme d'une image Docker et que tu peux faire tourner sur plusieurs architectures (x86, arm, windows) ainsi que sous windows + mac à base de conteneur linux. Y compris sur les derniers mac arm.

    • [^] # Re: Docker Desktop

      Posté par  . Évalué à 0 (+1/-1).

      La documentation sur Docker desktop semble ne pas être vraiment à jour. Dans la partie Linux / Ubuntu / Prerequisites :
      - Artful 17.10 (Docker Engine (Community) 17.11 Edge and higher only)
      - Xenial 16.04 (LTS)
      - Trusty 14.04 (LTS)

      La LTS 20.04 a presque un an. La LTS 18.04 a presque 3 ans. La LTS 16.04 ne sera plus supportée par Canonical dans quelques mois.

    • [^] # Re: Docker Desktop

      Posté par  . Évalué à 2 (+0/-0).

      Je ne connaissait pas Docker Desktop, merci.

      Est-ce que la version OSX permet d'avoir un conteneur OSX ? L'idéal pour moi serait de pouvoir lancer par exemple XCode dans un conteneur, ou quelque chose dans le genre.

      • [^] # Re: Docker Desktop

        Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

        Est-ce que la version OSX permet d'avoir un conteneur OSX ?

        Nope, ça n'existe pas.
        Les conteneurs (Docker, OCI) sont basés sur des fonctionnalités Linux uniquement. Les conteneurs Windows c'est autre chose, basé sur l'isolation intrinsèque à windows.
        Pour mac je n'ai pas connaissance de solution équivalente. L'idéal serait que ce soit développé pas Apple, mais je n'en ai pas entendu parlé.

    • [^] # Re: Docker Desktop

      Posté par  . Évalué à 4 (+2/-0).

      Mouais, c'est pas totalement vrai. Docker Desktop te fait tourner Docker sous OSX et sous Windows, où d'ailleurs tu as le choix de faire tourner des conteneurs windows ou des conteneurs linux. Les dernières versions utilisent WSL2 et ça marche quand même vachement bien.

      Dans les 2 cas, il s'agit de systèmes de VM. Sur windows, j'ai cru comprendre que c'etait pas trop mal meme si les acces disques sont pas encore au top, sur macOS, c'est nettement plus lent.

      Si c'est pour utiliser tous les jours des containers docker sur ces environnements, je crois que le plus simple et plus performant reste encore d'avoir une VM linux avec Virtualbox et executer docker a partir de cette machine.

      • [^] # Re: Docker Desktop

        Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

        Si c'est pour utiliser tous les jours des containers docker sur ces environnements, je crois que le plus simple et plus performant reste encore d'avoir une VM linux avec Virtualbox et executer docker a partir de cette machine.

        Oui et non. C'est une solution, mais de là à dire que c'est plus performant je ne pense pas.

        Comme tu dis, le gros point noir c'est le partage de fichier.
        Sous Windows comme sous Mac les choses s'améliorent et c'est justement ce que tu ne peux pas retrouver avec une VM sous Virtualbox ou autre.
        Voir par exemple ce ticket sur la roadmap publique : https://github.com/docker/roadmap/issues/7
        Certaines améliorations sont activées par défaut depuis la version 2.4.0.0 sortie fin septembre : https://docs.docker.com/docker-for-mac/release-notes/#docker-desktop-community-2400

        Pour Windows il y a un certain nombre de posts sur le blog de docker sur ce sujet, par exemple https://www.docker.com/blog/new-filesharing-implementation-in-docker-desktop-windows/

        Dans tous les cas, partager de nombreux fichiers entre 2 VM est toujours compliqué. Que la VM supporte les fichiers ou que ce soit la machine, le problème reste entier et tout dépend l'action qu'on fait.
        C'est aussi très variable suivant les frameworks par exemple.

        • [^] # Re: Docker Desktop

          Posté par  . Évalué à 3 (+1/-0).

          Oui et non. C'est une solution, mais de là à dire que c'est plus performant je ne pense pas.

          Comme tu dis, le gros point noir c'est le partage de fichier.
          Sous Windows comme sous Mac les choses s'améliorent et c'est justement ce que tu ne peux pas retrouver avec une VM sous Virtualbox ou autre.

          J'avais teste il y a un an avec des collègues sur windows et plus récemment sur macOS, c'etait pour du developpement python, et c'etait le jour et la nuit. Ca marchait nettement mieux. Par contre, je me souviens plus si les fichiers étaient montes dans la VM ou si ils faisaient parti de la VM.

          Apres tant mieux si docker a bosse dessus depuis, j’étais pas au courant.

          • [^] # Re: Docker Desktop

            Posté par  (site Web personnel) . Évalué à 3 (+1/-0).

            Si tous tes fichiers sont uniquement dans la VM et si tu n'édites tes fichiers que dans la VMs, alors oui forcément c'est mieux. C'est indiscutable, puisque les fichiers sont locaux.

            Si tes fichiers sont dans la VM mais que tu les édites depuis l'hôte ou l'inverse, alors tu peux avoir un problème. Sauf que Docker a spécifiquement changé des choses pour l'améliorer, ce que tu ne peux pas avoir avec Virtualbox.

            Mais le problème est complexe, et oui c'est un frein malheureusement.

            Mitchell Hashimoto (Hashicorp) utilise le principe de la VM, éditant tous ces fichiers dans la VM directement (donc en fait sans partage de fichier) https://twitter.com/mitchellh/status/1346136404682625024
            C'est un bon exemple de ce qui peut se faire.

    • [^] # Re: Docker Desktop

      Posté par  . Évalué à 3 (+2/-0).

      Mouais…
      Ça impose parfois des couches de complexité supplémentaires.
      Quand on se retrouve avec un PC Windows d'entreprise, derrière un proxy http, au secours ! Ce n'est pas impossible, mais ça fait perdre beaucoup de temps en paramétrage.
      J'ai fini par faire du remote sur kimsufi: VSCode en mode "remote ssh".
      Ça impose une connexion permanente mais:
      - ça permet de pouvoir retrouver son environnement, même en changeant de PC (PC de boulot, vieux PC perso, mac de riche)
      - ça permet de résoudre le pb de bande passante (machine chez OVH)

      • [^] # Re: Docker Desktop

        Posté par  (site Web personnel) . Évalué à 4 (+2/-0).

        Ça n'a pas grand chose à voir avec Docker ça. C'est la même chose quand tu as du maven, des jar, des gems, ou n'importe quel besoin totalement classique.
        Le problème est ici lorsque c'est mal fait :

        entreprise, derrière un proxy http

  • # docker sans être root

    Posté par  . Évalué à 4 (+4/-0).

    À la rigueur tu peux partager ton dossier de dev pour coder sur l'hôte et compiler dans l'image, mais alors tous les fichiers deviennent la propriété de root et ça devient vite crado.

    Tu peux essayer de démarrer tes container de build avec les options

    --user $(id -u $USER) --net host
    De cette manière, des fichiers restent les tiens, et tu profites de la config réseau de ton linux.

  • # Python

    Posté par  . Évalué à 5 (+3/-0).

    Venant du C++ je n'apprécie guère le duck typing et le côté interprété du langage. Lors du premier jet c'est assez agréable, le script est propre et on a tout en tête, mais lorsqu'il faut y revenir plus tard par exemple pour ajouter un paramètre à une fonction, alors ça devient galère. On se met à la recherche de tous les appelants pour les modifier, puis les appelants des appelants. Et si tu en oublies tu ne le sauras pas tant que le chemin d'exécution ne passera pas par l'appel erroné. Quelle galère.

    Il faut configurer ton IDE pour tourner flake8, ca t'attrape pas mal de soucis:

    https://flake8.pycqa.org/en/latest/index.html#quickstart

    Un truc qui me perturbe un peu en Python, c'est qu'il y a une certaine résistance à l'algorithmique — le côté négatif de la concision. Genre si t'écris une boucle for à l'ancienne, avec initialisation, condition d'arrêt et incrément, tu te sens sale. Par exemple, pour extraire les éléments d'une liste selon deux propriétés indépendantes, en Python on préférera « filtrer » deux fois la liste avec un prédicat pour chaque propriété, plutôt que de boucler une seule fois et tester les deux propriétés à chaque itération. Ce que je trouve discutable puisque le deuxième filtre va retester des éléments sélectionnés par le premier.

    Lapin compris. Tu aurais un exemple ?

    • [^] # Re: Python

      Posté par  . Évalué à 5 (+4/-0).

      Lapin compris. Tu aurais un exemple ?

      En l'occurrence je pensais à un truc qui ressemble à ce qui suit. Intuitivement j'aurais écris quelque chose comme :

      some_list = # …
      
      list_a = []
      list_b = []
      
      for s in some_list:
          if s[0] == 'a':
              list_a.append(s)
          elif s[0] == 'b':
              list_b.append(s)

      Mais j'ai l'impression qu'on me dirait que ce serait plus Pythonic de faire :

      def starts_with_a(s):
          return s[0] == 'a'
      
      def starts_with_b(s):
          return s[0] == 'b'
      
      some_list = # …
      
      list_a = list(filter(starts_with_a, some_list))
      list_b = list(filter(starts_with_b, some_list))

      Cette notion de "Pythonic", on la retrouve notamment dans des réponses sur StackOverflow. Par exemple :

      Using an additional state variable, such as an index variable (which you would normally use in languages such as C or PHP), is considered non-pythonic.

      Et là, perso, je me dis qu'utiliser des indices dans les boucles répond au problème de l'itération depuis 1792, et n'importe quel programmeur de n'importe quel langage peut comprendre une telle boucle. Est-ce que ça vaut le coup de l'éviter ? Pour quel gain ?

      • [^] # Re: Python

        Posté par  . Évalué à 5 (+6/-0). Dernière modification le 23/02/21 à 11:54.

        La version "pythonic" tendrait plus vers ça

        list_a = [s for s in some_list if s.startswith('a')]
        list_b = [s for s in some_list if s.startswith('b')]

        Je suis d'accord qu'il faut un peu changer sa façon de penser.

        Note: il y a certainement une formule alambiquée pour ne passer qu'une seule fois dans some_list tout en restant "pythonic"

        • [^] # Re: Python

          Posté par  . Évalué à 4 (+5/-0). Dernière modification le 23/02/21 à 12:16.

          Ici tu aura une idée de formules alambiquées:
          https://stackoverflow.com/questions/949098/python-split-a-list-based-on-a-condition

          Par contre, je te mets en garde de ne pas tomber dans le travers des gens qui font du python: passer son temps à trouver une manière plus élégant d'écrire 2 lignes de code sans rien gagner à côté

          • [^] # Re: Python

            Posté par  . Évalué à 1 (+0/-0).

            Ah, mais si tu veux de l'alambiqué, il y a toujours moyen de s'arranger:

            import numpy as N
            
            @N.vectorize 
            def starts_with(x, val): 
                return x[0] == val
            
            liste_a = list(N.array(some_list)[N.where(starts_with(some_list,"a"))])
            liste_b = list(N.array(some_list)[N.where(starts_with(some_list,"b"))])
            • [^] # Re: Python

              Posté par  (site Web personnel) . Évalué à 2 (+1/-0).

              import numpy as N

              Rien que ça c'est alambiqué, respectons les conventions pour commencer ;)
              Sinon avec Numpy il y a plus simple:

              import numpy as np
              arr = np.array(some_list)
              arr[np.chararray.startswith(arr, 'a')]
              • [^] # Re: Python

                Posté par  . Évalué à 1 (+0/-0).

                Le but n'étais pas de faire simple :-p

                Par contre je suis surpris. J'avais sorti numpy en pensant que ce serait forcément tordu vu qu'adapté pour les tableaux numériques et pas pour le texte… je ne connaissais pas les chararray.

        • [^] # Re: Python

          Posté par  . Évalué à 4 (+3/-0).

          Du coup, si l'idée est de passer par des générateurs pour éviter de créer des listes pour les parser, il y a encore mieux:

          generator_a = (s for s in some_list if s.startswith('a'))
          generator_b = (s for s in some_list if s.startswith('b'))

          Notez la subtile distinction.

        • [^] # Re: Python

          Posté par  . Évalué à 4 (+2/-0).

          Allez, on dégaine un iPython, c'est parti.

          In [4]: some_list = 'setanéstebnsetanustebnésatenastubnestpnesanuastnbtsnastbnstanstbnsteanutsenbstanstbnstaà' * 1000
          
          In [7]: %%timeit
             ...: 
             ...: list_a = []
             ...: list_b = []
             ...: 
             ...: for s in some_list:
             ...:     if s[0] == 'a':
             ...:         list_a.append(s)
             ...:     elif s[0] == 'b':
             ...:         list_b.append(s)
             ...: 
             ...: 
          6.08 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          In [8]: %%timeit
             ...: 
             ...: list_a = [s for s in some_list if s.startswith('a')]
             ...: list_b = [s for s in some_list if s.startswith('b')]
             ...: 
             ...: 
          11.8 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          In [10]: def starts_with_a(s):
              ...:     return s[0] == 'a'
              ...: 
              ...: def starts_with_b(s):
              ...:     return s[0] == 'b'
              ...: 
          
          In [11]: %%timeit
              ...: 
              ...: list_a = list(filter(starts_with_a, some_list))
              ...: list_b = list(filter(starts_with_b, some_list))
              ...: 
              ...: 
          10.4 ms ± 187 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          In [12]: %%timeit
              ...: generator_a = (s for s in some_list if s.startswith('a'))
              ...: generator_b = (s for s in some_list if s.startswith('b'))
              ...: 
              ...: 
          401 ns ± 1.17 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
          
          In [13]: %%timeit
              ...: generator_a = (s for s in some_list if s.startswith('a'))
              ...: generator_b = (s for s in some_list if s.startswith('b'))
              ...: 
              ...: len(list(generator_a))
              ...: len(list(generator_b))
              ...: 
              ...: 
          12 ms ± 113 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
          
          In [14]: len(some_list)
          Out[14]: 88000
          

          Sur une telle liste, moyennement grosse, les générateurs ont l'air d'aller légèrement plus vite. C'était un test rapide et sûrement pourrave.

          • [^] # Re: Python

            Posté par  . Évalué à 1 (+1/-1).

            Si tu fais du python, c'est que les performances n'est pas ta priorité. Ça ne veut pas dire qu'il ne faut pas la prendre en compte lorsqu'on a plusieurs choix mais il vaut mieux choisir ce qui est le plus clair.

            • [^] # Re: Python

              Posté par  . Évalué à 4 (+3/-0).

              Tu risque vite de te retrouver avec des scripts inutilisables car trop lents ou trop en gourmand en mémoire si tu ne fais pas attention et fait n'importe quoi. Ceci-dit, utiliser les méthodes très "pythoniques" telles que les compréhension de listes ou les générateurs plutôt que faire de grosses boucles sur les itérables doivent te permettre de gagner sur tous les tableau: ce sont des écritures assez intuitives à lire et écrire (mais différentes de ce qu'on fait en C, donc il faut s'habituer), mais qui ont été /a priori/ optimisées.

              Écrire du code "pythonique" n'est pas du snobisme, c'est utiliser python tel qu'il a été pensé, et donc de manière /a priori/ optimisée. C'est dans ce sens là qu'il faut prendre l'écriture "la plus claire".

          • [^] # Re: Python

            Posté par  (site Web personnel) . Évalué à 6 (+5/-0).

            En l'occurrence la version la plus rapide est la simple boucle for. Et il y a pas de mal a en utiliser une quand le besoin se justifie (liste avec pas mal d'éléments). Je vois régulièrement des gens faire des compréhension de liste ([s for s in some_list if s.startswith('a')]) alambiquées juste pour ne pas faire une vraie boucle for, en oubliant que le plus important est encore de privilégier la lisibilité.

            Sinon une autre solution pour faire une seule itération en peu moins verbeux:

            from collections import defaultdict
            lists = defaultdict(list)
            for s in some_list: 
               lists[s[0]].append(s)
          • [^] # Re: Python

            Posté par  . Évalué à 6 (+4/-0).

            Attention. Dans la première version, tu n'utilises pas startswith, et en l'occurence ici, c'est startswith qui est lent.

            In [1]: some_list = 'setanéstebnsetanustebnésatenastubnestpnesanuastnbtsnastbnstanstbnsteanutsenbstanstbnstaà' * 1000
            
            In [2]: %%timeit
               ...: 
               ...: list_a = []
               ...: list_b = []
               ...: 
               ...: for s in some_list:
               ...:     if s[0] == 'a':
               ...:         list_a.append(s)
               ...:     elif s[0] == 'b':
               ...:         list_b.append(s)
            5.55 ms ± 260 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
            
            In [3]: %%timeit
               ...: 
               ...: list_a = [s for s in some_list if s.startswith('a')]
               ...: list_b = [s for s in some_list if s.startswith('b')]
            10.6 ms ± 253 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
            
            In [4]: %%timeit
               ...: 
               ...: list_a = [s for s in some_list if s[0]=='a']
               ...: list_b = [s for s in some_list if s[0]=='b']
            5.89 ms ± 48.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
            

            Et comme cela a été dit, la manière la plus pythonique, c'est probablement de se poser la question « ai-je vraiment besoin d'une liste ? Un iterable comme un générateur ou le résultat d'un filter ne me suffirait-il pas ? »

            Et c'est la limite de ce genre de truc assez stupide de construction d'objet. Construire une liste n'est pas l'objectif en soit. Et c'est un peu ce que je reproche à nombre de personnes qui font du python qui essaient de mimiquer des constructions classiques alors qu'il faudrait plutôt se poser la question avant. (Même problème dans d'autres langages, en particulier avec le C++ moderne).

            • [^] # Re: Python

              Posté par  . Évalué à 3 (+2/-0).

              Effectivement, passer par une boucle for explicite ou une compréhension de liste ne vas pas vraiment impacter la performance. Par contre, l'intérêt de la compréhension de liste est que l'on peut facilement basculer dans le monde des générateurs: il suffit de remplacer les [..] par des (..).

      • [^] # Re: Python

        Posté par  . Évalué à 2 (+1/-1).

        Et là, perso, je me dis qu'utiliser des indices dans les boucles répond au problème de l'itération depuis 1792, et n'importe quel programmeur de n'importe quel langage peut comprendre une telle boucle. Est-ce que ça vaut le coup de l'éviter ? Pour quel gain ?

        Oui, c'est un peu la règle d'or pour avoir des performances acceptables en python: ne pas écrire de for loop et vectoriser au maximum.

      • [^] # Re: Python

        Posté par  . Évalué à 5 (+4/-0).

        Je penses que l'intérêt de cette notation est de ne générer ta liste que quand tu en a besoin, mais du coup c'est con de convertir en list tes filtres. L'idée serait plutôt de pouvoir faire ici:

        filter_a=filter(lambda x:x[0]=="a", some_list)
        filter_b=filter(lambda x:x[0]=="b", some_list)
        
        for stuff in filter_a:
            play_with(stuff)
        
        for stuff in filter_b:
            play_with(stuff)

        Avec cette méthode, tu ne va jamais créer de liste list_a et list_b potentiellement très longue te bouffant ta mémoire. Bien entendu, cette méthode n'est pas nécessairement adaptée à toute situation, mais il s'agit une méthode très "pythonique" de gérer le problème en optimisant la mémoire.

        • [^] # Re: Python

          Posté par  . Évalué à 2 (+0/-0). Dernière modification le 25/02/21 à 16:51.

          Avec python c'est très intuitif, mais il ne faudrait pas oublier que le générateur et l'itérateur sont des patrons de conception qui date du GoF, valables pour tous les langages OO.

          Bref, l'exemple initial avec une liste vide/tableau qu'on étend (ou qu'on ne pourrait pré-allouer que si on connaissait la taille) est déjà un antipattern, y compris en C++.

      • [^] # Re: Python

        Posté par  . Évalué à 5 (+5/-0). Dernière modification le 23/02/21 à 14:25.

        Alors les index même en C++ maintenant on essaye aussi de les éviter au profit des iterateurs…

        Mais le passage que tu cite :

        Using an additional state variable, such as an index variable (which you would normally use in languages such as C or PHP), is considered non-pythonic.

        Est vraiment très spécifique pour essayer de dire de ne pas faire des boucles sur des conteneur en utilisant les indices genre ça :

        for i in range(len(ma_liste)):
          if ma_liste[i].startswith('a'):
            list_a.appends(ma_liste[i])
          elif ma_liste[i].startswith('b'):
            list_b.appends(ma_liste[i])

        Car effectivement c'est pas pythonique et assez lent. Les boucle en python sont optimisées pour itérer sur des conteneurs ou des generateurs.

        Mais ça ne veut pas dire que faire tout en fonctionnel est plus pythonique, juste qu'il faut iterer sur les élément et pas sur les index. Quand on a spécifiquement besoin de l'index (en plus de la valeur) on utilise enumerate() pour avoir en plus l'index. Mais c'est un peu pareil en C++11, C++14, C++17 ou C++20 car faire des boucles par iterateur évite beaucoup de soucis et peut être aussi rapide que par index. C'est juste un idiom plus récent. d'ailleurs on le voit comme idiom principale des boucles dans les langages récents comme le rust.

        Bon par contre quand on a appris en faisant toutes ses boucles par index c'est dur de ce le sortir de la tête.

      • [^] # Re: Python

        Posté par  . Évalué à 2 (+1/-1).

        De mon point de vue c’est pas tant une question d’idiome (encore que, ça l’est un peu), mais surtout une façon d’exprimer le code façon beaucoup plus robuste et concise.

        La version for loop force le lecteur à garder en tête l’input, garder un œil sur la logique d’incrémentation et traduire le if/append en semantique haut niveau.
        Tu peux aussi avoir des blagues, genre modifications concurrentes sur le tableau, ou autre effet de bords qui viennent foutre la grouille.
        T’es obligé de transformer en compilateur pour d’abord comprendre ce que ça fait en pratique, et c’est dur de savoir ce que le programmeur voulait faire en premier lieu.

        La version « fonctionnelle » supprime le bruit, se lit beaucoup plus facilement et surtout, communique l’intention de façon beaucoup plus claire. Je peux scanner le code et immédiatement voir que la liste est filtrée 2 fois.
        Ah, et ça évite (ou tout du moins, réduit considérablement) les risques d’effets de bords.
        Le runtime peut aussi décider de paralléliser le code s’il le veut/peut (non pas que je connaisse un seul language qui fait ça, mais il me semble que swift travaille dessus).
        En gros, la logique de 99% de ces boucles for sont du genre « pour chaque élément, applique cette opération sur l’element », ce qui n’est absolument pas ce qu’exprime une boucle for «fait tourner ce bout de code compliqué de 0 à infinité de fois ». La version fonctionnelle est beaucoup plus proche de la réalité.

        Ca ouvre aussi la porte à l’évaluation paresseuse. Les streams Java typiquement font ça, tu peux construire ton stream et rien ne se passe tant que t’essayes pas de le collecter.

        Et là, perso, je me dis qu'utiliser des indices dans les boucles répond au problème de l'itération depuis 1792, et n'importe quel programmeur de n'importe quel langage peut comprendre une telle boucle.

        Mouais. On pourrait dire la même chose à propos du if/else et des jumps assembleurs. Ou du c orienté objet. Ça marche, effectivement, c’est juste que la semantique n’est pas la.

        Linuxfr, le portail francais du logiciel libre et du neo nazisme.

    • [^] # Re: Python

      Posté par  . Évalué à 5 (+4/-0).

      Et pour le typage en python, il y a la solution mypy qui est assez efficace.

      Effectivement, ça va un peu ralentir l'écriture du code python vu que l'on recommence à se préoccuper des types, mais d'expérience, c'est bien utile pour éviter des bugs dès qu'un projet est un peu complexe.

      • [^] # Re: Python

        Posté par  . Évalué à 2 (+0/-0).

        Et surtout, tu peux l'introduire progressivement, par exemple au moment d'écrire le TU uniquement pour valider la sémantique comme l'évoque l'auteur. Et un TU d'évité !

    • [^] # Re: Python

      Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

      Il faut configurer ton IDE pour tourner flake8, ca t'attrape pas mal de soucis:

      En plus de flake8 et de mypy (conseillé par THE_ALF_), je conseille aussi d’utilisé black pour formater le code automatiquement. C’est l’équivalent de gofmt et ça évite les débats à rallonge sur le style.

      Pour mypy, je conseille d’utiliser --strict qui remonte pas mal d’erreur qui peuvent être catastrophique en production.

  • # Tu t'es fait pythonisé :)

    Posté par  (site Web personnel) . Évalué à 5 (+3/-0). Dernière modification le 23/02/21 à 10:31.

    J'en viens même à apprécier d'écrire mes scripts en Python plutôt qu'en Bash.

    Désolé mais tu viens de croquer la pomme :)

    T'es foutu maintenant, chaque fois que tu vas vouloir écrire un script, il faudra que tu te poses la question : python / shell ou un mélange des 2 ?

    Et je peu même te recommander un bon bouquin : Scripting Python Sous Linux sur le sujet ;) (enfin en partie)

  • # Docker, faux problèmes

    Posté par  . Évalué à 1 (+0/-0).

    En vrai, les soucis que tu décris sur la lenteur de téléchargement des images, ça n'existe que si tu refuses de garder en local les images téléchargées. En plus tu parles du cas d'une IC, mais justement, tu vas vouloir avoir les images sous la main dans ce cas précis. Et si tu veux accélérer tes téléchargements, tu peux toujours te faire un repo proxy avec Nexus, un peu comme tu ferais un repo Debian local pour accélérer les téléchargements de paquets Debian. Bref, c'est plutôt à toi de voir selon ton usage.
    À noter qu'on dit Docker par ci, docker par là, mais il y a plein d'autres outils compatibles. Par exemple, Buildah est largement plus pratique pour tous ce qui est cas d'utilisation en intégration continue. Et ça tourne en rootless.

    • [^] # Re: Docker, faux problèmes

      Posté par  (site Web personnel) . Évalué à 2 (+0/-0).

      Par exemple, Buildah est largement plus pratique pour tous ce qui est cas d'utilisation en intégration continue

      Ça m'intéresse, tu aurais des exemples concrets ?
      Parce que à chaque fois que j'ai voulu regarder buildah, j'ai l'impression de revenir en arrière. Utiliser un script shell pour construire une image, ben c'est tout sauf pratique dans la majorité des cas que j'ai vu.

      • [^] # Re: Docker, faux problèmes

        Posté par  . Évalué à 1 (+0/-0). Dernière modification le 24/02/21 à 15:14.

        J'ai un example fonctionnel sous le coude, pour faire une image docker :

        buildah pull docker.io/alpine:3.12
        buildah pull docker.io/adoptopenjdk/openjdk8-openj9:alpine-jre
        
        timestamp=$(date +%Y%m%dT%H%M%S)
        
        image_name=minecraft-papercraft
        
        build_and_push() {
            local papercraft_build=$1
            local branch=$2
            local version=${branch}-${timestamp}
            local image_full_name=${image_name}:${version}
            local image_name_no_timestamp=${image_name}:${branch}
        
            # use docker format as a workaround for corruption
            # https://github.com/containers/buildah/issues/1589 and https://github.com/containers/image/pull/1089
            buildah bud -t "${image_full_name}" \
                --format=docker \
                --build-arg BUILD=${papercraft_build} \
                --build-arg BRANCH=${branch} .
            buildah tag "${image_full_name}" ${image_name_no_timestamp}
            buildah push "${image_full_name}" ${BUILDAH_PUSH_REPOSITORY}/${image_full_name}
            buildah push "${image_name_no_timestamp}" ${BUILDAH_PUSH_REPOSITORY}/${image_name_no_timestamp}
        }

        Il y a aussi buildah run, quand c'est pour faire un build avec une inage docker, mais je n'ai pas d'exemple. Je sais juste qu'on peut l'utiliser à peu près comme docker run

        docker run --rm -ti -v $PWD:/root ubuntu /bin/bash -xe <<EOF
          git clone http://...
          ...
        EOF
        • [^] # Re: Docker, faux problèmes

          Posté par  (site Web personnel) . Évalué à 3 (+1/-0).

          J'ai du mal à voir quel est vraiment la différence avec ceci :

          docker pull docker.io/alpine:3.12
          docker pull docker.io/adoptopenjdk/openjdk8-openj9:alpine-jre
          
          timestamp=$(date +%Y%m%dT%H%M%S)
          
          image_name=minecraft-papercraft
          
          build_and_push() {
              local papercraft_build=$1
              local branch=$2
              local version=${branch}-${timestamp}
              local image_full_name=${image_name}:${version}
              local image_name_no_timestamp=${image_name}:${branch}
          
              docker build -t "${image_full_name}" \
                  --build-arg BUILD=${papercraft_build} \
                  --build-arg BRANCH=${branch} \
                  .
              docker tag "${image_full_name}" ${BUILDAH_PUSH_REPOSITORY}/${image_full_name}
              docker tag "${image_full_name}" ${BUILDAH_PUSH_REPOSITORY}/${image_name_no_timestamp}
              docker push ${BUILDAH_PUSH_REPOSITORY}/${image_full_name}
              docker push ${BUILDAH_PUSH_REPOSITORY}/${image_name_no_timestamp}
          }

          En quoi buildah est plus intéressant que docker ici ?

          • [^] # Re: Docker, faux problèmes

            Posté par  . Évalué à 2 (+1/-0).

            Pas besoin d'avoir de droits sur le groupe docker ou équivalent, ou d'avoir accès au socket fichier du daemon docker, donc c'est nettement plus sécurisé que Docker. Ça tourne facilement même dans des chroot ou des containers, ça tourne sans daemon donc tu peux en avoir des dizaines qui tournent en parallèle sur la même machine sans impact. Pour faire le ménage, tu peux te contenter d'effacer bêtement ton $HOME/.buildah (chemin non contractuel), alors que sous docker nettoyer les déchets du build est une vraie douleur.
            Bref, c'est vraiment mieux pour de l'intégration continue.

    • [^] # Re: Docker, faux problèmes

      Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

      À noter qu'on dit Docker par ci, docker par là, mais il y a plein d'autres outils compatibles. Par exemple, Buildah…

      ou encore nix

  • # LLVM

    Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

    La grande force de LLVM c'est aussi…

    C'est aussi la license BSD et les front-ends pour plein de langages

    • [^] # Re: LLVM

      Posté par  . Évalué à 4 (+3/-1).

      Ainsi qu’un project lead qui est pas obsédé par saborder son propre projet pour des raisons politiques douteuses.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

  • # Refactoring en Python

    Posté par  (site Web personnel) . Évalué à 0 (+1/-3).

    Venant du C++ je n'apprécie guère le duck typing et le côté interprété du langage. Lors du premier jet c'est assez agréable, le script est propre et on a tout en tête, mais lorsqu'il faut y revenir plus tard par exemple pour ajouter un paramètre à une fonction, alors ça devient galère. On se met à la recherche de tous les appelants pour les modifier, puis les appelants des appelants. Et si tu en oublies tu ne le sauras pas tant que le chemin d'exécution ne passera pas par l'appel erroné. Quelle galère.

    Je ne comprends pas trop ce problème, en fait : il suffit d'aller sur la fonction ou méthode à renommer, puis faire Shift-F6 (ou équivalent dans l'IDE) et choisir le nouveau nom de la méthode. L'IDE s'occupera tout seul de modifier les appels de cette méthode.
    Concernant le duck-typing (ou plutôt le typage dynamique), je trouve que c'est, la majeure partie du temps, un mauvais argument.
    Un IDE et un code corrects vont permettre de l'inférence de type et tu te retrouves rapidement avec les mêmes garanties de type que dans un langage typé statiquement (au moins dans l'IDE). Quand il n'est pas possible d'inférer le type, tu peux aider l'IDE avec des annotations.
    Même quand je prototype, je mets les annotations nécessaires pour avoir un typage complet, par exemple.

    • [^] # Re: Refactoring en Python

      Posté par  . Évalué à 2 (+1/-0).

      Un IDE et un code corrects vont permettre de l'inférence de type et tu te retrouves rapidement avec les mêmes garanties de type que dans un langage typé statiquement (au moins dans l'IDE). Quand il n'est pas possible d'inférer le type, tu peux aider l'IDE avec des annotations.

      Sur une liste/dict/… où le type n'a pas été spécifié tu n'auras pas l'inférence de type. Et pareil si tu te retrouves avec une fonction qui ne retourne pas toujours le même type (on est d'accord qu'on sort du code correct).

      • [^] # Re: Refactoring en Python

        Posté par  . Évalué à 2 (+0/-0). Dernière modification le 25/02/21 à 17:10.

        Honnêtement, même lorsque je ne l'ai pas je tape un espace après la variable, puis si je veux typer une chaine: "". et c'est parti pour la complétion

        Le problème est le même pour n'importe quel langage qui comporte des collections non typées ou la généricité. any, void* même combat. Python a aussi la généricité et le cast si besoin https://mypy.readthedocs.io/en/stable/casts.html

        • [^] # Re: Refactoring en Python

          Posté par  . Évalué à 2 (+1/-0).

          Sauf que sur les autres langages le type est toujours indiqué parce que c'est obligatoire (même si c'est Any/Object/…), en python une majeur partie de l'éco-système n'a pas (encore) été mis à jour pour l'indiquer. Après il suffit d'aller jeter un oeil au code donc c'est pas dramatique

  • # Emscripten

    Posté par  (site Web personnel) . Évalué à 4 (+3/-1).

    À titre personnel, je trouve que combiner du C et du JavaScript c'est un peu unir deux mondes complètement opposés ; et en dehors du fun de la technique j'ai quelques doutes sur l'utilité de l'outil.

    Perso ça me permet d'utiliser mon code en ligne : MediaInfoOnline.

    Pas besoin de tout recoder, j'utilise le même code partout, juste à compiler avec les navigateurs comme cible + petite glue et c'est parti! C'est l'utilité de l'outil (et le C/C++ c'est vraiment un bon choix à long terme, pas à changer tous les X années de langage à la mode…)

    Emscripten v2 (basé sur LLVM direct) améliore la taille, les améliorations se voient…

    Emscripten, c'est bon, mangez-en!

    • [^] # Re: Emscripten

      Posté par  . Évalué à 4 (+2/-0).

      As-tu rencontré des soucis dans les limitations d'Emscripten concernant les threads, exceptions ou autres ?

      • [^] # Re: Emscripten

        Posté par  (site Web personnel) . Évalué à 6 (+4/-0).

        Pour un autre projet on avait des threads et ça merdait, on est passé par une autre solution plus pertinente donc on n'a pas investigué plus loin et donc on n'utilise en pratique pas les threads.

        Pour les exceptions, on n'utilise pas :-p. Mais déjà entendu parlé de problème dessus, la v2.0.0 a "Store exceptions metadata in wasm memory instead of JS. This makes exception handling almost 100% thread-safe" donc peut-être qu'il y a du mieux avec la v2.

        Donc les problèmes les plus connus de Emscripten ne nous posaient pas vraiment soucis.

        Les plus grosses limitations vient de la sécurité elle-même des navigateurs (impossible de lire en direct les fichiers locaux, faut passer par les API des navigateurs et adapter notre propre API pour accepter des buffers plutôt que des fichiers; bref, des limitations classiques JS/navigateurs plutôt que Emscripten).

        • [^] # Re: Emscripten

          Posté par  . Évalué à 3 (+1/-0). Dernière modification le 24/02/21 à 16:46.

          Tu aurais de bons liens pour s'y mettre ?

          Je voudrais convertir dune-dynasty (port open-source de Dune 2 modernisé, basé sur open-dune) dans le navigateur à l'aide de Emscripten. Oui bon pour un premier projet c'est ambitieux.

          Pour le moment j'ai soit l'original, soit open-dune compilé pour DOS tournant sous WASM à l'aide du port WASM de DOSBox nommé JS-DOS 6.22.43. C'est bien mais très lourd.

          Je voudrais faire pareil pour Star Control 2 UQM HD MegaMod d'ailleurs. :)

          "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

          • [^] # Re: Emscripten

            Posté par  (site Web personnel) . Évalué à 5 (+3/-0).

            Tu aurais de bons liens pour s'y mettre ?

            Je ne peux que conseiller la doc sur leur site, et après de la recherche sur le net quand il y a des messages d'erreur… Surtout que ce n'est pas moi qui ai mis la main de la cambouis, désolé!

            Oui bon pour un premier projet c'est ambitieux.

            Euh… Oui! Mieux de commencer petit pour apprendre…

          • [^] # Re: Emscripten

            Posté par  . Évalué à 4 (+2/-0). Dernière modification le 24/02/21 à 22:17.

            Je vois que dune-dynasty se configure avec CMake, du coup pour commencer cloner le SDK d'Emscripten et lance

            ./emsdk install latest
            ./emsdk activate latest
            

            et puis ensuite tu peux essayer cmake . -DCMAKE_C_COMPILER=path/to/emcc -DCMAKE_CXX_COMPILER=path/to/em++ du côté de dune-dynasty.

            À partir de là c'est essai/erreur :)

  • # je suis vieux

    Posté par  . Évalué à 4 (+3/-1).

    De mon temps, Emscripten ça compilait vers asmjs

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.