Nous allons aborder la mise en cache en Python.
La mise en cache est une technique d'optimisation que vous pouvez utiliser dans vos applications pour conserver les données récentes ou souvent utilisées dans des emplacements mémoire qui sont plus rapides ou moins coûteux en calcul à accéder que leur source.
Imaginez que vous créez une application de lecture des ordres en tarification du marché financier qui récupère les dernières actualités de différentes sources du monde. Au fur et à mesure que l'utilisateur navigue dans la liste, votre application télécharge les ordres et les affiche à l'écran. Que se passerait-il si l'utilisateur décidait de manipuler les différentes agrégations et filtrages sur ces ordres ? À moins que vous ne mettiez en cache les données, votre application devrait récupérer le même contenu du monde ailleurs à chaque fois ! Cela rendrait le système de votre utilisateur lent et exercerait une pression supplémentaire sur le serveur hébergeant les ordres en tarification. Une meilleure approche serait de stocker le contenu localement après l'avoir récupéré une fois. Ensuite, la prochaine fois que l'utilisateur décidera de gérer ces ordres, votre application pourrait obtenir le contenu à partir d'une copie stockée localement au lieu de revenir à la source externe. En informatique, cette technique s'appelle la mise en cache.
Les stratégies de mise en cache
Le langage Python est bien utilisé dans le domaine de la finance grâce à son efficacité et sa performance pendant le développement des modèles et outils financiers. L’alimentation des données massives et la réutilisation des données intermédiaires au calcul des modèles deviennent fréquentes dans les applications financières, dont le temps d’une exécution fiable doit être le plus court possible. Python fournit les caches à la base d’un dictionnaire qui contrôle la vie de la donnée par différentes stratégies. C’est une manière efficace d'éviter le débordement de la mémoire. On a besoin de définir une capacité maximale de la mémoire avant d’appeler ces caches.
Voici les stratégies retrouvées dans les librairies du Python [2] :
Premier entré / premier sorti (FIFO) : il supprime la plus ancienne des entrées, pour que les nouvelles entrées soient les plus susceptibles d'être réutilisées.
Dernier entré / premier sorti (LIFO) : il supprime la dernière des entrées, pour que les anciennes entrées soient les plus susceptibles d'être réutilisées.
Moins récemment utilisé (LRU) : il expulse l'entrée la moins récemment utilisée, pour que les entrées récemment utilisées soient les plus susceptibles d'être réutilisées.
Dernière utilisation (MRU) : il expulse l'entrée la plus récemment utilisée, pour que les entrées les moins récemment utilisées soient les plus susceptibles d'être réutilisées.
La moins fréquemment utilisée (LFU) : il expulse l'entrée la moins souvent consultée, pour que les entrées ayant beaucoup d'appels soient le plus susceptibles d'être réutilisées.
Remplacement aléatoire (RR) : il expulse l'entrée aléatoirement, pour que les entrées soient uniformément susceptibles d'être réutilisées.
Ils présentent les avantages comme la simplicité et l’efficacité de leurs mises en place. Néanmoins, il manque une option pour manipuler la mise à jour de données raffinées lors du déclenchement d'un incident spécifique dans le programme.
Les autres libraires de cache sont à la base des stratégies, qui visent à renvoyer rapidement les mêmes données coûteuses à acquérir. Par exemple, la libraire cached_property [1] est populairement utilisée dans les applications liées aux bases de données et réseaux [4]. Pour être accompagné sur ces sujets, rapprochez-vous de notre expertise Python.
Les nouvelles stratégies raffinées et flexibles
Dans certains contextes, on voudrait garder les informations cachées jusqu’à un démarrage événementiel spécifié dans l’application.
On démontre ici que les implémentations de décorateur en Python, permettent de satisfaire ce besoin où les résultats des appels répétitifs sont mis en cache ou rejetés quand on veut.
1. Cache au niveau de la méthode libre
On conçoit le décorateur de cache en définissant une méthode __call__ qui gère un dictionnaire comme une mémoire locale.
Le dictionnaire prend le nom de la méthode à décorer et les arguments d’entrée comme les clés, alors que la valeur est le retour de la méthode. Du coup, on ne calcule qu’une fois dans la méthode et on répète le même retour à suite, avant le ramasse-miettes (garbage_collect).
On pourrait effacer les résultats aux trois niveaux quand on veut. Si l’on ne met rien en argument d’entrée pour garbage_collect, on supprime tous les retours cachés. Si on définit l’obj par un ensemble de noms de méthodes qu’on a décoré par le cache, on n’enlève que les résultats calculés de ces méthodes. Sinon, on expulse la valeur obtenue par le nom de la méthode précisé par l’obj.
Le retour d’une méthode pourrait être un objet de type basique ou complexe. On prend en compte dans le cache le générateur et le tee (liste des générateurs) comme le type.
2. Cache au niveau de l’instance de class
L'instance d’une classe dépend des arguments d’entrée pour __init__. On prend alors ces arguments comme les clés du dictionnaire « instances » défini dans la classe à décorer. On libera l’objet par ces arguments. Il est possible d’avoir la méthode statique dans la classe, ce qui est pris en compte dans l’implémentation.
3. Cache python au niveau de la propriété de class
L’objectif ici est d’obtenir le même résultat par les appels répétitifs d’une méthode statique ou classique d’une classe. La mise en œuvre pourrait être considérée comme une application de la mise en cache au niveau de la méthode libre.
Par rapport aux deux caches définis dans les classes auparavant, on essaie d’implémenter ce cache par une méthode décoratrice. Les fonctions Python sont des descripteurs et on profite du design de lazy_properties (c.f. [3]). C'est ainsi que les objets de méthode sont créés. Lorsque vous faites obj.method, le protocole du descripteur est activé et la méthode __get__ de la méthode est appelée. Cela renvoie une méthode liée. Le cache de la propriété de classe cherche à mettre en mémoire les descripteurs, alors que la mise en cache de la méthode libre pourrait servir à mémoriser la méthode de la classe. On encapsule les méthodes __get__ et __set__ dans la classe ClassPropetyrDescriptor et on l'hérite dans la classe CachedClassPropetyrDescriptoren introduisant le cache de la méthode libre. Dans le décorateur on passe un argument to_cache pour déterminer si l'application du cache de propriété est activée.
On teste les trois caches définis danscaches.py par les tests unitaires.
Application
Dans cette section, on développe une application en profitant des caches mis en oeuvre.
Cette application cherche les informations des clients des sources externes, dont la latence est émulée par le sleep(3) dans le code. On peut décorer les méthodes coûteuses de façon raffinée et flexible, ainsi que libérer ses données de retour si besoin.
Lors de l'exécution du code, on trouve que le temps d'attente par les appels répétitifs estbien diminué avant le ramasse-miette.
Les résultats de l'application sont ainsi avec les caches.
Conclusion
Dans cet article on résume les stratégies et les applications des mises en cache en Python ; et les nouvelles mises en œuvre avec une gestion de ramasse-miette flexible.
L’idée du cache est de mettre les valeurs dans un dictionnaire en Python. On les retrouvera localement par leurs clés plus tard au lieu de rechercher dans les bases coûteuses. On a les stratégies basiques : premier entré / premier sorti (FIFO), dernier entré / premier sorti (LIFO), Moins récemment utilisé (LRU), Dernière utilisation (MRU), La moins fréquemment utilisée (LFU) et Remplacement aléatoire (RR) qui sont bien implémentées et intégrées dans beaucoup d’applications.
Malgré leurs simplicités et efficacités, on préfère parfois dans certains contextes gérer les ramasse-miettes du cache de façon raffinée et flexible. Le mécanisme de suppression des clés dans le dictionnaire est conçu et présenté pour s’adapter aux méthodes libres, les objets et les propriétés de la classe. Leurs performances des caches sont bien testées par les tests unitaires, alors que l'on développe enfin une application qui s'appuie sur les caches pour atténuer la latence des recherches de données coûteuses.
Références
1. https://pypi.org/project/cached-property/
2. https://pypi.org/project/cacheout/