본문 바로가기
python

파이썬 기초 - Mutable과 Immutable 객체

by kyeongseo.oh 2025. 3. 19.

Mutable vs Immutable

Python에서 모든 변수는 객체를 참조하며, 이 객체들은 크게 두 가지 유형으로 나눌 수 있다.

Immutable (변경 불가능한) 객체

  • 생성된 후에는 내용을 변경할 수 없는 객체
  • 값을 수정하려면 새로운 객체를 생성해야 함
  • 예: int, float, str, tuple, bytes

Mutable (변경 가능한) 객체

  • 생성된 후에도 내용을 변경할 수 있는 객체
  • 동일한 객체를 유지하면서 내부 값 수정 가능
  • 예: list, dict, set, 사용자 정의 클래스

Python 기본 자료형의 특성

Immutable 타입

정수(int)

a = 5
id_before = id(a)
a += 1  # a = a + 1
id_after = id(a)
print(f"이전 ID: {id_before}, 이후 ID: {id_after}")
print(f"같은 객체? {id_before == id_after}")  # False

문자열(str)

s = "hello"
id_before = id(s)
s += " world"  # s = s + " world"
id_after = id(s)
print(f"이전 ID: {id_before}, 이후 ID: {id_after}")
print(f"같은 객체? {id_before == id_after}")  # False

 

튜플(tuple)

t = (1, 2, 3)
id_before = id(t)
t += t
id_after = id(t)
print(f"이전 ID: {id_before}, 이후 ID: {id_after}")
print(f"같은 객체? {id_before == id_after}")  # False

Mutable 타입

리스트(list)

my_list = [1, 2, 3]
id_before = id(my_list)
my_list.append(4)  # 내용 변경
id_after = id(my_list)
print(f"이전 ID: {id_before}, 이후 ID: {id_after}")
print(f"같은 객체? {id_before == id_after}")  # True

딕셔너리(dict)

my_dict = {'a': 1, 'b': 2}
id_before = id(my_dict)
my_dict['c'] = 3  # 내용 변경
id_after = id(my_dict)
print(f"이전 ID: {id_before}, 이후 ID: {id_after}")
print(f"같은 객체? {id_before == id_after}")  # True

객체 ID와 값 비교: is vs ==

is 연산자

  • 두 변수가 메모리 상의 동일한 객체를 참조하는지 확인
  • 객체의 ID를 비교 (id(a) == id(b))
  • 정체성(identity) 비교

== 연산자

  • 두 객체의 값이 같은지 확인
  • 객체의 내용을 비교
  • 동등성(equality) 비교
# 예시
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(f"a == b: {a == b}")    # True (값이 같음)
print(f"a is b: {a is b}")    # False (다른 객체)
print(f"a == c: {a == c}")    # True (값이 같음)
print(f"a is c: {a is c}")    # True (같은 객체)

# ID 확인
print(f"id(a): {id(a)}")
print(f"id(b): {id(b)}")
print(f"id(c): {id(c)}")

주의해야 할 점: 정수 객체 인터닝

Python은 메모리 최적화를 위해 작은 정수(-5부터 256 사이)를 미리 생성하여 재사용하는 데, 이를 인터닝(interning)이라고 한다.

a = 5
b = 5
print(a is b)  # True: 같은 객체를 참조

x = 257
y = 257
print(x is y)  # 구현에 따라 다를 수 있음 (대개 False)

참조와 복사

변수 할당과 참조

Python에서 변수 할당은 값을 복사하는 것이 아닌, 객체에 대한 참조(reference)를 생성하는 것이다.

# Mutable 객체의 참조
original_list = [1, 2, 3]
reference = original_list  # 참조 생성

reference.append(4)  # 원본 객체 수정
print(original_list)  # [1, 2, 3, 4] - 원본도 변경됨

Shallow Copy

shallow copy는 객체 자체는 새로 생성하지만, 내부의 항목은 원본 객체의 항목과 동일한 참조를 유지한다. 따라서 해당 참조의 값이 변경되면, 원본과 복사 객체의 값이 변경된다.

import copy

# 얕은 복사의 예
original = [[1, 2, 3], [4, 5, 6]]
shallow_copied = copy.copy(original)  # 또는 original.copy() 또는 list(original)

# 최상위 객체는 다름
print(original is shallow_copied)  # False

# 내부 객체는 동일한 참조
print(original[0] is shallow_copied[0])  # True

# 내부 객체 변경 시 영향
original[0][0] = 99
print(shallow_copied)  # [[99, 2, 3], [4, 5, 6]]

Deep Copy 

deep copy는 객체와 그 내부의 모든 중첩된 객체까지 재귀적으로 복사한다. 전부를 복사하여 새 주소에 담기에 참조를 공유하지 않는다.

import copy

# 깊은 복사의 예
original = [[1, 2, 3], [4, 5, 6]]
deep_copied = copy.deepcopy(original)

# 최상위 객체는 다름
print(original is deep_copied)  # False

# 내부 객체도 다름
print(original[0] is deep_copied[0])  # False

# 내부 객체 변경 시 영향 없음
original[0][0] = 99
print(deep_copied)  # [[1, 2, 3], [4, 5, 6]]

복사 방법 비교

import copy

# 원본 리스트
original = [[1, 2], [3, 4]]

# 참조
reference = original

# shallow copy (여러 방법)
shallow_copy1 = original.copy()
shallow_copy2 = list(original)
shallow_copy3 = original[:]
shallow_copy4 = copy.copy(original)

# deep copy
deep_copy = copy.deepcopy(original)

# 원본 수정
original[0][0] = 9999
original.append([5, 6])

# 결과 확인
print("참조:", reference)
print("shallow 1:", shallow_copy1)
print("shallow 2:", shallow_copy2)
print("shallow 3:", shallow_copy3)
print("shallow 4:", shallow_copy4)
print("deep:", deep_copy)

 

함수 매개변수 전달 시 주의점

Python에서 함수에 인자를 전달할 때도 참조 방식이 적용된다.

def modify_list(lst):
    lst.append(999)  # 원본 객체 수정
    
def modify_number(num):
    num += 10  # 새 객체 할당 (함수 외부에 영향 없음)

# 리스트 (mutable) 전달
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # [1, 2, 3, 999] - 원본이 수정됨

# 숫자 (immutable) 전달
my_number = 5
modify_number(my_number)
print(my_number)  # 5 - 원본이 수정되지 않음

 

활용과 함정

1. 기본 인자 함정

파이썬에서 함수 정의는 코드가 로드될 때 실행되며, default parameter의 값은 그 시점에 한 번만 생성된다. 그리고 이 기본값 객체는 함수 객체의 속성으로 저장된다.

# 위험한 패턴
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b'] - 이전 호출의 영향을 받음

# 올바른 패턴
def add_item_safe(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

print(add_item_safe("a"))  # ['a']
print(add_item_safe("b"))  # ['b']

2. 튜플 속 mutable 객체

# 튜플은 immutable이지만 내용물은 mutable일 수 있음
t = (1, [2, 3], 4)
# t[0] = 99  # TypeError: 'tuple' object does not support item assignment
t[1].append(99)  # 내부 리스트는 변경 가능
print(t)  # (1, [2, 3, 99], 4)

3. 문자열 수정의 비효율성

# 문자열은 immutable이므로 수정은 새 객체 생성을 의미함
import time

# 비효율적 방식
start = time.time()
s = ""
for i in range(10000):
    s += str(i)  # 매번 새 문자열 생성
print(f"걸린 시간: {time.time() - start:.5f}초")

# 효율적 방식
start = time.time()
parts = []
for i in range(10000):
    parts.append(str(i))
s = "".join(parts)  # 한 번만 문자열 생성
print(f"걸린 시간: {time.time() - start:.5f}초")

 

연습 문제

연습 1: 데이터 변경 추적하기

다음 코드의 출력 결과를 예측한다.

def modify_data(data):
    if isinstance(data, list):
        data.append(100)
    elif isinstance(data, dict):
        data['new'] = 100
    elif isinstance(data, int):
        data += 100
    return data

my_list = [1, 2, 3]
my_dict = {'a': 1, 'b': 2}
my_number = 5

result_list = modify_data(my_list)
result_dict = modify_data(my_dict)
result_number = modify_data(my_number)

print("원본 리스트:", my_list)
print("반환된 리스트:", result_list)
print("원본 딕셔너리:", my_dict)
print("반환된 딕셔너리:", result_dict)
print("원본 숫자:", my_number)
print("반환된 숫자:", result_number)

연습 2: 중첩 데이터 구조의 복사

다음 중첩 데이터 구조를 shallow copy와 deep copy로 복제한 후, 변경 사항이 어떻게 전파되는지 확인한다.

import copy

nested_data = {
    'name': 'Project A', 
    'members': ['Alice', 'Bob'],
    'details': {
        'status': 'active',
        'priority': 'high'
    }
}

shallow_copy = copy.copy(nested_data)

deep_copy = copy.deepcopy(nested_data)

nested_data['members'].append('Charlie')
nested_data['details']['status'] = 'completed'

# 결과 확인
print("origin:", nested_data)
print("shallow:", shallow_copy)
print("deep:", deep_copy)

'python' 카테고리의 다른 글

파이썬 기초 - 예외처리  (0) 2025.03.19
파이썬 기초 - Context Manager  (0) 2025.03.19
파이썬 기초 - Iterable, Iterator, Generator  (0) 2025.03.18
파이썬 기초 - 클래스 기초  (0) 2025.03.16
파이썬 기초 - 함수 기초  (0) 2025.03.16

댓글