클로저와 스코프 (Closures & Scope)
클로저와 스코프 (Closures & Scope)¶
1. 변수 스코프¶
파이썬에서 변수는 정의된 위치에 따라 접근 범위가 결정됩니다.
LEGB 규칙¶
변수를 찾는 순서입니다.
┌─────────────────────────────────────────────┐
│ B - Built-in (내장) │
│ print, len, range, ... │
│ ┌─────────────────────────────────────────┐ │
│ │ G - Global (전역) │ │
│ │ 모듈 레벨에서 정의된 변수 │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ E - Enclosing (감싸는 함수) │ │ │
│ │ │ 바깥 함수의 지역 변수 │ │ │
│ │ │ ┌─────────────────────────────────┐ │ │ │
│ │ │ │ L - Local (지역) │ │ │ │
│ │ │ │ 현재 함수 내부 │ │ │ │
│ │ │ └─────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
예제¶
# Built-in (B)
# print, len, str, ...
# Global (G)
x = "global"
def outer():
# Enclosing (E)
x = "enclosing"
def inner():
# Local (L)
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
2. global 키워드¶
함수 내에서 전역 변수를 수정할 때 사용합니다.
count = 0
def increment():
global count # 전역 변수 사용 선언
count += 1
increment()
increment()
print(count) # 2
global 없이 수정하면?¶
count = 0
def increment():
count += 1 # UnboundLocalError!
# count = count + 1 에서 count를 지역 변수로 인식
increment()
읽기만 할 때는 불필요¶
name = "Python"
def greet():
print(f"Hello, {name}") # global 없이 읽기 가능
greet() # Hello, Python
3. nonlocal 키워드¶
감싸는 함수의 변수를 수정할 때 사용합니다.
def outer():
count = 0
def inner():
nonlocal count # 바깥 함수의 변수 사용 선언
count += 1
inner()
inner()
print(count) # 2
outer()
global vs nonlocal¶
x = "global"
def outer():
x = "outer"
def inner():
nonlocal x # outer의 x 수정
x = "inner"
inner()
print(f"outer에서: {x}") # inner
outer()
print(f"global에서: {x}") # global (변경 안됨)
4. 클로저 (Closure)¶
클로저는 자신이 정의된 환경(스코프)을 기억하는 함수입니다.
클로저의 조건¶
- 중첩 함수가 있어야 함
- 내부 함수가 외부 함수의 변수를 참조
- 외부 함수가 내부 함수를 반환
기본 예제¶
def make_multiplier(n):
"""n을 곱하는 함수를 반환"""
def multiplier(x):
return x * n # n을 기억함
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20
클로저가 기억하는 변수 확인¶
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c.__closure__) # 클로저 셀 확인
print(c.__closure__[0].cell_contents) # 0
c()
print(c.__closure__[0].cell_contents) # 1
5. 클로저 활용 패턴¶
팩토리 함수¶
def make_power(exp):
"""거듭제곱 함수 생성"""
def power(base):
return base ** exp
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(4)) # 64
상태 유지¶
def make_accumulator(initial=0):
"""누적 합계기 생성"""
total = initial
def add(value):
nonlocal total
total += value
return total
return add
acc = make_accumulator(100)
print(acc(10)) # 110
print(acc(20)) # 130
print(acc(30)) # 160
설정 저장¶
def make_logger(prefix):
"""접두어가 붙는 로거 생성"""
def log(message):
print(f"[{prefix}] {message}")
return log
error_log = make_logger("ERROR")
info_log = make_logger("INFO")
error_log("문제 발생!") # [ERROR] 문제 발생!
info_log("시작합니다") # [INFO] 시작합니다
함수 커스터마이징¶
def make_formatter(template):
"""템플릿 기반 포매터 생성"""
def format_data(**kwargs):
return template.format(**kwargs)
return format_data
user_format = make_formatter("이름: {name}, 나이: {age}")
product_format = make_formatter("{name} - {price}원")
print(user_format(name="Alice", age=30))
# 이름: Alice, 나이: 30
print(product_format(name="사과", price=1000))
# 사과 - 1000원
6. 클로저 vs 클래스¶
같은 기능을 클로저와 클래스로 구현할 수 있습니다.
클로저 버전¶
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 2
클래스 버전¶
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
c = Counter()
print(c()) # 1
print(c()) # 2
언제 무엇을 사용할까?¶
| 상황 | 권장 |
|---|---|
| 단순한 상태 유지 | 클로저 |
| 여러 메서드 필요 | 클래스 |
| 상속/확장 필요 | 클래스 |
| 함수형 스타일 | 클로저 |
| 복잡한 상태 관리 | 클래스 |
7. 클로저 주의사항¶
루프와 클로저¶
# 잘못된 예
functions = []
for i in range(3):
def f():
return i
functions.append(f)
# 모두 2를 반환! (마지막 i 값)
print([f() for f in functions]) # [2, 2, 2]
해결책 1: 기본 인자 사용¶
functions = []
for i in range(3):
def f(x=i): # 기본 인자로 값 캡처
return x
functions.append(f)
print([f() for f in functions]) # [0, 1, 2]
해결책 2: 클로저로 감싸기¶
def make_func(i):
def f():
return i
return f
functions = [make_func(i) for i in range(3)]
print([f() for f in functions]) # [0, 1, 2]
해결책 3: lambda 사용¶
functions = [lambda x=i: x for i in range(3)]
print([f() for f in functions]) # [0, 1, 2]
8. 실용적 예제¶
메모이제이션 (캐싱)¶
def memoize(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 빠르게 계산됨
디바운스 (연속 호출 제한)¶
import time
def debounce(wait):
"""지정 시간 내 연속 호출 무시"""
def decorator(func):
last_call = [0]
def wrapper(*args, **kwargs):
now = time.time()
if now - last_call[0] >= wait:
last_call[0] = now
return func(*args, **kwargs)
return wrapper
return decorator
@debounce(1.0) # 1초 내 재호출 무시
def save_data():
print("데이터 저장됨")
재시도 로직¶
import time
def retry(max_attempts=3, delay=1):
"""실패 시 재시도"""
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
print(f"재시도 {attempts}/{max_attempts}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("연결 실패")
return "성공"
속성 검증¶
def validated(validator, message):
"""값 검증 클로저"""
def getter_setter():
value = [None]
def getter():
return value[0]
def setter(new_value):
if not validator(new_value):
raise ValueError(message)
value[0] = new_value
return getter, setter
return getter_setter
# 사용
get_age, set_age = validated(
lambda x: isinstance(x, int) and 0 <= x <= 150,
"나이는 0-150 사이의 정수여야 합니다"
)()
set_age(25)
print(get_age()) # 25
# set_age(-1) # ValueError!
9. 스코프 관련 함수¶
locals()와 globals()¶
x = 10
def func():
y = 20
print(f"지역 변수: {locals()}") # {'y': 20}
print(f"전역 변수 x: {globals()['x']}") # 10
func()
vars()¶
class MyClass:
def __init__(self):
self.a = 1
self.b = 2
obj = MyClass()
print(vars(obj)) # {'a': 1, 'b': 2}
10. 요약¶
| 키워드/개념 | 설명 |
|---|---|
| LEGB | Local → Enclosing → Global → Built-in 순서로 변수 탐색 |
| global | 전역 변수 수정 시 사용 |
| nonlocal | 감싸는 함수의 변수 수정 시 사용 |
| 클로저 | 외부 함수의 환경을 기억하는 내부 함수 |
__closure__ |
클로저가 참조하는 변수 확인 |
11. 연습 문제¶
연습 1: 카운터 팩토리¶
시작값과 증가값을 설정할 수 있는 카운터 팩토리를 작성하세요.
# counter = make_counter(start=10, step=5)
# counter() → 10
# counter() → 15
# counter() → 20
연습 2: 함수 호출 기록¶
함수 호출 기록을 저장하는 클로저를 작성하세요.
# tracked_add, get_history = track_calls(add)
# tracked_add(1, 2)
# tracked_add(3, 4)
# get_history() → [(1, 2, 3), (3, 4, 7)]
연습 3: Rate Limiter¶
초당 호출 횟수를 제한하는 클로저를 작성하세요.
다음 단계¶
06_Metaclasses.md에서 클래스의 클래스, 메타클래스를 배워봅시다!