C.W.K.
Stream
Lesson 05 of 07 · published

Closure 와 scope — LEGB, 그리고 inner function 의 이상한 동작

~22 min · closure, scope, LEGB, nonlocal, global

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

LEGB 룰 — Python 이 이름 찾는 곳

x 같은 이름 참조하면 Python 은 (순서대로) Local scope, Enclosing 함수 scope, Global (모듈) scope, 마지막으로 Built-ins (print, len 등) 을 찾아. 처음 찾는 곳이 이김. 그래서 함수 안에 정의된 이름이 같은 이름의 모듈 레벨 이름을 가려 — Python 이 local 을 먼저 찾아.

Closure — 출생지를 기억하는 함수

inner 함수가 enclosing 함수의 이름을 참조할 수 있어. 그 inner 함수를 반환하면 — 그 바인딩들을 들고 가. 그게 closure. inner 함수가 enclosing 함수의 변수에 대한 참조를 들고, enclosing 함수가 끝난 후에도. decorator 가 작동하는 방식, factory 함수가 작동하는 방식, 콜백이 컨텍스트 기억하는 방식 — 다 이거.

읽기 vs 쓰기 — 비대칭

inner 함수는 enclosing 변수를 *읽는* 건 자유. *쓰기* 는 nonlocal 필요. 모듈 레벨 변수 *쓰기* 는 global 필요. 이 선언 없이 할당하면 — 같은 이름의 새 local 바인딩 만들어서 바깥 이름 가려. closure 안에서 카운터 변경하려는 거의 모든 초보가 여기서 걸려.

주의: 클래식 late-binding closure 함정 — fns = [lambda: i for i in range(5)]. 모든 lambda 가 같은 i 를 참조, 끝나면 4. *현재 값* 캡쳐하려면 lambda i=i: i (디폴트 인자가 정의 시점에 캡쳐). 모두가 한 번씩 물려.

closure vs class — 언제 어느 쪽

state 들고 있는 함수가 필요하면 closure 가 가볍고 idiomatic. state 공유하는 메서드 여러 개 필요하면 class 가 더 명확. 그 사이의 흐릿한 영역은 Python 디자인의 영원한 질문 — 모든 경우에 맞는 룰 같은 건 없어.

Code

LEGB 동작·python
x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)            # local 먼저 찾음
    inner()

outer()                     # local

# inner 에 x 정의 안 하면 바깥으로 걸음
def outer2():
    x = "enclosing"
    def inner():
        print(x)            # enclosing 찾음
    inner()

outer2()                    # enclosing

# 둘 다 정의 안 하면 global 찾음
def outer3():
    def inner():
        print(x)            # global
    inner()

outer3()                    # global
Closure factory — 정석 패턴·python
def make_counter(start=0):
    count = start
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c1 = make_counter()
c2 = make_counter(100)

print(c1())     # 1
print(c1())     # 2
print(c1())     # 3

print(c2())     # 101
print(c2())     # 102
# c1 / c2 가 각자 자기 count 를 가짐 — closure 는 독립적
nonlocal 없으면 새 local 만들어·python
def outer():
    n = 10
    def inner():
        n = 99             # 새 local — 바깥 n 안 건드림
        print("inner sees:", n)
    inner()
    print("outer still:", n)

outer()
# inner sees: 99
# outer still: 10

# nonlocal 로
def outer2():
    n = 10
    def inner():
        nonlocal n
        n = 99             # 바깥 n 변경
    inner()
    print("outer now:", n)

outer2()                   # outer now: 99
late-binding closure 함정·python
# 함정
fns = []
for i in range(5):
    fns.append(lambda: i)

print([f() for f in fns])      # [4, 4, 4, 4, 4]   <- 다 같음

# 왜? 각 lambda 가 같은 i 를 참조, loop 끝나면 4.

# 해결 1 — 디폴트 인자가 *정의 시점* 에 값 캡쳐
fns = []
for i in range(5):
    fns.append(lambda i=i: i)

print([f() for f in fns])      # [0, 1, 2, 3, 4]

# 해결 2 — closure factory
def make(i):
    return lambda: i

fns = [make(i) for i in range(5)]
print([f() for f in fns])      # [0, 1, 2, 3, 4]
global — 정말 필요할 때만·python
counter = 0

def bump():
    global counter
    counter += 1

bump()
bump()
bump()
print(counter)             # 3

# 근데 — `global` 쓰는 건 보통 class / closure / 명시적 인자로 가야 한다는 신호.
# 아껴 써.

External links

Exercise

함수 make_accumulator() 작성 — 함수 반환. 반환된 함수가 숫자 받으면 누적 총합에 더하고 새 총합 반환. 각 accumulator 가 독립된 총합. accumulator 두 개 만들고 각각 여러 번 호출해서 서로 안 섞인다는 거 보여. (closure + nonlocal. class X.)

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.