## Context manager 파이썬의 컨텍스트 매니저는 초기화 작업과 정리 작업을 반드시 해야 하는 인스턴스를 쉽게 다루기 위해 추가된 기능이다. 일반적으로는 `with` 문으로 더 많이 알려져 있다. ```python with open('hello_world.txt', 'w') as file: file.write("Hello, world!") ``` ### 작동 과정 해당 기능을 사용하게 되면 indented된 코드 블럭이 실행되기 전에 `__enter__` 함수가 호출되어 초기화 작업이 이뤄지고, 코드 블럭이 실행된 후 `__exit__` 함수가 호출되어 정리 작업이 이뤄진다. 즉, 아래와 같다. | # | 작동 | | --- | ------------------------------------------------------------------------------------------------------------------------------- | | 0 | `open('hello_world.txt', 'w')`가 File 인스턴스 반환 | | 1 | File 인스턴스의 `__enter__` 함수 호출, 반환값으로 `file` 변수 **초기화** | | 2 | Indented 코드 블럭 실행(`file.write("Hello, world!")`) | | 3 | `file.__exit__` 함수 호출하여 변수 **정리**<br><br>(Indented 코드 블럭에서 예외 발생 시, 예외 정보를 `exc_type`, `exc_value`, `traceback`에 담아서 인자로 같이 넘김) | 사실 여기까지만 보면 컨텍스트 매니저의 의의는 '**초기화 함수와 정리 함수를 자동으로 호출하니 참 편하다!**' 정도다. ## Context manager techiniques 하지만 컨텍스트 매니저의 숨겨진 의의가 하나 더 있다. 그건 바로 Composition 관계를 이루는 클래스 쌍을 편하게 다룰 수 있다는 점이다. 정확하게는 해당 클래스 쌍의 초기화 코드를 유지 보수하기 편하게 작성할 수 있다는 점이다. ### 예시 코드 : Book & Page 아래의 상황을 가정하자. ![[Pasted image 20240719091738.png]] [^1] `Book`과 `Page` 클래스는 대략 아래와 같은 구조로 이루어져 있을 것이다. ```python class Page: def __init__(self, content): self.content = content #... class Book: def __init__(self): self.pages = [] def add_page(self, page): self.pages.append(page) #... ``` 10개의 `Page`를 추가하는 전형적인 코드는 아래와 같다. ```python book = Book() for i in range(10): book.add_page( Page(f"page : {i}") ) ``` 위 코드에서 문제점은 Book 인스턴스 메서드를 일일이 호출하고 있다는 점이다. 이런 방식은 기존 메서드를 다른 메서드로 교체해야 하는 경우가 생겼을 때 문제가 된다. 모든 파일을 돌아다니며 일일이 코드를 고쳐야 하기 때문이다. ### 예시 코드 개선 : Book & Page 이 문제점의 해결 방안 중 하나가 바로 컨텍스트 매니저를 이용하는 것이다. 일단 먼저 `Book.__enter__` 메서드와 `Book.__exit__` 메서드를 추가한다. ```python #... class Book: #... def __enter__(self): global _BOOK _BOOK = self return self def __exit__(self, *args): global _BOOK _BOOK = None #... ``` 추가한 `Book.__enter__`에서는 `_BOOK`라는 전역 변수에 인스턴스 자신을 할당한 다음 자기자신을 반환하고, `Book.__exit__`에서는 `_BOOK`를 `None`으로 초기화한다. `*args`는 예외 처리를 위한 변수들을 인자로 받기 위해 존재한다. 다음으로 `Page.__init__` 메서드를 수정하자. ```python #... class Page: def __init__(self, content): self.content = content if _BOOK: _BOOK.add_page(self) #... ``` 수정한 `Page.__init__`에서는 `_BOOK`라는 전역 변수가 `None`이 아니면 `_BOOK.add_page`를 호출하여 자기 자신을 추가한다. 이제 `Page` 인스턴스 생성 시 자동으로 `_BOOK`에 추가가 되는 것이다. 이제 아래 코드를 실행하여 정상적으로 작동하는지 확인하자. ```python with Book() as book: for i in range(10): Page(f"page : {i}") for page in book.pages: print(page.content) print(f"_BOOK : {_BOOK}") ``` ``` # 실행 결과 page : 0 page : 1 page : 2 page : 3 page : 4 page : 5 page : 6 page : 7 page : 8 page : 9 _BOOK : None ``` 코드가 정상적으로 작동한다! ### 전체 코드 ```python class Page:     def __init__(self, content):         self.content = content     def __init__(self, content):         self.content = content         if _BOOK:             _BOOK.add_page(self) class Book:     def __init__(self):         self.pages = []             def add_page(self, page):         self.pages.append(page)             def __enter__(self):         global _BOOK         _BOOK = self         return self     def __exit__(self, *args):         global _BOOK         _BOOK = None with Book() as book:     for i in range(10):         Page(f"page : {i}") for page in book.pages:     print(page.content) print(f"_BOOK : {_BOOK}") ``` ## 활용 사례 - [[1. DAG 기본 구조 익히기#Operator 설정|Airflow DAG 설정]] - Airflow에서 DAG 설정할 때 따로 추가적인 설정 없이 Operator가 DAG에 등록이 되는 이유가 바로 위 테크닉을 활용했기 때문이다. [^1]:[이미지 출처](https://miro.medium.com/v2/resize:fit:1400/1*D7TuM5p9Dl-tEELM3HnySQ.png)