Разница между a = b и a = a - b в Python
Недавно я применил это решение для усреднения каждых 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?
Ответы
Ответ 1
Примечание: использование операций на месте в массивах 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
) для записи обратно в один из массивов.
Ответ 2
Внутри различие состоит в том, что это:
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 создает на них представления, поэтому их использование напрямую обращается к памяти "оригинального" объекта.
Ответ 3
Документы говорят:
Идея расширенного назначения в Python заключается в том, что это не просто более простой способ написать обычную практику хранения результат двоичной операции в ее левом операнде, но также и для левого операнда, о котором идет речь, чтобы знать, что он должен действовать "на себя", а не создавать модифицированную копию сам по себе.
В качестве правила большого пальца добавленная субтракция (x-=y
) равна x.__isub__(y)
, для операции IN -place IF, когда нормальное выражение (x = x-y
) x=x.__sub__(y)
. На не изменяемых объектах, таких как целые, это эквивалентно. Но для изменчивых, таких как массивы или списки, как в вашем примере, они могут быть очень разными.