C언어 변수와 파이썬 변수의 근본적 차이를 이해하고, 참조와 메모리 관리를 파악한다.
🎯 이 강의에서 배우는 것
파이썬을 배울 때 가장 먼저 만나는 개념이 변수입니다. 그런데 파이썬의 변수는 여러분이 C나 Java에서 배웠거나 상상하는 변수와 근본적으로 다릅니다. 이 차이를 제대로 이해하지 못하면, 앞으로 배울 리스트 복사, 함수 인자 전달, 가비지 컬렉션 등에서 계속 혼란을 겪게 됩니다. 이 강의는 그 혼란의 근원을 완전히 제거합니다.
📦 박스 모델 vs 이름표 모델
C 언어에서 변수를 선언하면 메모리에 "박스"가 만들어집니다:
/* C 언어 */
/* int x = 5; */
/* 스택 메모리에 4바이트 박스가 생기고, 그 안에 5가 저장됨 */
/* x라는 이름 = 그 메모리 위치(주소) */
/* x = 10; 하면 같은 박스에 10을 덮어씀 */
# 파이썬에서는 완전히 다릅니다
x = 5
# 1. 힙 메모리 어딘가에 "정수 5" 객체가 생성됨
# 2. x라는 이름표(참조/포인터)가 그 객체를 가리킴
# 3. x라는 이름표에는 타입 정보가 없음 — 객체에 있음
x = 10
# 이전의 정수 5 객체는 그대로 있고
# x라는 이름표가 새로운 "정수 10" 객체를 가리킴
# (이전 객체의 참조 카운트가 줄어들고, 0이 되면 메모리 해제)
이것을 이름표 모델(name-binding model) 또는 참조 모델(reference model)이라고 합니다. 파이썬에서 =는 "값을 저장한다"가 아니라 "이름을 객체에 바인딩한다"는 의미입니다.
🔍 객체의 3요소: id, type, value
파이썬의 모든 객체는 세 가지 요소를 가집니다:
x = 42
# 1. id: 객체의 고유 식별자 (CPython에서는 메모리 주소)
print(id(x)) # 예: 140234567890
# 2. type: 객체의 타입
print(type(x)) # <class 'int'>
# 3. value: 객체가 담고 있는 값
print(x) # 42
# 세 가지를 한번에 확인
print(f"id={id(x)}, type={type(x).__name__}, value={x}")
# id=140234567890, type=int, value=42
id()가 반환하는 값은 CPython에서 실제 메모리 주소입니다(다른 구현체에서는 다를 수 있습니다). 두 변수가 같은 객체를 가리키면 id()가 같습니다:
a = [1, 2, 3]
b = a # b는 a와 같은 리스트 객체를 가리킴
print(id(a)) # 예: 140000000001
print(id(b)) # 예: 140000000001 (동일!)
print(a is b) # True
b.append(4)
print(a) # [1, 2, 3, 4] — a도 변경됨!
# 이유: a와 b가 같은 객체를 공유하기 때문
🆚 is vs == — 자주 혼동하는 두 연산자
파이썬에서 비교 연산자가 두 가지인 이유를 정확히 이해해야 합니다:
# == : 값이 같은가? (동등성, equality)
# is : 같은 객체인가? (동일성, identity)
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — 값은 같음
print(a is b) # False — 다른 객체 (다른 메모리 주소)
c = a
print(a is c) # True — 같은 객체
소형 정수 캐싱(-5~256): CPython은 자주 사용되는 -5에서 256까지의 정수 객체를 미리 만들어 재사용합니다. 이 범위에서는 is가 ==처럼 동작하는 것처럼 보입니다:
# 소형 정수 캐싱 예시
a = 100
b = 100
print(a is b) # True — 같은 캐시된 객체
a = 1000
b = 1000
print(a is b) # False — 캐시 범위 초과, 별도 객체 생성
# 인터랙티브 셸에서는 같은 블록의 리터럴을 최적화할 수 있어
# 결과가 다를 수 있음 (구현 세부사항)
핵심 규칙: is는 None, True, False와 비교할 때만 사용하세요. 숫자나 문자열 값 비교에는 항상 ==를 사용하세요. is를 일반 값 비교에 쓰면 소형 정수 캐싱 때문에 버그가 발생할 수 있습니다.
# 올바른 None 확인
def process(value=None):
if value is None: # 권장
return "기본값 사용"
if value is not None: # 권장
return f"값: {value}"
# 틀린 방법 (기능은 같지만 명시성이 떨어짐)
if value == None: # None과 ==로 비교 — 작동은 하지만 관례에 어긋남
pass
🔢 참조 카운팅 — 메모리가 해제되는 순간
CPython은 참조 카운팅(reference counting)으로 메모리를 관리합니다. 각 객체는 자신을 가리키는 참조(이름표)의 수를 기록합니다:
import sys
x = [1, 2, 3] # 리스트 객체 생성, 참조 카운트 = 1
print(sys.getrefcount(x)) # 2 — getrefcount 인자로 전달되면서 임시 참조 +1
y = x # 참조 카운트 = 2
z = x # 참조 카운트 = 3
print(sys.getrefcount(x)) # 4 (getrefcount 임시 참조 포함)
del y # 참조 카운트 = 2
del z # 참조 카운트 = 1
del x # 참조 카운트 = 0 → 메모리 즉시 해제!
# 이 시점에서 [1, 2, 3] 객체는 메모리에서 사라짐
del x는 x라는 이름표를 제거하는 것이지, 객체를 직접 삭제하는 것이 아닙니다. 참조 카운트가 0이 될 때 객체가 해제됩니다. 다른 참조가 남아있으면 객체는 계속 살아있습니다.
참조 카운팅이 즉각적인 메모리 해제를 가능하게 하는 이유:
- 마지막 참조가 사라지는 순간 즉시
__del__메서드 호출 및 메모리 해제 - 파일 핸들, 네트워크 연결 등 자원이 예측 가능한 시점에 해제됨
- Java의 GC처럼 "언젠가 해제될 것"이 아니라 "즉시 해제"
♻️ 순환 참조와 세대별 가비지 컬렉션
참조 카운팅의 한계: 순환 참조(circular reference)는 해제할 수 없습니다:
import gc
# 순환 참조 만들기
a = []
a.append(a) # a가 자기 자신을 참조
# a의 참조 카운트는 최소 2 (a라는 이름 + a 자신의 내부 참조)
# del a를 해도 내부 참조 때문에 참조 카운트가 0이 되지 않음!
del a
# a라는 이름이 사라져도 내부 순환 참조로 인해 메모리 미해제
# CPython의 cyclic garbage collector가 주기적으로 처리
gc.collect() # 즉시 순환 참조 수집 요청
# GC 상태 확인
print(gc.get_count()) # (세대0 카운터, 세대1 카운터, 세대2 카운터)
print(gc.get_threshold()) # (700, 10, 10) — 기본 임계값
CPython의 세대별 GC(generational GC)는 객체를 세 세대(generation)로 분류합니다. 새로 생성된 객체는 세대 0에 속하고, GC 사이클에서 살아남으면 세대 1, 2로 승격됩니다. 오래된 세대일수록 GC 빈도가 낮아 성능 영향이 줄어듭니다.
🔄 다중 할당과 변수 교환
파이썬에서 변수를 교환하는 방법이 얼마나 우아한지 보여드립니다:
# C 언어에서의 변수 교환 (임시 변수 필요)
# int temp = a;
# a = b;
# b = temp;
# 파이썬에서는 임시 변수 없이
a = 10
b = 20
a, b = b, a # 단 한 줄!
print(a, b) # 20 10
# 이것이 작동하는 내부 원리:
# 1. 오른쪽 b, a를 튜플로 패킹: (20, 10)
# 2. 왼쪽 a, b에 튜플 언패킹: a=20, b=10
# 임시 튜플이 생성되지만, 매우 빠르고 우아한 방식
# 다중 할당
x = y = z = 0 # x, y, z 모두 같은 정수 0 객체를 가리킴
print(x is y) # True (소형 정수 캐싱으로 같은 객체)
# 주의: 가변 객체로 다중 할당하면 위험
a = b = [] # a와 b가 같은 리스트를 공유!
a.append(1)
print(b) # [1] — b도 변경됨!
# 올바른 방법: 각각 독립적인 리스트 생성
a = []
b = []
a.append(1)
print(b) # [] — b는 영향 없음
언패킹(unpacking)은 파이썬의 강력한 기능입니다:
# 기본 언패킹
first, second, third = [10, 20, 30]
print(first, second, third) # 10 20 30
# 확장 언패킹 (Python 3+)
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
*head, last = [1, 2, 3, 4, 5]
print(head) # [1, 2, 3, 4]
print(last) # 5
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last) # 1 [2, 3, 4] 5
⚠️ 자주 하는 실수
- 할당이 복사라고 생각함:
b = a는 a의 복사본을 만드는 것이 아니라 같은 객체에 이름표를 하나 더 붙이는 것입니다. 가변 객체에서 특히 주의하세요. is를 값 비교에 사용: 소형 정수 캐싱 때문에 작은 정수에서는 우연히 작동하지만, 큰 정수나 다른 객체에서는 예상과 다른 결과가 나옵니다.- 다중 할당으로 가변 객체 공유:
a = b = []는 같은 리스트를 공유합니다. 각각 독립적인 리스트가 필요하면a = []; b = []처럼 따로 생성하세요.
📝 정리 및 다음 강의 예고
- 파이썬 변수는 "박스"가 아니라 객체를 가리키는 "이름표"입니다.
- 모든 파이썬 객체는 id(메모리 주소), type(타입), value(값)를 가집니다.
is는 동일 객체 여부,==는 동등한 값 여부를 비교합니다.- 참조 카운팅으로 메모리를 즉시 해제하고, 순환 참조는 세대별 GC가 처리합니다.
a, b = b, a는 튜플 패킹·언패킹으로 임시 변수 없이 교환합니다.
다음 강의: 파이썬 정수(int)의 내부를 완전히 해부합니다. 왜 파이썬 정수는 오버플로우가 없는지, 비트 연산은 어떻게 작동하는지, 그리고 / 와 // 나눗셈의 차이를 배웁니다.
관련 주제
- 변수 이름표 모델
- is vs == 차이
- 소형 정수 캐싱
- 참조 카운팅
- 순환 참조와 가비지컬렉션
- 다중 할당·변수 교환
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
