Różnica między a - = b i a = a - b w Pythonie

90

Niedawno zastosowałem to rozwiązanie do uśrednienia każdego N wierszy macierzy. Chociaż rozwiązanie ogólnie działa, miałem problemy, gdy zastosowałem je do macierzy 7x1. Zauważyłem, że problem występuje podczas korzystania z -=operatora. Aby zrobić mały przykład:

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

które wyjścia:

[1 1 2]
[1 1 1]

Tak więc w przypadku tablicy a -= bdaje inny wynik niż a = a - b. Do tej pory myślałem, że te dwa sposoby są dokładnie takie same. Jaka jest różnica?

Jak to się dzieje, że metoda, o której wspominam w celu sumowania wszystkich N wierszy w macierzy, działa np. Dla macierzy 7x4, ale nie dla macierzy 7x1?

iasonas
źródło

Odpowiedzi:

80

Uwaga: używanie operacji w miejscu na tablicach NumPy, które współużytkują pamięć, nie stanowi już problemu w wersji 1.13.0 i nowszych (zobacz szczegóły tutaj ). Te dwie operacje dadzą ten sam wynik. Ta odpowiedź dotyczy tylko wcześniejszych wersji NumPy.


Mutowanie tablic, gdy są używane w obliczeniach, może prowadzić do nieoczekiwanych wyników!

W przykładzie w pytaniu odejmowanie za pomocą -=modyfikuje drugi element programu, aa następnie natychmiast używa tego zmodyfikowanego drugiego elementu w operacji na trzecim elemencie programu a.

Oto, co dzieje się a[1:] -= a[:-1]krok po kroku:

  • ato tablica z danymi [1, 2, 3].

  • Mamy dwa poglądy na te dane: a[1:]jest [2, 3]i a[:-1]jest [1, 2].

  • Rozpoczyna się odejmowanie w miejscu -=. Pierwszy element a[:-1]1 jest odejmowany od pierwszego elementu a[1:]. To zostało zmienione ana [1, 1, 3]. Teraz mamy a[1:]to widok danych [1, 3], a a[:-1]jest to widok danych [1, 1](drugi element tablicy azostał zmieniony).

  • a[:-1]jest teraz [1, 1]i NumPy musi teraz odjąć drugi element, który wynosi 1 (już nie 2!) od drugiego elementu a[1:]. To daje a[1:]pogląd na wartości [1, 2].

  • ajest teraz tablicą zawierającą wartości [1, 1, 2].

b[1:] = b[1:] - b[:-1]nie ma tego problemu, ponieważ najpierw b[1:] - b[:-1]tworzy nową tablicę, a następnie przypisuje wartości w tej tablicy do b[1:]. Nie modyfikuje bsię podczas odejmowania, więc widoki b[1:]i b[:-1]nie zmieniają się.


Ogólna rada jest taka, aby unikać modyfikowania jednego widoku w miejscu innym, jeśli się pokrywają. Obejmuje operatorów -=, *=itp i za pomocą outparametru w funkcji uniwersalnych (jak np.subtracti np.multiply), aby odpisać na jednej z tablic.

Alex Riley
źródło
4
Wolę tę odpowiedź bardziej niż obecnie akceptowaną. Używa bardzo jasnego języka, aby pokazać efekt modyfikacji obiektów podlegających mutacji w miejscu. Co ważniejsze, ostatni akapit bezpośrednio podkreśla znaczenie modyfikacji w miejscu nakładania się poglądów, co powinno być lekcją, jaką należy wyciągnąć z tego pytania.
Reti43
43

Wewnętrzna różnica polega na tym, że:

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

jest równoważne z tym:

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)))

kiedy to:

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

mapuje do tego:

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)))

W niektórych przypadkach, __sub__()i __isub__()pracują w podobny sposób. Ale zmienne obiekty powinny mutować i zwracać się podczas używania __isub__(), podczas gdy powinny zwracać nowy obiekt z __sub__().

Wykonywanie operacji na plasterkach na obiektach numpy tworzy na nich widoki, więc ich użycie daje bezpośredni dostęp do pamięci „oryginalnego” obiektu.

glglgl
źródło
11

Doktorzy mówią:

Ideą przypisania rozszerzonego w Pythonie jest to, że nie jest to tylko łatwiejszy sposób zapisania powszechnej praktyki przechowywania wyniku operacji binarnej w jej operandzie po lewej stronie, ale także sposób, aby dany operand po lewej stronie wiedz, że powinien działać „na sobie”, zamiast tworzyć zmodyfikowaną kopię samego siebie.

Z reguły odejmowanie powiększone ( x-=y) jest możliwe x.__isub__(y)dla operacji IN- miejsce JEŻELI możliwe, gdy jest możliwe normalne odejmowanie ( x = x-y) x=x.__sub__(y). W przypadku obiektów niemodyfikowalnych, takich jak liczby całkowite, jest to równoważne. Ale w przypadku zmiennych, takich jak tablice lub listy, jak w twoim przykładzie, mogą to być bardzo różne rzeczy.

BM
źródło