<개인공부>/[Python]

[python] 파이썬 mutable, immutable 객체에 관해서

BlockDMask 2021. 10. 15. 21:00
반응형

안녕하세요. BlockDMask입니다.
오늘은 파이썬에 있는 mutable 객체, immutable 객체에 대한 차이점에 대해서 알아보겠습니다.

<목차>
1. 파이썬 mutable, immutable 설명
2. 파이썬 mutable, immutable 값이 변경될 때 에제

 

1. 파이썬 immutable, mutable 객체에 대해서 


1-1) mutable , immutable 객체 구분

파이썬에서는 객체의 종류를 두 가지로 구분할 수 있습니다.
mutable - 변경되는 객체 (객체의 상태를 변경할 수 있음)
immutable - 변경되지 않는 객체 (객체의 상태를 변경할 수 없음

변경이 되는 mutable 객체의 종류는 list, set, dictionary 정도가 있고
변경이 되지 않는 immutable 객체의 종류는 int, float, tuple, str, bool 이 있습니다.

 

1-2) 객체의 상태가 변경된다 안된다. 그래서 뭐 어쩌라고?

사실 파이썬을 그냥 사용하는 입장에서는 생각하지 않아도 되는 부분이긴 합니다만.
다음 포스팅인 얕은 복사, 깊은 복사를 이해하기 위해서는 필요합니다.

파이썬에서는 immutable 객체의 값이 같은 경우에 변수에 상관없이 동일한 곳을 참조합니다.
mutable (값이 변경될 수 있는) 객체의 경우에는 모든 객체를 각각 생성해서 참조해 줍니다.
이게 무슨 이야기냐면 id를 이용해서 바로 예제로 보겠습니다.

# immutable 객체 (상태 변경 X)
print("immutable 객체")
a = 99
b = 99
c = 99
d = 99
e = 99

print(hex(id(a)))
print(hex(id(b)))
print(hex(id(c)))
print(hex(id(d)))
print(hex(id(e)))


# mutable 객체 (상태 변경 O)
print("\nmutable 객체")
arr1 = [1, 2, 3]
arr2 = [1, 2, 3]
arr3 = [1, 2, 3]
arr4 = [1, 2, 3]

print(hex(id(arr1)))
print(hex(id(arr2)))
print(hex(id(arr3)))
print(hex(id(arr4)))

python mutable, immutable

- Immutable
변수 a, b, c, d, e 에는 각각 99라는 값이 있습니다.
보통 c, c++에서는 각 변수마다 메모리를 주고 그 주소 값이 값이 달라야 하는데 
파이썬에서는 다릅니다. 하나의  immutable 값에 여러 개의 참조가 붙게 됩니다.

그래서 a, b, c, d, e의 주소를 보면 같은 곳을 가리키는 게 보이나요?
99라는 값이 존재하는 메모리 주소를 다 참조하고 있는 것입니다.

immutable 객체들은 값이 바뀌면 참조만 바꾸면 되기 때문에 이런 식으로 설계를 한 것으로 판단됩니다.

- mutable 
반대로 arr1, arr2, arr3, arr4 도 모두 같은 값을 집어넣어서 리스트를 생성해보았습니다.
리스트는 mutable (값이 변할 수 있는) 객체입니다.
주소의 결과를 보면 arr1, 2, 3, 4가 참조하는 [1,2,3]이 모두 다른 주소인 것을 알 수 있습니다.

그림을 그려보면 아마 메모리 상에서 이렇게 되어있겠죠?

python 메모리 그림

이런 식으로 arr 은 각각 할당받은 메모리를 가리키게 되고
a, b, c, d, e는 하나의 메모리 주소를 가리키게 됩니다. 신기하죠?

mutable과 차이는 이런 차이가 있습니다.
arr1.append 와 같이 mutable 한 리스트는 값이 자유롭게 바뀔 수 있기 때문에,
각각의 메모리를 할당해주는 게 관리가 더 용이하다고 판단한 것 같습니다.

반대로 immutable 한 값들은 99를 100으로 바꾼다 한다면,
실제로 그 메모리의 99를 100으로 바꾸는 게 아니라
값이 100인 객체를 생성해서 그 자리를 가리키도록 해야 하기 때문에, 굳이 99를 n 개 만들 필요가 없지 않을까요?
참조만 바꾸면 되니까요!

**참고. 파이썬에서 객체(변수)가 가리키는 주소를 보기 위해서는 id라는 함수를 사용합니다.
- id 함수 : 변수(객체)가 가리키는 메모리 주소 값 반환
- id(객체)를 16진수로 표현하는 hex 함수로 출력하면 우리가 보기 편하게 16진수의 주소 값이 나오게 됩니다.
ex) print(hex(id(객체))  # 객체가 가리키는 값의 16진수 주소 값

 

 

2. 파이썬 mutable, immutable 값이 변경될 때


2-1) immutable (상태 변경 불가능 - int, float, str...)

# immutable 객체
print("=" * 50)
print("immutable 객체 예제.")
print("=" * 50)

print("1. int 값이 변경되면?")
num1 = 99
num2 = 99
num3 = 99
num4 = 99

print(f"num1 값 : {num1} \t주소 : {hex(id(num1))}")
print(f"num2 값 : {num2} \t주소 : {hex(id(num2))}")
print(f"num3 값 : {num2} \t주소 : {hex(id(num3))}")
print(f"num4 값 : {num4} \t주소 : {hex(id(num4))}")

num1 += 1   # num1 값 증가
num3 += 1   # num3 값 증가
num4 += 10  # num4 값 증가

print(f"num1 값 : {num1} \t주소 : {hex(id(num1))}")
print(f"num2 값 : {num2} \t주소 : {hex(id(num2))}")
print(f"num3 값 : {num3} \t주소 : {hex(id(num3))}")
print(f"num4 값 : {num4} \t주소 : {hex(id(num4))}")

print("\n2. str 값이 변경되면?")
s1 = "BlockDMask"
s2 = "BlockDMask"
s3 = "BlockDMask"
s4 = "BlockDMask"

print(f"s1 값 : {s1} \t주소 : {hex(id(s1))}")
print(f"s2 값 : {s2} \t주소 : {hex(id(s2))}")
print(f"s3 값 : {s3} \t주소 : {hex(id(s3))}")
print(f"s4 값 : {s4} \t주소 : {hex(id(s4))}")

s1 = s1.replace('D', 'ZZZ')  # replace 로 값을 변경하고, 새로운 문자열을 s1에 대입하게됨.
s2 = "BlockZZZMask"          # replace 로 변경한 문자열과 동일한 문자열로 변경함
s4 = s3.upper()              # s4를 대문자로 변경

print(f"s1 값 : {s1} \t주소 : {hex(id(s1))}")
print(f"s2 값 : {s2} \t주소 : {hex(id(s2))}")
print(f"s3 값 : {s3} \t주소 : {hex(id(s3))}")
print(f"s4 값 : {s4} \t주소 : {hex(id(s4))}")

python immutable 예제

int 타입의 경우
99 일 때 주소가 뒤에 네 자리만 보면 05b0이었는데
100으로 변경되니 05d0로 변경된 것을 볼 수 있습니다.
또한, 100으로 변경한 것이 num1, num3인데 둘 다 또 같은 100을 참조하는 것을 볼 수 있습니다.
109로 변경한 num4 도 당연히 참조도 바뀌었겠죠?

str 타입의 경우에도 동일합니다.
최초 s1~s4가 동일한 "BlockDMask"라는 문자열을 참조하고 있는 것을 알 수 있습니다.
각각 따로 "BlockDMask" 문자열을 생성했음에도 불구하고 동일한 메모리를 참조하는 것이 immutable 객체의 특징입니다.

그러나 항상 값이 동일하다고 같은 곳을 가리키는 것은 아닙니다.
s1, s2 값을 각각 변경해서
BlockZZZMask로 동일하게 세팅을 했음에도 c8 b0, c7 b0로 각각 주소 값이 다른 것을 볼 수 있습니다.

정리하자면
int 타입의 경우에는 위에서 보셨듯이 99가 100으로 변경되어도 다른 변수들이 같은 100번을 참조하는 반면 

str 같은 경우에는 변경된 s1, s2 항상 문자열이 같은지 보고 같은 곳 참조할지 판단하기가 쉽진 않기 때문에
값이 같다고 항상 같은 곳을 참조 하진 않는 것으로 보입니다.

여기서중요한 것은 immutable 객체는 거의 대부분 같은 값을 참조한다. 는 사실입니다.

 

 

2-2) mutable (상태 변경 가능 - list, set, dictionary...)

# mutable 객체
print("=" * 50)
print("mutable 객체 예제.")
print("=" * 50)

print("1. list 값이 변경되면?")
arr1 = ['a', 'b', 77]
arr2 = ['a', 'b', 77]
arr3 = ['a', 'b', 77]

print(f"arr1 값 : {arr1} \t주소 : {hex(id(arr1))}")
print(f"arr2 값 : {arr2} \t주소 : {hex(id(arr2))}")
print(f"arr3 값 : {arr3} \t주소 : {hex(id(arr3))}")

arr1.append(10)  # ['a', 'b', 77, 10]
arr2.append(10)  # ['a', 'b', 77, 10]

print(f"arr1 값 : {arr1} \t주소 : {hex(id(arr1))}")
print(f"arr2 값 : {arr2} \t주소 : {hex(id(arr2))}")
print(f"arr3 값 : {arr3} \t주소 : {hex(id(arr3))}")


print("\n2. dictionary 값이 변경되면?")
d1 = {'a': 11, 'b': 22, 'c': 33}
d2 = {'a': 11, 'b': 22, 'c': 33}
d3 = {'a': 11, 'b': 22, 'c': 33}

print(f"d1 값 : {d1} \t주소 : {hex(id(d1))}")
print(f"d2 값 : {d2} \t주소 : {hex(id(d2))}")
print(f"d3 값 : {d3} \t주소 : {hex(id(d3))}")

d1['a'] = 99
d2['d'] = 44

print(f"d1 값 : {d1} \t주소 : {hex(id(d1))}")
print(f"d2 값 : {d2} \t주소 : {hex(id(d2))}")
print(f"d3 값 : {d3} \t주소 : {hex(id(d3))}")

python mutable

mutable에서 볼 것은 두 가지입니다.

1. d1, d2, d3의 값이 설사 같다 하더라도,
메모리에 각각 값 들이 생성되고 참조를 각각 하기 때문에 다들 주소가 다르게 나온다.

2. d1, d2, d3에서 내부의 값이 변한다 하더라도,
최초 참조 메모리 주소가 변경되지 않는다.

 

 

2-3) mutable 심화. (그냥 넘어가도 좋습니다.)

마지막 단계입니다. 이제 mutable 객체 심화입니다.
mutable 객체의 값은 메모리에 각각 할당이 되고
immutable 객체의 값은, 동일한 값에 참조가 여러 개 붙는다.
여기까지는 2-1, 2-2에서 예제를 보여드렸습니다.

그럼 mutable 객체 안에 있는 immutable 값은 어떨까요? (슬슬 복잡해지죠)

위 문장을 풀어쓰면 이렇게 될 것입니다.
arr1 = [55, 66, [11, 22], 'a', 'b']
arr2 = [55, 66, [11, 22], 'a', 'b']

arr1과 arr2가 가리키는 리스트의 주소는 분명 다를 것입니다. (mutable 이니까)
그럼 그 안에 있는 immutable 인 55, 66, 'a', 'b'는 어떨까요?
바로 예제 코드로 가보겠습니다.

# mutable 객체
print("=" * 50)
print("mutable 객체 요소로 존재하는 immutable, mutable")
print("=" * 50)

arr1 = [55, 66, [11, 22], 'a', 'b']
arr2 = [55, 66, [11, 22], 'a', 'b']

# 리스트(immutable) 객체의 주소
print(f"arr1 : {arr1} \t주소 : {hex(id(arr1))}")
print(f"arr2 : {arr2} \t주소 : {hex(id(arr2))}")


# 리스트 내부의 mutable 요소
print()
print("-" * 50)
print('리스트 내부의 mutable 요소들')

print(f"arr1[0] : {arr1[0]} \t주소 : {hex(id(arr1[0]))}")
print(f"arr2[0] : {arr2[0]} \t주소 : {hex(id(arr2[0]))}")

print(f"arr1[1] : {arr1[1]} \t주소 : {hex(id(arr1[1]))}")
print(f"arr2[1] : {arr2[1]} \t주소 : {hex(id(arr2[1]))}")

print(f"arr1[3] : {arr1[3]} \t주소 : {hex(id(arr1[3]))}")
print(f"arr2[3] : {arr2[3]} \t주소 : {hex(id(arr2[3]))}")

print(f"arr1[4] : {arr1[4]} \t주소 : {hex(id(arr1[4]))}")
print(f"arr2[4] : {arr2[4]} \t주소 : {hex(id(arr2[4]))}")


# 리스트 내부의 immutable 요소
print()
print("-" * 50)
print('리스트 내부의 immutable 요소들')

print(f"arr1[2] : {arr1[2]} \t주소 : {hex(id(arr1[2]))}")
print(f"arr2[2] : {arr2[2]} \t주소 : {hex(id(arr2[2]))}")

python list 내부 mutable, immutable 요소

- arr1, arr2의 참조 주소를 보면 분명 각각 다른 리스트를 가리키고 있는 것을 알 수 있습니다. (주소 다름)

- 그러나 리스트 내부에 있는 immutable 요소들을 보면
arr1[0], arr2[0] 이 가리키고 있는 55의 주소가 동일한 것을 볼 수 있으며 (주소 동일)
66, 'a', 'b' 또한 각각 동일한 것을 볼 수 있습니다.

- 리스트 내부에 있는 mutable 요소를 보면 (주소 다름)
arr1[2], arr2[2] 에 있는 리스트 [11, 22]는 각각 주소가 또 다른 것을 볼 수 있습니다.

이처럼 파이썬 리스트 안에 있는 각각의 요소들도 결국에는 어떤 값을 가리는 참조 형태 이기 때문에
이런 결과가 나오는 것입니다.

 

이렇게 오늘은 mutable, immutable에 대해서 알아보았습니다.
최대한 쉽게 설명하려고 그림까지 그려봤는데, 잘 이해되셨으면 좋겠습니다.

반응형