A banner is created from here

"전문가를 위한 파이썬" 이라는 책을 읽던 중
정리하고 싶은 내용이 생겨 이 글을 작성합니다

📚 Contents


  1. ✍🏻 Augmented assignment statements
  2. ☀️ 일반적인 상황
  3. 🌒 예외 상황
  4. 마치며

✍🏻 Augmented assignment statements


파이썬의 복합 할당 연산자를
스펙에서는 Augmented assignment statements (확장 된 할당문) 이라 합니다

파이썬의 복합 할당 연산자는 다른 언어와는 다르게
단순히 축약 문법으로 동작하지 않습니다

문서에 따른 정확한 표현은 아래와 같습니다

An augmented assignment expression like x += 1 can be rewritten as x = x + 1 to achieve a similar, but not exactly equal effect. In the augmented version, x is only evaluated once. Also, when possible, the actual operation is performed in-place, meaning that rather than creating a new object and assigning that to the target, the old object is modified instead.

(죽이고 싶은 영어와의 10선)

요약하자면, 비슷하지만 정확한 동작은 다르다! 라는 점 입니다
자바스크립트의 단축 연산자가 단순한 문법적인 단축을 의미하는 것과 달리
파이썬은 문법적인 단축만을 의미하는 것은 아니라는 뜻이죠

"확장 된 할당문" 은 보통의 할당문과는 두 가지가 다릅니다

  1. 위의 인용된 문서의 예 처럼, x 값은 한 번만 평가된다
  2. x 는 (가능 하다면) 새 객체를 만들지 않고, 기존 객체의 값을 수정한다

예를 들어 a += 1 의 경우에는
a 의 값을 한 번만 평가하여, 할당합니다
내부적으로 __iadd__() 메소드를 사용하고,
a = a + 1__add__() 메소드를 사용합니다.

하지만 보통의 경우에는 add와 iadd 의 차이를 알기 힘듭니다

☀️ 일반적인 상황


보통의 사칙연산과 비트연산을 지원합니다

a = 0
a += 1
print(a) // 1

일반적으로 아는 사칙연산과 비트연산의 축약 문법은 모두 지원합니다

augmented_assignment_stmt ::=  augtarget augop (expression_list | yield_expression)
augtarget                 ::=  identifier | attributeref | subscription | slicing
augop                     ::=  "+=" | "-=" | "*=" | "@=" | "/=" | "//=" | "%=" | "**="
                               | ">>=" | "<<=" | "&=" | "^=" | "|="

정확한 내용은 이 곳을, 연산자에 대한 자세한 내용은 이 곳을 참고하세요!

🐍 사족


이 글을 쓰기 위해 문서를 읽다가 특이한 연산자 Symbol 을 알게 되었습니다
바로 @ ("at" operator) 인데요
numpy 배열의 행렬곱을 하기 위해 사용합니다

import numpy

x = numpy.ones(3) # array([1., 1., 1.])
y = numpy.ones(3) # array([1., 1., 1.])
z = x @ y

print(z) // 3

바로 위와 같이 행렬곱을 할 수 있습니다

관련 내용의 Proposal 은 PEP-465 를 참고하세요

🌒 예외 상황


예외 상황1 - 튜플과 가변 시퀀스

보통의 확장된 연산자는 축약 문법과 비슷하게 동작 하지만
"가변값"에 대한 연산을 수행하게 되면 그 동작은 조금 달라집니다

파이썬의 mutable object 는 dict, list, bytesarray, array.array 등이 있습니다만
(사칙 연산) operator 를 지원하는 것은 list, bytesarray 이므로
이에 관한 설명입니다

a =  tuple([1,2,3])

a[0] += 3
---------------------------------------------------------------------------
TypeError: Traceback (most recent call last)
<ipython-input-73-c8548dbc0a7e> in <module>
----> 1 a[0] += 3

TypeError: 'tuple' object does not support item assignment

tuple 은 기본적으로 불변 객체입니다
이렇게 기존 값을 변경하는 것이 불가능 합니다

그런데, 만약 tuple 안에 가변 시퀀스(list, bytesarray)가 위치하면 어떻게 될까요?

In [74]: a = tuple([[], 1, 2])

In [75]: a
Out[75]: ([], 1, 2)

In [76]: a[0] += [1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-76-2d8bcd3a4d75> in <module>
----> 1 a[0] += [1]

TypeError: 'tuple' object does not support item assignment

In [77]: a
Out[77]: ([1], 1, 2)

Out[77] 을 보시면, 빈 리스트에 1이 추가된 것을 볼 수 있습니다
분명히 TypeError 가 발생했는데, 어떻게 값이 추가 되었을까요?

위의 과정을 순서대로 나누어 보면 아래와 같습니다

tuple_obj = ([[], 2, 3])
temp = tuple_obj[0]  # temp 는 재 할당을 위한 임시변수 입니다
temp += [1]  # 이 부분 까지는 문제 없습니다
tuple_obj[0] = temp # TypeError!

3번째 라인의 코드까지는 리스트를 수정하는 것이기 때문에 문제가 없습니다
마지막 4번째 라인의 코드가, 튜플의 값을 수정하려 하므로 TypeError 가 발생합니다

그런데, 파이썬에서 list 값은 참조로 전달되고
이미 temp 라는 가변 변수는 값이 변경되었기 때문에

tuple_obj의 0번째 값인 리스트에 값이 추가되어 버린 것입니다

위의 풀어쓴 예제는 아래와도 같습니다

temp = tuple_obj[0]
temp = temp.__iadd__([1])
tuple_obj[0] = temp  # TypeError

+= 연산자의 경우, __iadd__() 라는 내장 메소드를 호출하고,
in-place (자신의 값을 변경) 형태로 동작합니다
list.extend 메소드와도 동일하게 동작합니다

예외 상황2 - 함수 내에서의 __iadd__()


아래와 같은 함수가 있습니다

def expand_inplace(a, b):
    a += b
    return a

def expand(a, b):
    a = a + b
    return a

얼핏 보기에는 expand_inplace 함수와 expand 함수가
동일하게 동작할 것이라 예상됩니다

실제로 불변값의 경우는 같습니다

In [86]: c = expand_inplace(1, 2)

In [87]: c
Out[87]: 3

In [88]: c = expand(1, 2)

In [89]: c
Out[89]: 3

In [90]: c = expand_inplace(tuple([1,2,3]), tuple([4,5,6]))

In [91]: c
Out[91]: (1, 2, 3, 4, 5, 6)

하지만, 가변 시퀀스의 경우에는 다르게 동작합니다

In [93]: a = [1,2,3]; b = [4,5,6]

In [94]: c = expand_inplace(a, b)

In [95]: c
Out[95]: [1, 2, 3, 4, 5, 6]

In [96]: a
Out[96]: [1, 2, 3, 4, 5, 6]

In [97]: a = [1,2,3]; b = [4,5,6]

In [98]: c = expand(a, b)

In [99]: c
Out[99]: [1, 2, 3, 4, 5, 6]

In [100]: a
Out[100]: [1, 2, 3]

c의 값은 동일하지만, 인자로 넘긴 a 변수의 값이
expand_inplace 함수의 경우에는 변경되었습니다

__iadd__() 의 경우에는 좌항의 변수에 대한 평가를 한 번만 합니다
expand_inplace 함수 내의 a 라는 변수를 평가할 때
a 는 local scope 에 존재하지 않기 때문에
함수의 인자인 a 에다가 값을 재 할당한 것입니다

함수의 실행에 대해 좀 더 자세히 알아보면

In [101]: dis.dis(expand_inplace)
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 INPLACE_ADD
              6 STORE_FAST               0 (a)

  3           8 LOAD_FAST                0 (a)
             10 RETURN_VALUE

In [102]: dis.dis(expand)
  6           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 STORE_FAST               0 (a)

  7           8 LOAD_FAST                0 (a)
             10 RETURN_VALUE

각각의 4번째 실행부에서, INPLACE_ADD 와 BINARY_ADD 로
바이트코드에서의 실행부가 다른것을 확인할 수 있습니다

따라서, a += 1 를 완전한 a = a + 1 이라고 할 수는 없는 것입니다

마치며


튜플 안의 리스트 값의 변경에 대해서는 꾸준히 논의된 주제로
그 내용은 Python FAQ 에도 언급 되어 있으니
한 번 읽어보시길 추천합니다!

파이썬이 어떻게 변수를 탐색하고 코드를 실행하는지
연산자와 Augmented assignment statements 는 무엇인지에 대해
자세히 공부하게 된 좋은 챕터 였습니다!

의식의 흐름대로 작성한 긴 글을
혹시 여기까지 읽어주신 분이 계시다면 감사합니다!
문제가 있다면 댓글 부탁 드립니다