통계, IT, AI

[python] mutable vs immutable 본문

IT/기타

[python] mutable vs immutable

Harold_Finch 2017. 11. 28. 21:11

1. mutable vs immutable

    파이썬의 오브젝트를 나누는 기준의 하나는 mutable 여부이다. mutable하지 않은 오브젝트는 immutable하다고 한다. mutable 오브젝트는 내용을 수정할 수 있고 immutable한 오브젝트는 그럴 수 없다. mutable한 오브젝트의 대표적인 예는 리스트list 등이 있고 immutable한 오브젝트는 튜플tuple, 문자열string 등이 있다.


    리스트와 같은 mutable 오브젝트는 다음과 같이 내용을 수정할 수 있다.

lst_1 = [1, 2, 3, 4]
lst_1[0] = 100
print(lst_1)  # [100, 2, 3, 4]


    그런데 아래의 코드는 어떻게 작동 하는 것일까? immutable한 오브젝트는 '내용을 수정 할 수 없다'고 하지 않았는가?

str_1 = 'abcde'
str_1 = str_1.replace('c', 'x')
print(str_1)  # abxde
tup_1 = (1, 2, 3)
tup_1 += (4, )
print(tup_1)  # (1, 2, 3, 4)


    사실 위의 코드는 문자열과 튜플을 수정하는 것이 아니라 새로 생성하는 것이다. 비유하자면, 책을 복사하면서 특정 부분만 원하는 내용으로 바꿔치는 것과 같다. 즉, 위의 코드에 등장하는 문자열과 튜플은 원본이 아니다.

lst_1 = [1, 2, 3, 4]
org_id = id(lst_1)
lst_1[0] = 100
mod_id = id(lst_1)
print('same list?', org_id == mod_id)  # same list? false

str_1 = 'abcde'
org_id = id(str_1)
str_1 = str_1.replace('c', 'x')
mod_id = id(str_1)
print('same string?', org_id == mod_id)  # same string? False

tup_1 = (1, 2, 3)
org_id = id(tup_1)
tup_1 += (4, )
mod_id = id(tup_1)
print('same tuple?', org_id == mod_id)  # same tuple? False

2. 복사

    자신이 다루는 오브젝트의 mutable 여부에 대해서 명확하게 인지하지 못하면 코드가 의도치않은 방향으로 작동할 수 있다. 특히 오브젝트를 복사하고 수정하는 코드에서 주의를 기울여야 한다.

# mutable object
lst_1 = [1, 2, 3]
lst_2 = lst_1
print('same list?', id(lst_1) == id(lst_2))
lst_2[0] = 100

print(lst_1)  # [100, 2, 3]
print(lst_2)  # [100, 2, 3]
print('same list?', id(lst_1) == id(lst_2))

# immutable object
str_1 = 'abcde'
str_2 = str_1
print('same string?', id(str_1) == id(str_2))  # same string? True 

str_2 = str_2.replace('a', 'q')
print(str_1)  # abcde
print(str_2)  # qbcde
print('same string?', id(str_1) == id(str_2))  # same string? False


    나도 이와 같은 실수를 저지른 적이 있다. 신경망을 학습시키면서 가중치 매트릭스를 업데이트 해야 했는데 '복사'가 의도한 대로 되지 않았던 것이다. 복사본을 변경하니 원본도 변경되어 애를 먹었고 그 사실을 깨닫는 데에도 꽤 시간이 걸렸다. 그렇다면 리스트와 같은 오브젝트를 복사하기 위해선 어떻게 해야할까? 이를 5.1. 복사에서 다룬다.

3. 속도

    immutable한 오브젝트가 수정될 때 복사본이 만들어진다는 사실은 코드의 수행 속도에도 영향을 준다. 이를 확인하기 위해서 \(1\)부터 \(10^k\)까지의 정수를 서로 연결하는 상황을 가정해 보았다.

import datetime as dt

# immutable way
def immutable_concat(k):
  start = dt.datetime.now()
  str_result = ''
  for n in range(10**k):
    str_result += str(n)
  return str_result, dt.datetime.now() - start

# mutable way
def mutable_concat(k):
  start = dt.datetime.now()
  str_result = ''.join(map(str, (n for n in range(10**k))))
  return str_result, dt.datetime.now() - start
 
# 10^5 case
k = 5
_, immutable_time = immutable_concat(k)
_, mutable_time = mutable_concat(k)
print('10^5 case')
print('immutable:', immutable_time)
print('mutable:', mutable_time)

# 10^6 case
k = 6
_, immutable_time = immutable_concat(k)
_, mutable_time = mutable_concat(k)

print('10^6 case')
print('immutable:', immutable_time)
print('mutable:', mutable_time)


    \(k\)가 작을 때에는 특별한 차이가 없지만 큰 \(k\)에 대해서는 큰 차이가 있음을 확인할 수 있다. 특기할 만한 것은 \(k=5\)과 \(k=6\)의 속도 차이이다. mutable한 방식을 사용하면 10배 정도의 차이를 보이는데 이는 어느 정도 이해가 간다. 데이터가 10배로 늘었기 때문이다. 그런데 문자열을 직접 붙이는 방식은 그 차이가 10배보다 더 크다.

4. hashable

    파이썬 문서에서는 hashable에 대해서 다음과 같이 적고 있다. 어떤 오브젝트가 hashable하다는 것은, 그 오브젝트의 전 생애(lifetime)에 걸쳐 hash 값이 변하지 않고 그 값을 다른 오브젝트와 비교 가능하다는 것을 의미한다. 모든 immutable한 오브젝트는 hashable하며 mutable한 오브젝트는 그렇지 않다. 어떤 오브젝트가 hashable하면 그 오브젝트를 dict에서 키로 사용할 수 있다. 그래서 문자열 뿐만 아니라 튜플도 dict의 키가 될 수 있다. 이것이 중요한 이유는 dict의 키가 빠른 검색을 보장하기 때문이다.

k=7
dict_dat = {n: None for n in range(10**k)}
list_dat = [n for n in range(10**k)]

start = dt.datetime.now()

if 10**k - 1 in dict_dat:
  print('found in dict', dt.datetime.now() - start)

start = dt.datetime.now()

if 10**k - 1 in list_dat:
  print('found in list', dt.datetime.now() - start)


    위의 예시는 오브젝트의 hashable한 성질을 이용하면 더 빠른 검색이 가능하다는 것을 보인 것이다.

5. 기타

5.1. 오브젝트 복사

    = operator를 사용해서는 리스트를 복사할 수 없음을 보였다. 그렇다면 리스트를 복사하기 위해서는 어떻게 해야 할까? [:]를 사용하면 된다.

lst_1 = [1, 2, 3, 4]
lst_2 = lst_1[:]
print('same id?', id(lst_1) == id(lst_2))  # same id? False

lst_1[0] = 100
print(lst_1)  # [100, 2, 3, 4]
print(lst_2)  # [1, 2, 3, 4]


    하지만 이 복사는 얕은 복사(shallow copy)이다. 파이썬 문서에서는 얕은 복사란 해당 오브젝트를 복사하여 새로 구성하되 내용물은 reference로 채우는 것이라 한다. 간단히 예를 들면 다음과 같다.

lst_1 = [1, [2, 3], 4]
lst_2 = lst_1[:]

print('same id?', id(lst_1) == id(lst_2))  # same id? False

lst_1[0] = 100  # lst_2의 첫번째 원소에 영향을 미치지 않는다.
lst_1[1][0] = 100  # lst_2의 두번째 원소의 첫번째 원소에 영향을 미친다.
print(lst_1)  # [100, [100, 3], 4]
print(lst_2)  # [1, [100, 3], 4]


    어떤 오브젝트의 전체를 복사하고자 한다면 copy 패키지의 deepcopy를 사용하면 된다.

import copy

lst_1 = [1, [2, 3], 4]
lst_2 = copy.deepcopy(lst_1)
print('same id?', id(lst_1) == id(lst_2))  # same id? False

lst_1[0] = 100 # lst_2에 아무런 영향을 미치지 않는다.
lst_1[1][0] = 100 # lst_2에 아무런 영향을 미치지 않는다.
print(lst_1) # [100, [100, 3], 4]
print(lst_2) # [1, [2, 3], 4]


    왜 굳이 복사 방식을 두가지로 설정해 두었을까? 이유는 두가지가 있다. 첫째는 자기 자신을 참조하는 오브젝트를 복사하게 되면 recursive loop가 발생할 수 있기 때문이다. 두번째 이유는 deep copy가 수행되면 원본과 공유해야 하는 데이터까지도 복사해 버리기 때문이다.

5.2. 튜플 안의 리스트는 수정 가능할까?

    튜플 안에 리스트는 수정 가능할까? 이런 코드를 작성하게 될 일은 보통 없지만 시도해 보았다. 결론은, 수정 가능하다[각주:1]. 그런데 다소 특이하게 동작한다. 튜플의 원소를 변경하려는 시점에서 예외가 발생하지만 변경은 된다.

try:
  a = ([1, 2, 3], 4)
  a[0] += [4]
except Exception as e:
  print(e)  # 'tuple' object does not support item assignment
finally:
  print(a)  # ([1, 2, 3, 4], 4)


  1. Python 3.6.2 [본문으로]

'IT > 기타' 카테고리의 다른 글

[TensorFlow] 모델 저장 및 복원  (0) 2017.09.06
[기타] 티스토리 기초 설정  (1) 2017.07.09
C#에서 이벤트 사용하기  (1) 2017.01.29
C#에서 Zip 및 익명형식의 배열 사용하기  (0) 2016.11.14
C#과 R을 연동하기  (2) 2016.09.27
Comments