in-place operator 주의점

한재민·2023년 5월 21일
0
post-custom-banner

목적

a = a + b
a += b
둘의 차이점을 알아보자

배경

In-place Operator

파이썬은 당연히 여러 기본적인 연산을 지원하고, 이러한 연산은 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 + ba += b 나 똑같은 작업을 수행하네? 그러면 더 편한 += 만 사용하면 되겠네~"

결론부터 말하자면 그렇지 않다!

mutable and immutable

파이썬에서 객체는 두 가지 속성으로 나눌 수 있다. (물론 이렇게만 나눌 수 있는 것은 아니다.)

mutable과 immutable로, 객체를 변경 가능한 지의 여부이다.
"변경 가능하다." 라는 말의 의미는 파이썬에서의 id 내장 함수로 알아낼 수 있다.
id는 파이썬의 is 구문에 사용되는 함수로, 만약 A is B 라는 구문은 id(A) == id(B)라는 말과 같다.

어떤 객체가 mutable 이라면, 객체를 수정할 수 있고, 수정 이후 id 값이 변하지 않는다.

반대로 immutable 이라면, 객체를 변경할 수 없기 떄문에 변경을 시도하면 id 값이 바뀌고, 새로운 객체가 된다.

갑자기 mutable이니 immutable 이니 이야기를 하는 이유는, 이게 in-place 연산자에서 중요한 변수로 작용하기 때문이다.

본론

immutable case

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에 대해서는 영향을 미치지 않는다

mutable

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 += ba = 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 인 경우에는 다르게 동작함을 인지해야 한다. 당연해 보일 수도 있지만 코드를 작성할 때 의도치 못한 곳에서 에러가 발생하면 찾아내기 힘들 수도 있다. 그러니까 아무튼 중요함..!

profile
열심히 하는 사람
post-custom-banner

2개의 댓글

comment-user-thumbnail
2023년 11월 21일

똑똑한 청년.

1개의 답글