a = a + b
a += b
둘의 차이점을 알아보자
파이썬은 당연히 여러 기본적인 연산을 지원하고, 이러한 연산은 in-place version을 가지고 있다. 예를 들어 보자
a = 2
b = 3
a = a + b # 2 + 3
print(a) # 5
여기서 a 를 한번 더 쓰는 것 조차 귀찮았던 모양인지, 원래 값을 변경하는 경우에 한해서(in-place) 간단한 연산자를 사용할 수 있다.
여기서 in-place는 pandas 같은 라이브러리를 사용해본 사람이라면 익숙할 것이다. 많은 method, function 에서 inplace
라는 paramter 가 존재하는 것을 본 적이 있을 텐데, 이는 새로운 객체를 만들지(inplace=False
) 아니면 기존 객체를 바로 변경할지(inplace=True
)를 결정하는 parameter 이다
in-place operator 도 마찬가지로 기존 값을 바로 변경하는 경우에 사용할 수 있는 연산자이다
a = 2
b = 3
a += b
print(a) # 5
그러면 이렇게 생각할 수 있다.
"a = a + b
나 a += b
나 똑같은 작업을 수행하네? 그러면 더 편한 +=
만 사용하면 되겠네~"
결론부터 말하자면 그렇지 않다!
파이썬에서 객체는 두 가지 속성으로 나눌 수 있다. (물론 이렇게만 나눌 수 있는 것은 아니다.)
mutable과 immutable로, 객체를 변경 가능한 지의 여부이다.
"변경 가능하다." 라는 말의 의미는 파이썬에서의 id
내장 함수로 알아낼 수 있다.
id
는 파이썬의 is
구문에 사용되는 함수로, 만약 A is B
라는 구문은 id(A) == id(B)
라는 말과 같다.
어떤 객체가 mutable 이라면, 객체를 수정할 수 있고, 수정 이후 id 값이 변하지 않는다.
반대로 immutable 이라면, 객체를 변경할 수 없기 떄문에 변경을 시도하면 id 값이 바뀌고, 새로운 객체가 된다.
갑자기 mutable이니 immutable 이니 이야기를 하는 이유는, 이게 in-place 연산자에서 중요한 변수로 작용하기 때문이다.
original_int = 3
new_int = original_int
print("Before")
print(f'original_int: {original_int}')
print(f'new_int: {new_int}')
print(f'id(original_int) is id(new_int) -> {id(original_int) == id(new_int)}')
original_int += 1
print("After")
print(f'original: {original_int}')
print(f'new_int: {new_int}')
print(f'id(original_int) is id(new_int) -> {id(original_int) == id(new_int)}')
# Before
# original_int: 3
# new_int: 3
# id(original_int) is id(new_int) -> True
# After
# original: 4
# new_int: 3
# id(original_int) is id(new_int) -> False
'int' 타입의 경우는 immutable이기 때문에, in-place operator를 사용했음에도 id 값이 바뀌기 때문에 기존의 값을 참조한 new_int
에 대해서는 영향을 미치지 않는다
original_list = [1, 2, 3]
new_list = original_list
print("Before")
print(f'original: {original_list}')
print(f'new_list: {new_list}')
print(f'id(original_list) is id(new_list) -> {id(original_list) == id(new_list)}')
original_list += [4]
print("After")
print(f'original: {original_list}')
print(f'new_list: {new_list}')
print(f'id(original_list) is id(new_list) -> {id(original_list) == id(new_list)}')
# Before
# original: [1, 2, 3]
# new_list: [1, 2, 3]
# id(original_list) is id(new_list) -> True
# After
# original: [1, 2, 3, 4]
# new_list: [1, 2, 3, 4]
# id(original_list) is id(new_list) -> True
mutable 객체의 대표주자인 list 의 경우, list 끼리의 + 연산자가 extend
method 와 유사한 역할을 하기 때문에 +=
in-place operator를 사용가능하고, id 가 바뀌지 않기 떄문에 기존의 list를 참조한 new_list도 같은 id 값을 가지는 것을 확인할 수 있다.
그러면 a += b
를 a = a + b
로 변경하면 어떨까?
original_list = [1, 2, 3]
new_list = original_list
print("Before")
print(f'original: {original_list}')
print(f'new_list: {new_list}')
print(f'id(original_list) is id(new_list) -> {id(original_list) == id(new_list)}')
original_list = original_list + [4]
print("After")
print(f'original: {original_list}')
print(f'new_list: {new_list}')
print(f'id(original_list) is id(new_list) -> {id(original_list) == id(new_list)}')
# Before
# original: [1, 2, 3]
# new_list: [1, 2, 3]
# id(original_list) is id(new_list) -> True
# After
# original: [1, 2, 3, 4]
# new_list: [1, 2, 3]
# id(original_list) is id(new_list) -> False
original_list + [4]
는 기존의 리스트가 아닌 새로운 리스트(id 값이 다름)이기 때문에 서로 다른 객체가 되어버린 모습을 확인할 수 있다.
이는 mutable 객체라면 모두 유사하게 작동한다
import numpy as np
a = np.array([1, 2, 3])
b = a
a += np.array([1, 2, 3])
# a = a + np.array([1, 2, 3])
print(a, b)
print(f"id(a) is id(b) -> {id(a) == id(b)}")
# [2 4 6] [2 4 6] or [2, 4, 6] [1, 2, 3]
# id(a) is id(b) -> True or False
a += b
, a = a + b
는 a가 mutable 일 때는 동일하다고 여겨도 무관하지만, imutable case 인 경우에는 다르게 동작함을 인지해야 한다. 당연해 보일 수도 있지만 코드를 작성할 때 의도치 못한 곳에서 에러가 발생하면 찾아내기 힘들 수도 있다. 그러니까 아무튼 중요함..!
똑똑한 청년.