기술면접 보완 1. 파이썬의 mutable과 immutable (+ copy vs deep copy)

재혁·2024년 7월 4일

면접 회고

목록 보기
7/7
post-thumbnail

틀린 부분이 있다면 댓글로 알려주신다면 감사하겠습니다.

서론

기술 면접과는 관련이 없지만, 파이썬과 메모리에 대해 어느 정도 이해한 후에 갑자기 생각난 일입니다. 작년 이맘때쯤 사이드 프로젝트를 하고 있었던 상황입니다. 딕셔너리에 대한 키 값을 할당받은 다른 변수에서 값을 수정했더니 동일하게 값이 수정되는 문제가 발생했습니다. 이 문제의 원인을 몰라서 파이썬 단톡방에 물어봤을 때, 깊은 복사에 대해 알아보라는 조언을 얻었습니다. 그 후 문제를 메모만 해놓고 잊어버리고 있었는데, 이번 기회에 공부해보면 좋을 듯해서 작성해봅니다.

그때 제가 동일한 값을 변수에 할당받았을 때 객체의 타입은 딕셔너리였습니다. 파이썬을 처음 공부할 때 immutable과 mutable에 대해서 살펴봤던 적이 있습니다. 그때 배웠던 내용을 어렴풋이 기억해보면, immutable 객체는 변경할 수 없고, mutable 객체는 변경할 수 있다는 것이었습니다. 그래서 다른 변수에 값을 할당받은 후에도 값을 수정하면 같이 수정되었던 이유가 딕셔너리가 mutable 객체였기 때문이라는 생각에 도달했습니다.

mutable vs immutable

https://www.pythonpool.com/python-data-types
https://docs.python.org/es/3.11/reference/datamodel.html#the-standard-type-hierarchy
https://www.geeksforgeeks.org/mutable-vs-immutable-objects-in-python/

mutable 객체

list, dict, set, bytearray 등이 있으며 객체는 생성된 이후에도 그 값을 변경할 수 있습니다.

하나씩 예시를 본다면 이해가 빠릅니다.

list

# 리스트 생성
my_list = [1, 2, 3]

# 요소 추가
my_list.append(4)
print(my_list)  # [1, 2, 3, 4]

# 요소 삽입
my_list.insert(1, 5)
print(my_list)  # [1, 5, 2, 3, 4]

# 요소 삭제
my_list.remove(2)
print(my_list)  # [1, 5, 3, 4]

# 요소 수정
my_list[0] = 10
print(my_list)  # [10, 5, 3, 4]

dict

# 딕셔너리 생성
my_dict = {"name": "Alice", "age": 25}

# 요소 추가 및 수정
my_dict["age"] = 26
my_dict["city"] = "New York"
print(my_dict)  # {'name': 'Alice', 'age': 26, 'city': 'New York'}

# 요소 삭제
del my_dict["city"]
print(my_dict)  # {'name': 'Alice', 'age': 26}

# 키-값 쌍 접근
print(my_dict["name"])  # Alice

set

# 집합 생성
my_set = {1, 2, 3}

# 요소 추가
my_set.add(4)
print(my_set)  # {1, 2, 3, 4}

# 요소 삭제
my_set.remove(2)
print(my_set)  # {1, 3, 4}

# 집합 연산
another_set = {3, 4, 5}
union_set = my_set.union(another_set)
print(union_set)  # {1, 3, 4, 5}

intersection_set = my_set.intersection(another_set)
print(intersection_set)  # {3, 4}

bytearray

# 바이트 배열 생성
my_bytearray = bytearray(b"hello")

# 요소 변경
my_bytearray[0] = 72  # ASCII 코드 72는 'H'
print(my_bytearray)  # bytearray(b'Hello')

# 요소 추가
my_bytearray.append(33)  # ASCII 코드 33은 '!'
print(my_bytearray)  # bytearray(b'Hello!')

# 슬라이싱 및 부분 변경
my_bytearray[1:5] = b"i"
print(my_bytearray)  # bytearray(b'Hi!')

id

mutable 객체는 객체를 변경해도 메모리 주소값이 변경되지 않습니다.

# 리스트 (List) 예제
list1 = [1, 2, 3]
print(f"Original list ID: {id(list1)}")

# 리스트 변경
list1.append(4)
print(f"Modified list ID: {id(list1)}")

# 딕셔너리 (Dictionary) 예제
dict1 = {"key1": "value1"}
print(f"Original dict ID: {id(dict1)}")

# 딕셔너리 변경
dict1["key2"] = "value2"
print(f"Modified dict ID: {id(dict1)}")

# 집합 (Set) 예제
set1 = {1, 2, 3}
print(f"Original set ID: {id(set1)}")

# 집합 변경
set1.add(4)
print(f"Modified set ID: {id(set1)}")

Original list ID: 140200636486016
Modified list ID: 140200636486016
Original dict ID: 140200636559488
Modified dict ID: 140200636559488
Original set ID: 140200636524896
Modified set ID: 140200636524896

immutable

int, float, str, tuple, frozenset 등이 있으며 객체는 생성된 이후에는 그 값을 변경할 수 없습니다.

immutable에서 변경을 하려고 할 때 에러가 발생합니다.

a = "hello"
a[0] = "H"  # 오류 발생, 문자열은 변경 불가

id

immutable 객체는 객체를 변경할 경우 다른 메모리 주소를 참조하게 됩니다.

# 정수 (Integer) 예제
int1 = 42
print(f"Original int ID: {id(int1)}")

# 정수 변경
int1 = 43
print(f"Modified int ID: {id(int1)}")

# 문자열 (String) 예제
str1 = "hello"
print(f"Original str ID: {id(str1)}")

# 문자열 변경
str1 = "world"
print(f"Modified str ID: {id(str1)}")

# 튜플 (Tuple) 예제
tuple1 = (1, 2, 3)
print(f"Original tuple ID: {id(tuple1)}")

# 튜플 변경
tuple1 = (4, 5, 6)
print(f"Modified tuple ID: {id(tuple1)}")

Original int ID: 9794400
Modified int ID: 9794432
Original str ID: 140472287810672
Modified str ID: 140472287810992
Original tuple ID: 140472287805920
Modified tuple ID: 140472287805440

mutable과 immutable의 차이

Mutable 객체는 데이터를 조작할 수 있어 유연성이 좋습니다. 반면에 immutable 객체는 상태를 변경할 수 없는 객체로, 경합 조건(race condition)이 발생하지 않으며 락(lock)이 필요하지 않습니다. 이러한 특성 덕분에 immutable 객체는 스레드 안전성을 보장받을 수 있으며, 무단 변경이 불가능하여 mutable 객체보다 보안성이 높습니다.

mutable의 경우도 스레드 안정성을 보장받기 위해 기본적으로 파이썬에서는 GIL(Global Interpreter Lock)라는 하나의 스레드로만 바이트 코드를 실행할 수 있도록 하는 것입니다. (이 부분에 대해서는 다음장에 알아보려고 합니다.)

immutable 객체 존재 이유 및 장점
https://en.wikipedia.org/wiki/Thread_safety
https://wiki.python.org/moin/GlobalInterpreterLock

Copy vs Deep Copy

https://docs.python.org/ko/3/library/copy.html

mutable 객체는 같은 메모리 주소를 참조하기 때문에, 서론에서 설명한 것과 같은 문제가 발생할 수 있습니다. 이를 해결하기 위해 deep copy를 사용하여 독립적인 객체로 만들어 문제를 해결할 수 있습니다. 아래는 그와 관련된 개념입니다.

copy.copy()

얕은 복사는 객체의 최상위 레벨의 데이터만 복사하고, 중첩된 객체(예: 리스트 안의 리스트)는 원본 객체와 같은 참조를 가집니다.

import copy

original_list = [1, 2, [3, 4]]
shallow_copied_list = copy.copy(original_list)

print(original_list)  # [1, 2, [3, 4]]
print(shallow_copied_list)  # [1, 2, [3, 4]]

shallow_copied_list[2].append(5)
print(original_list)  # [1, 2, [3, 4, 5]]
print(shallow_copied_list)  # [1, 2, [3, 4, 5]]

original_list와 shallow_copied_list는 최상위 레벨에서 다른 객체이지만, 내부의 리스트 [3, 4]는 동일한 객체를 참조합니다. 따라서, shallow_copied_list의 내부 리스트를 변경하면 original_list에도 영향을 미치며 객체의 중첩된 내용이 변경되지 않을 때 유용합니다.

copy.deepcopy()

깊은 복사는 모든 레벨의 객체를 재귀적으로 복사하여 원본 객체와 완전히 독립적인 복사본을 만듭니다.

import copy

original_list = [1, 2, [3, 4]]
deep_copied_list = copy.deepcopy(original_list)

print(original_list)  # [1, 2, [3, 4]]
print(deep_copied_list)  # [1, 2, [3, 4]]

deep_copied_list[2].append(5)
print(original_list)  # [1, 2, [3, 4]]
print(deep_copied_list)  # [1, 2, [3, 4, 5]]

위 예제에서 deep_copied_list는 original_list와 완전히 독립적이기 때문에, deep_copied_list의 내부 리스트를 변경해도 original_list에는 영향을 미치지 않으며 객체의 중첩된 내용이 변경될 수 있고, 원본 객체에 영향을 미치지 않도록 하고 싶을 때 유용합니다.

그래서 mutable과 같이 변경될 수 있는 객체의 경우 내용을 독립적인 객체로 변경하고 싶을 때 깊은 복사를 사용합니다. 하지만 이로 인해 생길 수 있는 메모리를 할당 하게된다면 이것또한 메모리를 쓰기에 메모리 낭비를 조심해야합니다.

profile
저는 기술적 호기심은 좋지만 좋은 제품을 만드는 것을 우선시하는 개발자입니다.

0개의 댓글