backend / python
Internals CPython : mémoire, garbage collector, interning
Explication
Ce que vous allez apprendre
- Comprendre le comptage de références, le mécanisme principal de gestion mémoire de CPython
- Expliquer pourquoi un cycle de références nécessite un garbage collector dédié
- Reconnaître le mécanisme d'interning et pourquoi
ispeut parfois surprendre sur des entiers ou chaînes - Ne jamais confondre
is(identité) et==(égalité de valeur) dans du code de production - Utiliser
weakrefpour référencer un objet sans empêcher sa collecte (utile pour les caches)
Dans quel contexte ?
Un développeur relit une pull request où un collègue a écrit if statut is "actif": pour comparer une chaîne. Le test passe en local car CPython interne (réutilise) certaines chaînes courtes ressemblant à des identifiants, mais échoue de façon aléatoire une fois déployé, selon la façon dont la chaîne a été construite (littéral direct vs concaténation). Comprendre que l'interning est une optimisation interne, jamais une garantie du langage, explique pourquoi il faut toujours utiliser == pour comparer des valeurs.
Ce qui se passe "sous le capot" quand vous écrivez x = [1, 2, 3]
Jusqu'ici, vous avez utilisé Python sans vous soucier de la mémoire : les objets apparaissent, existent, puis disparaissent. CPython (l'implémentation de référence de Python, celle que vous utilisez très probablement) gère tout ça pour vous, mais comprendre le mécanisme aide à écrire du code plus sûr et à diagnostiquer des bugs subtils.
Le comptage de références : le mécanisme principal
Chaque objet Python porte un compteur invisible : le nombre de "références" (de variables, de listes, d'attributs...) qui pointent vers lui. Quand vous faites b = a, vous ne copiez pas l'objet, vous créez une nouvelle référence vers le même objet, et le compteur augmente. Quand une référence disparaît (del, fin de fonction, réaffectation), le compteur diminue. Dès qu'il atteint zéro, l'objet est immédiatement libéré — pas d'attente, contrairement à d'autres langages avec un garbage collector "pur".
Le problème des cycles, et le garbage collector
Le comptage de références a une faille : deux objets qui se référencent mutuellement (un cycle) ne verront jamais leur compteur tomber à zéro, même si plus rien d'extérieur ne les utilise. C'est là qu'intervient le garbage collector générationnel : il détecte périodiquement ces cycles orphelins et les brise.
Interning et identité
Pour économiser de la mémoire, CPython réutilise certains objets immuables très courants (petits entiers, chaînes ressemblant à des identifiants). C'est pourquoi is peut parfois répondre True de façon surprenante — c'est une optimisation interne, jamais une garantie sur laquelle construire une logique métier (utilisez == pour comparer des valeurs, is seulement pour tester l'identité réelle, comme is None).
Le piège à éviter
Ne confondez jamais is (identité, "est-ce le même objet en mémoire ?") et == (égalité de valeur). Le comportement de l'interning varie selon le contexte et ne doit jamais être présupposé dans du code de production.
| Comparaison | Que teste-t-elle | Fiable pour... |
|---|---|---|
a == b | Égalité de valeur | Toute comparaison métier |
a is b | Même objet en mémoire | is None, is True, tests d'identité voulus |
a is 256 (petit entier) | Identité, souvent True par interning | Jamais à présupposer en production |
Piège fréquent
if statut is "actif": peut fonctionner par accident grâce à l'interning des chaînes courtes, puis échouer de façon imprévisible avec une chaîne construite dynamiquement ("act" + "if"). Utilisez toujours == pour comparer des valeurs, et réservez is à des tests d'identité volontaires comme is None.
Commandes & code
Internals CPython
Comment l'interpréteur de référence gère réellement la mémoire et les objets.
import sys
import gc
# --- Comptage de references : le mecanisme PRINCIPAL de gestion memoire ---
a = [1, 2, 3]
print(sys.getrefcount(a)) # >= 2 (la variable "a" + l'argument temporaire de getrefcount)
b = a # nouvelle reference vers le MEME objet
print(sys.getrefcount(a)) # incremente de 1
del b # decremente le compteur de references
# quand le compteur atteint 0, l'objet est immediatement desalloue (pas d'attente du GC)
# --- Le garbage collector generationnel : ne gere QUE les cycles de references ---
class Noeud:
def __init__(self):
self.suivant = None
n1 = Noeud()
n2 = Noeud()
n1.suivant = n2
n2.suivant = n1 # cycle : n1 reference n2 qui reference n1
# Le comptage de references seul ne peut PAS liberer ce cycle (compteurs jamais a 0)
del n1, n2
# C'est le GC generationnel (gc module) qui detecte et casse ce genre de cycle
gc.collect() # force une collecte (normalement automatique)
print(gc.get_stats()) # statistiques par generation (0, 1, 2)
# 3 generations : gen0 (jeunes objets, collectee souvent), gen1, gen2 (vieux objets, rarement)
print(gc.get_threshold()) # seuils de declenchement par generation
# --- Interning : reutilisation d'objets immuables pour economiser la memoire ---
a = 256
b = 256
print(a is b) # True : les petits entiers [-5, 256] sont "internes" (cache par CPython)
x = 257
y = 257
print(x is y) # peut etre False (hors du cache des petits entiers), dependant du contexte
s1 = "hello"
s2 = "hello"
print(s1 is s2) # True : les litteraux de chaine "identifier-like" sont internes
s3 = "hello world!"
s4 = "hello world!"
print(s3 is s4) # resultat variable : depend du compilateur/contexte (a ne jamais presupposer)
# sys.intern() : forcer l'interning explicitement (utile pour de grosses quantites de cles repetees)
s5 = sys.intern("un_identifiant_repete_des_milliers_de_fois")
# --- Mutable default et identite : verifier avec is, pas == ---
print(1 == 1.0) # True : egalite de valeur
print(1 is 1.0) # False : types differents, objets differents
# --- Le layout memoire d'un objet Python (simplifie) ---
# Chaque objet Python porte : ob_refcnt (compteur de refs), ob_type (pointeur vers son type),
# puis les donnees specifiques au type. C'est pourquoi meme un int occupe plus que 4/8 octets :
print(sys.getsizeof(0)) # 28 octets (CPython 3.x, overhead de l'objet)
print(sys.getsizeof(10**100)) # bien plus gros : les grands entiers sont des tableaux de "digits"
# --- id() : adresse memoire (CPython) utilisee comme identite unique ---
x = object()
print(id(x)) # identifiant unique pendant la duree de vie de l'objet
# --- weakref : reference qui n'empeche PAS la collecte de l'objet ---
import weakref
class Cache:
pass
obj = Cache()
ref_faible = weakref.ref(obj)
print(ref_faible()) # l'objet, tant qu'il existe encore
del obj
print(ref_faible()) # None : l'objet a ete collecte, la reference faible ne l'a pas retenu
# Cas d'usage typique : caches qui ne doivent pas empecher le GC
class RegistreObjets:
def __init__(self):
self._objets = weakref.WeakValueDictionary() # ne retient pas les objets artificiellement
# --- GIL revisite : pourquoi il existe (simplifie le comptage de references) ---
# Sans GIL, chaque incrementation/decrementation de ob_refcnt devrait etre atomique
# (verrouillage fin), ce qui degraderait les performances mono-thread. C'est le compromis
# historique de CPython. Le projet "no-GIL" (PEP 703, Python 3.13+ en optionnel) explore
# une alternative avec des couts differents.Résumé
- CPython libère un objet immédiatement quand son compteur de références atteint zéro.
- Le garbage collector générationnel ne gère que les cycles de références (non résolubles par comptage seul).
- Les petits entiers et certaines chaînes littérales sont "internées" :
ispeut renvoyerTruepar optimisation, jamais à présupposer pour la logique métier. weakrefréférence un objet sans empêcher sa collecte : utile pour les caches.
Exercices pratiques
Mission : traquer un bug d'identité qui n'apparaît qu'en production
Objectif : Diagnostiquer un test is qui fonctionne par accident en local grâce à l'interning, le corriger, puis casser un cycle de références avec weakref.
Contexte
Un test if statut is "actif": passe systématiquement en local, mais échoue de façon aléatoire une fois déployé en production, selon la façon dont la chaîne statut a été construite plus tôt dans le code (littéral direct, ou résultat d'une concaténation comme "act" + "if").
Tu dois expliquer précisément pourquoi ce test réussit parfois par accident, le corriger définitivement, puis casser un cycle de références entre deux objets avec weakref pour éviter une fuite mémoire dans un cache.