167
votes

Comprendre l'échange de python: pourquoi a, b = b, A n'est pas toujours équivalent à b, a = a, b?

Comme nous le savons tous, la manière pythonique d'échanger les valeurs de deux éléments a et b est xxx

et il devrait être équivalent à

nums = [1, 2, 4, 3]
i = 2
nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
print(nums)
# [1, 2, 4, 3]

nums = [1, 2, 4, 3]
i = 2
nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
print(nums)
# [1, 2, 3, 4]

Cependant, aujourd'hui, quand je travaillais sur un code, j'ai accidentellement découvert que les deux swaps suivants donnent des résultats différents:

b, a = a, b


22 commentaires

Après avoir étudié votre extrait de code, la meilleure réponse que je puisse donner est "Ne faites pas ça". L'ordre des opérations est ce qui fait la différence, je pense, mais wow c'est déroutant.


@Nicomp Ce n'est pas une réponse terriblement satisfaisante. Je trouve souvent que connaître la raison pour laquelle quelque chose fonctionne comme cela m'aide dans d'autres domaines connexes.


C'est pourquoi je l'ai ajouté comme commentaire.


Mais pour les entiers, je ne vois rien de tel. a, b = b, a et b, a = a, b Les deux donnent le même résultat.


@Ram L'exemple qui échoue utilise le numéro échangé comme index dans la liste. Vérifiez les réponses.


"Comme nous le savons tous, la façon pythonique d'échanger deux éléments A et B est", non comme vous échangez deux variables . Si vous utilisez des expressions complexes, l'ordre d'évaluation entre en jeu.


En effet, il se passe beaucoup plus que a, b = b, a


Pour réaliser ce que j'attendais, vous pourriez faire à la place j = nums [i] -1; nums [i], nums [j] = nums [j], nums [i] qui est plus sûr à IMHO; Mais tu as compris ça je suppose


@Markransom: Cela peut être controversé, mais je ne recommande en fait pas de maîtrise l'ordre des opérations dans la mesure où le code comme celui-ci est lisible. S'il n'est pas vital pour votre travail, rester volontairement ignorant volontairement des considérations d'ordre de fonctionnement comme celle-ci (au-delà de la reconnaissance quand ils apparaissent) a également de la valeur; Il est plus difficile de reconnaître le mauvais code lorsque vous le comprenez immédiatement.


C'est vraiment votre intention d'utiliser nums [i] -1 comme index, plutôt que i-1 par exemple?


@jcaron J'essayais d'échanger l'index 2 et 3 afin que [1, 2, 4, 3] puisse devenir [1, 2, 3, 4] . Cependant, puisque nums [2] == 4 , je pensais qu'il serait intéressant de le faire comme nums [nums [2] -1] au lieu de simplement faire nums [3] , puis j'ai trouvé cet étrange comportement.


Rien d'étrange du tout, vous modifiez l'index que vous utilisez au milieu de votre «échange»…


Je me demande si cette question doit être renommée "Comprendre l'échange de python: pourquoi a, b = , pas toujours équivalent à b, a = , ?". Je ne sais pas quelle est la pensée acceptée sur les renoms dans la communauté de débordement de pile. Comme c'est le titre, le titre est un clic et une erreur.


@jcaron C'est étrange pour moi parce que je pensais qu'un échange de python se produit simultanément et indépendamment.


Comme vous l'avez trouvé, ce n'est pas en fait une opération d'échange. C'est juste une affectation avec une copie intermédiaire au milieu.


@Davidwiniecki: Cela n'aurait pas le problème, vous devez ", = a, b" dans le titre car cela se produit lorsque le côté gauche est complexe, le côté droit n'a pas d'importance.


Je suis d'accord que cette question a besoin d'un renommée. Rien sur cette question n'est lié à l'échange comme a, b = b, a . Son nom devrait être dans le sens de "l'ordre de fonctionnement en affectation", faisant potentiellement référence à un déballage de tuple. Idem avec les étiquettes sur cette question.


Je peux dire que le programmeur expérimenté - votre code est trop compliqué - s'il n'y a pas de super exigences de performances, n'écrivez pas dans le code complexe - il est difficile de lire et d'analyser plus tard et de conduire à des bogues car non lisibles. Je peux écrire du code beaucoup plus compliqué mais j'essaie de ne pas être gros :)


Juste curieux, sur quoi travailliez-vous lorsque vous avez découvert ce comportement? Cela ressemble au type d'échange que vous feriez si vous jouiez avec des bulles.


Un swap ne serait-il pas comme nums [i], nums [i + 1] = nums [i + 1], nums [i] ? Ce code ne ressemble pas à un échange.


Intéressant. cette autre question et cette réponse Les deux utilisent les mêmes noms de variables exacts que cette question.


Est-ce que cela répond à votre question? comportement étrange échangeant deux éléments de tableau tout en indexant par des éléments


8 Réponses :


72
votes

En effet D'abord nums [i] est attribué, puis que la valeur est utilisée pour déterminer l'index dans l'affectation à nums [nums [i] -1] Code>

lors de l'affectation comme ceci:

nums[nums[i]-1], nums[i] =

... L'index de nums [nums [i] -1] dépend de l'ancienne valeur de nums [i] , car l'affectation à nums [i] suit toujours plus tard ...

p >


2 commentaires

Le tableau a été muté. L'utilisation de valeurs du tableau muté en tant qu'indice dans le tableau aura des résultats qui dépendent de l'ordre d'exécution des mutations.


@ user253751, oui, mais le problème de l'OP n'était pas tant avec le RHS. Le RHS a déjà été évalué lorsque les affectations (sur le LHS) commencent à être terminées. Ma réponse se concentre sur cette séquence d'affectation du côté gauche.



139
votes

de python.org

L'attribution d'un objet à une liste cible, éventuellement enfermée entre parenthèses ou crochets, est défini récursivement comme suit.

...

  • Sinon: l'objet doit être un itérable avec le même nombre d'éléments qu'il y a des cibles dans la liste cible, et les éléments sont attribués, de gauche à droite, aux cibles correspondantes.

donc j'interprète que cela signifie que votre affectation

tmp = nums[i], nums[nums[i]-1]
nums[nums[i] - 1] = tmp[0]
nums[i] = tmp[1]

est à peu près équivalent à

nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]

(avec une meilleure vérification des erreurs, bien sûr)

tandis que l'autre

tmp = nums[nums[i]-1], nums[i]
nums[i] = tmp[0]
nums[nums[i] - 1] = tmp[1]

est comme

nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]

Ainsi, le côté droit est évalué d'abord dans les deux cas. Mais les deux pièces du côté gauche sont évaluées dans l'ordre, et les affectations sont effectuées immédiatement après l'évaluation. Surtout, cela signifie que le deuxième terme sur le côté gauche n'est évalué qu'après que la première affectation est déjà . Donc, si vous mettez d'abord à mettre à jour nums [i] , alors le nums [nums [i] - 1] fait référence à un index différent de ce que vous mettez à jour nums [i ] seconde.


5 commentaires

Comme exemple plus simple: si vous avez a = [2, 2, 2, 2, 2] et b = 2 alors a [b], b = 3, 4; print (a) devrait imprimer [2, 2, 3, 2, 2] car b devient 4 après a [b] est mis à jour vers 3 mais b, a [b] = 4, 3; print (a) devrait imprimer [2, 2, 2, 2, 3] car b devient 4 avant Un [b] est mis à jour vers 3 .


@Shaun La partie importante est que l'affectation précédente doit modifier l'index de cette dernière opération d'attribution. Étant donné que le retour de LVALUes de la fonction ne fonctionne pas (ou ne fonctionne-t-il pas dans Python, cela signifie vraisemblablement la seule option pour cela est cette supercherie.


Soit le tableau de tablerie, soit des montants insensés de __ getAttr __ / __ setAttr __ Fun. La ruse du tableau est probablement dix fois plus facile, cependant.


Votre dernière phrase est la clé de toute la réponse. Parfait.


L'ordre de fonctionnement du swap peut inverser une implémentation de Python différente, ce qui a fait revivre le bug, il est donc préférable de faire explicitement tous les swaps à effet secondaire potentiels!



34
votes

Cela se produit selon les règles:

  • Le côté droit est évalué d'abord
  • Ensuite, chaque valeur du côté gauche obtient sa nouvelle valeur, de gauche à droite.

Ainsi, avec nums = [1, 2, 4, 3] , votre code dans le premier cas

nums[nums[2]-1], nums[2] = nums[2], nums[nums[2]-1]

nums[nums[2]-1], nums[2] = nums[2], nums[3]

nums[nums[2]-1], nums[2] = 4, 3

nums[nums[2]-1] = 4
nums[2] = 3

nums[4-1] = 4
nums[2] = 3

nums[3] = 4
nums[2] = 3
print(nums)
# [1, 2, 3, 4]

est équivalent à:

print(nums)
# [1, 2, 4, 3]

et comme le côté droit est maintenant évalué, les affectations sont équivalentes à:

nums[2] = 3
nums[nums[2]-1] = 4

nums[2] = 3
nums[3-1] = 4

nums[2] = 3
nums[2] = 4

qui donne:

nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]

nums[2], nums[nums[2]-1] = nums[3], nums[2]

nums[2], nums[nums[2]-1] = 3, 4

Dans le deuxième cas, nous obtenons:

nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]


0 commentaires

11
votes

Sur le côté gauche de votre expression, vous lisez et écrivez des numéros [i], je ne sais pas si Python Gaurantese Traitement des opérations de déballage dans la gauche à droite, mais supposons que c'est le cas, votre premier exemple serait équivoque à .

t = nums[i], nums[nums[i]-1]  # t = (4,3)
n = nums[i]-1 # n = 3
nums[n] = t[0] # nums = [1,2,4,4]
nums[i] = t[0] # nums = [1,2,3,4]

Alors que votre deuxième exemple serait équivoque à

t = nums[nums[i]-1], nums[i]  # t = (3,4)
nums[i] = t[0] # nums = [1,2,3,3]
n = nums[i]-1 # n = 2
nums[n] = t[1] # nums = [1,2,4,3]

qui est cohérent avec ce que vous avez.

>


0 commentaires

7
votes

Pour comprendre l'ordre d'évaluation, j'ai fait une classe «variable» qui imprime lorsque les décors et se produisent dans sa «valeur».

b get 2
a get 1
a set 2
b set 1

Lors de l'exécution, il en résulte:

xxx Pre>

Cela montre que le côté droit est d'abord évalué (de gauche à droite), puis le côté gauche est évalué (encore une fois, de gauche à droite).

concernant le L'exemple d'OP: Le côté droit évaluera les mêmes valeurs dans les deux cas. Le premier terme du côté gauche est fixé et cela a un impact sur l'évaluation du deuxième terme. Il n'a jamais été simultané et évalué indépendamment, c'est juste la plupart du temps que vous voyez cela utilisé, les termes ne dépendent pas les uns des autres. Définir une valeur dans une liste, puis prendre une valeur de la liste à utiliser comme index dans la même liste n'est généralement pas une chose et si c'est difficile à comprendre, vous le comprenez. Comme changer la longueur d'une liste dans une boucle pour une boucle, cela a la même odeur. (Question stimulante cependant, comme vous l'avez peut-être deviné en courant vers un pavé à gratter)


0 commentaires

4
votes

Une façon d'analyser les extraits de code dans cpython est de démonter son bytecode pour sa machine de pile simulée.

>>> dis.dis("nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]")
  1           0 LOAD_NAME                0 (nums)
              2 LOAD_NAME                1 (i)
              4 BINARY_SUBSCR

              6 LOAD_NAME                0 (nums)
              8 LOAD_NAME                0 (nums)
             10 LOAD_NAME                1 (i)
             12 BINARY_SUBSCR
             14 LOAD_CONST               0 (1)
             16 BINARY_SUBTRACT
             18 BINARY_SUBSCR

             20 ROT_TWO

             22 LOAD_NAME                0 (nums)
             24 LOAD_NAME                0 (nums)
             26 LOAD_NAME                1 (i)
             28 BINARY_SUBSCR
             30 LOAD_CONST               0 (1)
             32 BINARY_SUBTRACT
             34 STORE_SUBSCR

             36 LOAD_NAME                0 (nums)
             38 LOAD_NAME                1 (i)
             40 STORE_SUBSCR

             42 LOAD_CONST               1 (None)
             44 RETURN_VALUE

J'ai ajouté les lignes vierges pour faciliter la lecture. Les deux expressions de récupération sont calculées en octets 0-13 et 14-19. Binary_subscr remplace les deux valeurs supérieures de la pile, un objet et un indice, avec la valeur obtenue à partir de l'objet. Les deux valeurs récupérées sont échangées de sorte que le premier calculé est la première liaison. Les deux opérations de magasin sont effectuées en octets 22-27 et 28-41. Store_subscr utilise et supprime les trois premières valeurs de la pile, une valeur à stocker, un objet et un indice. (La partie de retour aucune est apparemment toujours ajoutée à la fin.) La partie importante de la question est que les calculs des magasins sont effectués séquentiellement dans des lots distincts et indépendants.

La description la plus proche de Python du Python du Le calcul CPYthon nécessite l'introduction d'une variable de pile

stack = []
stack.append(nums[nums[i]-1])
stack.append(nums[i])
stack.reverse()
nums[i] = stack.pop()
nums[nums[i]-1] = stack.pop()

Voici le démontage de l'instruction inversée

>>> import dis
>>> dis.dis("nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]")
  1           0 LOAD_NAME                0 (nums)
              2 LOAD_NAME                0 (nums)
              4 LOAD_NAME                1 (i)

              6 BINARY_SUBSCR
              8 LOAD_CONST               0 (1)
             10 BINARY_SUBTRACT
             12 BINARY_SUBSCR
             14 LOAD_NAME                0 (nums)
             16 LOAD_NAME                1 (i)
             18 BINARY_SUBSCR

             20 ROT_TWO

             22 LOAD_NAME                0 (nums)
             24 LOAD_NAME                1 (i)
             26 STORE_SUBSCR

             28 LOAD_NAME                0 (nums)
             30 LOAD_NAME                0 (nums)
             32 LOAD_NAME                1 (i)
             34 BINARY_SUBSCR
             36 LOAD_CONST               0 (1)
             38 BINARY_SUBTRACT
             40 STORE_SUBSCR

             42 LOAD_CONST               1 (None)
             44 RETURN_VALUE


0 commentaires

1
votes

Il me semble que cela ne se produirait que lorsque le contenu de la liste est dans la plage des index de la liste pour la liste. Si par exemple:

>>> nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

, le code échouera avec:

nums = [10, 20, 40, 30]

donc certainement, un gotcha. N'utilisez jamais le contenu d'une liste comme index dans cette liste.


1 commentaires

pls utilisent des clôtures de code au lieu de blockquotes



0
votes

Thierry a donné une bonne réponse, permettez-moi d'être plus clair. Sachez que si nums = [1, 2, 4, 3] ,

dans ce code:

>>> print(nums)
>>> [1, 2, 4, 3]
>>> nums[i], nums[i-1] = nums[i-1], nums[i]
>>> print(nums)
>>> [1, 4, 2, 3]
  • i est 2,
  • num [nums [i] -1] est nums [4-1], donc num [3], (la valeur est 3)
  • num [i] est num [2], (la valeur est 4)
  • Le résultat est: (3, 4)

Dans ce code:

nums[i], nums[nums[i]-1]
  • num [i] est num [2] devient 3, (=> [1, 2, 3 , 3])
  • mais num [num [i] -1] est pas nums [ 4 -1] mais num [ 3 -1], donc num [2] aussi, devient (de retour à) 4 (=> [1, 2, 4 , 3])

Peut-être que la question bonne concernant un swap, utilisait:

nums [i], nums [i -1] 0


0 commentaires