+ =가 목록에서 예기치 않게 작동하는 이유는 무엇입니까?


118

+=파이썬 의 연산자가 목록에서 예기치 않게 작동하는 것 같습니다. 아무도 여기서 무슨 일이 일어나고 있는지 말해 줄 수 있습니까?

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내가 기대하는 방식으로 작동 하는 것 같습니다.

+=연산자를 "복합 할당 연산자"라고합니다.


목록에서 '확장'과 '추가'의 차이점도 확인하십시오
N 1.1

3
나는 이것이 파이썬에 문제가 있다고 생각하지 않습니다. 대부분의 언어는 +배열 에서 연산자 를 사용하는 것을 허용하지 않습니다 . 이 경우 +=추가 되는 것이 완벽하다고 생각합니다 .
Skilldrick

4
공식적으로는 '증강 과제'라고합니다.
Martijn Pieters

답변:


138

일반적인 대답은 특수 메서드 +=를 호출 __iadd__하려고 시도하고 사용할 수없는 경우 __add__대신 사용하려고하는 것입니다. 따라서 문제는 이러한 특수 방법의 차이점입니다.

__iadd__특별한 방법은 변이가에 작용하는 객체 즉, 인플레 이스 (in-place) 추가 할 수 있습니다. __add__특별한 방법은 새로운 객체를 반환하고 또한 표준에 사용되는 +연산자입니다.

따라서 +=연산자가 __iadd__정의 된 객체에 사용되면 객체가 제자리에서 수정됩니다. 그렇지 않으면 대신 일반을 사용하고 __add__새 객체를 반환 하려고 합니다.

그렇기 때문에 목록과 같은 변경 가능한 유형 +=의 경우 객체의 값이 변경되는 반면 튜플, 문자열 및 정수와 같은 변경 불가능한 유형의 경우 새 객체가 대신 반환됩니다 ( a += b와 동일하게 됨 a = 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동등합니다. 이것은 +=불변 유형에 사용할 수있게 해주는 것인데, 그렇지 않으면 +=숫자와 같은 불변 유형에 사용할 수 없다고 생각할 때까지 이상한 디자인 결정 처럼 보일 수 있습니다!


4
또한이 __radd__가끔 (이 대부분 서브 클래스를 포함하는 표현식 관련)를 호출 할 수있는 방법은.
jfs

2
관점에서 : + = 메모리와 속도가 중요한 경우에 유용합니다
Norfeldt

3
+=실제로 목록을 확장 한다는 것을 알면 잠시 동안 이 반환 되는 이유 x = []; x = x + {}를 설명 합니다 . TypeErrorx = []; x += {}[]
zezollo

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; a + = 1; 유효한 Python이지만 int에는 "extend ()"메소드가 없습니다. 이것을 일반화 할 수 없습니다.
e-satis

2
몇 가지 테스트를 마쳤습니다. Scott Griffiths가 맞았으니 -1입니다.
e-satis

11
@ e-statis : OP는 목록에 대해 명확하게 말했고, 나도 목록에 대해 이야기하고 있다고 분명히 밝혔습니다. 나는 아무것도 일반화하지 않습니다.
AndiDog

-1을 제거하면 대답이 충분합니다. 그래도 그리피스의 대답이 더 낫다고 생각합니다.
e-satis 2013 년

처음에 그 생각하는 이상한 느낌이 a += b다른 a = a + b두 목록에 ab. 하지만 말이됩니다. extend시간 복잡성이 더 높은 전체 목록의 새 복사본을 만드는 것보다 목록과 관련된 작업이 더 자주 발생합니다. 개발자가 원래 목록을 제자리에서 수정하지 않도록주의해야하는 경우 튜플이 변경 불가능한 객체 인 더 나은 옵션입니다. +=튜플을 사용하면 원래 튜플을 수정할 수 없습니다.
Pranjal Mittal 2017-06-05

22

여기서 문제 bar는 인스턴스 변수가 아닌 클래스 속성으로 정의된다는 것입니다.

에서는 메서드 foo에서 클래스 속성이 수정되므로 init모든 인스턴스가 영향을받습니다.

에서 foo2인스턴스 변수는 (빈) 클래스 속성을 사용하여 정의되며 모든 인스턴스는 고유 한 bar.

"올바른"구현은 다음과 같습니다.

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

물론 클래스 속성은 완전히 합법적입니다. 실제로 다음과 같이 클래스의 인스턴스를 만들지 않고도 액세스하고 수정할 수 있습니다.

class foo:
    bar = []

foo.bar = [x]

8

여기에는 두 가지가 포함됩니다.

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새로 만든 목록을 할당합니다. 참고 bar할당의 우항은 다른 bar좌변에.

클래스의 인스턴스의 경우 foo, barclass 속성이 아닌 인스턴스 속성입니다. 따라서 클래스 속성에 대한 변경 사항 bar은 모든 인스턴스에 반영됩니다.

반대로, 클래스의 각 인스턴스 에는 동일한 이름의 클래스 속성과 다른 foo2고유 한 인스턴스 속성 barbar있습니다.

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

이것이 문제를 해결하기를 바랍니다.


5

많은 시간이 지났고 많은 정답이 들었지만 두 효과를 모두 묶는 답은 없습니다.

두 가지 효과가 있습니다.

  1. "특별한", 아마도 눈에 띄지 않는 목록의 동작 +=( Scott Griffiths에 의해 언급 됨 )
  2. 클래스 속성과 인스턴스 속성이 관련되어 있다는 사실 ( Can Berk Büder에 의해 언급 )

class foo에서 __init__메서드는 클래스 속성을 수정합니다. 으로 self.bar += [x]번역 되기 때문 self.bar = self.bar.__iadd__([x])입니다. __iadd__()내부 수정을위한 것이므로 목록을 수정하고 그에 대한 참조를 반환합니다.

인스턴스 dict가 수정되었지만 클래스 dict에 이미 동일한 할당이 포함되어 있으므로 일반적으로 필요하지 않습니다. 따라서이 세부 사항은 foo.bar = []나중에 수행하는 경우를 제외하고는 거의 눈에 띄지 않습니다 . 여기에서 인스턴스 bar는 말한 사실 덕분에 동일하게 유지됩니다.

클래스에서 foo2, 그러나, 클래스의는 bar사용하지만, 감동하지 않습니다. 대신 여기에 a [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 

객체의 신원을 확인할 수 있습니다 print id(foo), id(f), id(g)( ()Python3을 사용하는 경우 추가 s를 잊지 마십시오).

BTW : +=연산자는 "증강 할당"이라고하며 일반적으로 가능한 한 내부 수정을 수행하도록되어 있습니다.


5

다른 답변은 Augmented Assignments PEP 203 을 인용하고 참조 할 가치가있는 것처럼 보이지만 거의 다룬 것처럼 보입니다 .

그들은 [증대 된 할당 연산자가] 작업이 완료되는 것을 제외하고, 정상 바이너리 형식과 같은 연산자를 구현`의 장소 '왼쪽은 일단 평가 왼쪽 측면 객체가 지원하는 그것을, 그 때.

...

파이썬에서 증강 할당의이면에있는 아이디어는 이진 연산의 결과를 왼쪽 피연산자에 저장하는 일반적인 관행을 작성하는 것이 더 쉬운 방법 일뿐만 아니라 문제의 왼쪽 피연산자가 자신의 수정 된 사본을 생성하는 것이 아니라 '자체적으로'작동해야한다는 것을 알고 있어야합니다.


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은 단순히 다른 객체를 제공합니다. 다른 한편으로, 우리는 변경 가능한 객체 (목록)를 변경하고 전체적으로 동일한 객체를 유지할 수 있습니다.

심판 : 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/


# 목록의 ID는 동일합니다
roshan ok
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.