Retour au cours

backend / python

Internals CPython : mémoire, garbage collector, interning

Leçon 271 exercice

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 is peut parfois surprendre sur des entiers ou chaînes
  • Ne jamais confondre is (identité) et == (égalité de valeur) dans du code de production
  • Utiliser weakref pour 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.

ComparaisonQue teste-t-elleFiable pour...
a == bÉgalité de valeurToute comparaison métier
a is bMême objet en mémoireis None, is True, tests d'identité voulus
a is 256 (petit entier)Identité, souvent True par interningJamais à 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.

python
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" : is peut renvoyer True par optimisation, jamais à présupposer pour la logique métier.
  • weakref référence un objet sans empêcher sa collecte : utile pour les caches.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →