Je viens de réaliser qu'en Python, si l'on écrit
for i in a:
i += 1
Les éléments de la liste d'origine a
ne seront en fait pas affectés du tout, car la variable i
se révèle être simplement une copie de l'élément d'origine dans a
.
Afin de modifier l'élément d'origine,
for index, i in enumerate(a):
a[index] += 1
serait nécessaire.
J'ai été vraiment surpris par ce comportement. Cela semble être très contre-intuitif, apparemment différent des autres langues et a entraîné des erreurs dans mon code que j'ai dû déboguer pendant longtemps aujourd'hui.
J'ai déjà lu le didacticiel Python. Juste pour être sûr, j'ai revu le livre tout à l'heure, et il ne mentionne même pas du tout ce comportement.
Quel est le raisonnement derrière cette conception? Est-il censé être une pratique standard dans de nombreuses langues, de sorte que le didacticiel estime que les lecteurs devraient le comprendre naturellement? Dans quelles autres langues le même comportement à l'itération est-il présent, auquel je devrais faire attention à l'avenir?
i
est immuable ou si vous effectuez une opération sans mutation. Avec une liste imbriquéefor i in a: a.append(1)
aurait un comportement différent; Python ne copie pas les listes imbriquées. Cependant, les entiers sont immuables et l'addition renvoie un nouvel objet, il ne change pas l'ancien.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. Idem en C #i = i + 1
à affectera
?Réponses:
J'ai déjà répondu à une question similaire récemment et il est très important de réaliser que cela
+=
peut avoir différentes significations:Si le type de données implémente l'ajout sur place (c'est-à-dire qu'il a une
__iadd__
fonction qui fonctionne correctement ), les données auxquelles il esti
fait référence sont mises à jour (peu importe qu'elles soient dans une liste ou ailleurs).Si le type de données n'implémente pas de
__iadd__
méthode, l'i += x
instruction n'est que du sucre syntaxiquei = i + x
, donc une nouvelle valeur est créée et affectée au nom de la variablei
.Si le type de données implémente
__iadd__
mais fait quelque chose de bizarre. Il pourrait être possible qu'il soit mis à jour ... ou non - cela dépend de ce qui est mis en œuvre là-bas.Les entiers Pythons, les flottants et les chaînes ne sont pas implémentés
__iadd__
, ils ne seront donc pas mis à jour sur place. Cependant, d'autres types de données commenumpy.array
oulist
s l'implémentent et se comporteront comme prévu. Donc, ce n'est pas une question de copie ou de non-copie lors de l'itération (normalement, cela ne fait pas de copies pourlist
s ettuple
s - mais cela dépend aussi de l'implémentation des conteneurs__iter__
et de la__getitem__
méthode!) - c'est plus une question de type de données vous avez stocké dans votrea
.la source
Clarification - terminologie
Python ne fait pas de distinction entre les concepts de référence et de pointeur . Ils utilisent généralement le terme référence , mais si vous comparez avec des langages comme C ++ qui ont cette distinction - c'est beaucoup plus proche d'un pointeur .
Étant donné que le demandeur provient clairement de l'arrière-plan C ++ et que cette distinction - qui est requise pour l'explication - n'existe pas en Python, j'ai choisi d'utiliser la terminologie de C ++, qui est:
void foo(int x);
est la signature d'une fonction qui reçoit un entier par valeur .void foo(int* x);
est la signature d'une fonction qui reçoit un entier par pointeur .void foo(int& x);
est la signature d'une fonction qui reçoit un entier par référence .Que voulez-vous dire par "différent des autres langues"? La plupart des langues que je connais qui prennent en charge chaque boucle copient l'élément sauf indication contraire.
Spécifiquement pour Python (bien que plusieurs de ces raisons puissent s'appliquer à d'autres langages avec des concepts architecturaux ou philosophiques similaires):
Ce comportement peut provoquer des bogues pour les personnes qui ne le connaissent pas, mais le comportement alternatif peut provoquer des bogues même pour ceux qui en sont conscients . Lorsque vous affectez une variable (
i
), vous ne vous arrêtez généralement pas et considérez toutes les autres variables qui seraient modifiées à cause de cela (a
). Limiter la portée sur laquelle vous travaillez est un facteur majeur pour empêcher le code spaghetti, et donc l'itération par copie est généralement la valeur par défaut même dans les langues qui prennent en charge l'itération par référence.Les variables Python sont toujours un pointeur unique, il est donc bon marché d'itérer par copie - moins cher que d'itérer par référence, ce qui nécessiterait un report supplémentaire chaque fois que vous accédez à la valeur.
Python n'a pas le concept de variables de référence comme - par exemple - C ++. Autrement dit, toutes les variables en Python sont en fait des références, mais dans le sens où ce sont des pointeurs - pas des références de constat en arrière-plan comme des
type& name
arguments C ++ . Puisque ce concept n'existe pas en Python, implémenter l'itération par référence - et encore moins en faire la valeur par défaut! - nécessitera d'ajouter plus de complexité au bytecode.L'
for
instruction de Python fonctionne non seulement sur les tableaux, mais sur un concept plus général de générateurs. Dans les coulisses, Python appelleiter
vos tableaux pour obtenir un objet qui - lorsque vous l'appeleznext
- renvoie soit l'élément suivant, soitraise
saStopIteration
. Il existe plusieurs façons d'implémenter des générateurs en Python, et il aurait été beaucoup plus difficile de les implémenter pour l'itération par référence.la source
*it = ...
- mais ce type de syntaxe indique déjà que vous modifiez quelque chose ailleurs - ce qui rend la raison n ° 1 moins problématique. Les raisons # 2 et # 3 ne s'appliquent pas aussi bien, car en C ++ la copie est chère et le concept de variables de référence existe. Comme pour la raison # 4 - la possibilité de renvoyer une référence permet une implémentation simple pour tous les cas.Aucune des réponses ici ne vous donne de code à utiliser pour illustrer vraiment pourquoi cela se produit en terre Python. Et c'est amusant à regarder dans une approche plus profonde, alors voilà.
La principale raison pour laquelle cela ne fonctionne pas comme prévu est parce qu'en Python, lorsque vous écrivez:
il ne fait pas ce que vous pensez qu'il fait. Les entiers sont immuables. Cela peut être vu lorsque vous regardez ce qu'est réellement l'objet en Python:
La fonction id représente une valeur unique et constante pour un objet au cours de sa durée de vie. Conceptuellement, il mappe de manière lâche à une adresse mémoire en C / C ++. Exécution du code ci-dessus:
Cela signifie que le premier
a
n'est plus le même que le seconda
, car leurs identifiants sont différents. En fait, ils se trouvent à différents endroits de la mémoire.Avec un objet, cependant, les choses fonctionnent différemment. J'ai remplacé l'
+=
opérateur ici:L'exécution de cette opération entraîne la sortie suivante:
Notez que l'attribut id dans ce cas est en fait le même pour les deux itérations, même si la valeur de l'objet est différente (vous pouvez également trouver la
id
valeur int de l'objet, qui changerait au fur et à mesure de sa mutation - car les entiers sont immuables).Comparez cela à lorsque vous exécutez le même exercice avec un objet immuable:
Cela produit:
Quelques choses ici à remarquer. Tout d'abord, dans la boucle avec le
+=
, vous n'ajoutez plus à l'objet d'origine. Dans ce cas, comme les entiers font partie des types immuables de Python , python utilise un identifiant différent. Il est également intéressant de noter que Python utilise le même sous-jacentid
pour plusieurs variables avec la même valeur immuable:tl; dr - Python a une poignée de types immuables, qui provoquent le comportement que vous voyez. Pour tous les types mutables, votre attente est correcte.
la source
La réponse de @ Idan explique bien pourquoi Python ne traite pas la variable de boucle comme un pointeur comme vous le feriez en C, mais cela vaut la peine d'expliquer plus en détail comment les extraits de code sont décompressés, comme dans Python, beaucoup de bits d'apparence simple de code seront en fait des appels à des méthodes intégrées . Pour prendre votre premier exemple
Il y a deux choses à décompresser: la
for _ in _:
syntaxe et la_ += _
syntaxe. Pour prendre la boucle for en premier, comme d'autres langages, Python a unefor-each
boucle qui est essentiellement du sucre de syntaxe pour un modèle d'itérateur. En Python, un itérateur est un objet qui définit une.__next__(self)
méthode qui renvoie l'élément actuel dans la séquence, passe au suivant et déclenche unStopIteration
lorsqu'il n'y a plus d'éléments dans la séquence. Un Iterable est un objet qui définit une.__iter__(self)
méthode qui renvoie un itérateur.(NB: an
Iterator
est aussi unIterable
et revient de sa.__iter__(self)
méthode.)Python aura généralement une fonction intégrée qui délègue à la méthode de soulignement double personnalisée. Il a donc
iter(o)
qui se résout ào.__iter__()
etnext(o)
qui se résout ào.__next__()
. Notez que ces fonctions intégrées essaient souvent une définition par défaut raisonnable si la méthode à laquelle elles délégueraient n'est pas définie. Par exemple,len(o)
se résout généralement eno.__len__()
mais si cette méthode n'est pas définie, elle essaiera alorsiter(o).__len__()
.Une boucle est essentiellement défini en termes de
next()
,iter()
et des structures de contrôle plus basiques. En général, le codesera déballé à quelque chose comme
Donc dans ce cas
est déballé à
L'autre moitié est
i += 1
. En général, il%ASSIGN% += %EXPR%
est déballé%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Voici l'__iadd__(self, other)
ajout en place et se retourne.(NB Il s'agit d'un autre cas où Python choisira une alternative si la méthode principale n'est pas définie. Si l'objet ne l'implémente pas,
__iadd__
il retombera__add__
. Il le fait dans ce cas commeint
ne l'implémente pas__iadd__
- ce qui est logique car sont immuables et ne peuvent donc pas être modifiés sur place.)Donc, votre code ressemble ici
où nous pouvons définir
Il se passe un peu plus dans votre deuxième morceau de code. Les deux nouvelles choses que nous devons savoir sont celles qui sont
%ARG%[%KEY%] = %VALUE%
déballées(%ARG%).__setitem__(%KEY%, %VALUE%)
et%ARG%[%KEY%]
déballées(%ARG%).__getitem__(%KEY%)
. En rassemblant ces connaissances, nous sommesa[ix] += 1
décompressésa.__setitem__(ix, a.__getitem__(ix).__add__(1))
(encore une fois:__add__
plutôt que__iadd__
parce que__iadd__
n'est pas implémenté par ints). Notre code final ressemble à:Pour répondre à réellement votre question de savoir pourquoi le premier ne modifie pas la liste tandis que le second fait, dans notre premier extrait que nous obtenons à
i
partirnext(_a_iter)
, ces moyensi
seront unint
. Puisqueint
s ne peut pas être modifié sur place,i += 1
ne fait rien à la liste. Dans notre deuxième cas, nous ne modifions pas leint
mais modifions la liste en appelant__setitem__
.La raison de tout cet exercice élaboré est parce que je pense qu'il enseigne la leçon suivante sur Python:
Les méthodes de double soulignement sont un obstacle au début, mais elles sont essentielles pour soutenir la réputation de "pseudocode exécutable" de Python. Un programmeur Python décent aura une compréhension approfondie de ces méthodes et de la façon dont elles sont invoquées et les définira partout où cela sera judicieux de le faire.
Edit : @deltab a corrigé mon utilisation bâclée du terme "collection".
la source
__len__
__contains__
+=
fonctionne différemment selon que la valeur actuelle est modifiable ou immuable . C'était la principale raison pour laquelle il fallait attendre longtemps pour qu'elle soit implémentée en Python, car les développeurs de Python craignaient que ce soit déroutant.Si
i
est un int, alors il ne peut pas être changé car les entiers sont immuables, et donc si la valeur dei
change alors il doit nécessairement pointer vers un autre objet:Cependant, si le côté gauche est modifiable , alors + = peut réellement le changer; comme si c'était une liste:
Dans votre boucle for,
i
fait référence à chaque élément dea
tour à tour. S'il s'agit d'entiers, le premier cas s'applique et le résultat dei += 1
doit être qu'il se réfère à un autre objet entier.a
Bien sûr, la liste contient toujours les mêmes éléments qu'elle avait toujours.la source
i = 1
définiti
sur un objet entier immuable, alorsi = []
devrait définiri
sur un objet de liste immuable. En d'autres termes, pourquoi les objets entiers sont-ils immuables et les objets de liste sont-ils mutables? Je ne vois aucune logique derrière cela.list
implémente des méthodes qui changent son contenu,int
non.[]
est un objet liste mutable, eti = []
permet dei
faire référence à cet objet.+=
opérateur / la méthode pour se comporter de manière similaire (principe de la moindre surprise) pour les deux types: soit changer l'objet d'origine soit renvoyer une copie modifiée pour les entiers et les listes.+=
est surprenant en Python, mais on a estimé que les autres options que vous mentionnez auraient également été surprenantes, ou du moins moins pratiques (changer l'objet d'origine ne peut pas être fait avec le type de valeur le plus courant vous utilisez + = avec, ints. Et copier une liste entière est beaucoup plus cher que de la muter, Python ne copie pas des choses comme des listes et des dictionnaires, sauf indication contraire explicite). C'était un énorme débat à l'époque.La boucle ici est un peu hors de propos. Tout comme les paramètres de fonction ou les arguments, la configuration d'une boucle for comme celle-ci est essentiellement une affectation de fantaisie.
Les entiers sont immuables. La seule façon de les modifier consiste à créer un nouvel entier et à lui attribuer le même nom que l'original.
La sémantique de Python pour l'affectation est directement mappée sur les C (sans surprise étant donné les pointeurs PyObject * de CPython), avec les seules mises en garde étant que tout est un pointeur, et vous n'êtes pas autorisé à avoir des pointeurs doubles. Considérez le code suivant:
Ce qui se produit? Il imprime
1
. Pourquoi? Il est en fait à peu près équivalent au code C suivant:Dans le code C, il est évident que la valeur de
a
n'est pas du tout affectée.Quant à savoir pourquoi les listes semblent fonctionner, la réponse est essentiellement que vous attribuez le même nom. Les listes sont modifiables. L'identité de l'objet nommé
a[0]
changera, maisa[0]
reste un nom valide. Vous pouvez vérifier cela avec le code suivant:Mais ce n'est pas spécial pour les listes. Remplacez
a[0]
ce code pary
et vous obtenez exactement le même résultat.la source