본문 바로가기

프로그래밍

python 의 변경가능(Mutable) vs 변경불가능(Immutable) 객체들

python 의 모든것은 객체(object) 입니다. 그리고 객체는 mutable(변경가능) 하거나 immutable(변경불가능) 하죠.

조금더 자세히 들어가 보겠습니다. python 의 모든것은 객체로 표현된다고 했기때문에 모든 변수들은 object instance 를 가지고 있습니다. (쉽게 말하면 붕어빵을 만들때, object는 붕어빵을 만드는 틀이고 instance는 찍어나온 붕어빵이라 생각하시면 되겠습니다.) 그리고 object가 개시(initiate)되면, unique 한 object id를 부여받습니다.
object 의 타입은 프로그램 실행 시간에 정해지며, 한번 정해진 이상 절대 바뀌지 않습니다.
하지만 object의 상태는 만약 object가 mutable 하다면 바뀔 수 있습니다.
간단히 말해서, object가 mutable 하다는것은 만들어진 이후에는 바뀔 수 있다는것을 말하며, immutable 하다는것은 절대 바뀔 수 없다는 것을 말합니다.

그렇다면 mutable 하거나 immutable 한 object는 어떤것들이 있을까요?
예를 들어, int, float, bool, str, tuple, unicode 는 immutable 합니다.
그리고, list, set, dict 은 mutable 합니다.
listtuple 은 둘다 일련의 object 들을 나열한 class 이지만, list 는 mutable 하고 tuple 은 immutable 하네요.

'ID' 와 'TYPE' 함수들이 뭘 위해 있는지 이해한다면 우리가 만든 변수들이 mutable 한지 immutable 한 object 인지 알 수 있습니다.

ID 그리고 TYPE

built-in 함수 id() 는 object의 신원(identity)을 정수(integer)형태로 반환합니다. 이 정수들은 대개 object 가 있는 메모리 장소에 일치하게끔 만들어 집니다. 물론 python 의 실행 환경과 플랫폼에 따라 달라질 수 있습니다.
is 연산자는 두 object들의 신원을 비교합니다.
그리고 built-in 함수 type() 은 object의 type 을 반환합니다.
아래의 예제를 보세요.

  ''' 예제 1 '''
  >>> x = "Holberton"
  >>> y = "Holberton"
  >>> id(x)
  # 출력값 : 140135852055856
  >>> id(y)
  # 출력값 : 140135852055856
  >>> print(x is y) '''type 들을 비교합니다'''
  # 출력값 : True
  ''' 예제 2 '''
  >>> a = 50
  >>> type(a)
  # 출력값 : <class: ‘int’>
  >>> b = "Holberton"
  >>> type(b)
  # 출력값 : <class: 'string'>

위의 예제에서는 두개의 string 을 비교하고 type과 id를 알아보았습니다.
이제 두개의 함수들을 이용해서, 여러 변수들에 관련된 object 들의 type 들과 어떻게 object 들이 변할 수 있는지 알아보겠습니다.

Mutable 그리고 Immutable Object들

위에서 우리가 배웠듯이, mutable object는 그 상태나 내용들을 바꿀 수 있고 immutable object들은 바꿀 수 없습니다.

object type의 변경가능성 (mutability) 를 찾아내 보는 실용적인 예제를 보겠습니다.

x = 10
y = x

int type 의 object 를 만들고 xy 를 같은 object를 바라보게 만들었습니다.

id(x) == id(y)
id(y) == id(10)

이제 간단한 변화를 줘보겠습니다.

x = x + 1

이렇게 된다면은

id(x) != id(y)
id(x) != id(10)

이 됩니다.

x 라는 변수에 태그되어진 object는 바뀌었습니다.하지만, object 10 은 절대 바뀌지 않습니다. 왜냐하면 int type 의 object 이므로 immutable 하기 때문이죠.
immutable object는 만들어진 다음 절대로 수정을 허락하지 않습니다.
x 는 어떻게 된걸까요? immutable 한 int를 바꾸려 했으니 새로운 메모리 공간에 11 이라는 값을 가진 int 형 object를 만듭니다. 그리고 x 에 새롭게 연결시킵니다. 위에서 y10 object들은 여전히 id가 같습니다.

이제는 mutable object 들에 대한 경우를 보겠습니다.

m = list([1, 2, 3])
n = m

list type 을 가진 object를 만들었습니다. mn 은 같은 list object를 가리킵니다. 이 list 는 3개의 immutable 한 int object들을 가지고 있죠.

id(m) == id(n)

그리고 list 의 원소중 하나를 pop() 함수를 이용해 빼내보겠습니다.

m.pop()

object id 는 여전히 바뀌지 않습니다.

id(m) == id(n)

mnlist object가 변경되었는데도 여전히 같은 list 를 가르키고 있습니다. 이제 list object는 [1,2] 만 가지고 있습니다.

위의 예제들을 통해서 배운것들을 정리해보겠습니다.

  • python은 각각 mutable 과 immutable 한 object 들을 다르게 다룬다.
  • Immutable object들은 mutable object들 보다 좀더 접근이 빠르다.
  • Mutable object 들은 나중에 크기나 값에 대한 변화가 필요한 object 를 만들때 사용하는것이 좋다. (listdict 같은 type 들) Immutable 한 object들은 항상 같은 값을 확정하고 싶은 object들을 필요로 할때 유용하다.
  • Immutable object들은 근본적으로 '바꾸는'것에 대해 민감하다. 그 이유는 object를 바꾸는 작업이 복사본을 만드는 작업을 수반하기 때문에 비싼값을 치룬다. (복사본에 변수를 바꿔 연결) Mutable 한 object를 바꾸는것은 복사본을 만들 필요가 없기때문에 과정이 저렴하다.

변경 불가능(immutability) 한 것에 대한 예외

사실은 모든 immutable 한 object들이 immutable 한것만은 아닙니다.
뭔 자다가 봉창 두드리는 소리냐구요? 좀더 자세히 말씀드리겠습니다.

위에서 언급하였듯이, tuple과 같은 뭔가를 담는 object type 은 immutable 합니다. 그 말은, tuple의 값은 만들어진 이후에는 바뀌지 않는다는 것을 뜻하죠. 하지만 tuple 의 값 이란것은 사실 object 들과 연결된 불변한 바인딩(binding)들을 말합니다. 여기서 중요한 사실은 진짜로 바뀌지 않는것은 binding 이고 object 들은 바뀔 여지가 충분하다는 것입니다.

tuple 을 예로 들어 보겠습니다. t = (‘holberton’, [1, 2, 3])

위의 tuple t 는 여러 종류의 데이터 타입들을 가지고 있습니다. 첫번째 원소는 immutable 한 string 이고 두번째 원소는 mutable 한 list 네요.
tuple 자기 자신은 immutable 합니다. 이 말은 tuple 의 내용을 바꿀 수 있는 변환 메소드(mutating method)는 어디에도 존재하지 않는다는 것입니다.
이처럼 stringstring 은 변환 메소드가 없기 때문에 immutable 합니다. 즉, 바꿀 수 있는 방법이 없다는것이죠. 하지만 list object는 변환 메소드를 가지고 있기 때문에 바꿀 수 있습니다. 정리하면, tuple immutable 해서 그 안에 있는 list 마저 바꿀 수 없다고 생각하시겠지만, list 자체는 mutable 해서 바꿀 수 있습니다. 이건 미묘한 차이긴 하지만 그래도 중요합니다 : immutable 한 object의 "값(value)" 은 바꿀 수 없습니다. 하지만 그것을 구성하는 object 들은 바꿀 수 있습니다.

object들은 어떻게 함수로 전달 될까요?

mutable 과 immutable 타입들을 알아놓고, 이들이 어떻게 함수들에게 전달되어 다뤄지는지 아는것은 메모리 효율성은 높이기 위해서라면 중요합니다. 적절한 object들을 쓰는것이야 말로 메모리 활용을 최적화 하는데 필수적인 작업이기 때문입니다.

예를 들어 mutable object들은 함수에 call by reference 형식으로 불린다고 가정하겠습니다, 이렇게 되면 object가 전달되기 전의 원본(변수) 자체를 바꿀 수 있습니다. 이런 현상을 막기 위해서 또 다른 변수에 원래의 값을 복사해서 넘겨주게 됩니다. Immutable object들은 call by reference 형식으로 불리어도 무방합니다. 어찌되었든 값 자체는 바뀌지 않을테니까요.

def updateList(list1):
    list1 += [10]
n = [5, 6]
print(id(n))                  # 140312184155336
updateList(n)
print(n)                      # [5, 6, 10]
print(id(n))                  # 140312184155336

위의 예제를 보시면, call by reference 를 이용해서 list n을 불러냈습니다. 그래서 원래의 list 자체를 바꿨죠.

다른 예제를 하나더 보겠습니다 :

def updateNumber(n):
    print(id(n))
    n += 10
b = 5
print(id(b))                   # 10055680
updateNumber(b)                # 10055680
print(b)                       # 5

위의 예제에서는 object가 전달 되었지만 바뀌지 않았군요. 하지만 함수 안에서의 n 과 밖에서의 bid() 를 통해 확인한것 처럼 서로 같은 주소값을 지칭하고 있습니다. 그럼에도 불구하고 n += 10 에서 바뀌지 않은 이유는 뭘까요? 예상대로라면 15가 되어야 할텐데 말이죠. 이것은 pass by value 에 의해 발생한 현상입니다. 말 그대로 함수에 의해 어떤 값(n) 이 필요해 질때 object 자체를 전달하지 않고 변수의 '값'만을 전달하게 된거죠. 그래서 변수가 참조하는 object는 변하지 않았지만, 함수 범위 내에서의 object 자체는 변했습니다. (updateNumber 함수에서 print(n)의 결과값은 15 일 것입니다.) 결과적으로는 함수내의 변화는 실제 변수 b 로는 전달되지 않습니다.

아까 말씀드렸듯이, immutable 한 int object를 바꾸겠다고 function 에서 call by reference 형식으로 함수에서 object를 호출해도 어차피 int 자체는 바꿀 생각이 없으니 차라리 값(value) 자체만 넘겨주고 함수 범위(scope)내에서 새로운 object를 만드는것이 더 편한 방법이므로, immutable 은 pass by value, mutable 은 call by reference 를 사용하는것이 일반적입니다.


사실 이 부분은 포스팅할 예정에 없던것인데 dp를 공부하다 보니까 이런것도 쓰게 되네요.

덕분에 블로그에서 코드 작성 실력이 한층더 늘어갑니다.