Context Manager - 컨텍스트 관리자

Context Manager란?

가장 흔하게 사용되는 컨텍스트 관리자는 파일 입출력에 사용되는 "with open()" 구문이다. 

with open("file.txt", "r") as f:
    f.read()

위 구문은 처음에 파일을 열고, 구문이 종료될 때 파일을 닫는다. 따라서 내부적으로 open과 close를 모두 수행하고 있는 것이다. 즉, 컨텍스트 관리자(context manager)특정 작업의 시작과 끝에 정해진 행동을 수행할 수 있도록 한다. 마치 함수의 데코레이터(decorator)와 비슷한 형태이다. 


객체 활용

class Context(object):
    def __enter__(self):
        # 사전 작업
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        # 사후 작업

__enter__컨텍스트의 시작에서 수행할 작업을, __exit__컨텍스트가 종료될 때 수행되는 코드를 담고 있다. 주의할 점은 __exit__가 내부적으로 발생한 예외를 파라미터로 받아온다는 것이다. 이렇게 만들어진 객체는 아래와 같이 사용된다. 

with Context() as context:
    # 필요한 처리
    context  # __enter__의 return 값

as 뒤에 선언된 변수는 __enter__ 메서드의 반환값을 받는다. 위 코드의 경우, self를 반환함으로써 Context 객체를 받을 수 있었다. 

주의할 점은 __exit__ 메서드가 True를 반환하도록 하면 예외가 발생해도 암묵적으로 넘어간다는 것이다. 아래 예시를 보면 이해할 수 있다. 

class Context(object):
    def __enter__(self):
        print("-- 시작 --")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("-- 종료 --")
        return True


with Context() as context:
    raise TypeError
-- 시작 --
-- 종료 --

일반적으로는 TypeError가 발생해야 정상이지만 True를 반환함으로써 예외를 발생시키지 않고 넘어가게 된다. 


객체 활용 안 하는 경우

위에서 봤던 with문을 사용하는 문법의 경우 as를 통해 객체에 대한 정보를 받아올 수 있었다. 하지만 만약 이러한 반환값이 필요하지 않다면 아래와 같이 ContextDecorator를 활용해 선언하는 방법도 있다. 

import contextlib

class Context(contextlib.ContextDecorator):
    def __enter__(self):
        ...

    def __exit__(self, exc_type, exc_value, traceback):
        ....

위에서 봤던 코드와 같이 __enter____exit__을 선언하면 된다. 대신 ContextDecorator 객체를 상속받아야 한다. 그리고 사용법에도 차이가 있다. 

@Context
def test():
    pass

with문을 사용하지 않고 데코레이터를 활용해 구현할 수 있다. with문을 활용할 때보다 코드가 간결해지는 장점이 있지만 객체로부터 반환값을 받아올 수는 없다. 


제너레이터 활용

앞에서는 __enter__와 __exit__ 메서드를 포함한 객체를 활용했다면 함수를 이용해 컨텍스트 매니저를 구현하는 방법도 있다. yield를 활용한 제너레이터가 실행된 라인 정보를 기억한다는 점을 활용한 것이다. 

import contextlib

@contextlib.contextmanager
def context():
    # 사전 작업: __enter__과 같은 역할
    yield 반환값
    # 사후 작업: __exit__과 같은 역할

yield 키워드를 중심으로 앞에 선언된 코드는 __enter__과 같이 먼저 실행되고, yield 후에 선언된 코드는 __exit__과 같이 작업이 모두 끝난 후에 실행된다. 만들어진 제너레이터는 with 구문을 통해 활용된다. 

with context() as 반환값:
    # 필요한 작업

__enter__메서드의 반환값을 as 뒤 변수를 통해 받던 것과 같이 yield를 통해 반환되는 값은 as 뒤 변수에 할당된다.

이러한 방식은 기존의 데코레이터와 같이 함수의 재사용을 용이하게 해준다.