이번 포스트에서는 파이썬의 mutable변수와 immutable변수에 대해 알아 보도록 하겠다. 어렵지 않은 내용이지만 제대로 이해하지 못하면 버그를 양산 할 수 있으므로 꼭 이해하고 넘어가도록 하자(관련 버그에 대한 내용은 [여기]에서 자세하게 다루고 있다). 최소한 mutable은 "변경이 가능한 것", immutable은 "변경이 불가능한 것" 만큼이라도 알고 다음으로 넘어 가도록 하자.
이번 포스트를 이해하기 위해서는 파이썬의 변수란 '메모리 어딘가에 값을 가지고 있는 값 객체'를 가리키고 있는 참조자라는 것을 먼저 이해하고 있어야 한다. 모른다면 먼저 [여기]를 살펴 보고 오도록 하자.
목차
- 파이썬 변수의 소개
- 변수의 타입
- 변수의 다양한 정보 확인
- ctype 모듈을 활용한 변수의 정보 확인
- == 와 is 연산자
- >> mutable 변수와 immutable 변수
- 변수의 삭제
- 파이썬 정수는 Overflow가 없다?
들어가기 전에
파이썬에서 변수는 mutable 변수와 immutable 변수라고 크게 두가지로 구분될 수 있다. 이게 무슨 소릴까? 먼저 mutable과 immutable의 사전적 정의를 살펴보면, mutable의 뜻은 '변경할 수 있는', immutable은 '변경할 수 없는, 불변의'라는 뜻을가지고 있다.
변수란 값을 값을 변경할 수 있기에 변수라고 부르는데, 값을 변경 할 수 없는 변수가 있다는게 무슨 소린가? 이를 이해하기 위해선, 먼저, 파이썬에서 변수는 C/C++, C#, Java에서 처럼 직접 값을 저장하고 있는 것이 아니라 '메모리 어딘가에 값을 가지고 있는 값 객체'를 만들어 놓고 그것을 가리키고 있는 참조자라는 것을 알아야 한다(이에 관련해서는 [여기]에 자세히 설명을 해두었으니 모르신다면 먼저 살펴 보자).
mutable 변수? immutable 변수?
파이썬 변수 값을 가지고 있는 객체를 가리키는 참조자라고 했다. 그리고 이번 장에서는 "값을 가지고 있는 객체는, 값을 변경 할 수 있는 mutable 객체와 값을 변경할 수 없는 immutable 객체로 구분 된다"라는 설명을 추가 하도록 하겠다.
- mutable - 값을 가지고 있는 '객체의 상태를 변경할 수 있는 것'
list, bytearray, set, ... - immutable - 값을 가지고 있는 '객체의 상태를 변경할 수 없는 것'
int, float, str, tuple, ...
list, bytearray, set 등과 같은 데이터 객체는 객체가 가지고 있는 값을 변경 할 수 있고, int, float, str, tuple과 같은 객체는 가지고 있는 값을 변경 할 수 없다.
n1 = 10
n1 = 11 # 변경 되는데요?
하지만 뭔가 이상하다. 분명히 위 예제와 같이 정수형 변수의 값을 했는데 어떻게 immutable이란 말인가?
이를 이해하기 위해 우리는 파이썬의 좀 더 깊은 곳으로 들어가볼 필요가 있다.
n1 = 10
n2 = n1
먼저 위와 같은 코드에서 파이썬 내부 메모리 구조를 살펴 보면 아래와 같이, 내부적으로 10이라는 값을 가진 정수 객체가 있고, n1과 n2가 해당 객체를 가리킨다.
n1 = n1 + 10
그리고 아래와 같이 n1의 값에 10을 더하게 되면, 파이썬은 객체의 값을 10에서 20으로 변경하는 대신 20이라는 값을 가지고 있는 새로운 객체를 하나 더 만들고 n1 객체가 새로운 객체를 가리키도록 수정한다.
이렇게 값이 변경될 때 마다 새로운 값을 가지고 있는 객체를 만들고 변수가 가리키는 객체를 변경해줌으로써 immutable 객체임에도 불구하고 변수로써 사용 할 수있는 것이다.
이해를 돕기 위해 아래 예제를 살펴 보자.
import sys
n1 = 10
n2 = n1
print( n1 is n2 ) # True
print( sys.getrefcount(n2) )
n1 = n1 + 10
print( n1 is n2 ) # False
print( sys.getrefcount(n2) )
6라인에서는 같은 객체를 가리키고 있으므로 True를 리턴하지만, 11라인에서는 다른 객체를 가리키게 되므로 False를 리턴하는 것을 확인 할 수 있다. 그리고 sys.getrefcount는 현재 객체를 참조하고 있는 변수들의 카운트를 리턴하는데, 7라인과 12라인의 결과가 1이 줄어든것 또한 확인할 수 있다.
그럼 또 다른 의문이 든다. 이렇게 값이 변경 될때 마다 객체를 생성하게 되면 성능상 문제가 발생하지는 않을까? 바로 다음에서 파이썬이 어떻게 성능 이슈를 해결하는지 확인 해보도록 하자.
Object Interning
Object Interning이란 속성이 동일한 immutable 객체를 공유할 수 있게 하는 최적화 기술이다.
n1 = 100
n2 = 100
n3 = 99999999
n4 = 99999999
print('n1(' + str(n1) + ') \t' + str(hex(id(n1))))
print('n2(' + str(n2) + ') \t' + str(hex(id(n2))))
print('n3(' + str(n3) + ') \t' + str(hex(id(n3))))
print('n4(' + str(n4) + ') \t' + str(hex(id(n4))))
위의 n1과 n2, 그리고 n3와 n4는 각각 같은 값을 가지고 있다. 위 예제를 실행시켜 보면 아래와 같은 결과가 출력 된다.
> python main.py
n1(100) 0x1a6b34155d0
n2(100) 0x1a6b34155d0
n3(99999999) 0x1a6b34fa990
n4(99999999) 0x1a6b34fa990
n1, n2와 n3, n4가 각각 같은 객체를 가리키는 것을 알 수 있다. 이렇게 변수가 같은 값을 가지는 경우 파이썬은 각각의 변수마다 객체를 생성하는 것이 아니라 동일한 객체하나를 만들고 변수들이 동일한 객체를 공유할 수 있게 함으로써 성능 향상을 꾀한다.
이번 에는 아래 예제를 추가하여 다시 실행 시켜 보자
n1 = 100
n2 = 100
n3 = 99999999
n4 = 99999999
print('n1(' + str(n1) + ') \t' + str(hex(id(n1))))
print('n2(' + str(n2) + ') \t' + str(hex(id(n2))))
print('n3(' + str(n3) + ') \t' + str(hex(id(n3))))
print('n4(' + str(n4) + ') \t' + str(hex(id(n4))))
n1 += 1
n2 += 1
n3 += 1
n4 += 1
print('n1(' + str(n1) + ') \t' + str(hex(id(n1))))
print('n2(' + str(n2) + ') \t' + str(hex(id(n2))))
print('n3(' + str(n3) + ') \t' + str(hex(id(n3))))
print('n4(' + str(n4) + ') \t' + str(hex(id(n4))))
각 n1, n2, n3, n4에 1씩 더한 후 결과를 출력하면 아래와 같다.
> python main.py
n1(100) 0x1a6b34155d0
n2(100) 0x1a6b34155d0
n3(99999999) 0x1a6b34fa990
n4(99999999) 0x1a6b34fa990
n1(101) 0x1a6b34155f0
n2(101) 0x1a6b34155f0
n3(100000000) 0x1a6b34facb0
n4(100000000) 0x1a6b34fac90
n1, n2 객체는 값이 달라졌으므로 다른 값을 가지고 있는 다른 객체를 공유하는 것을 볼 수 있다. 하지만 n3, n4의 경우는 같은 값을가지고 있음에도 다른 객체를 가리키고 있다. 이는 내부적으로 파이썬이 -5부터 256까지 자주 사용하는 정수 객체들에 대해 미리 만들어 놓고 특별하게 관리하고 있기 때문이다.
변수가 -5부터 256중의 값을 가지게 된다면 매번 객체를 생성하는 것이 아니라 미리 만들어진 객체를 공유한다. 하지만 각 파이썬 버전마다 세부 내용이 달라질 수 있으므로 절대적인 것은 아니니, 개념만 숙지하도록 하자.
다음 시간에는 변수의 삭제에 대해 살펴 보도록 하겠다.