연습10

류한선·2025년 6월 29일

실기연습-2

목록 보기
57/95

좋아!
이번 문제는 리스트의 슬라이싱(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]

🔍 한 줄씩 완전 해부


1️⃣ 시작

data = [10, 20, 30]
  • 리스트 [10, 20, 30]가 생성되고, data가 이를 가리킴
data → [10, 20, 30]

2️⃣ 함수 호출

tricky(data)
  • 리스트는 mutable(가변) 자료형이므로
  • data 리스트의 참조lst로 전달됨
  • 즉, lstdata는 같은 객체를 가리킴
lst → [10, 20, 30]  (같은 객체)
data → [10, 20, 30]

3️⃣ 함수 내부 1번째 줄

lst[0] += 5
  • lst[0] → 현재는 10
  • 10 + 5 = 15

→ 이건 리스트 내부 요소 수정이므로 원본 객체도 바뀜

lst → [15, 20, 30]
data → [15, 20, 30]

datalst 모두 같은 리스트를 참조하므로 같이 바뀜


4️⃣ 함수 내부 2번째 줄

lst = lst[1:]

⚠️ 이 줄이 가장 중요한 포인트

  • lst[1:] 은 슬라이싱을 통해 새로운 리스트 객체를 생성함
    → 결과: [20, 30]

  • lst = ... 구문은 이제 lst가 새로운 리스트를 가리키게 됨
    → 이 시점 이후로 lstdata다른 객체를 가리킴

lst  → [20, 30]    (새로운 리스트)
data → [15, 20, 30] (원래 리스트, 바뀌지 않음)

5️⃣ 함수 내부 3번째 줄

lst[0] += 100
  • 현재 lst[20, 30] 이므로
  • lst[0] = 20 + 100 = 120
lst → [120, 30]
data → [15, 20, 30]
  • 이제 완전히 분리된 리스트이므로, data는 더 이상 영향 없음

6️⃣ 함수 종료

  • lst는 사라짐
  • data는 여전히 [15, 20, 30]

7️⃣ 출력

print(data)
  • 출력 결과는:
[15, 20, 30]

✅ 최종 정답: [15, 20, 30]


💡 핵심 개념 요약

코드의미결과
lst[0] += 5기존 리스트의 0번째 요소 직접 수정data도 같이 바뀜
lst = lst[1:]슬라이싱으로 새 리스트 생성lstdata 연결 끊김
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) 에서 자주 발생하는 치명적인 함정을 다루는 문제로 가볼게.
이건 정보처리기사 실기 뿐 아니라, 현업 개발자도 종종 실수하는 부분이야.
리스트와 결합하면 예상치 못한 동작을 일으키기 때문에 꼭 짚고 넘어가야 해.


✅ 문제: 다음 Python 코드의 출력 결과를 구하시오.

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]

✅ 정답: 2번

[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]

🧠 기억 포인트

  • 기본 인자에 mutable 객체 절대 금지!
  • 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 → {}

✅ 1번째 호출: weird_behavior(2)

if 2 in cache:False
cache[2] = 2 * 2 = 4
return 4
  • cache 상태: {2: 4}
  • 출력: 4

✅ 2번째 호출: weird_behavior(3)

if 3 in cache:False
cache[3] = 3 * 2 = 6
return 6
  • cache 상태: {2: 4, 3: 6}
  • 출력: 6

✅ 3번째 호출: weird_behavior(2)

if 2 in cache:True
return cache[2] = 4
  • 캐시에 이미 저장돼 있어서 재계산하지 않고 기존 값 재사용

  • 출력: 4


🔁 최종 출력 결과

4
6
4

📌 디버깅 요약: 함수 호출별 상태 변화

호출xcache 상태반환값
12{2: 4}4
23{2: 4, 3: 6}6
32(동일)4

⚠️ 핵심 개념 요약

항목설명
기본 인자 cache={}함수 정의 시 단 한 번만 생성됨
호출마다 새로운 dict가 생기지 않음매번 같은 cache를 계속 씀
dictmutable 객체내부 수정이 호출 간에도 유지됨
즉, 캐시처럼 동작 가능하지만...원하지 않을 때 위험 요소!

✅ 안전하게 고치는 방법

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에서 실수하기 쉬운 핵심 주제야.
하나씩 디버깅 수준으로 완전 해설해줄게. 먼저 첫 번째부터 시작하자.


✅ 1. 클래스에서 공유되는 클래스 변수 (static과 비슷한 개념)

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의 모든 인스턴스에서 공유됨.
  • ab는 각각 객체지만, self.shared_list는 둘 다 같은 리스트를 가리킴.

✅ 클래스 vs 인스턴스 변수 차이 정리

선언 위치공유 여부
클래스 바깥 (클래스 내부, 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)

✅ 2. 함수 안에서 전역변수를 실수로 덮어쓰는 경우

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를 사용하겠다고 명시해야 함

✅ 3. 리스트 슬라이싱 vs 깊은 복사 vs 얕은 복사

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()깊은 복사내부까지 완전 분리

✅ 4. lambda / closure 변수 캡처 오작동

funcs = []

for i in range(3):
    funcs.append(lambda: i)

for f in funcs:
    print(f())

❓ 출력 결과:

2  
2  
2

🔍 해설

  • lambda는 변수를 "지금 값"이 아닌 "참조"로 캡처
  • irange(3) 반복 끝나고 2가 되어 있음 → 모든 람다가 i=2를 가리킴

✅ 해결 방법 1: 기본값 인자로 고정시키기

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: 사용

0개의 댓글