Petites questions théoriques : peut-on comparer une variable liste avec un pointeur en C?
Le
Francois

Bonjour à tous,
J'ai quelques questions qui peut-être auront un rapport avec
l'implémentation, mais cela m'intéresse quand même.
1) Quand on fait
a = 2
b = a
Les deux variables a et b sont deux variables différentes, la zone
mémoire allouée pour a et celle pour b sont distinctes. Une modification
de a n'affectera pas b etc. Chacun vit ça vie.
Ce qui me chagrine, c'est que id(a) et id(b) me donne exactement la même
valeur, alors que, pour moi, a et b sont distinctes. D'ailleurs quand je
modifie b (via b = 50 par exemple), là, ce petit coquin de Python ne me
donne plus le même id (et heureusement). Avez vous une explication ?
2) Pour les listes, c'est différent et c'est bien dit dans les docs. Par
exemple :
l1 = [0,1,2,3,4]
l2 = l1
J'ai bien compris que "le nom l2 est un alias du nom l1" qui désigne le
même objet dans la mémoire de l'ordinateur. Une modification via l1 se
retrouvera en appelant la liste via l2. Par exemple :
l1[0] = 100
entraînera l2 de la forme [100,1,2,3,4]
Je voulais trouver une petite explication à cela. J'aimerais avoir votre
avis. Je me suis dit que, peut-être, l1 et l2 étaient des sortes de
pointeurs (comme en C) sur le premier élément de la liste.
3) J'ai lu ceci "avec a = 3, a est une référence, c'est-à-dire un nom
désignant l'emplacement mémoire d'une valeur (objet)". Cela veut-il dire
qu'en fait les variables dans Python sont toutes "des pointeurs en C" ?
--
François
J'ai quelques questions qui peut-être auront un rapport avec
l'implémentation, mais cela m'intéresse quand même.
1) Quand on fait
a = 2
b = a
Les deux variables a et b sont deux variables différentes, la zone
mémoire allouée pour a et celle pour b sont distinctes. Une modification
de a n'affectera pas b etc. Chacun vit ça vie.
Ce qui me chagrine, c'est que id(a) et id(b) me donne exactement la même
valeur, alors que, pour moi, a et b sont distinctes. D'ailleurs quand je
modifie b (via b = 50 par exemple), là, ce petit coquin de Python ne me
donne plus le même id (et heureusement). Avez vous une explication ?
2) Pour les listes, c'est différent et c'est bien dit dans les docs. Par
exemple :
l1 = [0,1,2,3,4]
l2 = l1
J'ai bien compris que "le nom l2 est un alias du nom l1" qui désigne le
même objet dans la mémoire de l'ordinateur. Une modification via l1 se
retrouvera en appelant la liste via l2. Par exemple :
l1[0] = 100
entraînera l2 de la forme [100,1,2,3,4]
Je voulais trouver une petite explication à cela. J'aimerais avoir votre
avis. Je me suis dit que, peut-être, l1 et l2 étaient des sortes de
pointeurs (comme en C) sur le premier élément de la liste.
3) J'ai lu ceci "avec a = 3, a est une référence, c'est-à-dire un nom
désignant l'emplacement mémoire d'une valeur (objet)". Cela veut-il dire
qu'en fait les variables dans Python sont toutes "des pointeurs en C" ?
--
François
a et b sont deux noms différents. A ce stade, ils sont associés à une
référence sur le même objet.
Si tu commences à raisonner avec les concepts du C, tu vas te faire du
mal pour rien. En Python, une variable n'est pas une étiquette sur une
adresse mémoire, c'est un nom associé à une référence sur un objet . Le
nom lui-même n'est rien d'autre que ça : un nom. Une clé dans une
table de hachage, si tu préfère (et accessoirement, hormis pour les
variables locales d'une fonction, c'est exactement ça : une clé dans
une table de hachage). Quant à la zone mémoire où réside l'objet (le s
zones mémoires, d'ailleurs, vu que c'est un type de donnée structuré
composé en partie de références sur d'autres objets...), c'est
l'affaire de la machine virtuelle. De ton point de vue, il pourrait
aussi bien être sur une tablette d'argile.
Donc,en bref, oublie les zones mémoires, et pense en termes d'objets.
Et dans ton cas, non, a et b référencent le *même* objet. En Python,
une assignation signifie "associe ce nom avec une référence sur cet
objet (ie : a = 2) ou sur l'objet référéncé par ce nom (ie: b = a)".
Un objet n'est *jamais* copié si tu ne le demande pas très
explicitement (en utilisant copy.copy() ou copy.deepcopy()).
Pour quelle définition de "modification" ? changement de l'état de
l'objet référencé par a, ou rebinding (réassignation) d'un autre obj et
sur a ?
Dans le premier cas, vu que 2 est un objet immutable, tu ne peux tout
simplement pas le modifier. Avec un objet mutable, par contre, toute
modification de l'objet sera 'visible' depuis tous les noms
référençant cet objet (et pour cause...). Par exemple:
c = [1]
d = c
c[0] = 2
print d
d.append(42)
print c
Dans le second cas (réassignation), faire pointer un nom sur un autre
objet est bien sûr sans incidence sur les autres paires nom:référence
pointant sur l'objet d'origine, ie:
c = [1]
d = c
assert id(c) == id(d)
# equivalent:
assert c is d
c = 'toto'
assert d is not c
cf ci-dessus.
C'est normal.
les noms sont distinct. Mais ils pointent sur le même objet. C'est
équivalent à:
ns = {}
ns['a'] = 2
ns['b'] = ns['a']
Là, tu t'attends bien à ce que ns['a'] et ns['b']
"soient" (référencent) le même objet. Après, si tu fais:
ns['b'] = 50
tu t'attends aussi à ce que ns['b'] pointe sur un objet différent.
Si ton code est au top-level de ton script ou module (ou de
l'interpréteur), tu peux s/ns/globals()/g
Tu ne modifie pas 'b', tu le fait pointer sur un autre objet. Ce qui
est sans conséquence sur l'objet précédemment référencé par b (s i ce
n'est de décrémemter son compteur de références).
cf ci-dessus.
Absolument pas. Ni pour aucune autre sorte d'objet.
Il y a une nette différence entre :
l1[0] = 100
et
1l = 100
Le premier est en fait du sucre syntaxique pour
l1.__setitem__(0, 100)
Bref, un appel de méthode qui va modifier l'état de la liste
référencée par l1.
une référence
en l'occurence, comme en C++ ou en Java
sur l'objet liste.
En Python, tu n'a *que* des références sur des objets, *jamais* de
types 'immédiat' comme en C. Ne laisse pas la syntaxe te tromper, ce
n'est pas parce que tu définis un objet via une expression littérale
que tu a autre chose qu'une référence sur un objet.
Bien qu'à peu près correcte (au moins pour CPython), cette explication
n'est AMHA pas très bonne en ce qu'elle fait appel à une notion
(l'emplacement mémoire) qui n'existes tout simplement pas dans le
langage (même si elle existe, bien sûr, dans l'implémentation du
langage...).
Non, même pas. Une variable C de type pointeur contient une adresse
mémoire. Un nom ne contient rien - une clé dans une table de hachage
ne contient rien.
Ah oui, tiens, et les variables locales d'une fonctions ? C'est aussi un
nom associé à une référence sauf que l'espace des noms dans lequel elle
se trouve est dans un espace temporaire disjoint de l'espace de nom du
script principal ?
Je comprends cette logique : il faut rester dans le cadre du langage
abstrait et ne pas rentrer dans l'implémentation. Tu as raison. Mais,
dès fois, je trouve que rentrer (un tout petit peu) dans
l'implémentation, ça m'aide à comprendre.
Là, je crois que *tout* est dit dans ce paragraphe très clair. Je résume
pour voir.
1) En *C*, une variable est un objet (un peu au sens de Python, avec un
type [taille + façon d'être interprété], une adresse et une valeur). On
peut donc parler de l'adresse d'une variable. Deux variables avec des
noms différents ont chacune une adresse différente.
2) Avec *Python*, une variable est un simple nom faisant référence à un
objet (avec un type [taille + façon d'être interprété], une adresse et
une valeur). Une variable n'a pas d'adresse. Deux variables avec des
noms différents peuvent très bien référencer un même objet.
Par exemple avec ceci (en Python)
a=1
b=1
c=1
finalement, dans la mémoire se trouve un seul objet créé (l'entier 1).
Alors qu'avec l'équivalent en C, trois objets seraient crées (de même
valeur, mais à trois endroits différents).
Si j'ai bon, je crois que j'ai franchi un petit cap dans ma
compréhension de Python. :-)
Remarque en passant : Je me doutais bien que d'un langage à un autre,
des mêmes notions pouvaient avoir une implémentation différente. Mais
quand même, je n'imaginais pas que cela serait le cas pour la basique
notion de variable par exemple ! J'espère que d'un langage à un autre,
il ne faut pas tout réapprendre à chaque fois (car là, finalement, j'ai
du réapprendre ce qu'est une variable :-) ) ?
D'accord. Dans mes autres messages, j'ai du écrire cette erreur 20 fois.
Je crois que c'est compris. Ce qui m'intéresse ici, c'est la notion de
compteur de référence. Chaque objet possède un compteur de référence,
c'est-à-dire, si je comprends bien, un compteur qui donne le nombre de
variable qui font référence à cet objet. C'est ça ?
Heu ..., mais ça sert à quoi ? À effacer l'objet de la mémoire quand le
compteur est à 0 peut-être ?
Ok, on garde une certaine cohérence sur le tout objet : avec [], on
applique sans le savoir une méthode à l'objet l1. Tu ne l'as pas précisé
mais avec l1 = 100, évidemment, on réassigne l1 vers un nouvel objet qui
n'est plus une liste.
Ok, ok. Allez, quand même, en interne de chez interne (dans le code C de
CPython par exemple), il n'y a pas de pointeur qui traîne quelque part
dans la définition d'une variable Python ? Hein, pour me faire plaisir. :-)
Heu, un type immédiat c'est quoi ?
Allez, il n'y a pas un petit pointeur caché quelque part ...
--
François
Plus j'y pense, plus je me dis que la syntaxe l1[0]0 est vraiment
traître. Elle fait vraiment penser à une réassignation comme l1 = 100,
alors qu'en fait c'est une méthode appliquée à un objet ("mutable") pour
le modifier (évidemment la syntaxe l1[0]0 est quand même
incontournable du point de vue pratique). C'est ça qui m'a induit en
erreur :
a = 2
b = a # a et b font référence au même objet
b = 5 # c'est une assignation de b vers un nouvel objet
# ce n'est pas une modification de l'objet référencé
# par b qui est non mutable
print a # donne 2
print b # donne 5 car b ne se réfère plus au même objet que a
a = [0,1,2,3]
b = a # a et b font référence au même objet
b[0] = 7 # ce n'est pas une assignation
# c'est une modification de l'objet (mutable)
# référencé par b, via une méthode
print a # donne [7,1,2,3] l'objet référencé par a et b est le
# même et il a subi une modification en cours de route
print b # donne [7,1,2,3]
Une réassignation ne modifie pas un objet, mais créé une référence vers
un nouvel objet. On peut modifier un objet, à condition qu'il soit
mutable, mais cela ne peut pas passer par une réassignation. Par une
méthode par exemple. Et b[0]=7, n'est pas une réassignation mais une
méthode cachée (la bougresse).
--
François
Bon, là j'exagère un peu. b[0]=7 est (je pense) une assignation du
premier élément de l'objet séquence référencé par a (ou par b), mais ce
n'est pas une modification de l'objet 0 (ancien objet référencé par
b[0]) mais plutôt une création d'une référence de b[0] vers le nouvel
objet 7. Cette réassignation, de fait, entraîne une modification de
l'objet séquence référencé par b et aussi par a.
--
François qui commence à ce mélanger les pinceaux et qui ferait mieux
d'aller se coucher. :-)
Exactement. Note bien que ce sont les *noms* qui sont locaux, pas les
objets qu'ils référencent. C'est particulièrement important à
comprendre pour les paramètres de fonction: si tu modifie (mutation)
un objet passé en paramètre, bien que le nom par lequel tu accèdes à
l'objet soit local, l'objet, lui, est bien celui qui a été passé à l a
fonction. Par contre, si tu rebinde un paramètre, ça n'affecte que
l'espace de nommage local. Exemple:
def unefonc(liste, dico):
liste.append(42)
dico = 'allo'
uneliste = [1, 2, 3]
undico = dict(a=1, b=2, c=3)
print uneliste, undico
unefonc(uneliste, undico)
print uneliste, undico
Tout à fait d'accord. "dès fois", et "un petit peu". En sachant que
d'autres implémentations sont possibles - au moins théoriquement, mais
parfois aussi pratiquement. Le problème pour toi, ici, c'est de
commencer par faire abstraction (justement) de ce que tu a appris pour
le C.
admettons. minus le "un peu au sens de Python".
La notion de type est totalement différente.
Selon les catégories du C, il n'existe qu'un seul type en Python, le
type Py_Object_ptr, qui est un pointeur sur une structure de données
(je parle bien sûr de l'implémentation CPython - en Jython ou
IronPython, c'est probablement différent) (et bien sûr, je simplifie
outrageusement).
Selon la sémantique propre à Python, le 'type' d'un objet, c'est
l'objet class (int, dict, function, etc) qu'il référence via son
attribut __class__. A ce niveau, il n'y a pas de notion de 'taille',
de 'façon d'être interprété' ou autre. Il y a juste une référenc e sur
un autre objet dans lequel l'interpréteur ira chercher les attributs
non trouvés dans l'objet lui-même.
Oui.
Avec CPython, oui, mais parce que les petits entiers sont conservé en
cache. Si tu remplace 1 par 33333, tu aura trois objets différents.
Par contre, si tu fais:
a = 33333
b = a
c = b
Tu aura bien trois noms référençant un seul et même objet.
Oui.
Y a du progrès.
Attends de voir un langage fonctionnel pur (comme Haskell par
exemple) : tu ne peux tout simplement pas modifier la valeur d'une
variable une fois qu'elle est crée !-)
Non, la sémantique des variables de Python (de ce point de vue là) se
retrouve, à l'identique ou de façon très proche, dans pas mal d'autres
langages de haut niveau. De même que la sémantique des variables C se
retrouve dans pas mal de langages de bas niveau.
pile-poil.
bingo !-)
moi, je le sais !-)
En pratique, à part l'assignation à un nom simple (ie: sans '.' ni []
dans l'affaire), toutes les opérations de Python sont en fait des
appels de méthode, et toutes sont surchargeables.
Indeed.
Si, bien sûr. cf ci-dessus pour l'implémentation du type objet dans
CPython. A vrai dire, y a même tellement de pointeurs que tu ne sais
plus où donner de la tête. Content ?-)
un entier en C.
Tu y tiens, hein ?-)
Mais bon, il y a d'autres implémentations de Python, dans des langages
qui ne supportent pas les pointeurs (Jython et IronPython). Ce qui
démontre bien que la notion de pointeur n'est pas nécessaire ni pour
implémenter Python, ni pour le comprendre. (NB : bon, ok, Java (et
probablement .NET) est aussi implémenté en C, donc on finis *toujours*
par avoir des bits à des adresses mémoires...)
Il y en a au moins un qui se couchera plus savant cette nuit.
--
Amaury
T'inquiètes pas, t'es pas le premier (et c'est pas non plus la
première fois que j'explique ça).
Par une méthode uniquement. Même si l'appel de méthode n'est pas
explicite.
Certes, mais elle est tellement plus élégante comme ça qu'on lui
pardonne !-)
Bon, je crois que t'es bon, là.
Dans la cas d'une liste, oui, on en arrive finalement à ça. Mais je
pourrais écrire une classe telle que la même syntaxe n'effectue aucune
assignation.
Je ne sais pas pourquoi, mais cette formulation me semble ambigüe...
Tout à fait. Mais ne modifie pas l'espace de nommage en cours, et
n'affecte pas les liaisons entre a et b et l'objet séquence (précision
à l'adresse de l'OP).
A ce propos, je ne sais pas si je suis plus savant, mais il serait
temps que j'y aille...
Ah, désolé. Faut dire, à ma décharge, que tout cela n'est pas vraiment
bien expliqué dans le peu de livres que j'ai.
Par une méthode *uniquement*, Ok. C'est précis et simple. Ça me va !
Oui, demain matin tout cela sera pardonné. Oops, on est déjà demain
matin. :-)
Moins mal qu'il y a quelques heures en tout cas. Merci encore.
--
François
Ok, là, ça fait aussi une sacrée différence avec le C.
Et là, c'est un peu comme le C, mais pour des raisons différentes. Ok,
pour l'exemple. Merci, c'est très clair.
D'accord, ça c'est d'un point de vue du langage abstrait. Tu
m'accorderas bien qu'un objet aura, in fine, une taille, une adresse et
une manière d'être interprété par la machine virtuelle Python. Non ?
Heu, c'est quoi le cache ? Pour moi, c'est une sorte de mémoire RAM qui
est seulement plus rapide et moins volumineuse que la RAM classique.
Est-ce juste ?
Si oui, en quoi cela justifie que 1 se trouve en un seul endroit de la
mémoire ?
Et zut. Ça me chagrine un peu ça. En effet :
et donc la comparaison "id(a) == id(b)" n'est pas logiquement
équivalente à la comparaison "a==b". C'est dommage, non ?
Je pensais que si Python tombait sur une affectation d'un objet déjà
existant, il ne le récréait pas, mais faisait une référence à celui déjà
existant, cela dans le but de prendre moins de place dans la mémoire.
Pourquoi diable adopter cette stratégie quand on a des «petits» entiers,
et ne pas l'adopter pour des «gros» entiers ??? Je ne saisis pas bien la
logique là dedans ?
Ouf !
On laissera ça pour ... une autre vie :-)
Ouf !
Oui ! :-)
Mais s'il y a *que* des références à des objets, cela veut dire que le
simple entier "1" en Python est un réalité un objet plus complexe qu'un
simple int ou char en C. Son codage binaire par exemple sera plus
complexe que le naturel "00000001" ?
--
François