Python closure는 바깥 함수의 local name을 안쪽 함수가 기억하는 구조다. 이를 이용하면 작은 상태를 함수와 함께 묶고 factory function이나 decorator를 만들 수 있다. 다만 closure를 곧바로 ‘private data’나 ‘보안상 은닉’이라고 부르면 범위를 과장하게 된다. Python에서는 introspection이 가능하고 access control을 강제하는 장치가 아니기 때문이다.
closure가 기억하는 것은 lexical scope의 binding이다
다음 counter는 make_counter() 실행이 끝난 뒤에도 count binding을 유지한다.
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment
counter = make_counter(10)
print(counter())
print(counter(3))
print(counter.__code__.co_freevars)
print([cell.cell_contents for cell in counter.__closure__])
실행 결과는 다음과 같다.
11
14
('count',)
[14]
안쪽 함수의 count는 free variable이고 실제 cell에 값이 연결된다. __closure__로 들여다볼 수 있으므로 closure state는 interface를 작게 만드는 encapsulation 기법이지 secret을 보관하는 security boundary가 아니다. password, token, encryption key를 숨기기 위한 수단으로 사용하지 않는다.
nonlocal은 읽기가 아니라 재바인딩에 필요하다
바깥 scope의 name을 읽기만 할 때는 nonlocal이 필요 없다. 안쪽 함수에서 그 name에 새 값을 대입하려면 Python이 local variable로 판단하므로, 가장 가까운 enclosing non-global binding을 다시 쓰겠다는 nonlocal 선언이 필요하다.
def make_power(exponent):
def power(number):
return number**exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(5), cube(5))
여기서는 exponent를 읽기만 하므로 선언이 없다. 반면 counter는 count += step이 재바인딩을 포함하므로 nonlocal count가 필요하다.
가변 객체의 내용만 바꾸는 경우도 구분한다.
def make_collector():
items = []
def add(item):
items.append(item)
return tuple(items)
return add
append()는 items name을 다른 list로 재바인딩하지 않아 nonlocal이 필요 없다. 이 차이는 Python nonlocal과 재바인딩에서 더 자세히 볼 수 있다.
loop 안 lambda의 late binding
closure는 함수를 만들 때 현재 값을 자동으로 복사하는 것이 아니라, 호출 시점에 name을 조회한다. 그래서 다음 결과는 [0, 1, 2]가 아니다.
functions = [lambda: index for index in range(3)]
print([function() for function in functions])
[2, 2, 2]
모든 lambda가 같은 index binding을 보고 loop가 끝난 뒤 호출되기 때문이다. 현재 값을 각 함수에 고정하려면 default argument로 capture할 수 있다.
functions = [lambda index=index: index for index in range(3)]
print([function() for function in functions])
[0, 1, 2]
default argument는 function definition 시점에 평가된다. 더 복잡한 상태라면 의도가 드러나는 factory function을 쓰는 편이 읽기 쉽다.
def capture(value):
def read():
return value
return read
functions = [capture(index) for index in range(3)]
decorator는 함수를 받아 새 callable을 반환한다
decorator syntax는 function을 다른 callable로 감싸는 표현이다.
@trace
def add(left, right):
return left + right
개념적으로 다음 대입과 같다.
def add(left, right):
return left + right
add = trace(add)
실무 wrapper는 positional·keyword argument와 반환값을 보존하고, functools.wraps로 원래 함수의 metadata를 복사하는 것이 기본이다.
from functools import wraps
def trace(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
result = function(*args, **kwargs)
print(f"returned {result!r}")
return result
return wrapper
@trace
def add(left: int, right: int = 0) -> int:
"""두 정수를 더한다."""
return left + right
print(add(3, right=4))
print(add.__name__)
print(add.__doc__)
wraps가 없으면 __name__, __doc__, annotation과 signature를 사용하는 debugger·documentation tool이 wrapper 정보만 보게 될 수 있다. wraps는 update_wrapper()를 적용하고 __wrapped__도 설정해 원래 callable을 따라갈 수 있게 한다.
logging decorator에서 argument와 반환값을 그대로 출력하면 token, 개인정보와 대용량 payload가 log에 남을 수 있다. 함수 이름과 처리 시간처럼 필요한 정보만 남기고 값은 allowlist·masking 정책을 둔다. exception을 catch한다면 원래 traceback과 실패 의미를 잃지 않도록 다시 발생시키거나 명시적인 error contract를 사용한다.
closure보다 class가 나은 순간
closure는 상태와 동작이 하나둘일 때 간결하다. 다음 조건에서는 class나 dataclass가 더 명확할 수 있다.
- 상태 field가 늘고 각 field를 조회·수정하는 operation이 많다.
- serialization, type validation, inheritance 또는 protocol 구현이 필요하다.
- 여러 method가 같은 invariant를 지켜야 한다.
- concurrent task나 thread가 state를 함께 변경한다.
closure state라고 해서 thread-safe한 것은 아니다. count += 1 같은 read-modify-write에는 별도의 synchronization이나 single-owner execution model이 필요할 수 있다. 작은 factory에는 closure, 명시적인 domain state에는 class라는 식으로 의도와 유지보수성을 기준으로 고른다.
Python의 일급 함수와 closure 기본 개념은 일급 객체와 클로저, 다른 언어의 capture와 callback 경계는 Swift closure와 callback에서 비교해 볼 수 있다.
참고 자료
'배움과 성장 > 소프트웨어 개발' 카테고리의 다른 글
| 동기·비동기와 블로킹·논블로킹 차이: 통신과 코드에서 헷갈리지 않기 (0) | 2024.08.11 |
|---|---|
| Python nonlocal은 언제 필요한가: 중첩 함수의 변수 재바인딩 (0) | 2024.08.09 |
| 파이썬 일급 함수와 클로저: 함수가 상태를 기억하는 원리 (0) | 2024.08.09 |
| Python 변수 스왑: a, b = b, a의 평가 순서와 주의점 (0) | 2024.03.30 |
| 클린 아키텍처 학습 메모: 정책 수준과 의존성 방향 (0) | 2022.12.30 |
댓글