Computer Engineering/Fluent Python 정리

Fluent Python Chapter 15. Context manager와 else 블록

jordan.bae 2022. 3. 6. 17:21

Introduction

15장에서는 주로 Context manager에 대해서 설명합니다. Context manager가 어떻게 도입되게 되었고, 어떤 프로토콜을 구현해야 하는지등을 설명합니다. 단순히 Resource관리를 위해서 사용하는 것이 아닌 다양한 Context를 유지하는데 사용할 수 있는 멋있는 기능입니다. 또, else 블록을 끼워서? 설명합니다. else 블록은 if문 외에도 for, while, try 블록에서도 사용할 수 있는데 이런 부분에 대해서 저자의 생각과 어떨 때 사용하면 좋은지 설명합니다.

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

Fluent Python Chapter 13. 연산자 오버로딩(feat. 제대로 하기)

Fluent Python Chapter 14. 반복형, 반복자, 제너레이터

 

 

 

if 문 이외에서의 else 블록

else 절은 if 문 이외에도 for, while, try 문에서도 사용할 수 있습니다. 필자는 if 문 이외에 사용하는 것이 keyword의 의미상 좋지 않은 선택인 것 같다는 의견을 제시합니다. 그 이유는 for, while, try문에서도 보면 main block 정상 적인 수행(break문으로 중간에 멈추지 않고, try의 경우 예외가 발생하지 않을 때) 후에 실행되는데 then같은 키워드가 의미가 더 잘 맞지 않는다고 생각하기 때문이라고 합니다. 하지만 귀도 반 로섬은 키워드 추가를 끔찍이 싫어하기 때문에 else keyword를 확장해서 사용한 것 같습니다.

 

정상적인 수행에 대한 코드를 for loop의 경우로 예로 들면 아래와 같습니다.

# for else

for i in range(2):
    print(i)
else:
    print("finish for loop without break")

0
1
finish for loop without break

# for else

for i in range(2):
    print(i)
    break
else:
    print("finish for loop without break")

0​

 

for

for loop가 완전히 실행된 후에(break 문으로 중간에 멈추지 않고) else 블록이 실해된다.
fruits = ["apple", "grape"]

# else를 사용하지 않았을 때
is_exist_banana = None
for fruit in fruits:
    if fruit == "banana":
        is_exist_banana = True
        break
if not is_exist_banana:
    raise ValueError('No banana flavor found!')
    
# else 사용

for fruit in fruits:
    if fruit == "banana":
        break
else:
    raise ValueError('No banana flavor found!')

for loop를 정상적으로 빠져나왔을 경우, break이 발생하지 않은 여부를 체크하여 핸들링 할 때 코드의 가독성과 간결성을 높여줄 수 있다.

 

 

try

try 블록에서 예외가 발생하지 않을 때만 else블록이 실행된다. else블록의 예외는 except블록에서 처리되지 않는다.

책의 예제를 가져와보면 dangeour_call()이라는 함수가 정상적으로(예외 처리가 되지않는 경우) 수행된 경우만 after_call()이라는 함수를 실행시키려고 할 때 else구문을 사용하면 유용하다. else를 사용하지 않으면 이를 정확하게 할 수 없다(변수를 따로 만들어서 체크하지 않는한). 

# else문을 사용하지 않으면 is_dangerous_call_success 같은 변수를 사용해서 구현하거나 or
# try block에 넣어야 하는데 예기치 않게 after_call()의 exception이 catch될 수 있다.
try:
    dangerous_call()
except OSError:
    print("OSError")
else:
    after_call()

 

while

조건식이 거짓이 되어 while 루프를 빠져나온 후에 (break 문으로 중간에 멈추지 않고) else 블록이 실행된다.

 

 

EAFP vs LBYL

위에 try/except같은 코드 스타일을 설명하면서 파이썬은 일반적인 제어 흐름에서 EAFP(Easier to Ask for Forgiveness than Permission)을 설명한다. 허락을 구하기보다 용서를 구하는 것이 더 쉽다. Duck typing과 너무 잘 어울린다. 일단 믿고 실행을 시킨다. 가끔 발생하는 예외를 처리하기 위해서 타입을 모두 체크하거나 타입이 다르다고 꽥꽥 울지 못할거라고 의심하지 않는다.

 

반대로 C등 다른 언어에서는 LBYL(Leap Before You leap)=누울 자리를 보고 다리를 뺃으라 이라는 코드 스타일을 사용한다.  호출이나 조회를 하기 전에 전체 조건을 명시적으로 검사한다.

 

이 부분은 책에서 잠깐 다루지만 그냥 너무 명명을 잘해서 정리해봤다.

 

 

Context manager와 with 블록

with문은 try/finally 패턴 어떤 context를 유지하고 해당 context 종료 후에 context를 rollback하기 위해서 해야 하는 작업(리소스 해제, monkey patch 복구, 변경된 상태 복원 등)을 단순화하기 위해 설계됐다.

 

위에 정의를 기반으로 보면 context manager 프로토콜을 쉽게 이해 할 수 있다. context manager의 프로토콜은 아래 두 가지이다.

- __enter__() : with block이 시작될 때 호출 (어떤 컨텍스트를 만들기 위한 초기화 작업이 들어갈 것 이다.)

- __exit__(): with block이 끝날 때 호출(주로 __enter__()에서 설정한 context를 복원하는 동작)

 

 

책에서 소개한 예제 클래스 코드를 한 번 보면 이해가 쉽다.

class LookingGlass:

    def __enter__(self):  # <1>
        import sys
        self.original_write = sys.stdout.write  # <2>
        sys.stdout.write = self.reverse_write  # <3>
        return 'JABBERWOCKY'  # <4>

    def reverse_write(self, text):  # <5>
        self.original_write(text[::-1])

    def __exit__(self, exc_type, exc_value, traceback):  # <6>
        import sys  # <7>
        sys.stdout.write = self.original_write  # <8>
        if exc_type is ZeroDivisionError:  # <9>
            print('Please DO NOT divide by zero!')
            return True  # <10>

with LookingGlass() as what:
    print("i'm in the reverse world")
    print(what)

print(what)
print("i came back in the normal world")

dlrow esrever eht ni m'i
YKCOWREBBAJ
JABBERWOCKY
i came back in the normal world

위에 코드를 보면 __enter__ 매직 메서드에서 sys.stdout.write()를 month patch해서 가칭 reverse world를 만들었다.  with문안에서 출력이 거꾸로 되는 것을 확인할 수 있다. __exit__ 매직메서드에서는 이 부분을 다시 되돌려 놓았기 때문에 with block박에서는 정상 출력된다. what 을 print한 이유는 what은 __enter__() 매직메서드의 return값이라는 설명하기 위해서다. 그렇기 때문에 as는 사용하지 않을 수도 있다. (return None을 할 수도 있음 필요에 따라.)

 

파이썬 인터프리터는 __exit__() 메서드를 호출 할 때 3개 인수를 전달하는데 아래와 같다.

- exc_type: context안에서 예외가 발생한 경우 예외 클래스.

- exc_valu: 예외 객체(예외 메시지 등 exception()생성자에 전달된 인수는 exe_value.args속성을 이용해 접근할 수 있음.)

- traceback: traceback 객체

 

 

 

Contextlib 유틸리티와 @contextmanager

파이썬에서는 with context manager를 만들기위한 표준 라이브러리로 contextlib를 지원한다.

- closing()

- suppress

- @contextmanager

- ContextDecorator

- ExitStack

 

위와 같은 유틸리티들이 제공된다. 이 중에서 @contextmanager decorator를 사용해서 __enter__(), __exit__()을 구현하지 않은 함수를 context manager로 만드는 방법을 살펴본다.

 

@contextmanager decorator는 일반 함수에서 __enter__()메서드가 반환할 것을 생성하는 yield문 하나를 가진 generator를 구현하면 context manager로 만들어준다.

import contextlib


@contextlib.contextmanager  # <1>
def looking_glass():
    import sys
    original_write = sys.stdout.write  # <2>

    def reverse_write(text):  # <3>
        original_write(text[::-1])

    sys.stdout.write = reverse_write  # <4>
    yield 'JABBERWOCKY'  # <5>
    sys.stdout.write = original_write  # <6>

yield문 전까지가 __enter__()메서드에서 실행되는 코드이고, yield가 __enter__()메서드가 return하는 객체를 생성한다. 그리고 yield문 이하가 __exit__() 메서드의 역할을 하게 된다.

위의 코드는 context manger안에서 예외가 발생한 경우 복원(__exit__()에 해당하는 부분)을 하지 못하고 종료될 수 있고, 이는 문제를 발생시킬 수 있다. 아래와 같이 구현하면 위의 부분을 해결할 수 있다.

 

@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write

    def reverse_write(text):
        original_write(text[::-1])

    sys.stdout.write = reverse_write
    msg = ''  # <1>
    try:
        yield 'JABBERWOCKY'
    except ZeroDivisionError:  # <2>
        msg = 'Please DO NOT divide by zero!'
    finally:
        sys.stdout.write = original_write  # <3>
        if msg:
            print(msg)  # <4>

context manager의 사용자가 안에서 어떤 코드를 실행시킬지 알 수 없기 때문에 try/finally 구문을 사용해야 한다.

 

 

마무리

with문은 단순히 리소스 해제 하기 위한 keyword가 아니라, 어떤 컨텍스트를 준비하고 마무리 하는 작업을 안전하게 수행할 수 있는 기능이다. 즉, 준비/마무리 작업을 인수 분해하는 도구라는 것이다. 이 표현을 '파이썬의 멋진 이유' 라는 발표에서 레이몬드 헤팅거라는 분이 하셨다고 한다. 시간을 내서 꼭 한 번 봐야겠다는 생각이든다.

 

다른 분들도 꼭 한 번 봤으면 한다.

https://www.youtube.com/watch?v=oLHmoy9bKCY 

 

 

Reference

- https://github.com/fluentpython/example-code/tree/master/15-context-mngr

 

GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015) - GitHub - fluentpython/example-code: Example code for the book Fluent Python, 1st Edition (O'Reilly, 2015)

github.com

- Fluent Python Chapter 15

반응형