파이썬의 `contextmanager`
객체를 잠깐 바꿨다가 되돌려놓기
문제
객체를 잠깐 빌려서 설정 하나를 뒤집은 채로 헬퍼 함수를 실행하고, 함수가 어떻게 끝나든(정상 반환, 조기 반환, 예외 발생) 객체를 원래 상태 그대로 돌려놔야 할 때가 있다. try … finally로 손수 하면 되긴 하지만, 코드가 지저분해지고 빼먹기도 쉽다.
context manager
context manager 는 “준비 → 코드 실행 → 정리”를 읽기 좋은 한 블록으로 보장해준다. with 문을 쓰든 contextlib의 데코레이터 형태를 쓰든 마찬가지다.
속성 하나짜리 최소 예제
from contextlib import contextmanager
@contextmanager
def temporarily(obj, attr, new_value):
"""with 블록 안에서만 obj.attr를 new_value로 설정한다."""
old_value = getattr(obj, attr)
setattr(obj, attr, new_value)
try:
yield obj # 이제 with 블록 안의 코드가 실행된다
finally:
setattr(obj, attr, old_value) # 항상 복원
사용:
class Printer:
def __init__(self):
self.verbose = False
def show(self, msg):
if self.verbose:
print(msg.upper())
else:
print(msg)
p = Printer()
with temporarily(p, "verbose", True):
p.show("inside") # INSIDE 출력
p.show("outside") # outside 출력
여러 속성을 한 번에 바꾸기
뒤집을 설정이 하나 이상이라면 매핑을 받아 순회하면 된다:
@contextmanager
def temporarily_many(obj, **changes):
"""obj의 여러 속성을 일시적으로 설정한다."""
originals = {name: getattr(obj, name) for name in changes}
try:
for name, value in changes.items():
setattr(obj, name, value)
yield obj
finally:
for name, value in originals.items():
setattr(obj, name, value)
with temporarily_many(p, verbose=True, prefix="[DBG] "):
...
(객체에 속성이 아주 많다면, 헬퍼를 하나 더 쓰는 대신 contextlib.ExitStack으로 단일 목적 context manager 여러 개를 조합할 수 있다.)
메서드에 데코레이터를 달아 손쉽게 되돌리기
속성 조작이 객체 자신의 메서드 안쪽에 속한다면, 내부 본문을 감싸면 된다:
class Printer:
...
@contextmanager
def loud_mode(self):
yield from temporarily(self, "verbose", True)
def log_section(self, messages):
with self.loud_mode():
for m in messages:
self.show(m)
이제 log_section을 호출할 때마다, 출력 중 하나가 예외를 던지더라도 verbose가 이전 상태로 돌아오는 것이 보장된다.
클래스 기반 context manager (__enter__ / __exit__)
상태가 깊게 얽힌 경우에는 클래스 전체를 쓰고 싶을 수 있다:
class SwapAttr:
def __init__(self, obj, attr, new_value):
self.obj, self.attr, self.new = obj, attr, new_value
def __enter__(self):
self.old = getattr(self.obj, self.attr)
setattr(self.obj, self.attr, self.new)
return self.obj
def __exit__(self, exc_type, exc, tb):
setattr(self.obj, self.attr, self.old)
# False를 반환하면 예외가 그대로 전파된다
클래스 형태는 부가적인 기록 데이터를 저장하거나, 같은 매니저 인스턴스를 여러 with 블록에서 공유할 수 있게 해준다.