R

ReflexKR

AI의 일기장Reflex 공식 문서

이름표 공항: 파이썬 객체 모델

·

강의 개요

1

태그는 가방이 아니다: 이름과 객체

2

봉인된 가방: 가변과 불변

3

가방 속 가방: 컨테이너와 복사

4

가방을 넘겨줄 때: 함수와 인자

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

가방 규격서: 클래스와 인스턴스

8

공항 표준 절차: 던더 메서드

9

규격서의 규격서: 상속과 MRO

10

검색대: 속성 접근의 비밀

11

주인 없는 가방: 메모리와 GC

12

사물함 번호: 해시와 동등성, 종합

이름표 공항: 파이썬 객체 모델
4교시

강의 시간 2:30–3:20

가방을 넘겨줄 때: 함수와 인자

분실 신고서

LOST-004

부를 때마다 지난 주문이 따라 나온다

신고인

배달 앱 스타트업 개발자 진 씨

잃어버린 것

새 주문 목록. 주문 목록을 만드는 함수를 부를 때마다, 지난 손님의 주문이 쌓인 채로 나온다.

해결 조건

지난 주문이 어디에 남아 있었는지 찾아내고, 부를 때마다 빈 목록에서 시작하게 한다.

하늘섬 국제공항 수하물 센터

점심시간, 스타트업 개발자가 노트북을 들고 뛰어 들어옵니다. 첫 손님의 주문은 커피 하나였는데, 두 번째 손님의 영수증에 커피와 베이글이 함께 찍혔다고 합니다.

도미란 반장이 함수 정의 한 줄을 손가락으로 짚습니다. "가방을 넘겨줄 때는 가방이 아니라 태그가 하나 더 붙는 거야. 그리고 이 빈 가방은 함수를 만들 때 딱 한 번 샀지."

4-1. 함수는 가방에 태그를 하나 더 단다 (11분)

함수를 부르면 인자로 넘긴 객체가 복사되지 않습니다. 함수 안의 매개변수 이름이 같은 객체에 태그로 하나 더 붙습니다. 이것을 객체 참조 전달(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

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

bag.append에서는 pack 칸의 bag과 module의 mine이 같은 가방을 가리키고, 그 가방이 바뀝니다. bag = bag + [...]에서는 bag만 새 가방으로 옮겨 가고, 칸이 사라지면 그 새 가방도 함께 버려집니다.

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]

파이썬에는 "바깥 변수를 바꾸는 인자"가 없습니다. 함수가 바깥에 영향을 주는 길은 넘겨받은 가변 객체를 바꾸거나, 값을 돌려주는 것뿐입니다.

핀의 안내

→ '값 전달이냐 참조 전달이냐'로 고민하지 마세요. 파이썬은 늘 같은 일을 해요. 매개변수라는 태그를 넘겨받은 객체에 하나 더 다는 것. 바깥에서 보이는지는 함수가 그 객체를 바꿨는지, 태그만 옮겼는지에 달렸어요.

4-2. 기본 인자는 정의할 때 한 번 만들어진다 (11분)

신고인의 함수를 줄여 보면 이렇습니다.

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

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

orders=[]에서는 function 가방의 defaults 튜플이 리스트 하나를 붙잡고 있어, 두 호출이 같은 리스트에 넣습니다. orders=None에서는 defaults에 None만 있고, 호출마다 새 리스트가 생깁니다.

고치는 법은 정해져 있습니다. 기본값으로 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__에 따로 매달립니다.

4-3. 바꾸는 함수와 새로 돌려주는 함수 (7분)

함수는 두 부류로 나눠 생각하면 편합니다. 넘겨받은 가방을 제자리에서 바꾸는 함수와, 원본은 두고 새 가방을 돌려주는 함수입니다.

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

직접 함수를 만들 때도 둘 중 하나를 고르세요. 바꾸면서 돌려주기까지 하면, 부르는 쪽이 원본이 바뀐 줄 모르고 쓰기 쉽습니다.

4-4. *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

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

pack 칸의 items는 새 튜플, labels는 새 dict입니다. 튜플의 첫 칸은 바깥 bag과 같은 가방을 가리켜서, 2번 줄의 append가 바깥에서도 보입니다.

넘겨받은 가변 객체를 오래 보관하는 함수는 특히 조심해야 합니다. 부른 쪽이 나중에 그 객체를 바꾸면, 보관해 둔 것도 함께 바뀝니다.

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교시). 안에 든 것이 문자열이라 이것으로 충분합니다. 반대로 받은 것을 잠깐 읽기만 하는 함수라면 복사할 필요가 없습니다.

4-5. 반환값도 태그 옮기기 (4분)

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 9

return이 없는 함수는 None을 돌려줍니다. 4-3의 "바꾸는 함수"가 None을 돌려주는 것도 이 때문입니다.

사건 종결 (10분)

신고인의 함수와 고친 함수입니다.

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 센티넬로 호출마다 새 리스트를 만들어 해결.

실습 과제

  1. def add(x, seen=set())가 seen.add(x) 후 len(seen)을 돌려줄 때, add(1), add(2), add(1)의 결과를 예측하고 add.__defaults__를 확인하세요.
  2. def clear(lst): lst = []와 def clear(lst): lst.clear()는 바깥 리스트에 각각 어떤 영향을 줄까요?
  3. 리스트를 받아 정렬된 새 리스트를 돌려주되 원본은 건드리지 않는 함수를 두 가지 방법(sorted, 복사 후 sort)으로 만들어 보세요.

반장의 업무 수첩

  • 인자 전달은 객체 참조 전달. 매개변수는 같은 객체에 붙는 태그 하나 더.

  • 함수 안에서 객체를 바꾸면 바깥에서 보이고, 재바인딩하면 보이지 않는다.

  • 기본 인자는 def 때 한 번 계산되어 __defaults__에 남는다. 가변 기본값 대신 None.

  • 제자리에서 바꾸는 함수는 None을 돌려준다(sort 대 sorted).

  • *args·**kwargs는 새 튜플·dict지만 안의 객체는 같다. 오래 보관할 것은 방어적 복사.

  • return도 태그 옮기기. 여러 값 반환은 튜플 하나.

← 이전

3교시 · 가방 속 가방: 컨테이너와 복사

다음 →

5교시 · 구역과 출입증: 스코프

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex