R

ReflexKR

AI의 일기장Reflex 공식 문서

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

·

강의 개요

1

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

2

봉인된 가방: 가변과 불변

3

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

4

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

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

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

8

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

9

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

10

검색대: 속성 접근의 비밀

11

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

12

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

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

강의 시간 4:10–5:00

기내용 가방: 클로저

분실 신고서

LOST-006

세 승객이 모두 같은 탑승구로

신고인

탑승 안내 방송 담당 서 주임

잃어버린 것

탑승구 안내 함수 세 개. 반복문으로 A1, B7, C3용을 만들었는데, 셋 다 C3으로 가라고 안내한다.

해결 조건

세 함수가 무엇을 들고 다니는지 확인하고, 함수마다 제 탑승구를 기억하게 한다.

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

아침 방송 직후, 서로 다른 탑승구로 가야 할 승객 세 명이 모두 C3 앞에 서 있습니다. 서 주임은 안내 함수를 셋 만들었으니 안내도 셋이어야 한다고 말합니다.

반장이 기내용 가방 하나를 들어 보입니다. "함수도 가방을 들고 다녀. 문제는 그 안에 든 게 값이냐, 이름표냐지." 오늘은 함수가 들고 다니는 작은 가방, 클로저를 엽니다.

6-1. 함수를 돌려주는 함수 (7분)

1교시에서 본 것처럼 함수도 객체입니다. 이름에 묶고, 리스트에 넣고, 다른 함수에 넘기고, 돌려줄 수 있습니다.

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tag
  • make_tag를 부를 때마다 def tag():가 실행되어 새 함수 객체가 만들어집니다.
  • t()를 부르는 순간 make_tag는 이미 끝났습니다. 5교시의 규칙대로라면 make_tag의 지역 이름 owner도 함께 사라졌어야 합니다.
  • 그런데 t()는 "kim"을 기억합니다. 끝난 함수의 이름을 안쪽 함수가 어떻게 들고 있는 걸까요?

6-2. 기내용 가방: 셀과 __closure__ (9분)

안쪽 함수가 바깥 함수의 이름을 쓰면, 파이썬은 그 이름을 셀(cell)이라는 작은 가방에 담습니다. 바깥 함수의 칸과 안쪽 함수가 같은 셀을 가리킵니다. 바깥 함수가 끝나 칸이 사라져도, 안쪽 함수가 셀을 들고 있으니 셀과 그 안의 값은 남습니다. 이 셀 묶음을 들고 다니는 함수가 클로저입니다.

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)
속성뜻
inner.__code__.co_freevars안쪽 함수가 바깥에서 빌려 쓰는 이름(자유 변수)
inner.__closure__그 이름마다 하나씩, 셀의 튜플
cell.cell_contents셀이 지금 가리키는 객체
outer.__code__.co_cellvars바깥 함수 쪽에서 셀로 만들어 둔 이름

태그 보드에서 make_tag가 실행 중일 때 칸의 owner는 셀을 열어 본 값으로 표시됩니다. 칸이 사라진 뒤에도 function tag의 owner 줄이 cell을 거쳐 'kim'을 붙잡고 있는지 보세요.

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

make_tag가 끝나면 그 칸은 사라지지만, 함수 tag가 든 cell이 'kim'을 붙잡고 있습니다.
  • 기내용 가방에는 안쪽 함수가 실제로 쓰는 이름만 들어갑니다. 바깥 함수의 다른 지역 이름은 함수가 끝나면 함께 정리됩니다.
  • 반대로, 들어간 것은 그 함수가 살아 있는 동안 계속 남습니다. 큰 리스트를 안쪽 함수가 한 번이라도 쓰면, 그 리스트도 함께 남습니다.
def make_tag(owner):
    notes = ["memo"] * 1000
    def tag():
        return owner
    return tag

print(make_tag("kim").__code__.co_freevars)
# ('owner',)

핀의 안내

→ 클로저가 든 것은 값의 복사본이 아니라 셀, 즉 이름표가 매달린 자리예요. 그래서 바깥에서 그 이름을 옮기면 클로저가 보는 값도 바뀌어요.

6-3. 늦은 바인딩: 세 승객이 모두 C3으로 간 이유 (10분)

신고인의 코드입니다.

def make_guides(gates):
    guides = []
    for gate in gates:
        guides.append(lambda: gate)
    return guides

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# True
  • 반복문은 구역을 만들지 않습니다(5교시). 세 번 도는 동안 gate는 make_guides 안의 이름 하나이고, 셀도 하나입니다.
  • 세 lambda는 모두 그 셀 하나를 들고 갑니다. 셀 안의 값은 반복이 끝날 때 "C3"입니다.
  • lambda의 본문 gate는 부를 때 셀을 열어 봅니다. 만들 때 값을 적어 두지 않습니다. 이것을 늦은 바인딩(late binding)이라고 합니다.

리스트 컴프리헨션으로 써도 같습니다.

fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# True

반복문이 없어도 같은 일이 일어납니다. 함수를 만든 뒤 바깥에서 이름을 옮기면, 함수가 보는 값도 바뀝니다.

def make():
    gate = "A1"
    def guide():
        return gate
    gate = "Z9"
    return guide

print(make()())
# Z9

아래 태그 보드에서 세 함수의 화살표가 셀 하나로 모이는 모습을 보세요. 고친 버전에서는 함수마다 자기 defaults 튜플을 갖습니다.

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

lambda: gate에서는 세 함수의 gate가 모두 같은 cell을 가리킵니다. gate=gate로 고치면 함수마다 만들 때의 값이 기본 인자에 담깁니다.

고치는 방법은 "만드는 순간의 값"을 함수에 따로 담는 것입니다.

fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]

from functools import partial

def guide(gate):
    return "go to " + gate

guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']
  • lambda i=i: i: 기본 인자는 정의할 때 한 번 계산됩니다(4교시). 그 순간의 i가 함수의 __defaults__에 담깁니다.
  • functools.partial(guide, gate): 함수와 그 순간의 인자를 묶은 새 호출 가능 객체를 만듭니다. 기본 인자 꼼수보다 뜻이 분명합니다.
  • 기본 인자 방식은 부르는 쪽에서 f(5)처럼 값을 덮어쓸 수 있다는 단점이 있습니다.

6-4. 상태를 가진 함수: nonlocal 카운터 (7분)

셀은 읽기만 하는 것이 아닙니다. nonlocal(5교시)로 선언하면 안쪽 함수가 셀 속 태그를 옮길 수 있습니다. 그러면 함수가 부를 때마다 이어지는 상태를 갖습니다.

def counter():
    n = 0
    def tick():
        nonlocal n
        n += 1
        return n
    return tick

c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4
  • counter()를 부를 때마다 새 셀이 생깁니다. c와 c2는 서로의 숫자를 모릅니다.
  • nonlocal n이 없으면 n += 1이 n을 지역 이름으로 만들어 UnboundLocalError가 납니다(5교시와 같은 원인).

태그 보드

단계 1 / 0

다음에 실행할 줄: 0

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

tick을 부를 때마다 cell 안의 값이 0에서 1, 2로 바뀝니다. 정수는 불변이라, 정수가 바뀌는 것이 아니라 셀 속 태그가 새 정수로 옮겨 가는 것입니다.

같은 일을 클래스로도 할 수 있습니다. 상태가 인스턴스의 속성에 담깁니다(7교시).

class Counter:
    def __init__(self):
        self.n = 0
    def tick(self):
        self.n += 1
        return self.n

k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2
클로저클래스
상태가 있는 곳셀인스턴스의 __dict__
바깥에서 보기__closure__로만k.n으로 바로
어울리는 때동작 하나, 상태 조금동작 여럿, 상태 여럿

6-5. 데코레이터 맛보기 (7분)

함수를 받아 새 함수를 돌려주는 클로저를 데코레이터라고 합니다. 원래 함수는 안쪽 함수의 셀에 담깁니다.

def log(func):
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper
  • @log는 board = log(board)와 같습니다. 이름 board는 이제 wrapper 함수를 가리키고, 원래 board는 wrapper의 셀 func에 들어 있습니다.
  • 그래서 board.__name__이 wrapper가 됩니다. 디버깅과 문서에서 곤란합니다.

functools.wraps 한 줄이 원래 함수의 이름과 문서 등을 옮겨 붙여 줍니다.

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board.__name__, board.__wrapped__.__name__)
# board board

데코레이터에 인자를 주는 법, 클래스로 만드는 데코레이터 같은 더 깊은 이야기는 제너레이터·데코레이터 강의에서 다룹니다.

사건 종결 (10분)

함수마다 만들 때의 탑승구를 담아 주면 됩니다.

from functools import partial

def announce(gate):
    return "go to " + gate

def make_guides(gates):
    return [partial(announce, gate) for gate in gates]

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']

def make_guides_v2(gates):
    return [lambda gate=gate: "go to " + gate for gate in gates]

print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']

다음 방송부터 세 승객은 각자 제 탑승구로 갑니다. 서 주임이 돌아간 뒤, 반장이 13번 컨베이어 기록을 넘기며 혼잣말을 합니다. "끝난 함수의 물건도 누가 기내용 가방에 넣어 들고 있으면 버려지지 않아. 그 두 가방도 누가 쥐고 있는 걸지 몰라."

종결 보고 — LOST-006. 세 안내 함수가 반복 변수 gate의 셀 하나를 함께 들고 있어 부를 때 마지막 값 C3을 읽음(늦은 바인딩). partial과 기본 인자로 만들 때의 값을 담아 해결.

실습 과제

  1. [lambda: i for i in range(3)]을 [lambda: i * 10 for i in range(3)]으로 바꾸면 결과는? 셀이 몇 개인지 __closure__로 확인하세요.
  2. counter()를 고쳐 tick()과 reset() 두 함수를 함께 돌려주게 해 보세요. 두 함수가 같은 셀을 쓰는지 is로 확인하세요.
  3. 함수가 몇 번 불렸는지 세는 데코레이터 count_calls를 만들어 보세요. 횟수는 wrapper의 속성에 둘까요, 셀에 둘까요? 둘 다 해 보세요.

반장의 업무 수첩

  • 함수도 객체. 함수를 만들어 돌려줄 수 있다.

  • 안쪽 함수가 쓰는 바깥 이름은 셀에 담기고, 함수는 __closure__로 그 셀을 들고 다닌다.

  • 클로저가 든 것은 값이 아니라 셀(이름표 자리). 부를 때 연다: 늦은 바인딩.

  • 반복문에서 만든 함수들은 셀 하나를 함께 쓴다. 고침: lambda i=i: i, functools.partial.

  • nonlocal 로 셀 속 태그를 옮기면 상태를 가진 함수가 된다. 상태가 많으면 클래스.

  • 데코레이터는 함수를 받아 함수를 돌려주는 클로저. functools.wraps 를 잊지 말 것.

← 이전

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

다음 →

7교시 · 가방 규격서: 클래스와 인스턴스

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

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