Différence entre a - = b et a = a - b en Python

90

J'ai récemment appliqué cette solution pour calculer la moyenne de toutes les N lignes de matrice. Bien que la solution fonctionne en général, j'ai eu des problèmes lorsqu'elle était appliquée à un tableau 7x1. J'ai remarqué que le problème est lors de l'utilisation de l' -=opérateur. Pour faire un petit exemple:

import numpy as np

a = np.array([1,2,3])
b = np.copy(a)

a[1:] -= a[:-1]
b[1:] = b[1:] - b[:-1]

print a
print b

qui sort:

[1 1 2]
[1 1 1]

Ainsi, dans le cas d'un tableau a -= bproduit un résultat différent de a = a - b. J'ai pensé jusqu'à présent que ces deux manières sont exactement les mêmes. Quelle est la différence?

Comment se fait-il que la méthode que je mentionne pour additionner toutes les N lignes dans une matrice fonctionne par exemple pour une matrice 7x4 mais pas pour un tableau 7x1?

iasonas
la source

Réponses:

80

Remarque: l'utilisation d'opérations sur place sur des tableaux NumPy qui partagent de la mémoire n'est plus un problème dans la version 1.13.0 et les versions ultérieures (voir les détails ici ). Les deux opérations produiront le même résultat. Cette réponse s'applique uniquement aux versions antérieures de NumPy.


La mutation des tableaux lors de leur utilisation dans les calculs peut entraîner des résultats inattendus!

Dans l'exemple de la question, la soustraction avec -=modifie le deuxième élément de a, puis utilise immédiatement ce deuxième élément modifié dans l'opération sur le troisième élément de a.

Voici ce qui se passe a[1:] -= a[:-1]étape par étape:

  • aest le tableau avec les données [1, 2, 3].

  • Nous avons deux vues sur ces données: a[1:]est [2, 3], et a[:-1]est [1, 2].

  • La soustraction sur place -=commence. Le premier élément de a[:-1], 1, est soustrait du premier élément de a[1:]. Cela a changé apour être [1, 1, 3]. Maintenant, nous avons a[1:]une vue des données [1, 3]et a[:-1]une vue des données [1, 1](le deuxième élément du tableau aa été modifié).

  • a[:-1]est maintenant [1, 1]et NumPy doit maintenant soustraire son deuxième élément qui est 1 (et non plus 2!) du deuxième élément de a[1:]. Cela fait a[1:]une vue des valeurs [1, 2].

  • aest maintenant un tableau avec les valeurs [1, 1, 2].

b[1:] = b[1:] - b[:-1]n'a pas ce problème car b[1:] - b[:-1]crée d'abord un nouveau tableau, puis affecte les valeurs de ce tableau à b[1:]. Il ne se modifie pas blors de la soustraction, donc les vues b[1:]et b[:-1]ne changent pas.


Le conseil général est d'éviter de modifier une vue en place avec une autre si elles se chevauchent. Cela inclut les opérateurs -=, *=etc. et l'utilisation du outparamètre dans les fonctions universelles (comme np.subtractet np.multiply) pour réécrire dans l'un des tableaux.

Alex Riley
la source
4
Je préfère cette réponse à celle actuellement acceptée. Il utilise un langage très clair pour montrer l'effet de la modification des objets mutables sur place. Plus important encore, le dernier paragraphe souligne directement l’importance de la modification sur place pour les points de vue qui se chevauchent, ce qui devrait être la leçon à retenir de cette question.
Reti43
43

En interne, la différence est que ceci:

a[1:] -= a[:-1]

équivaut à ceci:

a[1:] = a[1:].__isub__(a[:-1])
a.__setitem__(slice(1, None, None), a.__getitem__(slice(1, None, None)).__isub__(a.__getitem__(slice(1, None, None)))

alors que ce:

b[1:] = b[1:] - b[:-1]

correspond à ceci:

b[1:] = b[1:].__sub__(b[:-1])
b.__setitem__(slice(1, None, None), b.__getitem__(slice(1, None, None)).__sub__(b.__getitem__(slice(1, None, None)))

Dans certains cas, __sub__()et __isub__()fonctionnent de la même manière. Mais les objets mutables doivent muter et se retourner lors de l'utilisation __isub__(), alors qu'ils doivent renvoyer un nouvel objet avec __sub__().

L'application d'opérations de tranche sur des objets numpy crée des vues sur eux, donc leur utilisation permet d'accéder directement à la mémoire de l'objet "d'origine".

glglgl
la source
11

Les documents disent:

L'idée derrière l'affectation augmentée en Python est que ce n'est pas seulement un moyen plus simple d'écrire la pratique courante de stockage du résultat d'une opération binaire dans son opérande de gauche, mais aussi un moyen pour l'opérande de gauche en question de sachez qu'il doit fonctionner «sur lui-même», plutôt que de créer une copie modifiée de lui-même.

En règle générale, la soustraction augmentée ( x-=y) est x.__isub__(y), pour l' opération IN -place SI possible, lorsque la soustraction normale ( x = x-y) l'est x=x.__sub__(y). Sur les objets non mutables comme les entiers, c'est équivalent. Mais pour les mutables comme les tableaux ou les listes, comme dans votre exemple, il peut s'agir de choses très différentes.

BM
la source