ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 재사용 가능한 try/finally 동작을 만들려면 contextlib와 with 문을 고려
    언어/파이썬 & 장고 2017. 1. 6. 22:27

    파이썬의 with 문은 코드를 특별한 컨텍스트에서 실행함을 나타내는 데 사용합니다. 예를 들면 with문에 상호 배제 잠금을 사용하여 잠금이 설정되어 있는 동안만 들여 쓴 코드를 실행함을 나타냅니다.

    lock = Lock()
    with lock:
        print('Lock is held')


    Lock 클래스가 with 문을 제대로 지원하는 덕분에 위의 코드는 다음의 try/finally 구문에 상응합니다.

    lock.acquire()
    try:
        print('Lock is held')
    finally:
        lock.release()


    try/finally 구문에서 반복되는 코드를 작성할 필요가 없는 with 문 버전이 더 낫습니다. 내장 모듈 contextlib를 사용하면 객체와 함수를 with 문에 사용할 수 있게 만들기가 쉽습니다. 이 모듈은 간단한 함수를 with 문에 사용할 수 있게 해주는 contextmanager 데코레이터를 포함합니다. 이 데코레이터를 이용하는 방법이 __enter__와 __exit__라는 특별한 메서드를 담은 새 클래스를 정의하는 방법(표준방법)보다 훨씬 쉽습니다.


    예를 들어 가끔씩 코드의 특정 영역에 더 많은 디버깅 로그를 넣고 싶다고 가정합니다. 여기서는 로깅 심각성 수준(severity level) 두 개로 로그를 남기는 함수를 정의합니다.

    import logging
    
    
    def my_function():
        logging.debug('Some debug data')
        logging.error('Error log hear')
        logging.debug('More debug data')
    
    # 이 프로그램의 기본 로그 수준은 WARNING. 따라서 함수를 실행하면 오류메세지만 출력
    my_function()
    
    # 결과
    # ERROR:root:Error log hear


    컨텍스트 매니저를 정의하여 이 함수의 로그 수준을 임시로 높일 수 있습니다. 이 헬퍼 함수는 with 블록에서 코드를 실행하기 전에 로그 심각성 수준을 높이고 실행 후에는 다시 낮춥니다.

    from contextlib import contextmanager
    
    @contextmanager
    def debug_logging(level):
        logger = logging.getLogger()
        old_level = logger.getEffectiveLevel()
        logger.setLevel(level)
        try:
            yield 
        finally:
            logger.setLevel(old_level)


    yield 표현식은 with 블록의 내용이 실행되는 지점입니다. with 블록에서 일어나는 모든 예외를 yield 표현식이 다시 일으키므로 헬퍼 함수로 처리할 수 있습니다.


    이제 같은 로깅 함수를 debug_logging 컨텍스트에서 호출합니다. 이번에는 with 블록 안에 있는 디버그 메시지가 모두 화면에 출력됩니다. 같은 함수를 with 블록 외부에서 실행하면 디버깅 메시지가 출력되지 않습니다.

    import logging
    from contextlib import contextmanager
    
    @contextmanager
    def debug_logging(level):
        logger = logging.getLogger()
        old_level = logger.getEffectiveLevel()
        logger.setLevel(level)
        try:
            yield
        finally:
            logger.setLevel(old_level)
    
    
    def my_function():
        logging.debug('Some debug data')
        logging.error('Error log hear')
        logging.debug('More debug data')
    
    with debug_logging(logging.DEBUG):
        print('inside: ')
        my_function()
    
    
    print('after:')
    my_function()
    
    # 결과 -- print가 아래와 같이 순서대로 안나올 수 있음
    # inside: 
    # DEBUG:root:Some debug data
    # ERROR:root:Error log hear
    # DEBUG:root:More debug data
    # after:
    # ERROR:root:Error log hear

    with 타깃 사용하기

    with문에 전달되는 컨텍스트 매니저에서 객체를 반환할 수도 있습니다. 이 객체는 복합문(compound statement)의 as 부분에 있는 지역 변수에 할당됩니다. 이 기능을 이용하면 with 블록 안에 있는 코드에서 직접 컨텍스트와 상호작용할 수 있습니다. 

    예를 들어 파일에 쓰기를 수행한 후 해당 파일을 항상 올바르게 닫음을 보장하려 한다고 합니다. 이럴 때는 with 문에 open을 전달하면 됩니다. open은 with의 as 타깃에 파일 핸들을 반환하고 with 블록이 종료할 때 핸들을 닫습니다.

    with open('my_output.txt', 'w') as handle:
        handle.write('some data')


    이 방법이 매번 수동으로 파일 핸들을 여닫는 방법보다 낫습니다. 이 방법을 이용하면 with 문에서 실행이 끝날 때 파일이 결국 닫힌다고 확신할 수 있습니다. 또한 파일 핸들이 열린 동안 실행할 코드의 양을 줄일 수 있습니다. 코드의 양을 줄이는 건 일반적으로 좋은 습관입니다.

    함수에서 as 타깃에 값을 제공할 수 있게 하려면, 컨텍스트 매니저에서 yield를 사용하여 값을 넘겨주기만 하면 됩니다. 예를 들어 다음은 Logger 인스턴스를 가져와서 심각성 수준을 설정한 후 yield로 인스턴스를 as에 전달하도록 정의한 예입니다.

    import logging
    from contextlib import contextmanager
    
    @contextmanager
    def log_level(level, name):
        logger = logging.getLogger(name)
        old_level = logger.getEffectiveLevel()
        logger.setLevel(level)
        try:
            yield logger
        finally:
            logger.setLevel(old_level)


    with 블록에서 로깅 심각성 수준을 충분히 낮게 설정했으니 as 타깃으로 debug 같은 로깅 메서드를 호출하면 출력이 나올 것입니다. 기본 프로그램 로거(logger)의 기본 로깅 심각성 수준은 WARNING이므로 logging 모듈을 직접 사용하면 아무것도 출력되지 않습니다.

    import logging
    from contextlib import contextmanager
    
    
    @contextmanager
    def log_level(level, name):
        logger = logging.getLogger(name)
        old_level = logger.getEffectiveLevel()
        logger.setLevel(level)
        try:
            yield logger
        finally:
            logger.setLevel(old_level)
    
    
    with log_level(logging.DEBUG, 'my-log') as logger:
        logger.debug('This is my message')
        logging.debug('This will not print')
    
    # 결과 -- 아래가 나온다고 하지만 제너레이터로 반환된 logger.debug를 써도 심각성 수준이 debug로 되있지 않아 나오지 않음....
    # This is my message
    
    # with 문이 종료한 후에 'my-log'라는 Logger의 디버그 로깅 메서드를 호출하면, 기본 로깅 심각성 수준으로 되돌아간 뒤라서 아무것도 출력되지 않음
    logger = logging.getLogger('my-log')
    logger.debug('Debug will not print')
    logger.error('Error will print')
    
    # 결과
    # ERROR:my-log:Error will print

    요약

    with문을 이용하면 try/finally 블록의 로직을 재사용할 수 있고, 코드를 깔끔하게 만들 수 있음

    내장모듈 contextlib의 contextmanager 데코레이터를 이용하면 직접 작성한 함수를 with문에서 쉽게 사용할 수 있음

    컨텍스트 매니저에서 넘겨준 값은 with 문의 as 부분에 할당됨. 컨텍스트 매니저에서 값을 반환하는 방법은 코드에서 특별한 컨텍스트에 직접 접근하려는 경우에 유용


    댓글