Technologie
J’ai travaillé sur Textual pendant plus d’un an. Voici quelques éléments que j’ai découverts (ou redécouverts) concernant les terminaux en Python et le développement logiciel en général.
Les terminaux sont rapides
Un émulateur de terminal moderne est un logiciel remarquablement sophistiqué. Bien que le protocole qu’ils utilisent soit ancien, beaucoup d’entre eux sont alimentés par les mêmes technologies graphiques que celles utilisées dans les jeux vidéo. Cependant, une animation fluide n’est pas toujours garantie dans le terminal. Si vous avez déjà essayé d’appliquer des effets visuels dans un terminal, vous avez peut-être été déçu par des clignotements ou des déchirures.
Il est néanmoins possible d’obtenir une animation fluide, comme le montre la vidéo suivante. Quels sont donc les astuces que nous utilisons ?
Il existe plusieurs facteurs qui réduisent le clignotement dans le terminal. Le premier est l’émulateur de terminal que vous utilisez. Les terminaux modernes utilisent le rendu accéléré par le matériel et synchronisent les mises à jour avec votre affichage pour minimiser le clignotement. Cependant, d’après mon expérience, d’autres facteurs ont un impact plus important sur la réduction du clignotement que le choix de l’émulateur. Même sur des terminaux plus anciens, vous pouvez généralement obtenir une animation sans clignotement grâce à cette petite astuce (en réalité trois).
La première astuce consiste à « écraser, ne pas effacer ». Si vous effacez l’écran et ajoutez ensuite un nouveau contenu, vous risquez de voir un cadre vide ou partiellement vide pendant un bref instant. Il est bien préférable de remplacer complètement le contenu dans le terminal pour éviter tout cadre vide intermédiaire.
La deuxième astuce consiste à écrire le nouveau contenu en une seule opération d’écriture sur la sortie standard. Il peut être pratique de faire plusieurs appels à file.write
pour afficher une mise à jour, mais comme pour le cadre vide, vous risquez qu’une mise à jour partielle devienne visible.
La troisième astuce consiste à utiliser le protocole de sortie synchronisée ; une addition relativement récente au protocole du terminal, mais déjà prise en charge par de nombreux terminaux. vous indiquez au terminal quand vous commencez et terminez un cadre. Il peut alors utiliser ces informations pour fournir des mises à jour sans clignotement.
Avec ces trois astuces, vous pouvez créer des animations très fluides tant que vous pouvez fournir des mises à jour à intervalles réguliers. Textual utilise 60 images par seconde comme référence. Au-delà de cela, il est probable que cela ne soit pas perceptible.
Maintenant que vous pouvez avoir une animation fluide dans le terminal, la question se pose : devriez-vous le faire ? Toutes les animations ne sont pas perçues de la même manière. Certaines peuvent sembler superflues. Par exemple, la barre latérale dans la vidéo qui glisse depuis la gauche de l’écran. Je trouve cela astucieux, mais cela n’ajoute rien à l’expérience utilisateur. Les détracteurs des animations pourraient le considérer comme un »non souhaité », c’est pourquoi Textual disposera d’un mécanisme pour désactiver de telles animations. D’autres types d’animations sont plus qu’un simple effet visuel. Le défilement fluide est une animation que je trouve particulièrement utile pour garder ma place dans un mur de texte. Toutes les animations se situent quelque part entre l’utile et le superflu, et je doute qu’il y ait beaucoup de personnes qui souhaitent ne pas avoir d’animation du tout.
Les DictViews sont impressionnants
Vous connaissez probablement les méthodes keys()
et items()
sur les dictionnaires Python qui renvoient respectivement un KeysView
et un ItemsView
. Vous ne savez peut-être pas que ces objets ont des interfaces très similaires à celles des ensembles. Une information que j’ai redécouverte après avoir écrit inutilement une douzaine de lignes de code complexes.
Dans Textual, le processus de mise en page crée une « carte de rendu ». En gros, il s’agit d’une correspondance entre le Widget et sa position à l’écran. Dans une version antérieure, Textual effectuait un rafraîchissement inutile de tout l’écran si même un seul widget changeait de position. Je voulais éviter cela en comparant la carte de rendu avant et après.
J’ai découvert que je pouvais prendre la différence symétrique de deux objets ItemsView
, ce qui me donnait les éléments qui étaient soit nouveaux, soit modifiés. C’était exactement ce dont j’avais besoin, mais réalisé au niveau C. Dans Textual, cela est utilisé pour obtenir les régions modifiées de l’écran lorsqu’une propriété CSS change, afin que nous puissions effectuer des mises à jour optimisées.
lru_cache est rapide
Peut-être pas surprenant étant donné que lru_cache
est littéralement conçu pour accélérer votre code, mais @lru_cache
est extrêmement rapide. J’ai été surpris de sa rapidité.
Si vous n’êtes pas familier avec lru_cache
, il s’agit d’un décorateur trouvé dans le module functools
de la bibliothèque standard. Ajoutez-le à une méthode et il mettra en cache la valeur de retour d’une fonction. Si vous définissez le paramètre maxsize
, il veillera à ce que votre cache ne croisse pas indéfiniment.
En examinant l’implémentation de lru_cache
dans les dépôts CPython, j’ai pensé que je pourrais l’améliorer. Spoiler : je ne l’ai pas fait. Il s’avère que CPython utilise cette version C qui est très rapide tant pour les succès que pour les échecs de cache.
Savoir cela m’a convaincu de faciliter l’utilisation de @lru_cache
. Il existe un certain nombre de petites fonctions dans Textual qui ne sont pas exactement lentes, mais qui sont appelées un grand nombre de fois. Beaucoup d’entre elles étaient hautement mises en cache, et une utilisation judicieuse de @lru_cache
a permis d’obtenir un gain significatif. En général, un maxsize
d’environ 1000 à 4000 était suffisant pour garantir que la majorité des appels étaient des succès de cache.
Voici un exemple de type de fonction qui a bénéficié du cache. Cette méthode calcule où deux régions rectangulaires se chevauchent. Vous pouvez voir qu’elle ne fait pas beaucoup de travail, mais elle était appelée des milliers de fois.
Un conseil lors de l’utilisation de lru_cache
: vérifiez toujours vos hypothèses en inspectant cache_info()
. Pour un cache efficace, vous devriez vous attendre à voir les hits
croître plus rapidement que les misses
.
L’immuable est préférable
En lien avec le conseil précédent, j’aimerais vanter les mérites des objets immuables. Python n’a pas d’objets véritablement immuables, mais vous pouvez tirer parti des tuples, des NamedTuples ou des dataclasses gelées.
Il semble que ce soit une limitation arbitraire de ne pas pouvoir modifier un objet, mais cela s’avère rarement être le cas en pratique. Les informaticiens soulignent que de nombreux langages sont immuables par défaut, et ce, pour de bonnes raisons.
Dans Textual, le code qui utilise des objets immuables est le plus facile à comprendre, à mettre en cache et à tester. Principalement parce que vous pouvez écrire du code exempt d’effets secondaires. Cela est difficile à réaliser lorsque vous passez des instances de classe à une fonction.
L’art Unicode est bénéfique
Certaines choses techniques sont difficiles à expliquer par des mots, et un diagramme créé à partir de caractères de boîte Unicode peut être extrêmement utile dans la documentation. Ce diagramme est extrait d’une docstring dans Textual pour une méthode qui divise une région en quatre sous-régions :
L’Utilisation des Fractions en Python
Introduction aux Fractions
Bien que les commentaires de code soient essentiels, l’ajout de diagrammes peut considérablement améliorer leur compréhension. Pour cela, j’utilise Monodraw, un outil exclusivement disponible sur MacOS, mais il existe sans doute d’autres alternatives pour les utilisateurs d’autres systèmes d’exploitation.
Précision des Fractions
Python inclut un module fractions
dans sa bibliothèque standard depuis Python 2.6. Pendant longtemps, je n’avais pas trouvé d’application pour ce module dans mes projets. Je pensais qu’il était principalement destiné aux mathématiciens et peu utile pour des développeurs comme moi. Cependant, j’ai réalisé à quel point il pouvait être précieux, notamment pour le projet Textual.
Une fraction représente une manière alternative d’exprimer un nombre. Une fois que vous avez un objet Fraction, vous pouvez l’utiliser à la place des nombres à virgule flottante. Mais quels sont les avantages d’utiliser des fractions plutôt que des flottants ?
Les nombres à virgule flottante présentent certaines limitations, un problème qui n’est pas propre à Python. Voici un exemple classique qui illustre ce problème :
>>> 0.1 + 0.1 + 0.1 == 0.3
False
Dans Textual, ces erreurs d’arrondi des flottants posaient des problèmes. Certaines mises en page nécessitaient de diviser l’écran en proportions variées. Par exemple, un panneau pouvait occuper un tiers de la largeur de l’écran, tandis que les deux tiers restants étaient divisés. Des erreurs d’arrondi pouvaient entraîner un espace vide là où du contenu était attendu.
Une solution simple à ce problème était de remplacer les flottants par des fractions. Contrairement aux flottants, les fractions ne souffrent pas de ce type d’erreur d’arrondi. Dans le monde des fractions, trois dixièmes s’additionnent correctement :
from fractions import Fraction as F
>>> F(1, 10) + F(1, 10) + F(1, 10) == F(3, 10)
True
Voici un exemple qui divise un nombre fixe de caractères en plusieurs parties. Les deux fonctions accomplissent essentiellement la même tâche, mais l’une utilise des flottants et l’autre des fractions.
Voici le résultat du code ci-dessus. Notez que la version flottante (première ligne de chiffres) est courte d’un caractère :
------------------------
00011122223334444555666
000111222233344445556666
Les Défis des Emojis
Le support des emojis dans les terminaux a toujours été un défi pour Rich, et nous avons hérité de ce problème en travaillant sur Textual. C’était l’une de mes priorités lorsque Textualize a été fondé. Nous avions de grands projets, mais plus nous explorions ce sujet, plus la situation semblait complexe.
Quel est le problème avec les emojis ? Cela découle du fait qu’un caractère écrit dans le terminal peut avoir une taille variable. Les caractères chinois, japonais et coréens occupent deux fois plus d’espace que ceux de l’alphabet occidental, ce qui complique le formatage, comme le centrage ou le dessin de boîtes autour du texte. Un formatage de base nécessite que Rich sache combien d’espace un texte occupera dans le terminal. Le support des caractères à double largeur complique l’utilisation de len(text)
pour déterminer la largeur en terminal.
Heureusement, la base de données Unicode contient une correspondance entre les caractères à largeur simple et double. Rich (et Textual) consulte cette base de données pour chaque caractère qu’il imprime. Ce n’est pas une opération bon marché, mais avec un peu d’ingénierie et de mise en cache, cela devient suffisamment rapide.
Les emojis sont également présents dans la base de données Unicode, donc problème résolu ? Malheureusement, ce n’est pas si simple. Alors que les caractères asiatiques changent peu, les emojis évoluent constamment. Chaque nouvelle version de la base de données Unicode introduit de nouveaux emojis. Si vous imprimez ces nouveaux emojis dans le terminal, les résultats peuvent être imprévisibles. Vous pouvez obtenir un caractère à largeur simple ou double, et il se peut qu’il ne s’affiche même pas correctement.
Nous avons envisagé d’inclure des informations sur chaque version de Unicode avec Rich, mais cela pose un autre problème : comment détecter la version de Unicode utilisée par un émulateur de terminal donné ? Il n’existe pas de méthode fiable pour cela. Il y a une approche heuristique où vous écrivez diverses séquences et demandez à l’émulateur de terminal la position du curseur, ce qui permet de faire une estimation de la version de Unicode. Malheureusement, nos tests ont montré que les terminaux rendent encore les emojis de manière imprévisible, même si vous pensez connaître la base de données Unicode utilisée.
Pour aggraver les choses, il existe des emojis à plusieurs points de code. Un point de code est le numéro de référence d’un glyphe Unicode (image de caractère). En Python, vous pouvez le consulter avec ord
. Par exemple, ord("A")
renvoie le point de code 65 représentant un A majuscule. Vous pourriez penser que cela est vrai pour tous les caractères, mais ce n’est pas le cas. De nombreux emojis combinent plusieurs points de code pour produire un glyphe unique. Par exemple, 👨🦰 (homme, teint clair, cheveux roux) se compose de 4 points de code. Essayez de le copier dans le REPL Python.
Tous les émulateurs de terminal ne rendent pas ces caractères correctement. Dans certains, ils apparaissent comme 4 caractères individuels, ou 2, ou 1, avec une largeur simple ou double, ou parfois comme 4 « ? » caractères. Même si vous implémentez le code pour comprendre ces caractères à plusieurs points de code, vous êtes confronté au problème fondamental de ne pas savoir quel sera le rendu réel dans un terminal donné.
C’est un véritable casse-tête, mais en pratique, ce n’est pas si mal. S’en tenir aux emojis de la version 9 de la base de données Unicode semble être fiable sur toutes les plateformes. Il est conseillé d’éviter d’utiliser des emojis plus récents et des caractères à plusieurs points de code, même s’ils semblent corrects sur votre émulateur de terminal.
Opportunités chez Textualize
Nous cherchons à développer un cadre TUI qui rivalisera avec les navigateurs. Consultez nos offres d’emploi.
Je recrute à nouveau pour Textualize. Nous recherchons des développeurs Python pour rejoindre notre équipe.
- Compétences techniques en Python très solides
- Expérience web
- Connaissance d’au moins un autre langage
- Bonnes compétences en conception d’API
Les partages sont appréciés !