분실 신고서
LOST-004
신고인
배달 앱 스타트업 개발자 진 씨
잃어버린 것
새 주문 목록. 주문 목록을 만드는 함수를 부를 때마다, 지난 손님의 주문이 쌓인 채로 나온다.
해결 조건
지난 주문이 어디에 남아 있었는지 찾아내고, 부를 때마다 빈 목록에서 시작하게 한다.
하늘섬 국제공항 수하물 센터
점심시간, 스타트업 개발자가 노트북을 들고 뛰어 들어옵니다. 첫 손님의 주문은 커피 하나였는데, 두 번째 손님의 영수증에 커피와 베이글이 함께 찍혔다고 합니다.
도미란 반장이 함수 정의 한 줄을 손가락으로 짚습니다. "가방을 넘겨줄 때는 가방이 아니라 태그가 하나 더 붙는 거야. 그리고 이 빈 가방은 함수를 만들 때 딱 한 번 샀지."
함수를 부르면 인자로 넘긴 객체가 복사되지 않습니다. 함수 안의 매개변수 이름이 같은 객체에 태그로 하나 더 붙습니다. 이것을 객체 참조 전달(call by object reference, call by sharing)이라고 합니다.
mine = ["laptop"]
def show(bag):
print(bag is mine)
show(mine)
# True태그가 하나 더 붙은 것이니, 1교시의 두 가지 동작이 그대로 적용됩니다.
bag.append(...)처럼 내용을 바꾸면, 바깥 이름에서도 보입니다. 같은 가방이니까요.bag = ...로 재바인딩하면, 함수 안의 태그만 옮겨 갑니다. 바깥 이름은 원래 가방에 그대로 붙어 있습니다.def pack(bag):
bag.append("charger")
def swap(bag):
bag = ["new"]
bag.append("charger")
mine = ["laptop"]
pack(mine)
print(mine)
# ['laptop', 'charger']
swap(mine)
print(mine)
# ['laptop', 'charger']아래 태그 보드에는 처음으로 호출 칸(프레임)이 등장합니다. 함수를 부르면 왼쪽에 pack 칸이 생기고, 그 안의 태그 bag이 바깥 mine과 같은 가방을 가리킵니다. 함수가 끝나면 칸은 사라집니다.
태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
2교시의 +=가 여기서 다시 나옵니다. 같은 +=라도 타입에 따라 바깥에서 보이기도 하고 안 보이기도 합니다.
def bump(n):
n += 1 # int: 새 객체, 함수 안의 태그만 옮김
def extend(lst):
lst += [1] # list: __iadd__, 같은 가방을 바꿈
count, items = 0, []
bump(count)
extend(items)
print(count, items)
# 0 [1]파이썬에는 "바깥 변수를 바꾸는 인자"가 없습니다. 함수가 바깥에 영향을 주는 길은 넘겨받은 가변 객체를 바꾸거나, 값을 돌려주는 것뿐입니다.
핀의 안내
→ '값 전달이냐 참조 전달이냐'로 고민하지 마세요. 파이썬은 늘 같은 일을 해요. 매개변수라는 태그를 넘겨받은 객체에 하나 더 다는 것. 바깥에서 보이는지는 함수가 그 객체를 바꿨는지, 태그만 옮겼는지에 달렸어요.
신고인의 함수를 줄여 보면 이렇습니다.
def f(x=[]):
x.append(1)
return x
print(f())
# [1]
print(f())
# [1, 1]
print(f.__defaults__)
# ([1, 1],)기본값 []은 함수를 부를 때가 아니라 def 문이 실행될 때 한 번 계산됩니다. 그렇게 만든 리스트는 함수 객체의 __defaults__ 튜플에 매달려 함수가 살아 있는 동안 남습니다. 인자를 생략하고 부를 때마다 매개변수 x는 그 하나뿐인 리스트에 붙습니다.
print(f() is f.__defaults__[0])
# True아래 태그 보드에서 두 번의 호출을 따라가 보세요. 호출 칸은 생겼다가 사라지지만, defaults에 매달린 리스트는 계속 남아 주문이 쌓입니다.
태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
고치는 법은 정해져 있습니다. 기본값으로 None을 두고, 함수 안에서 그때그때 새 리스트를 만듭니다.
def add_order(item, orders=None):
if orders is None:
orders = []
orders.append(item)
return orders
print(add_order("coffee"), add_order("bagel"))
# ['coffee'] ['bagel']
print(add_order.__defaults__)
# (None,)None이 의미 있는 값이라 "인자를 안 줬다"와 구별해야 한다면, 아무도 쓰지 않는 객체를 하나 만들어 센티넬(sentinel)로 씁니다. 세상에 하나뿐인 객체라 is로 비교합니다.
_MISSING = object()
def lookup(key, default=_MISSING):
if default is _MISSING:
return f"no default for {key}"
return default
print(lookup("gate"), lookup("gate", None))
# no default for gate None[], {}, set()만 위험한 것이 아닙니다. def할 때 한 번 계산된다는 규칙은 모든 기본값에 똑같이 적용됩니다.f.__kwdefaults__에 따로 매달립니다.함수는 두 부류로 나눠 생각하면 편합니다. 넘겨받은 가방을 제자리에서 바꾸는 함수와, 원본은 두고 새 가방을 돌려주는 함수입니다.
nums = [3, 1, 2]
print(nums.sort(), nums)
# None [1, 2, 3]
nums = [3, 1, 2]
out = sorted(nums)
print(out, nums, out is nums)
# [1, 2, 3] [3, 1, 2] False파이썬에는 관례가 있습니다. 제자리에서 바꾸는 함수와 메서드는 None을 돌려줍니다. list.sort(), list.append(), list.reverse(), dict.update(), random.shuffle()이 모두 그렇습니다. 바뀐 것이 원본이라는 사실을 숨기지 않으려는 약속입니다.
그래서 이런 실수가 생깁니다.
nums = [3, 1, 2]
nums = nums.sort()
print(nums)
# None제자리에서 바꿈(None 반환) | 새 객체를 돌려줌 |
|---|---|
lst.sort() | sorted(lst) |
lst.reverse() | lst[::-1], list(reversed(lst)) |
lst.append(x), lst.extend(xs) | lst + [x] |
d.update(other) | d | other |
직접 함수를 만들 때도 둘 중 하나를 고르세요. 바꾸면서 돌려주기까지 하면, 부르는 쪽이 원본이 바뀐 줄 모르고 쓰기 쉽습니다.
*args와 **kwargs, 그리고 방어적 복사 (7분)*args는 남는 위치 인자를 새 튜플에, **kwargs는 남는 키워드 인자를 새 dict에 모읍니다. 하지만 그 안에 꽂힌 것은 넘긴 객체의 태그입니다.
def g(*args, **kwargs):
return args, kwargs
items = ["a"]
opts = {"tag": items}
a, k = g(items, **opts)
print(type(a), type(k))
# <class 'tuple'> <class 'dict'>
print(a[0] is items, k is opts, k["tag"] is items)
# True False True**opts로 풀어 넘겨도 함수가 받는 kwargs는 새 dict입니다. 그래서 함수가 kwargs에 키를 더해도 opts는 그대로입니다. 그러나 값으로 들어 있던 리스트는 같은 객체입니다.
태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
넘겨받은 가변 객체를 오래 보관하는 함수는 특히 조심해야 합니다. 부른 쪽이 나중에 그 객체를 바꾸면, 보관해 둔 것도 함께 바뀝니다.
def make_flight(code, passengers):
return {"code": code, "passengers": passengers}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo', 'Cy']이럴 때는 받는 순간 자기 몫을 새로 만듭니다. 이것을 방어적 복사(defensive copy) 라고 합니다.
def make_flight(code, passengers):
return {"code": code, "passengers": list(passengers)}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo']list(passengers)는 얕은 복사입니다(3교시). 안에 든 것이 문자열이라 이것으로 충분합니다. 반대로 받은 것을 잠깐 읽기만 하는 함수라면 복사할 필요가 없습니다.
return도 객체를 복사하지 않습니다. 함수 안에서 만든 가방을 부른 쪽에 넘기고, 부른 쪽의 이름이 그 가방에 태그를 답니다. 함수의 칸이 사라져도, 바깥 태그가 있으니 가방은 남습니다.
def build():
bag = ["boarding pass"]
return bag
b = build()
print(b)
# ['boarding pass']여러 값을 돌려주는 것처럼 보이는 return a, b는 튜플 하나를 돌려주는 것입니다. 받는 쪽에서 언패킹하면 태그가 하나씩 붙습니다.
def stats(xs):
return min(xs), max(xs)
res = stats([4, 9, 2])
print(res, type(res))
# (2, 9) <class 'tuple'>
lo, hi = stats([4, 9, 2])
print(lo, hi)
# 2 9return이 없는 함수는 None을 돌려줍니다. 4-3의 "바꾸는 함수"가 None을 돌려주는 것도 이 때문입니다.
신고인의 함수와 고친 함수입니다.
def new_order(item, cart=[]):
cart.append(item)
return cart
print(new_order("coffee"))
# ['coffee']
print(new_order("bagel"))
# ['coffee', 'bagel']
print(new_order.__defaults__)
# (['coffee', 'bagel'],)
def new_order(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(new_order("coffee"), new_order("bagel"))
# ['coffee'] ['bagel']지난 주문은 함수 객체의 __defaults__에 매달린 리스트 안에 있었습니다. 서버가 함수를 한 번 정의하고 계속 부르니, 그 리스트는 서버가 꺼질 때까지 손님마다 주문을 모았습니다.
개발자가 돌아간 뒤 반장이 한마디 보탭니다. "태그는 함수가 끝나면 떨어지지만, 함수 객체가 붙잡은 가방은 안 떨어져. 함수도 가방이니까."
종결 보고 — LOST-004. 기본 인자 cart=[]는 def 때 한 번 만들어져 __defaults__에 남았음. 모든 호출이 같은 리스트를 공유. None 센티넬로 호출마다 새 리스트를 만들어 해결.
def add(x, seen=set())가 seen.add(x) 후 len(seen)을 돌려줄 때, add(1), add(2), add(1)의 결과를 예측하고 add.__defaults__를 확인하세요.def clear(lst): lst = []와 def clear(lst): lst.clear()는 바깥 리스트에 각각 어떤 영향을 줄까요?sorted, 복사 후 sort)으로 만들어 보세요.반장의 업무 수첩
인자 전달은 객체 참조 전달. 매개변수는 같은 객체에 붙는 태그 하나 더.
함수 안에서 객체를 바꾸면 바깥에서 보이고, 재바인딩하면 보이지 않는다.
기본 인자는 def 때 한 번 계산되어 __defaults__에 남는다. 가변 기본값 대신 None.
제자리에서 바꾸는 함수는 None을 돌려준다(sort 대 sorted).
*args·**kwargs는 새 튜플·dict지만 안의 객체는 같다. 오래 보관할 것은 방어적 복사.
return도 태그 옮기기. 여러 값 반환은 튜플 하나.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.