Разница между 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-объектам создает на них представления, поэтому их использование напрямую обращается к памяти «исходного» объекта.

glglgl
источник
11

В документах говорится:

Идея расширенного присваивания в Python заключается в том, что это не только более простой способ записать обычную практику сохранения результата двоичной операции в левом операнде, но и способ для левого операнда, о котором идет речь, знайте, что он должен действовать "сам по себе", а не создавать модифицированную копию самого себя.

Как правило, расширенное вычитание ( x-=y) x.__isub__(y)для операции IN -place, ЕСЛИ возможно, когда есть обычное вычитание ( x = x-y) x=x.__sub__(y). Для неизменяемых объектов, таких как целые числа, это эквивалентно. Но для изменяемых, таких как массивы или списки, как в вашем примере, они могут быть очень разными.

BM
источник