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


118

+=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 

出力

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

foo += barクラスのすべてのインスタンスに影響foo = foo + barするようですが、私が物事が期待するように動作するようです。

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


リストの「extend」と「append」の違いも確認してください
N 1.1

3
これはPythonに問題があるとは思わない。ほとんどの言語では+、配列で演算子を使用することさえできません。この場合、+=追加することは完全に理にかなっていると思います。
Skilldrick 2010

4
正式には「拡張割り当て」と呼ばれています。
Martijn Pieters

回答:


138

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

__iadd__特別な方法は、それはそれが機能しているオブジェクトを変異させるでインプレース加えて、ためです。__add__特別な方法は、新しいオブジェクトを返し、また標準のために使用される+演算子。

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

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

タイプについてはその両方のサポート__iadd____add__あなたはので、使用どちら注意する必要があります。a += b呼ぶ__iadd__とのmutate 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同等です。これが+=不変の型で使用できる理由です。そうでなければ+=、数値のような不変の型では使用できなかったと考えるまで、奇妙な設計上の判断に思えるかもしれません。


4
あり__radd__、時にはと呼ばれるかもしれない方法は、(それは主にサブクラスを伴う表現に関連しています)。
jfs

2
観点から:+ =は、メモリと速度が重要な場合に役立ちます
Norfeldt

3
これが+=実際にリストを拡張していることを知っているので、これはなぜしばらく時間が返されるのかx = []; x = x + {}を説明しています。TypeErrorx = []; x += {}[]
zezollo 2017

96

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

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

その場合foo += something、リストを適切に変更するためfoo、名前がfoo指す参照は変更しませんが、リストオブジェクトを直接変更します。ではfoo = foo + something、実際に新しいリストを作成しています。

このサンプルコードはそれを説明します:

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

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

barインスタンス変数ではなくクラス変数と同様に、適切に変更すると、そのクラスのすべてのインスタンスに影響します。ただし、を再定義するself.barと、インスタンスはself.bar他のクラスインスタンスに影響を与えることなく、個別のインスタンス変数を持ちます。


7
これは常に正しいとは限りません。a= 1; + = 1; は有効なPythonですが、intには「extend()」メソッドがありません。これを一般化することはできません。
e-satis、2010

2
いくつかのテストを行ったところ、スコットグリフィスはそれを正しく理解したので、-1を使用しました。
e-satis、2010

11
@ e-statis:OPはリストについて明確に話していました。私はリストについても話していることを明確に述べました。何も一般化していません。
AndiDog 2010

-1を削除すると、答えは十分です。私はまだグリフィスの答えが良いと思います。
e-satis 2013年

最初はそれが2つのリストととa += bは違うと思うのは奇妙に感じa = a + bます。しかし、それは理にかなっています。より多くの場合、時間の複雑性が高くなるリスト全体の新しいコピーを作成するのではなく、リストを処理するためのものです。開発者が元のリストを変更しないように注意する必要がある場合は、タプルが不変オブジェクトであるより良いオプションです。タプルを使用すると、元のタプルを変更できません。abextend+=
Pranjal Mittal 2017

22

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

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

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

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

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

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

class foo:
    bar = []

foo.bar = [x]

8

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

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

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

+=オペレーターは__iadd__リストのメソッドを呼び出します。これは、イテラブルを取り、イテラブルのすべての要素をリストに追加します。新しいリストオブジェクトは作成されません。

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

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

これは、リストを適切に変更し、listメソッドのように機能し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を作成し、新しく作成したリストをそれに割り当てます。bar割り当てのrhsはlhsのrhsとは異なることに注意してくださいbar

クラスのインスタンスの場合foobarはクラス属性であり、インスタンス属性ではありません。したがって、クラス属性への変更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

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

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

  1. 「特別な」、とリストの多分気付かれず行動+=(で述べたようにスコット・グリフィス
  2. クラス属性とインスタンス属性が関係しているという事実(Can BerkBüderが述べたように)

class fooでは、__init__メソッドはクラス属性を変更します。これはにself.bar += [x]変換されるためself.bar = self.bar.__iadd__([x])です。__iadd__()その場での変更のためのものなので、リストを変更してそれへの参照を返します。

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

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

との区別は... = ... + ...... += ...その後の割り当てにも影響します。

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 

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

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


5

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

これら(拡張代入演算子)は、通常のバイナリ形式と同じ演算子を実装します。ただし、左側のオブジェクトがサポートする場合に演算が「インプレース」で実行され、左側が一度だけ評価される点が異なります。

...

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


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

0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

不変オブジェクト(この場合は整数)を変更しようとすると、Pythonは代わりに別のオブジェクトを提供するだけです。一方、変更可能なオブジェクト(リスト)に変更を加え、それをずっと同じオブジェクトのままにすることができます。

ref:https : //medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

シャローコピーとディープコピーを理解するには、以下のURLも参照してください

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.