Effective Python Item 66. 재사용 가능한 try/finally 동작을 원한다면 contextlib과 with문을 사용하라.

    들어가기 전

     


    요약

    • contextlib.contextmanager 데코레이터를 이용하면 __enter__, __exit__ 구현없이 with 절에서 사용할 수 있음. 
    • with 절은 with절 블록 안과 밖을 격리시키는 역할을 함. 다음 의미를 가짐.
      1. With절 안쪽은 특별한 Context를 가지고 실행됨을 의미함.
      2. With절 안 / 밖은 서로 다른 Context에서 실행됨. 
    • contextmanager 데코레이터를 사용한다면, try ~ finally와 yield를 함께 사용해줘야 함. 
    • contextmanager를 사용하면 많은 코드가 절약됨.
      • try ~ finally를 반복할 필요가 없음. (lock을 쓸 때)
      • 실수해서 코드 마무리 작업을 까먹지 않음.
      • 재사용성이 좋음. 
    • With절을 이용하면 부가적인 것은 컨텍스트 매니저에서 처리하도록 하고, 개발자는 비즈니스 로직에 집중할 수 있음. 

     


    Item 66. 재사용 가능한 try/finally 동작을 원한다면 contextlib과 with문을 사용하라. 

    파이썬에서 with절에서 직접적으로 사용되고 싶은 경우, __enter__(), __exit__() 메서드를 구현한 클래스를 작성해야한다. 그러나 굳이 클래스를 사용하고 싶지 않은 경우가 있다. 이런 경우에는 contextlib이 제공하는 contextmanager 데코레이터를 사용하면 클래스 구현 없이 with 절을 사용할 수 있다.

    @contextlib.contextmanager
    def get_random_number():
        try:
            yield random.randint(1, 100)
        finally:
            print('end context')
    
    
    with get_random_number() as num:
        print(num)

    contextmanager 데코레이터를 이용해 with 절에서 사용되려면 위와 같이 구현하면 된다. 

    1. contextmanager 데코레이터를 명시한다. 
    2. try ~ finally / yield로 코드를 작성한다.
    3. with 절로 진입했을 때, yield까지 수행된다.
    4. with 절이 끝날 때, finally 절이 수행된다. 

    이처럼 contextmanager를 이용하면 with 절에서 손쉽게 사용할 수 있는 코드를 얻게 된다. 그렇다면 with절을 이용한다는 것은 개발자들에게는 어떤 장점을 줄 수 있을까?

     


    With절의 안/밖의 Context 분리(격리)

    With 절의 내부의 코드 블록은 독립적인 Context에서 실행된다는 것을 의미할 수 있다. 

    import logging
    import contextlib
    
    logging.basicConfig()
    
    @contextlib.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(f'대상: {logger.name}!')
    
    logger = logging.getLogger('my-log')
    logger.debug('디버그 메세지는 출력되지 않습니다.')
    logger.error('오류 메세지는 출력됩니다.')

    위 코드가 With 절 내부가 격리된 Context에서 실행된다는 것을 보여준다. 

    1. with절 내부에 log_level() 메서드가 호출되는 것을 볼 수 있다. log_level이 바뀌고, logger를 통해서 조절할 수 있음을 유추할 수 있음.
    2. with절 바깥에서는 with절 내부의 Context가 유지되지 않기 때문에 원리 logger 레벨로 원복된다. 

    코드만 읽을 때는 갸우뚱 할 수 있지만, 코드를 실행해보면 결과는 더 명확해진다.

    DEBUG:my-log:대상: my-log!
    ERROR:my-log:오류 메세지는 출력됩니다.

    With절 내부에서 찍은 디버그 레벨 로깅은 잘 찍히지만, With절 바깥에서 찍은 경우에는 그렇지 않음을 알 수 있다. 코드 / 출력 결과가 모두 'With절 내부는 독립적인 Context로 동작한다'를 잘 나타낸다. 

     


    try ~ finally 및 잊기 쉬운 자원 정리 코드 단축. 비즈니스 로직만 분리

    lock.acquire()
    try:
        # Lock 획득 후 실행할 코드.
        print(1)
    finally:
        lock.release()

    만약 내가 lock을 사용하는 경우라면 위 코드가 사용되는 부분이 많아질 것이다. 그러나 이런 코드를 사용하는 부분이 많아질수록 전체적으로 위험한 코드가 된다. 위 코드는 아래 단점들이 있기 때문이다.

    1. 사용자가 lock.release()를 빼먹거나 하는 치명적인 실수를 할 수도 있다.
    2. try ~ finally가 계속 사용될 것이기 때문에 코드에 잡음이 있다. 특히 If, For문과 함께 사용되어 Depth가 깊어질수록 그렇다. 

    실수하기도 쉽고, 읽기도 어려운 코드가 된다는 것이다. contextmanager는 이런 단점을 보완해준다.

    # 컨텍스트 매니저를 사용하게 되면 위 단점이 모두 극복됨. 
    import contextlib
    
    @contextlib.contextmanager
    def get_lock():
        lock.acquire()
        try:
            yield 
        finally:
            lock.release()
            
    with get_lock():
        print(1)
        
    with get_lock():
        print(2)

    컨텍스트 매니저를 이용해서 위와 같이 코드를 리팩토링 할 수 있다. 

    1. try ~ finally는 한 곳에만 작성되어 있다. try ~ finally가 반복 사용되지 않도록 함.
    2. lock.release() 같은 로직도 with절에서 내부적으로 처리됨. 

    락을 획득하고 반환하는 부가적인 작업(그러나 치명적인 작업)은 With절이 내부적으로 처리하도록 한다. 그리고 개발자는 With절 블록에 비즈니스 로직만 작성하면 된다. 코드는 더 읽기 쉬워지고, 어떤 로직을 처리하는지도 좀 더 명확해진다. 

    댓글

    Designed by JB FACTORY