Data science/Python

python with 구문 & context manager

연금(Pension)술사 2024. 6. 19. 00:15

 

if else구문이 아닌, 구문에서의 else의 사용

1. for - else 구문:

for loop가 정상적으로 완료되면 else 구문이 실행됩니다. break으로 중단되는 경우 else 블록은 실행되지 않습니다.

numbers = [1, 2, 3, 4, 5]

for number in numbers:
    if number == 3:
        print("Found 3!")
        break
else:
    print("3 is not in the list.")

 

2. try-except-else:

try-except에서 예외가 발생하지 않을 때, else 블록이 실행됩니다.

이거 왜 쓰냐는 질문이 종종있는데요. 아래의 예시를 들어보겠습니다. 아래의 try-except구문에서 FileNoteFoundError을 open()함수에서 발생할 수 있다는 것을 경험적으로 알 수 있습니다. 

try:
    file = open('example.txt', 'r')
    content = file.read()
    print(content)
    file.close()
except FileNotFoundError:
    print("File not found.")

이는 open()에서 자주 예외가 발생할 수 있는데, 만일 open이 아니라 my_fun1(), my_fun2()라면 어느구문에서 해당예외가 발생한다고 예측할 수 있을까요? 즉, 실제로 예외가 발생하지 않는 경우에도 try 블록에 많은 코드가 포함되어있어서 디버깅이 어렵습니다. try-except구문에서는 예외가 발생할 수 있는 함수의 호출을 최소한의 블록으로 담아야 더 명시적입니다. 

try:
    my_func1()
    my_func2()
except FileNotFoundError:
    print("file not found)

 

따라서, 이를 처리하기위해 아래와 같이 try-except-else 구문으로 처리해볼 수 있습니다. 

try:
    file = open('example.txt', 'r')  #open()에 대해서만 예외처리
except FileNotFoundError:
    print("File not found.")
else:
    content = file.read()
    print(content)
    file.close()

이는 EAFP(Easier to ask for forgiveness than permission, 허락보다 용서가 쉽다) 코딩스타일로 `if os.path.exist()`등으로 처리가 가능하나, 파이썬에서는 EAFP스타일을 사용을 많이합니다.

 

with 구문

with 구문은 크게 2가지로 구성됩니다.

  1. `__enter__`: with 구문의 실행 (진입)
  2. `__exit__`: with 구문의 종료. + with 구문에 끝에 finally 절의 역할이 수행됩니다.

 

with 구문은 컨텍스트 관리자 객체를 사용하여, 코드블록의 진입종료 시에 특별한 동작을 합니다. 컨텍스트 관리자 객체는 __enter__와 __exit__ 메서드를 구현하여, 코드 블록 진입 시에 __enter__ 메서드를, 종료 시에 __exit__ 메서드를 호출하게 합니다. 즉, 컨텍스트 관리자를 이용하면, 자원 해제, 예외 처리 등을 신뢰성 있게 관리할 수 있습니다.

아래의 예시를 하나 들어보겠습니다. 아래의 예시에서는 파일 객체를 반환하고,

  1. with구문으로 실행될때 (__enter__)가 실행되고,
  2. with 구문이 종료되면 (__exit__)이 종료되는 것 과 같습니다. 
with open('example.txt', 'r') as file:
    content = file.read()
    print(content)

실제로 `__enter__`, `__exit__`이 있는지 살펴보겠습니다.

In [1]: # open 함수로 반환된 파일 객체의 __enter__와 __exit__ 메서드 확인
   ...: with open('example.txt', 'r') as file:
   ...:     print(hasattr(file, '__enter__'))  # True 출력
   ...:     print(hasattr(file, '__exit__'))   # True 출력
   ...:     content = file.read()
   ...:     print(content)
   ...:
True
True

 

또 다른 예시로는 사용자 정의 컨테스트를 만들 수 있습니다. 이 때도, 마찬가지로 `__enter__`, `__exit__`의 메직메서드의 구현이 되어있어야합니다.

class Resource:
    def __enter__(self):
        print('Resource allocated')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('Resource released')

# 컨텍스트 관리자 사용
with Resource() as resource:
    print('Using the resource')

Resource allocated  # with 구문이 실행될 때
Using the resource  # print구문
Resource released   # with 구문의 종료

 

sqlite3나 mysqlDB등을 이용할때의 db connection을 명시적으로 종료하지않고 with구문으로 컨텍스트매니저로 사용할 수 있습니다.

import sqlite3

with sqlite3.connect('example.db') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    results = cursor.fetchall()
    print(results)

 

 

contextlib: python 표준라이브러리

1. @contextmanager: 이 데코레이터는 컨텍스트 메니저를 생성할때 코드를 줄여줍니다.

@contextmanager 데코레이터는 컨텍스트 관리자를 생성할 때 사용됩니다. 이 데코레이터를 사용하면 __enter__와 __exit__ 메서드를 명시적으로 구현하지 않고도 컨텍스트 관리자를 만들 수 있습니다. 대신, 함수 내부에서 yield를 사용하여 진입 및 종료 시 실행할 코드를 정의합니다.

아래와 같은 예시를 들어보겠습니다. 

from contextlib import contextmanager
import time

@contextmanager
def timer():
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print(f'Elapsed time: {end - start} seconds')

# 사용 예시
with timer():   # start = time.time()이 서브루틴에서 할당됨. yield로 메인루틴으로 전환
    time.sleep(2)  # yield 이후 메인루틴의 실행, with 구문종료시 서브루틴의 finally 구문으로 전환

 

또 다른 예시로,DB의  FastAPI의 contextmanager을 디팬던시로 사용하는 경우입니다. 

FastAPI에서 contextmanager역할로 `__enter__`, `__exit__`메서드를 구현했지만, contextlib 내의 contextmangager 데코레이터를 이용하면 그럴필요는 없습니다.

 

아래와 같이 구현이 가능합니다.

from contextlib import contextmanager

@contextmanager
def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

# FastAPI dependency 사용 예시
@app.get("/items/")
async def read_items(db: DBSession = Depends(get_db)):
    # 데이터베이스 작업
    items = db.execute("SELECT * FROM items").fetchall()
    return items
반응형