Résolue

Compréhension du Small Integer Caching en Python

# Python # Gestion de la mémoire

Le code ci-dessous va me retourner 2 fois le même id, puis un 3ème id différent.
Jusque la pas de problème, je comprends pourquoi.

def foo(n):
    print(id(n))
    n += 1
    print(id(n))

n = 123456789
print(id(n))
foo(n)

# va me retourner
>>> 2047800875504
>>> 2047800875504
>>> 2047805920848

Par contre avec l'exemple ci-dessous, la j'obtiens 3 fois le même id. Pourquoi ?

def foo(n):
    print(id(n))
    n = 123456789
    print(id(n))

n = 123456789
print(id(n))
foo(n)

# va me retourner
>>> 2307909523952
>>> 2307909523952
>>> 2307909523952

Pourquoi est ce que la ligne n = 123456789 de la fonction foo ne créé pas un nouvel objet avec un id différent ? Je connais le principe de small integer caching, mais il me semblait qu'il ne s'appliquait qu'à des nombres très petits (généralement de -5 à 256). Pourquoi alors est-ce que j'obtiens 3 fois le même id ?

Bonjour,

Le principe du small integer caching en Python ne se limite pas uniquement aux "petits" entiers. Python utilise une technique appelée interning (internement ?) qui permet de réutiliser les objets immuables existants chaque fois que possible, plutôt que de créer de nouveaux objets. Cela a pour but de d'économiser la mémoire et d'améliorer les performances.

Une plus large explication ici

Ok, mais si je pars du principe que le small integer caching n'est pas forcément limité à la plage -5 à 256, je n'arrive toujours pas à expliquer le code suivant :

# création littérale, x et y auront la même référence grâce au système de small integer caching (de -5 à 256)
x = 10
y = 10
print(f"exemple 1 : {x is y}")

# création littérale, x et y auront la même référence
# car le small integer caching n'est soit disant pas forcément limité au nombre de -5 à 256 (pas sur de cette info au vu de l'exemple n°4)
x = 257
y = 257
print(f"exemple 2 : {x is y}")

# création dynamique de y, x et y auront la même référence car 225 fait parti de l'intervalle des small integer caching
# même si la création est dynamique, Python va utiliser le mécanisme de "Constant folding" (optimisation lors de la compilation)
x = 255
y = int('255')
print(f"exemple 3 : {x is y}")

# création dynamique de y, x et y n'auront pas la même référence (et la je comprends pas pourquoi)
# pourquoi Python n'arrive pas à appliquer le mécanisme de "Constant folding". Si il y arrivait, il devrait attribuer la même
# référence à x et y comme dans l'exemple n°2
x = 257
y = int('257')
print(f"exemple 4 : {x is y}")

Ce script me retourne

exemple 1 : True
exemple 2 : True
exemple 3 : True
exemple 4 : False

exemple 1 : pas de soucis la dessus. Le small integer caching est utilisé, j'obtiens donc la même référence.
exemple 2 : le small integer caching n'est soit disant pas forcément limité à la plage -5, 256. Donc ça parait toujours logique d'obtenir la même référence.
exemple 3 : je m'étais dis qu'en faisant une assignation "dynamique", j'allais forcément obtenir une nouvelle référence. Mais visiblement non. Après quelques recherches, j'en ai déduit que c'était le mécanisme appelé "Constant Folding" qui était probablement à l'oeuvre. Si j'ai bien compris, le "Constant Folding" est une des étapes d'optimisations de la compilation et il vise à pré-calculer les expressions dont le résultat est connu à l'avance. En sachant ça, ok ça me parait logique de récupérer un True.
exemple 4 : la par contre je comprends pas. Si c'est bien le "Constant Folding" qui est à l'oeuvre dans l'exemple n°3, pourquoi ça ne fonctionne pas ici ? En principe il devrait comprendre que int('257') vaut 257 (comme dans l'exemple n°3) et il devrait ensuite attribuer la même référence à x et y comme dans l'exemple n°2 via le mécanisme de small integer caching.

Je précise que j'exécute le script dans un fichier, en python 3.13.3.

Gabriel Trouvé

Mentor

Tu es hors de la plage -5 / 256.

Donc tu passes par une func ici qui va te créer un nouvel objet.

Oui mais dans l'exemple 2 je suis hors de la plage -5 / 256 aussi, et pourtant je récupère la même référence.
Alors que dans l'exemple 4, je récupère 2 références différentes. Je comprends pas pourquoi.

x = 257
y = 257
print(f"exemple 2 : {x is y}")
>>> True

x = 257
y = int('257')
print(f"exemple 4 : {x is y}")
>>> False

Gabriel Trouvé

Mentor

Le problème c'est qu'il y a plusieurs mécanismes d'optimisation qui peuvent entrer en jeu, et ils ne s'appliquent pas tous dans les mêmes situations.

Pour x=257, y=257 : optimisation du compilateur (littéraux identiques = même objet), pour x=257, y=int('257'): appel de fonction = création d'un nouvel objet"
Je pense que c'est l'optimisation des littéraux identiques vs l'appel de fonction qui fait la différence !

Thibault houdon

Mentor

Salut Madem,

Ça dépend effectivement déjà du contexte d'exécution.

Ton exemple 2 dans un interpréteur interactif retourne bien False :

>>> x = 257
>>> y = 257
>>> print(f"exemple 2 : {x is y}")
exemple 2 : False

Dans ton fichier ça retourne True car il peut optimiser ton code en ayant tout le contexte à l'avance.

Par contre dans le cas de int, il y a une transformation, le résultat n'est donc pas forcément connu.

Tu vas me dire "oui mais int ça retourne forcément un nombre" et on pourrait donc penser que l'interpréteur pourrait arriver à la conclusion qu'on aura donc 2 nombres identiques, et qu'il peut optimiser.

Mais la fonction int peut être redéfinie, changeant complètement le résultat, par exemple :

def int(nombre):
    return 10

x = 257
y = int('257')

print(y)  # 10

Tu vois ici qu'on crée notre propre fonction int et que le résultat n'est ainsi plus prévisible : on transforme y en 10 au lieu de 257.

Bon là tu pourrais dire "l'interpréteur pourrait voir qu'on redéfini int ici et ne pas faire d'optimisation, et n'en faire que quand int n'est pas redéfini".

Oui, mais là je pense qu'on rentrerait dans des optimisations qui serait trop longues et complexes (imagine sur des gros scripts si tout le code doit être scanné pour vérifier que toutes les fonctions de base ne sont pas redéfinies, et ainsi faire des optimisations, ça serait un peu contre-productif).

Merci pour les retours.

Je m'étais un peu mélangé les pinceaux mais avec les derniers retours / pas mal de tests de mon côté ainsi que ce chapitre annexe (https://www.docstring.fr/formations/les-10-erreurs-du-debutant/legalite-avec-is-et-380/), je crois avoir compris :) Je n'avais en effet pas pensé au fait qu'on pouvait redéfinir (et écraser) une fonction / classe comme int avec une fonction perso.

Mais donc si je résume :

  • int de -5 à 256 -> pointera toujours (peu importe que ça soit une création littérale ou dynamique) vers un même et unique objet grâce au small integer caching

  • int en de hors de la plage -5;256 :

    • pointera vers un même et unique objet si il y a une création littérale et que le compilateur arrive à optimiser la compilation via le mécanisme de "Constant Folding"

    • pointera vers des objets différents si le cas ci-dessus n'est pas applicable

Gabriel Trouvé

Mentor

Pour cette partie

pointera vers un même et unique objet si il y a une création littérale et que le compilateur arrive à optimiser la compilation via le mécanisme de "Constant Folding"

Ce n'est pas toujours évident mais en gros oui.

Inscris-toi

(c'est gratuit !)

Inscris-toi

Tu dois créer un compte pour participer aux discussions.

Créer un compte

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.