web-dev-qa-db-ja.com

リストで+ =が予期しない動作をするのはなぜですか?

+=演算子pythonはリスト上で予期せず動作しているようです。

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

[〜#〜] output [〜#〜]

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += barはクラスのすべてのインスタンスに影響するようですが、foo = foo + barは、私が物事の振る舞いを期待する方法で振る舞うようです。

+=演算子は「複合代入演算子」と呼ばれます。

101
eucalculia

一般的な答えは、+=__iadd__特殊メソッドを呼び出そうとし、それが利用できない場合は、代わりに__add__を使用しようとします。したがって、問題はこれらの特別な方法の違いにあります。

__iadd__特殊メソッドは、インプレース追加用です。つまり、操作対象のオブジェクトを変更します。 __add__特殊メソッドは新しいオブジェクトを返し、標準の+演算子にも使用されます。

そのため、+=演算子が__iadd__が定義されているオブジェクトで使用されると、オブジェクトはその場で変更されます。それ以外の場合は、代わりにプレーン__add__を使用して新しいオブジェクトを返そうとします。

リストのような可変型では+=がオブジェクトの値を変更しますが、タプル、文字列、整数などの不変型では代わりに新しいオブジェクトが返されます(a += ba = a + bと同等になります)。

したがって、__iadd____add__の両方をサポートするタイプの場合、どちらを使用するかに注意する必要があります。 a += b__iadd__を呼び出してaを変更しますが、a = a + bは新しいオブジェクトを作成してaに割り当てます。それらは同じ操作ではありません!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

不変の型(__iadd__がない場合)a += ba = a + bは同等です。これは、不変型で+=を使用できるようにするものです。そうしないと、数字のような不変型で+=を使用できないと考えるまで、奇妙な設計上の決定に思えます。

110
Scott Griffiths

一般的なケースについては、 スコットグリフィスの答え を参照してください。ただし、あなたのようなリストを扱う場合、_+=_演算子はsomeListObject.extend(iterableObject)の省略形です。 extend()のドキュメント を参照してください。

extend関数は、パラメーターのすべての要素をリストに追加します。

_foo += something_を実行すると、リストfooを変更するため、名前fooが指す参照は変更しませんが、リストオブジェクトは変更します直接。 _foo = foo + something_を使用すると、実際にはnewリストを作成しています。

このサンプルコードで説明します。

_>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216
_

新しいリストをlに再割り当てすると、参照がどのように変化するかに注意してください。

barはインスタンス変数ではなくクラス変数であるため、インプレース変更はそのクラスのすべてのインスタンスに影響します。ただし、_self.bar_を再定義する場合、インスタンスは他のクラスインスタンスに影響を与えることなく、個別のインスタンス変数_self.bar_を持ちます。

89
AndiDog

ここでの問題は、barがインスタンス変数ではなくクラス属性として定義されていることです。

fooでは、クラス属性がinitメソッドで変更されているため、すべてのインスタンスが影響を受けます。

foo2、インスタンス変数は(空の)クラス属性を使用して定義され、すべてのインスタンスは独自のbarを取得します。

「正しい」実装は次のようになります。

class foo:
    def __init__(self, x):
        self.bar = [x]

もちろん、クラス属性は完全に合法です。実際、次のようにクラスのインスタンスを作成せずに、それらにアクセスして変更できます。

class foo:
    bar = []

foo.bar = [x]
22
Can Berk Güder

多くの時間が経過し、多くの正しいことが言われましたが、両方の効果をまとめる答えはありません。

次の2つの効果があります。

  1. 「特別な」、おそらく気付かない、_+=_のリストの動作( Scott Griffiths で述べられているように)
  2. クラス属性とインスタンス属性が関係しているという事実( Can BerkBüder で述べられているように)

クラスfooでは、___init___メソッドがクラス属性を変更します。 _self.bar += [x]_がself.bar = self.bar.__iadd__([x])に変換されるためです。 __iadd__()はインプレース変更用であるため、リストを変更し、リストへの参照を返します。

インスタンスdictは変更されますが、クラスdictには既に同じ割り当てが含まれているため、通常は必要ありません。したがって、この詳細はほとんど気付かれません-後で_foo.bar = []_を実行する場合を除きます。ここで、インスタンスのbarは、上記の事実のおかげで同じままです。

ただし、クラス_foo2_では、クラスのbarが使用されますが、変更されません。代わりに、self.bar.__add__([x])がここで呼び出され、オブジェクトを変更しないため、_[x]_がそれに追加され、新しいオブジェクトを形成します。結果はインスタンス辞書に入れられ、クラスの属性は変更されたままで、インスタンスに辞書として新しいリストが与えられます。

_... = ... + ..._と_... += ..._の違いは、その後の割り当てにも影響します。

_f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 
_

print id(foo), id(f), id(g)を使用してオブジェクトのIDを確認できます(Python3を使用している場合は、追加の_()_ sを忘れないでください)。

ところで:_+=_演算子は「拡張代入」と呼ばれ、通常は可能な限りインプレース変更を行うことを目的としています。

5
glglgl

ここには2つのことが関係します。

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+演算子は、リストの__add__メソッドを呼び出します。オペランドからすべての要素を取得し、それらの要素を含む新しいリストを作成して順序を維持します。

+=演算子は、リストの__iadd__メソッドを呼び出します。 iterableを取り、そのiterableのすべての要素をリストに追加します。新しいリストオブジェクトは作成されません。

クラスfooでは、ステートメントself.bar += [x]は割り当てステートメントではありませんが、実際には次のように変換されます

self.bar.__iadd__([x])  # modifies the class attribute  

リストを所定の場所に変更し、リストメソッドextendのように機能します。

クラスfoo2では、逆に、initメソッドの割り当てステートメント

self.bar = self.bar + [x]  

次のように分解できます。
インスタンスには属性barがありませんが(同じ名前のクラス属性があります)、クラス属性barにアクセスし、xを追加して新しいリストを作成します。ステートメントは次のように翻訳されます。

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

次に、インスタンス属性barを作成し、新しく作成したリストを割り当てます。割り当てのrhsのbarは、lhsのbarとは異なることに注意してください。

クラスfooのインスタンスの場合、barはクラス属性であり、インスタンス属性ではありません。したがって、クラス属性barへの変更はすべてのインスタンスに反映されます。

それどころか、クラスfoo2の各インスタンスには、同じ名前barのクラス属性とは異なる独自のインスタンス属性barがあります。

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

これが物事をクリアすることを願っています。

5
ajay

他の答えはそれをほとんどカバーしているように見えますが、 Augmented Assignments PEP 2 を引用して参照する価値があるようです。

それらは、[拡張代入演算子]は、通常のバイナリ形式と同じ演算子を実装します。ただし、左-左側のオブジェクトはそれをサポートし、左側は一度だけ評価されます。

...

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

5
mwardm
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])
1
tanglei