좋아!
이번 문제는 리스트의 슬라이싱(lst = lst[1:])을 통해 참조가 끊기는 구조와,
리스트 안의 요소를 직접 수정하는 구조가 섞여 있어서 굉장히 좋은 연습이야.
한 줄씩 완전히 디버깅하면서 무슨 일이 일어나는지 해설해줄게.
def tricky(lst):
lst[0] += 5
lst = lst[1:]
lst[0] += 100
data = [10, 20, 30]
tricky(data)
print(data)
[15, 20, 30]
data = [10, 20, 30]
[10, 20, 30]가 생성되고, data가 이를 가리킴data → [10, 20, 30]
tricky(data)
data 리스트의 참조가 lst로 전달됨lst와 data는 같은 객체를 가리킴lst → [10, 20, 30] (같은 객체)
data → [10, 20, 30]
lst[0] += 5
lst[0] → 현재는 10임10 + 5 = 15→ 이건 리스트 내부 요소 수정이므로 원본 객체도 바뀜
lst → [15, 20, 30]
data → [15, 20, 30]
✔ data와 lst 모두 같은 리스트를 참조하므로 같이 바뀜
lst = lst[1:]
⚠️ 이 줄이 가장 중요한 포인트
lst[1:] 은 슬라이싱을 통해 새로운 리스트 객체를 생성함
→ 결과: [20, 30]
lst = ... 구문은 이제 lst가 새로운 리스트를 가리키게 됨
→ 이 시점 이후로 lst와 data는 다른 객체를 가리킴
lst → [20, 30] (새로운 리스트)
data → [15, 20, 30] (원래 리스트, 바뀌지 않음)
lst[0] += 100
lst는 [20, 30] 이므로lst[0] = 20 + 100 = 120lst → [120, 30]
data → [15, 20, 30]
data는 더 이상 영향 없음lst는 사라짐data는 여전히 [15, 20, 30]print(data)
[15, 20, 30]
[15, 20, 30]| 코드 | 의미 | 결과 |
|---|---|---|
lst[0] += 5 | 기존 리스트의 0번째 요소 직접 수정 | data도 같이 바뀜 |
lst = lst[1:] | 슬라이싱으로 새 리스트 생성 | lst와 data 연결 끊김 |
lst[0] += 100 | 새 리스트만 수정 | data에는 영향 없음 |
초기 상태:
data → [10, 20, 30]
lst → data
lst[0] += 5
data → [15, 20, 30]
lst = lst[1:] → lst → [20, 30] (새 객체)
lst[0] += 100 → lst → [120, 30]
최종:
data → [15, 20, 30]
좋아, 이번에는 Python의 함수 기본 인자(default argument) 에서 자주 발생하는 치명적인 함정을 다루는 문제로 가볼게.
이건 정보처리기사 실기 뿐 아니라, 현업 개발자도 종종 실수하는 부분이야.
리스트와 결합하면 예상치 못한 동작을 일으키기 때문에 꼭 짚고 넘어가야 해.
def add_item(value, items=[]):
items.append(value)
return items
print(add_item(1))
print(add_item(2))
print(add_item(3))
[1]
[2]
[3]
[1]
[1, 2]
[1, 2, 3]
[1]
[2]
[1, 2, 3]
[1]
[1, 2]
[3]
[1]
[1, 2]
[1, 2, 3]
함수의 기본 인자(items=[])는 함수가 정의될 때 단 한 번만 생성된다.
→ 즉, items=[] 는 매 호출마다 새 리스트가 만들어지는 게 아니다!
기본 인자가 mutable 객체(list, dict 등) 인 경우에는
여러 번의 함수 호출에서 같은 객체를 공유한다.
add_item(1)
items가 기본값인 []를 사용1을 append → items = [1][1]지금 상태:
items (default 객체) → [1]
add_item(2)
items 인자가 생략되었으므로, 같은 기본 리스트를 사용2를 append → items = [1, 2][1, 2]상태:
items → [1, 2]
add_item(3)
3을 append → items = [1, 2, 3][1, 2, 3][1]
[1, 2]
[1, 2, 3]
| 현상 | 설명 |
|---|---|
기본 인자가 [] | 함수 정의 시 한 번만 생성됨 |
items.append() | 기본 인자 객체가 계속 누적됨 |
| 매번 새 리스트 쓰고 싶다면? | items=None 으로 하고 내부에서 처리해야 함 |
def add_item(value, items=None):
if items is None:
items = []
items.append(value)
return items
이렇게 하면 호출할 때마다 새 리스트가 만들어지기 때문에 예상한 대로 작동함:
[1]
[2]
[3]
list, dict 등은 반드시 None으로 초기화해서 내부에서 만들어야 안전함def weird_behavior(x, cache={}):
if x in cache:
return cache[x]
cache[x] = x * 2
return cache[x]
print(weird_behavior(2))
print(weird_behavior(3))
print(weird_behavior(2))
좋아!
그럼 방금 준 문제, weird_behavior 함수에서 기본 인자로 dict를 사용하는 트릭을 완전 디버깅 모드로 해설해줄게.
이건 진짜 자주 실수하는 유형이고, 기본 인자와 dict가 만나면 어떤 일이 벌어지는지 알 수 있는 대표 사례야.
def weird_behavior(x, cache={}):
if x in cache:
return cache[x]
cache[x] = x * 2
return cache[x]
print(weird_behavior(2))
print(weird_behavior(3))
print(weird_behavior(2))
정답은:
4
6
4
def weird_behavior(x, cache={}):
cache={} 기본 인자는 함수가 정의될 때 한 번만 실행됨cache라는 딕셔너리 객체가 메모리에 한 번 생성되고,cache → {}
weird_behavior(2)if 2 in cache: → False
cache[2] = 2 * 2 = 4
return 4
cache 상태: {2: 4}4weird_behavior(3)if 3 in cache: → False
cache[3] = 3 * 2 = 6
return 6
cache 상태: {2: 4, 3: 6}6weird_behavior(2)if 2 in cache: → True
return cache[2] = 4
캐시에 이미 저장돼 있어서 재계산하지 않고 기존 값 재사용
출력: 4
4
6
4
| 호출 | x 값 | cache 상태 | 반환값 |
|---|---|---|---|
| 1 | 2 | {2: 4} | 4 |
| 2 | 3 | {2: 4, 3: 6} | 6 |
| 3 | 2 | (동일) | 4 |
| 항목 | 설명 |
|---|---|
기본 인자 cache={} | 함수 정의 시 단 한 번만 생성됨 |
| 호출마다 새로운 dict가 생기지 않음 | 매번 같은 cache를 계속 씀 |
dict는 mutable 객체 | 내부 수정이 호출 간에도 유지됨 |
| 즉, 캐시처럼 동작 가능하지만... | 원하지 않을 때 위험 요소! |
def weird_behavior(x, cache=None):
if cache is None:
cache = {}
if x in cache:
return cache[x]
cache[x] = x * 2
return cache[x]
→ 이러면 매 호출마다 새 딕셔너리 생성됨
4
6
4
하지만 매번 새 cache를 쓰니까 캐싱 효과는 없음
def cached_square(x, cache={}):
if x not in cache:
cache[x] = x ** 2
return cache[x]
@lru_cache 같은 데코레이터를 쓰는 게 더 일반적임좋아! 네가 요청한 4가지 유형 모두 Python에서 실수하기 쉬운 핵심 주제야.
하나씩 디버깅 수준으로 완전 해설해줄게. 먼저 첫 번째부터 시작하자.
class MyClass:
shared_list = []
def add_item(self, item):
self.shared_list.append(item)
a = MyClass()
b = MyClass()
a.add_item(1)
b.add_item(2)
print(a.shared_list)
print(b.shared_list)
[1, 2]
[1, 2]
shared_list는 클래스 변수이므로 MyClass의 모든 인스턴스에서 공유됨.a와 b는 각각 객체지만, self.shared_list는 둘 다 같은 리스트를 가리킴.| 선언 위치 | 공유 여부 | 예 |
|---|---|---|
| 클래스 바깥 (클래스 내부, def 바깥) | 모든 인스턴스에서 공유 | shared_list = [] |
메서드 내부에서 self.xxx | 각 인스턴스 별도 보유 | self.my_list = [] |
클래스 변수로 리스트, 딕셔너리 같은 mutable 객체를 절대 두지 말 것!
인스턴스에서 독립적으로 쓰려면 이렇게:
class MyClass:
def __init__(self):
self.my_list = [] # 인스턴스 변수
def add_item(self, item):
self.my_list.append(item)
x = 10
def test():
x = x + 1 # NameError 발생
test()
UnboundLocalError: local variable 'x' referenced before assignment
x = x + 1을 작성하면 Python은 지역 변수 x를 만들었다고 가정x는 아직 초기화되지 않았기 때문에 에러 발생x = 10
def test():
global x
x = x + 1
test()
print(x) # 11
global x를 써서 전역 변수 x를 사용하겠다고 명시해야 함import copy
a = [[1, 2], [3, 4]]
b = a[:]
c = copy.deepcopy(a)
a[0][0] = 100
print(b) # 얕은 복사
print(c) # 깊은 복사
b = [[100, 2], [3, 4]]
c = [[1, 2], [3, 4]]
b = a[:] → 얕은 복사: 겉 리스트는 복사되지만, 내부 리스트는 그대로 참조됨c = copy.deepcopy(a) → 모든 내부 구조까지 새 객체로 복사| 방식 | 구조 | 참조 공유 여부 |
|---|---|---|
a[:] | 얕은 복사 | 내부 리스트 공유 |
copy.copy() | 얕은 복사 | 내부 구조 공유 |
copy.deepcopy() | 깊은 복사 | 내부까지 완전 분리 |
funcs = []
for i in range(3):
funcs.append(lambda: i)
for f in funcs:
print(f())
2
2
2
i는 range(3) 반복 끝나고 2가 되어 있음 → 모든 람다가 i=2를 가리킴funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
for f in funcs:
print(f())
→ 출력 결과:
0
1
2
| 항목 | 원인 | 해결책 |
|---|---|---|
| 클래스 변수 공유 | static처럼 작동 | 인스턴스 변수로 분리 |
| 전역 변수 덮어쓰기 | 지역으로 인식 | global 사용 |
| 슬라이싱 복사 | 얕은 복사 | copy.deepcopy() 사용 |
| lambda 변수 공유 | 참조로 캡처됨 | lambda i=i: 사용 |