Un code rapide fait la différence entre un script qu'on lance avec plaisir et un qu'on redoute d'exécuter. Personne n'aime attendre une réponse lente, qu'il s'agisse du chargement d'une page web, de l'entraînement d'un modèle ou d'une fonction qui recalcule la même réponse qu'elle a produite une seconde auparavant. L'une des façons les moins coûteuses d'accélérer Python est la mise en cache : conserver le résultat d'un traitement coûteux afin de le restituer instantanément la prochaine fois qu'on en a besoin.

Ce guide explique ce qu'est un cache et dans quels cas il aide réellement, puis présente les outils pratiques dans l'ordre : un cache manuel par dictionnaire que vous écrivez vous-même, les décorateurs intégrés functools.lru_cache et @cache, les caches à durée de vie limitée avec cachetools, et enfin la mise en cache des réponses HTTP pour qu'un scraper ne récupère pas deux fois la même page. Chaque extrait est réel et prêt à copier-coller.

Ce que vous allez construire

À la fin, vous disposerez d'une petite boîte à outils de patrons de mise en cache que vous pouvez intégrer à n'importe quel projet. Chacun cible une forme différente de travail répété.

  • Un décorateur de mémoïsation manuel. Un enveloppeur adossé à un dictionnaire qui stocke les résultats indexés sur les arguments d'une fonction.
  • lru_cache et @cache. Les décorateurs de la bibliothèque standard qui vous offrent un cache borné ou non borné en une seule ligne.
  • Un cache TTL. Un cache cachetools dont les entrées expirent après un nombre défini de secondes, pour les données qui deviennent obsolètes.
  • Un récupérateur avec cache. Une fonction qui met en cache les réponses HTTP pour que les requêtes répétées vers la même URL retournent instantanément au lieu de solliciter le réseau.

Qu'est-ce qu'un cache, et quand aide-t-il ?

Un cache est un stockage temporaire pour des données coûteuses à produire ou à récupérer. Plutôt que de recalculer un résultat ou de le redemander à une base de données ou à une API à chaque fois, on en conserve une copie dans un endroit rapide (généralement la mémoire) et on la relit à l'appel suivant. Le premier appel paie le coût complet ; tous les appels répétés suivants sont presque gratuits.

La mise en cache est rentable dans trois situations récurrentes :

  • Accès plus rapide. Lire depuis un cache en mémoire est bien plus rapide que recalculer un résultat ou l'extraire d'une source plus lente comme un disque, une base de données ou une API distante.
  • Moins de charge. Chaque succès de cache représente une requête en moins vers votre base de données ou une sollicitation en moins d'un service externe. Cela allège la pression sur ces systèmes et vous aide à éviter les goulots d'étranglement et les limites de débit.
  • Meilleure expérience. Une latence réduite signifie des pages plus réactives et des interactions plus fluides, ce qui importe surtout pour tout ce que les utilisateurs attendent directement.

Il vaut la peine d'être précis sur les cas où la mise en cache est le mauvais outil. Elle n'aide que lorsque les mêmes entrées se répètent et que le résultat sous-jacent ne change pas entre les appels (ou change assez lentement pour qu'une légère obsolescence soit acceptable). Une fonction recevant un argument différent à chaque appel n'en tire aucun bénéfice, seulement la surcharge mémoire. Et un cache qui n'expire jamais peut servir des données périmées indéfiniment, c'est pourquoi l'expiration est importante dès que la source peut changer.

Un mot sur les stratégies d'éviction

Un cache ne peut pas croître sans limite ; quand il est plein, il doit décider ce qu'il faut supprimer. Cette décision est la stratégie d'éviction, et la bonne dépend de la façon dont vos données sont accédées.

  • FIFO (Premier Entré, Premier Sorti). L'entrée la plus ancienne est évincée en premier, quelle que soit sa fréquence d'utilisation. Simple, et une valeur par défaut raisonnable lorsque l'ordre d'accès correspond grossièrement à l'utilité.
  • LRU (Least Recently Used, le moins récemment utilisé). L'entrée qui n'a pas été lue depuis le plus longtemps est évincée en premier. C'est le cheval de bataille des caches web et de bases de données, car les données récemment utilisées ont tendance à être réutilisées prochainement.
  • LFU (Least Frequently Used, le moins fréquemment utilisé). L'entrée ayant le moins de succès est évincée en premier, ce qui favorise les données régulièrement populaires par rapport à celles qui ont connu un pic une seule fois.
  • TTL (Time To Live, durée de vie). Chaque entrée porte une expiration, et les entrées périmées sont supprimées après un nombre fixe de secondes, que le cache soit plein ou non. C'est ce qu'on utilise quand les données source changent dans le temps.

La bibliothèque standard vous donne LRU d'emblée. Pour TTL et LFU, il faut recourir à une petite bibliothèque tierce, que nous abordons plus bas.

Prérequis

Python de base. Vous devez être à l'aise pour écrire des fonctions, exécuter un script et installer des paquets avec pip. La familiarité avec les décorateurs aide mais n'est pas indispensable, car la première section en construit un de zéro.

Python 3.9 ou ultérieur. Vérifiez votre version avec python --version. Le décorateur @cache utilisé ci-dessous a été ajouté dans Python 3.9 ; tout le reste fonctionne à partir de la version 3.2. Si vous devez installer Python, obtenez-le sur python.org.

Mise en cache manuelle avec un décorateur

La façon la plus claire de comprendre la mise en cache est d'en écrire une. Un décorateur est simplement une fonction qui enveloppe une autre fonction, et un décorateur de cache stocke chaque résultat dans un dictionnaire indexé sur les arguments avec lesquels il a été appelé. Si les mêmes arguments arrivent à nouveau, il retourne la valeur stockée au lieu d'exécuter la fonction enveloppée.

python
import requests

def memoize(func):
    cache = {}

    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result

    return wrapper

@memoize
def get_html(url):
    # Only the first call for a given URL hits the network
    response = requests.get(url, timeout=10)
    return response.text

print(get_html("https://example.com"))
print(get_html("https://example.com"))  # served from cache, no request

Le premier appel à get_html récupère la page et stocke le corps sous son URL. Le deuxième appel avec la même URL trouve l'entrée déjà présente dans cache et la retourne sans toucher au réseau. La clé est le tuple args, c'est pourquoi ce patron fonctionne uniquement avec des arguments positionnels hachables : les listes et les dictionnaires ne peuvent pas être des clés de dictionnaire, et les arguments nommés sont ignorés ici. Cette limitation est exactement la raison pour laquelle la version de la bibliothèque standard existe, et c'est ce que nous examinons ensuite.

Mettre en cache le résultat, pas l'effet de bord

La mémoïsation suppose que la fonction enveloppée est une recherche pure : même entrée, même sortie, pas d'effets de bord importants. Mettre en cache une fonction qui écrit dans un fichier ou modifie un état global ignorera ce travail lors des appels répétés, ce qui est généralement un bug. Mettez en cache les fonctions qui calculent ou récupèrent une valeur et la retournent.

functools.lru_cache et @cache

Python embarque un décorateur de mise en cache prêt pour la production dans le module functools, de sorte que vous avez rarement besoin d'écrire le vôtre. lru_cache met en cache les résultats et, quand le cache atteint sa limite de taille, évince l'entrée la moins récemment utilisée pour faire de la place. Vous fixez le plafond avec maxsize.

python
from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(50))
print(fib.cache_info())

Sans le cache, un fib(50) récursif naïf recalcule les mêmes sous-problèmes des milliards de fois et prend des heures. Avec lru_cache, chaque valeur de n est calculée une fois et réutilisée, transformant une fonction exponentielle en une fonction linéaire. La méthode cache_info() est un petit bonus : elle rapporte les succès, les ratés et la taille courante, afin que vous puissiez confirmer que le cache fait son travail. Voici le premier résultat que vous pouvez exécuter et vérifier.

json
12586269025
CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)

Si vous ne voulez pas du tout de limite de taille, @cache (ajouté dans Python 3.9) est lru_cache(maxsize=None) avec un nom plus court. Il n'évince jamais, donc utilisez-le uniquement quand l'ensemble des entrées possibles est borné et assez petit pour tenir en mémoire.

python
from functools import cache

@cache
def expensive_computation(x, y):
    return x * y

print(expensive_computation(5, 6))

Les deux décorateurs utilisent comme clé tous les arguments reçus par la fonction, positionnels et nommés, et exigent que ces arguments soient hachables. Si vous devez mettre en cache une méthode d'une classe ou une fonction recevant des entrées non hachables, vous devrez adapter l'entrée (par exemple convertir une liste en tuple) avant qu'elle n'atteigne l'appel mis en cache.

Caches à durée de vie avec cachetools

Les décorateurs de la bibliothèque standard n'expirent jamais une entrée par eux-mêmes ; une entrée LRU ne part que lorsque le cache est plein et que quelque chose de plus récent la chasse. C'est bien pour du calcul pur, mais c'est inadapté pour des données qui deviennent obsolètes, comme un prix, un taux de change, ou une réponse API qui se met à jour au fil de la journée. Pour cela, il faut un cache TTL, où chaque entrée expire après un nombre défini de secondes. La bibliothèque cachetools offre exactement cela, ainsi que LFU et d'autres politiques.

bash
pip install cachetools

On l'applique avec le décorateur @cached en passant une instance de TTLCache qui fixe à la fois la taille maximale et la durée de vie en secondes.

python
from cachetools import cached, TTLCache
import requests

# Up to 100 entries, each valid for 300 seconds
cache = TTLCache(maxsize=100, ttl=300)

@cached(cache)
def get_rate(symbol):
    response = requests.get(f"https://api.example.com/rate/{symbol}", timeout=10)
    return response.json()["price"]

print(get_rate("BTC"))  # fetches and caches
print(get_rate("BTC"))  # cached for up to 5 minutes

Pendant les cinq prochaines minutes, les appels répétés pour le même symbole retournent le prix mis en cache sans requête HTTP. Après l'expiration du TTL, l'entrée expire et l'appel suivant récupère une valeur fraîche. Cela vous donne la plupart des avantages de la mise en cache tout en maintenant les données raisonnablement à jour, ce qui est le bon équilibre pour tout ce qui change dans le temps. Si vous avez besoin d'une politique basée sur la fréquence, cachetools propose également LFUCache avec la même interface de décorateur.

Mise en cache des réponses HTTP pour un scraper

La mise en cache est la plus utile quand l'opération coûteuse est une requête réseau. Un scraper qui parcourt une liste, suit des liens ou réessaie des pages ayant échoué demandera souvent la même URL plusieurs fois au cours d'une exécution, et chaque récupération dupliquée coûte du temps et ajoute de la charge sur la cible. Un cache de réponses résout cela : récupérer chaque URL une seule fois, stocker le corps, et servir les répétitions depuis la mémoire.

Le patron combine un cache TTL avec une simple fonction de récupération. Indexer sur l'URL signifie qu'une même page n'est téléchargée qu'une seule fois dans la fenêtre TTL.

python
from cachetools import cached, TTLCache
import requests

page_cache = TTLCache(maxsize=500, ttl=3600)
headers = {"User-Agent": "Mozilla/5.0 (cache tutorial)"}

@cached(page_cache)
def fetch(url):
    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()
    return response.text

urls = [
    "https://quotes.toscrape.com/page/1/",
    "https://quotes.toscrape.com/page/1/",  # duplicate, served from cache
    "https://quotes.toscrape.com/page/2/",
]

for url in urls:
    html = fetch(url)
    print(f"{len(html)} chars from {url}")

La deuxième requête pour la page 1 ne quitte jamais votre machine ; elle retourne le corps stocké lors du premier appel. Avec un TTL d'une heure, relancer le scraper dans cette fenêtre réutilise les pages en cache au lieu de les retélécharger, ce qui est vraiment utile pendant les itérations sur le code d'analyse, quand on ne veut pas marteler le site à chaque test. Pour construire l'analyseur lui-même à partir du HTML récupéré, consultez comment scraper un site web avec Python.

Crawlbase Crawling API

Un cache de réponses vous épargne les récupérations dupliquées, mais la première récupération de chaque page doit tout de même réussir, et sur de vraies cibles cela implique le rendu de JavaScript et le contournement des défenses anti-bot. La Crawling API gère le rendu et fait tourner des IPs résidentielles côté serveur, retournant du HTML finalisé que vous pouvez insérer directement dans la fonction fetch mise en cache ci-dessus, afin de mettre en cache de vraies pages plutôt que des réponses bloquées. Essayez-la sur le niveau gratuit avant de construire votre propre flotte headless et votre pool de proxies.

La mise en cache des réponses est aussi une courtoisie envers les sites que vous récupérez. Chaque page servie depuis le cache est une requête en moins que la cible doit gérer, ce qui réduit votre empreinte et diminue les risques de déclencher des limites de débit. Associer un cache à un rythme raisonnable est l'une des façons les plus simples d'être un client bien conduit, un thème abordé en profondeur dans comment scraper des sites web sans se faire bloquer.

Choisir le bon cache

Avec quatre outils disponibles, le choix se résume généralement à deux questions : les données changent-elles, et avez-vous besoin de bornes sur la mémoire ?

  • Calcul pur qui ne change jamais. Utilisez @cache pour un cache non borné, ou lru_cache(maxsize=N) quand l'espace d'entrées est large et que vous voulez un plafond sur la mémoire.
  • Données qui deviennent obsolètes. Utilisez un TTLCache de cachetools pour que les entrées expirent et soient récupérées périodiquement.
  • Une petite politique personnalisée. Écrivez un décorateur par dictionnaire manuel quand vous avez besoin d'un comportement que les bibliothèques n'offrent pas, mais préférez les outils standards quand ils conviennent.

Pour un panorama plus large des bibliothèques qui s'associent bien à ces patrons lorsque vous passez de la mise en cache aux pipelines d'extraction complets, consultez les meilleures bibliothèques Python de web scraping.

Récapitulatif

Points clés

  • La mise en cache échange de la mémoire contre de la vitesse. Stockez le résultat d'un travail coûteux et restituez-le lors des appels répétés, afin que seul le premier appel paie le coût complet.
  • Commencez par la bibliothèque standard. functools.lru_cache vous donne un cache LRU borné en une ligne, et @cache est la version non bornée.
  • Utilisez un cache TTL pour les données qui changent. cachetools.TTLCache expire les entrées après un nombre défini de secondes pour ne jamais servir des valeurs périmées indéfiniment.
  • Mettez en cache les réponses HTTP pour éviter les récupérations dupliquées. Indexer une fonction de récupération sur l'URL transforme les requêtes répétées en succès instantanés de cache et allège la charge sur la cible.
  • Ne mettez en cache que du travail pur et répétable. Les fonctions avec des effets de bord ou des arguments toujours uniques n'en tirent aucun bénéfice et peuvent se comporter de façon subtile et erronée.

Foire aux questions

Qu'est-ce que la mise en cache en Python ?

La mise en cache en Python consiste à stocker le résultat d'un appel de fonction coûteux ou d'une récupération de données afin que les demandes ultérieures pour la même entrée soient retournées depuis un stockage temporaire rapide plutôt que de refaire le travail. Le premier appel calcule ou récupère la valeur et la sauvegarde ; chaque appel correspondant suivant lit la copie sauvegardée, ce qui réduit la latence et allège la charge sur ce qui a produit la valeur.

Quand utiliser lru_cache plutôt que @cache ?

Utilisez lru_cache(maxsize=N) quand l'ensemble des entrées possibles est grand et que vous voulez plafonner la mémoire utilisée par le cache ; une fois plein, l'entrée la moins récemment utilisée est évincée. Utilisez @cache quand l'espace d'entrées est petit et borné et que vous êtes prêt à conserver chaque résultat indéfiniment, car il n'évince jamais. Les deux se comportent de façon identique mis à part la limite de taille.

Comment mettre en cache des données qui expirent après un certain temps ?

Utilisez un cache TTL de la bibliothèque cachetools. Créez un TTLCache(maxsize=N, ttl=seconds) et appliquez-le avec le décorateur @cached. Chaque entrée est valide pour le nombre de secondes que vous définissez ; après cela, elle expire et l'appel suivant récupère une valeur fraîche. C'est le bon outil pour les prix, les taux et les réponses API qui se mettent à jour dans le temps.

Peut-on mettre en cache des réponses HTTP dans un scraper ?

Oui, et c'est l'une des utilisations les plus rentables de la mise en cache. Enveloppez votre fonction de récupération dans un cache indexé sur l'URL afin que chaque page ne soit téléchargée qu'une seule fois dans la fenêtre de cache. Les répétitions sont servies depuis la mémoire, ce qui accélère les exécutions et réduit les requêtes vers le site cible. Un cache TTL fonctionne bien ici pour que les pages en cache soient rafraîchies après un intervalle raisonnable.

Quand la mise en cache est-elle le mauvais choix ?

La mise en cache n'aide que lorsque les mêmes entrées se répètent et que le résultat est suffisamment stable pour qu'une copie stockée soit encore correcte. Si une fonction reçoit un argument différent à chaque appel, le cache n'ajoute que de la surcharge mémoire sans aucun succès. Si la fonction a des effets de bord importants, la mise en cache les ignorera lors des appels répétés, ce qui est généralement un bug. Et si les données source changent constamment, utilisez un TTL court ou évitez le cache plutôt que de servir des valeurs périmées.

lru_cache fonctionne-t-il avec des arguments non hachables ?

Non. Les décorateurs de la bibliothèque standard construisent leur clé de cache à partir des arguments, qui doivent donc être hachables. Les listes, les dictionnaires et les ensembles ne peuvent pas être utilisés directement. Si vous devez mettre en cache un appel qui prend l'un de ces types, convertissez-le d'abord en une forme hachable, par exemple en transformant une liste en tuple, avant de le passer à la fonction mise en cache.

Commencer à construire

Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.

Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.

En libre-service · Sans appel commercial requis · Volumes de crawl entreprise disponibles