## 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)