Cycles de références
Bonsoir, je n'arrive pas à bien comprendre le concept de " cycle de référence ", j'ai eu cet exemple avec GPT. Déja est ce qu'en mémoire, mario.friend et mario sont deux références distinctes ? Ou c'est construit d'une autre manière en python en mémoire ? mario.friend va référencer l'objet "luigi" ? J'espère que j'ai formulé la dernière phrase de façon correcte. Egalement, mario tout court sans le .name va référencer quoi ? Voilà, ça fait beaucoup d'intérrogations mais j'ai un peu de mal avec ce concept de " référence " et donc, logiquement de cycle de référence.
class GameCharacter:
def __init__(self, name):
self.name = name
self.friend = None # Au début, le personnage n'a pas d'ami
def add_friend(self, character):
self.friend = character # Ajoute un ami au personnage
# Création de deux personnages
mario = GameCharacter("Mario")
luigi = GameCharacter("Luigi")
# Mario devient ami avec Luigi
mario.add_friend(luigi)
# Luigi devient ami avec Mario, créant un cycle de références
luigi.add_friend(mario)
# À ce stade, Mario référence Luigi comme ami, et Luigi référence Mario comme ami.
# Cela crée un cycle de références
Salut Yanis,
Si on revient à un exemple plus basique, tu peux observer cela avec un cas très simple d'une variable qui s'auto-référence :
a = []
a.append(a)
Tu retrouves ça aussi parfois dans les imports avec des imports circulaires (un module a qui importe un module b, qui lui-même importe le module a).
Tu te retrouves dans des situations circulaires et récursives qu'il faut généralement éviter (tu peux en tirer profit parfois dans des cas particuliers comme les graphs mais c'est des cas assez spécifiques).
Pour revenir à ton exemple précis, tu peux répondre à ta question avec la fonction id :
>>> print(id(mario))
4794392048
>>> print(id(luigi.friend))
4794392048
Tu vois effectivement que oui, mario et luigi.friend font référence au même objet en mémoire. C'est normal car quand tu fais self.friend = character, tu fais juste créer un nom en mémoire qui pointe vers ton objet mario.
C'est le concept même des variables. Encore une fois on peut revenir à un exemple plus simple pour démontrer cela :
>>> a = []
>>> b = a
>>> id(a)
4307215296
>>> id(b)
4307215296
>>> a.append(1)
>>> b
[1]
>>> a
[1]
Ici a et b sont deux noms qui pointent vers le même objet en mémoire. Quand tu fais b = a tu ne dis pas à Python : "Crée un objet nommé b qui est égal à une liste vide", mais "Crée un objet b qui référence le même objet que celui contenu dans a".
Si tu veux créer une copie de a, tu dois le dire explicitement :
>>> from copy import copy
>>> a = []
>>> b = copy(a)
>>> id(a)
4305419904
>>> id(b)
4307209344
>>> a.append(1)
>>> a
[1]
>>> b
[]
Et pour ta dernière question, mario "tout court" représente l'instance de ta classe :
>>> print(mario)
<__main__.GameCharacter object at 0x15d469df0>
C'est un objet (comme tout en Python), qui représente une instance de GameCharacter, avec ses attributs, ses méthodes, etc.
name est un attribut de cette instance, dans ce cas-ci, un objet de type chaîne de caractères, qui a lui aussi une adresse en mémoire (car c'est lui aussi un objet, un objet str qui appartient à ton objet / instance mario de GameCharacter) :
>>> print(mario.name)
Mario
>>> print(id(mario.name))
4347445104
J'ai vu que c'etait des questions qui revenaient souvent en entretien : les cycles de références et les imports circulaires !
La POO apporte une couche d'abstraction en plus et c'est très compliqué en ce moment pour moi de comprendre ce qui se passe en mémoire (même si le but de python c'est justement d'eviter de se soucier de ça mais c'est important pour les entretients hahah), quand on créé une classe, un attributs d'instance etc. Mais corrige moi si je me trompe, en python tout est objet, même les classes ?
Si je fais a="bonjour" je créé une variable (référence) qui référence un objet de la classe str qui a également été créé au même moment, et on a : un nom ( la référence) qui est stocké quelque part en mémoire, et l'objet de la classe str qui est stocké ailleurs. Si je fais b=a, je créé juste un autre nom en mémoire mais pas un autre objet de la classe 'str'... Corrige moi si je me trompe.
La classe str par exemple est elle aussi un objet ( vu que tout est objet) qui est stocké en mémoire et qui est une instance d'une autre classe (type et object si j'ai bien compris jusqu'à ce qu'on arrive à l'implémentation du langage python en C) ? Je vais peut être un peu trop loin mais c'est toujours mieux de comprendre comment c'est implémenté !
Effectivement, la POO ajoute une couche d'abstraction qui peut rendre la visualisation en mémoire plus complexe. Mais les principes fondamentaux restent les mêmes.
Tu as bien compris le concept de base : en Python, tout est objet, y compris les classes elles-mêmes. Quand tu crées une variable, tu crées en fait une référence qui pointe vers un objet.
Si on reprend ton exemple :
a = "bonjour"
Ici, a est un nom qui fait référence à un objet de type str contenant la valeur "bonjour". Tu as raison de dire que la variable a et la valeur "bonjour" sont stockées séparément en mémoire.
Concernant les classes, tu as également raison. Les classes sont aussi des objets en Python. Elles sont des instances de la métaclasse type. Quand tu définis une classe, tu crées en fait un objet de type type qui représente cette classe. Ça peut paraître un peu fou au début 😅 Mais finalement je trouve que ça rend la chose plus simple : tout ce que tu manipules en Python est "similaire" et fonctionne selon le même principe d'objets :
class MaClasse:
pass
print(type(MaClasse)) # class 'type'
Donc oui, les classes sont stockées de la même manière que les autres objets, avec un nom (le nom de la classe) qui fait référence à l'objet classe en mémoire.
La différence principale avec un langage comme C est que Python gère automatiquement la mémoire et utilise ce système de référence pour tous les types de données. En C, tu as une distinction plus claire entre les types primitifs (stockés directement) et les types complexes (stockés par référence).
En Python, cette distinction n'existe pas vraiment au niveau du langage lui-même, bien que l'implémentation sous-jacente puisse optimiser certains cas (comme les petits entiers ou les chaînes courtes, qu'on appelle souvent par le terme "Small integer caching").
Le mieux pour visualiser cela, tu peux toujours utiliser la fonction id() pour voir l'identité unique de chaque objet en mémoire, et sys.getrefcount() pour voir combien de références pointent vers un objet donné.
import sys
a = "bonjour"
b = a
print(id(a)) # Adresse de l'objet en mémoire
print(id(b)) # Même adresse que a
print(sys.getrefcount(a)) # Nombre de références à l'objet (généralement 3 ici : a, b, et l'argument passé à getrefcount)
Ah ouii ! Génial, et je suis d'accord sur le fait que ça rende la chose plus simple au final quand on comprend. Je ne connaissais pas sys.getrefcount() ! Merci pour l'explication super claire, je vais garder ça quelque part pour l'avoir à portée de main.
Mais j'ai aussi une autre question : comment le garbage collector gère ces cycles de références et import circulaires ? Vu que c'est automatique en python pour l'allocation de la mémoire, ou plutot, comment les éviter (j'ai d'ailleurs un peu de mal avec le concept d'import circulaire).
Salut Yanis,
Python utilise le garbage collector (GC) pour gérer l'allocation et la désallocation de la mémoire sans que tu aies besoin d'y toucher.
Le GC en Python est principalement basé sur le comptage de références, mais il est également capable de détecter des cycles de références. Le comptage de références fonctionne en maintenant le nombre de références à chaque objet en mémoire. Lorsque ce nombre tombe à zéro, l'objet peut être immédiatement récupéré ("collecté"). Mais le comptage de références seul ne suffit pas à gérer les cycles de références, où deux objets se réfèrent mutuellement parce que ça empêche leur "compte" de tomber à 0.
Pour gérer ces cycles, il y a un ramasse-miettes supplémentaire (le collecteur de cycles), qui recherche périodiquement ces cycles et les nettoie. Ce collecteur de cycles parcourt les objets et détecte les objets qui ne sont plus accessibles par le programme (c'est-à-dire qui ne sont pas référencés directement ou indirectement par des variables globales ou locales). Quand il trouve de tels cycles, il est capable de les collecter et de libérer la mémoire allouée.
Pour les imports circulaires, ça arrive quand deux modules s'importent mutuellement. Par exemple, le module a importe le module b, et le module b importe le module a. En Python, les imports sont exécutés dès qu'ils sont rencontrés dans le fichier. Si un module b importe un module a, et que a importe également b, Python pourrait se retrouver à essayer d'importer a alors qu'il n'a pas fini d'importer b, et c'est ça qui crée la problématique.
Pour les éviter il faut généralement restructurer ton code, si ton code est bien découpé et séparé, normalement tu ne devrais pas avoir des fonctions qui s'appellent dans tous les sens.
Dans les cas les plus extrêmes, tu peux mettre l'import dans un espace local, à l'intérieur de ta fonction par exemple, ça règle le problème mais c'est vraiment en dernier recours :
# module_a.py
def some_function():
from module_b import some_other_function
some_other_function()
# module_b.py
def some_other_function():
pass
Ça permet d'éviter l'import circulaire, car l'import de module_b n'est effectué que lorsque some_function est appelée, et non au moment de l'import du module a :)
Inscris-toi
(c'est gratuit !)
Tu dois créer un compte pour participer aux discussions.
Créer un compte