web-dev-qa-db-ja.com

a-= bとa = a-bの違いPython

私は最近、マトリックスのN行ごとに平均化するために this ソリューションを適用しました。ソリューションは一般に機能しますが、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 -= ba = a - bとは異なる結果を生成します。今まで、これらの2つの方法はまったく同じだと思っていました。違いはなんですか?

マトリックスのN行ごとに合計する方法について言及している方法が機能しているのはなぜですか。 7x4マトリックスではなく、7x1アレイではないですか?

88
iasonas

注:バージョン1.13.0以降では、メモリを共有するNumPy配列でインプレース操作を使用することで問題はなくなりました(詳細は here を参照)。 2つの操作は同じ結果を生成します。この回答は、NumPyの以前のバージョンにのみ適用されます。


計算で使用されている配列を変更すると、予期しない結果が生じる可能性があります!

質問の例では、_-=_による減算はaの2番目の要素を変更し、aの3番目の要素に対する操作でmodified 2番目の要素をすぐに使用します。

_a[1:] -= a[:-1]_で何が起こるかを次に示します:

  • aは、データ_[1, 2, 3]_を持つ配列です。

  • このデータには2つのビューがあります:_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の2番目の要素が変更されました)。

  • _a[:-1]_は_[1, 1]_になり、NumPyは2番目の要素を減算する必要がありますこれは1(もう2ではありません!)から_a[1:]_の2番目の要素から。これにより、_a[1:]_が値_[1, 2]_のビューになります。

  • aは、値_[1, 1, 2]_を持つ配列になりました。

_b[1:] = b[1:] - b[:-1]_にはこの問題はありません。_b[1:] - b[:-1]_は最初にnew配列を作成し、この配列の値を_b[1:]_に割り当てるためです。減算中にb自体は変更されないため、ビュー_b[1:]_および_b[:-1]_は変更されません。


一般的なアドバイスは、あるビューが別のビューに重なる場合、別のビューでその場で変更しないようにすることです。これには、演算子_-=_、_*=_など、およびユニバーサル関数(_np.subtract_や_np.multiply_など)でoutパラメーターを使用して配列の1つに書き戻すことが含まれます。

79
Alex Riley

内部的には、違いはこれです:

_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オブジェクトにスライス操作を適用すると、それらのビューが作成されるため、それらを使用して「元の」オブジェクトのメモリに直接アクセスします。

43
glglgl

ドキュメント say:

Pythonの拡張代入の背後にある考え方は、左側のオペランドにバイナリ演算の結果を保存する一般的な方法を書くのが簡単な方法であるだけでなく、問題の左側のオペランドは、それ自体の変更されたコピーを作成するのではなく、「それ自体で」動作する必要があることを知っています。

経験則として、拡張減算(_x-=y_)はx.__isub__(y)です。これは[〜#〜] in [〜#〜]-place操作[ 〜#〜] if [〜#〜]可能、通常の減算(_x = x-y_)がx=x.__sub__(y)の場合。整数のような非可変オブジェクトでは、同等です。しかし、あなたの例のように、配列やリストのような可変のものの場合、それらは非常に異なるものになる可能性があります。

11
B. M.