Різниця між a - = b та a = a - b у Python


90

Нещодавно я застосував це рішення для усереднення кожних N рядків матриці. Хоча рішення працює загалом, у мене були проблеми із застосуванням до масиву 7x1. Я помітив, що проблема полягає у використанні -=оператора. Щоб зробити невеликий приклад:

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

який виводить:

[1 1 2]
[1 1 1]

Отже, у випадку масиву a -= bвиходить інший результат, ніж a = a - b. До цього часу я думав, що ці два шляхи абсолютно однакові. Яка різниця?

Як так, метод, про який я згадую для підсумовування кожного N рядка в матриці, працює, наприклад, для матриці 7x4, але не для масиву 7x1?

Відповіді:


80

Примітка: використання операцій на місці над масивами NumPy, які ділять пам’ять, вже не є проблемою у версії 1.13.0 і далі (докладніше див. Тут ). Ці дві операції дадуть однаковий результат. Ця відповідь стосується лише попередніх версій NumPy.


Мутація масивів під час їх використання в обчисленнях може призвести до несподіваних результатів!

У прикладі у питанні віднімання з -=модифікує другий елемент, aа потім негайно використовує цей модифікований другий елемент в операції над третім елементом a.

Ось що відбувається з a[1:] -= a[:-1]кроком за кроком:

  • a- це масив з даними [1, 2, 3].

  • У нас є два погляди на ці дані: a[1:]є [2, 3]і a[:-1]є [1, 2].

  • -=Починається віднімання на місці . Перший елемент a[:-1], 1, віднімається від першого елемента a[1:]. Це змінилося aна [1, 1, 3]. Тепер у нас a[1:]є такий вигляд даних [1, 3], і a[:-1]це перегляд даних [1, 1](другий елемент масиву aзмінено).

  • a[:-1]є зараз, [1, 1]і NumPy тепер повинен відняти його другий елемент, який дорівнює 1 (більше не 2!), від другого елемента a[1:]. Це робить a[1:]погляд на цінності [1, 2].

  • aтепер масив зі значеннями [1, 1, 2].

b[1:] = b[1:] - b[:-1]не має цієї проблеми, оскільки спочатку b[1:] - b[:-1]створює новий масив, а потім присвоює значення в цьому масиві b[1:]. Він не змінюється bпід час віднімання, тому подання b[1:]і b[:-1]не змінюються.


Загальна порада - уникати модифікації одного виду замість іншого, якщо вони перекриваються. Це включає в себе оператор -=, *=і т.д. , і з використанням outпараметра в універсальних функціях (наприклад , np.subtractі np.multiply) , щоб записати назад в один з масивів.


4
Я віддаю перевагу цій відповіді більше, ніж прийнятій зараз. Він використовує дуже чітку мову, щоб показати ефект модифікації змінних об'єктів на місці. Що ще важливіше, останній параграф прямо підкреслює важливість модифікації на місці для дублюючих думок, що має стати уроком для повернення додому з цього питання.
Reti43

43

Внутрішньо різниця полягає в тому, що це:

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

еквівалентно цьому:

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

в той час як це:

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

карти до цього:

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

У деяких випадках __sub__()і __isub__()працюють подібним чином. Але змінні об'єкти повинні мутувати і повертати себе при використанні __isub__(), тоді як вони повинні повертати новий об'єкт за допомогою __sub__().

Застосування операцій зрізу над об'єктами numpy створює їх перегляди, тому їх використання безпосередньо отримує доступ до пам'яті "оригінального" об'єкта.


11

У документах сказано:

Ідея доповненого призначення в Python полягає в тому, що це не просто простіший спосіб записати загальну практику зберігання результату двійкової операції в лівому операнді, а й спосіб для лівого операнда, про який йде мова знати, що вона повинна діяти `` сама по собі '', а не створювати модифіковану копію себе.

Як правило великого пальця, доповнене віднімання ( x-=y) є x.__isub__(y), по в -Місцеві операціях ЯКЩО це можливо, коли нормальне віднімання ( x = x-y) є x=x.__sub__(y). На незмінюваних об'єктах, таких як цілі числа, це еквівалентно. Але для таких змінних, як масиви або списки, як у вашому прикладі, це можуть бути різні речі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.