- Mutable vs Immutable
| 구분 | Mutable (가변) | Immutable (불변) |
| 정의 | 생성된 후 객체의 내부 상태를 변경 가능 | 생성된 후 객체의 내부 상태를 변경 불가능 |
| 대표 타입 | list, dict, set | int, float, str, tuple |
| 수정시 | 메모리 주소 유지, 값만 바뀜 | 새로운 객체 생성 (메모리 주소 바뀜) |
# [Mutable: 리스트]
a = [1, 2]
print(id(a)) # 주소: 1000 (예시)
a.append(3) # 내부 값 수정
print(id(a)) # 주소: 1000 (동일함! 상자는 그대로, 내용물만 추가)
# [Immutable: 문자열]
s = "Hello"
print(id(s)) # 주소: 2000 (예시)
s = s + "!" # 새로운 문자열 생성
print(id(s)) # 주소: 3000 (다름! 기존 "Hello"를 버리고 새 상자를 만듦)
# [Immutable: 튜플]
my_tuple = (1, 2, 3)
# 1. 인덱싱으로 접근은 가능
print(my_tuple[0]) # 1
# 2. 값 수정 시도 (에러 발생!)
# my_tuple[0] = 5 # TypeError: 'tuple' object does not support item assignment
# 3. 값 추가/삭제 메서드 없음
# my_tuple.append(4) # AttributeError: 'tuple' object has no attribute 'append'
- 튜플 안에 리스트가 있다면?
- 튜플 자체는 Immutable 이지만, 그 안에 든 Mutable 객체(리스트)는 수정 가능
- 튜플은 리스트의 주소값을 가지고 있는데 그 주소값 자체는 변하지 않기 때문
t = (1, [2, 3])
t[1][0] = 99
print(t) # (1, [99, 3]) -> 튜플이 가리키는 리스트 주소는 그대로라 허용됨
- 왜 굳이 Immutable 하게 만들었을까?
- 안전성 (Defensive Programming)
- 여러 곳에서 하나의 데이터를 참조할 때 누군가 몰래 값을 바꿔버리면 버그 발생함
- 누군가 실수로 원본 데이터를 수정하는 것을 방지함
- Immutable 객체는 절대 변하지 않으므로 믿고 쓸 수 있음
- 튜플로 데이터를 넘기는 것은 '이 데이터는 읽기 전용' 이라는 계약을 명시하는 것
- 성능과 최적화
- 값이 변하지 않기 때문에 파이썬은 해당 데이터를 위한 메모리 공간을 딱 맞게 계산해서 할당할 수 있음
- 리스트의 경우 값이 추가될 것을 대비해 메모리를 여유있게 잡아야 하므로 (Over-allocation) 더 무거움
- 해시 가능성 (Hashability)
- 딕셔너리의 키(Key)로 사용하기 위해!
- 파이썬의 딕셔너리는 해시테이블 구조
- 키 값이 중간에 변하면 해시값이 달라져서 데이터를 찾을 수 없음
- Immutable 객체는 불변으로 해시가 가능하여, 딕셔너리 키로 사용할 수 있음
- 안전성 (Defensive Programming)
- 가변 (mutable) 객체와 불변 (immutable) 객체의 차이
- append()는 in-place 수정
- 리스트는 가변 객체이므로 append()가 리스트 자체를 직접 수정함
data = []
print(id(data)) # 메모리 주소: 예) 140234567891234
data.append("A")
print(id(data)) # 같은 주소: 140234567891234
print(data) # ["A"]
# 같은 객체가 수정된 것!
data = []
data = data.append("A") # ❌ 잘못된 코드!
print(data) # None
# 왜 None?
# append()는 None을 반환하기 때문!
- 가변 객체 (Mutable) - 직접 수정 가능
# 리스트
my_list = [1, 2]
my_list.append(3) # 할당 없이 수정
print(my_list) # [1, 2, 3]
# 딕셔너리
my_dict = {"a": 1}
my_dict["b"] = 2 # 할당 없이 수정
print(my_dict) # {"a": 1, "b": 2}
# 세트
my_set = {1, 2}
my_set.add(3) # 할당 없이 수정
print(my_set) # {1, 2, 3}
- 불변 객체 (Immutable) - 새로운 객체 생성
# 문자열
text = "Hello"
text.upper() # ❌ text는 변하지 않음!
print(text) # "Hello" (그대로)
new_text = text.upper() # ✅ 새 객체 할당 필요
print(new_text) # "HELLO"
# 튜플
my_tuple = (1, 2)
# my_tuple.append(3) # ❌ 에러! 튜플은 수정 불가
# 숫자
x = 5
x + 1 # ❌ x는 변하지 않음!
print(x) # 5
x = x + 1 # ✅ 새 값 할당 필요
print(x) # 6
def modify_list(lst):
lst.append("X")
# return 없음!
my_data = [1, 2]
modify_list(my_data)
print(my_data) # [1, 2, "X"] - 변경됨!
# 하지만 문자열은?
def modify_string(s):
s = s + "X"
# 새 객체가 생성되었지만 로컬 변수에만 할당
my_str = "Hello"
modify_string(my_str)
print(my_str) # "Hello" - 변경 안 됨!
- += 연산자와 + 연산자가 리스트(mutable 객체)를 다루는 방식의 차이
- a += [4] (In-place 수정)
- a가 가리키던 메모리 공간에 직접 값을 추가함
- a += [4] : 기존의 a 리스트를 그대로 두고, 그 뒤에 4를 붙여라!
- a = a + [4] (새로운 객체 생성 및 할당)
- 두 리스트를 합쳐서 완전히 새로운 리스트 객체를 만들어냄
- a+[4]: 메모리 어딘가에 [1,2,3,4]라는 새로운 리스트를 만듦 (기존 a는 건드리지 않음)
- a = ... : 변수 a가 방금 만든 새로운 리스트의 주소를 가리키도록 업데이트(재할당)
a = [1, 2, 3] # 리스트 mutable
b = a
print(id(a),id(b))
a += [4]
print(a, b) # [1,2,3,4], [1,2,3,4]
print(id(a),id(b)) # 주소값 그대로
a = [1, 2, 3]
b = a
print(id(a),id(b))
a = a + [4]
print(a, b) # [1,2,3,4], [1,2,3]
print(id(a),id(b)) # a는 주소값 달라짐
- append()와 +=의 차이
- t[2].append(5)
- 리스트 내부만 수정함 (할당 작업 없음)
- 튜플에 아무런 간섭을 하지 않음
- t[2] += [6]
- 리스트 수정 후 다시 할당하려함
- 튜플에서 내용물 바꾸지 말라고 차단함
- 에러는 발생하지만 리스트 안의 값은 이미 수정 # (1, 2, [3, 4, 5, 6])
t = (1, 2, [3, 4]) # 튜플 안에 리스트 포함
t[2].append(5)
print(t) # (1, 2, [3, 4, 5])
# 아래 줄이 실행된다면 어떤 일이 일어나는지(출력/에러 포함) 정확히 쓰세요.
t[2] += [6] # TypeError: 'tuple' object does not support item assignment
- list.append() vs tuple += (In-place vs Reassignment)
d1 = {"x": [1, 2], "y": [3, 4]}
d2 = d1.copy()
d1["x"].append(999)
print(d1) # {"x": [1, 2, 999], "y": [3, 4]}
print(d2) # {"x": [1, 2, 999], "y": [3, 4]}
print(id(d1["x"]),id(d2["x"])) # 같음
d3 = {"x": (1, 2), "y": (3, 4)}
d4 = d3.copy()
d3["x"] += (999,) # 새로운 튜플 할당
print(d3) # {'x': (1, 2, 999), 'y': (3, 4)}
print(d4) # {'x': (1, 2), 'y': (3, 4)}
print(id(d3["x"]),id(d4["x"])) # 다름
- list.append(): 리스트는 가변 객체이므로 메모리 주소는 그대로 둔 채 내부 데이터만 수정함
- += 연산: 튜플은 불변 객체이고, += 연산은 내부적으로 d3["x"] = d3["x"] + (999,)와 같이 동작함
- 완전히 새로운 튜플 (1,2,999)를 메모리에 만들고 d3["x"]라는 키가 이제 이 새로운 주소를 가리키게 업데이트 함
- d4["x"]는 여전히 과거의 주소 (1,2)를 가리키고 있으므로 수정되지 않음
- 설정값을 딕셔너리에 담아 얕은 복사로 여러 모듈에 뿌렸다고 가정할 때
- 그 설정값이 리스트였다면?
- 한 모듈에서 설정을 바꿨는데 엉뚱한 다른 모듈의 동작도 꼬이게 됨
- 반면 튜플이었다면?
- 값을 변경하는 순간 새로운 객체가 할당되므로 다른 모듈에는 영향을 주지 않아 안전
- 그 설정값이 리스트였다면?
- 만약 d1["x"] = d1["x"] + [999] 라고 코드를 썼다면?
- d2의 값은 변하지 않는다!!!!!
- + 연산자는 새로운 리스트를 생성하여 대입하므로 튜플의 사례와 똑같이 작동함
- append나 extend같은 인플레이스(In-place) 메서드를 썼을 때만 얕은 복사에서 문제가 발생하는 것
- d2의 값은 변하지 않는다!!!!!
'Python 공부' 카테고리의 다른 글
| 리스트 컴프리헨션 (List Comprehension) (0) | 2026.01.09 |
|---|---|
| Truthiness (참 같은 값), Truthy/Falsy, 단락평가 (0) | 2026.01.09 |
| isalpha, isdigit, isinstance 함수 (0) | 2026.01.09 |
| 조건문 / 반복문 활용, 연산자 비교 우선순위, 단락평가 (1) | 2026.01.09 |
| 프로그램 종료 함수 (0) | 2026.01.09 |