[Python] Decorator

2 minute read

Published:

In this post, Python Decorator is introduced.

Decorator

Python에서 함수는 객체 이다. 그래서 함수를 다른 함수에 인자로 전달하거나 반환할 수 있다.

def hello():
    print("hello")
    
def wrapper(func):
    def new_func():
        print("before")
        func()
        print("after")
    return new_func
  
hello = wrapper(hello)

hello()
# 출력결과
before
hello
after

위 구조는 다음과 같다.

def 바깥 함수(인자 함수):
	def 안쪽 함수():
    ...
  	인자 함수()
    ...
	return 안쪽 함수()

변수 이름 = 바깥 함수(인자 함수) # decorator로 대체 가능!
변수 이름() # 안쪽 함수(인자 함수) 실행됨

이 때, 변수 이름 을 위 예처럼 인자 함수 이름과 동일하게 쓰는 경우가 흔하다. 이 경우, decorator를 쓰면 해당 줄을 대체할 수 있다.

@바깥함수
def 인자 함수():
    ...

위처럼 decorator를 이용한 코드는 아래 코드와 같다.

변수 이름(==인자 함수 이름) = 바깥 함수(인자 함수)

예시는 다음과 같다.

def log_decorator(func):

    def wrapper():
        print("function start")
        func()
        print("function end")

    return wrapper

@log_decorator
def say_hi():
    print("hi")

say_hi()

decorator를 사용하면 다음의 이점이 있다.

  • 코드의 가독성이 좋아진다.

  • 여러 decorator를 쉽게 쌓을 수 있다.

    • @auth
      @log
      @timer
      def api():
          pass
      
    • 위와 같이 코드를 적는 것이 아래와 같은 일을 하지만, 가독성 측면에서 뛰어나다.

    • api = auth(log(timer(api)))
      
  • 프레임워크 설계에 매우 중요하다.

    • Python 프레임워크 대부분이 decorator 기반이다. EX. FastAPI

    • @app.get("/users")
      def get_users():
          pass
      
    • 이건 실제로 get_users = app.get("/users")(get_users) 를 수행하는 코드이다. 위 코드의 수행 흐름을 단계별로 이해해보자.

    • from fastapi import FastAPI
          
      app = FastAPI()
          
      @app.get("/users") 
      def get_users():
          return {"users": user_list} # user_list는 현재 서버 메모리나 데이터베이스에 저장된 사용자 목록
      
      • FastAPI → 클래스, app → FastAPI 클래스의 객체(instance)

      • app.get 은 메서드이고, app.get("/users") 은 메서드 호출. 이 함수 호출의 결과로 decorator 함수가 반환됨(step 1). 그리고 get_users = decorator(get_users) 가 실행됨(step 2). 내부 구조는 아래와 같음.

      • class FastAPI:
              
            def get(self, path):
              
                def decorator(func):
                    self.router.add_route(
                        path,
                        func,
                        methods=["GET"]
                    )
                    return func
              
                return decorator
        
      • router.add_route 에서는 다음 정보를 Route 객체로 만들어 저장함. (step 2)

      • Route(
            path="/users",
            endpoint=get_users,
            methods=["GET"]
        )
        
      • 그리고 이것을 router.routes 에 추가함. (step 2)

      • router.routes = [
           Route("/users", GET, get_users),
           Route("/items", POST, create_item),
        ]
        
      • 클리이언트에서 /users endpoint로 요청이 들어오면 HTTP request → ASGI server (uvicorn) → FastAPI app → router.match(path, method) → handler 함수 실행, 즉 get_users() 가 실행된다.

      • decorator가 없었다면 다음과 같이 썼어야 했을 것이다.

      • def get_users():
            pass
              
        app.router.add_route("/users", get_users, methods=["GET"])
        
  • 메타 프로그래밍

    • decorator는 함수 정의 순간에 코드 수정 이 가능하게 한다. 그래서 logging, authentication, caching, profiling같은 기능을 함수 내부 수정 없이 추가할 수 있다.

즉, Decorator는 기존 함수나 클래스의 코드를 바꾸지 않고 기능을 추가하는 Python 문법이다. 함수/클래스를 다른 함수로 감싸서 기능을 확장하는 것이다.

decorator 아래 정의되는 인자 함수 자리에는 함수뿐 아니라 클래스도 가능하다. dataclass decorator의 예시를 보자.

@dataclass
class Shot:
    game_id: str
    event_id: int

위 코드는 실제로 Shot = dataclass(Shot) 를 수행하며 dataclass() 함수가 Shot 클래스를 받아서 수정된 클래스를 반환한다. 참고로 dataclass 함수는 위처럼 field만 정의된 class를 인자로 주었을 때, 그 class의 type annotation 정보를 읽어 (Shot.__annotations__ 에 {'name': str, 'score': int}가 저장되어 있음) 자동으로 __init__ , __repr__, __eq__ 등을 만들어 주는 함수이다.

이 외에도 자주쓰이는 decorator로 @staticmethod, @property 등이 있다.

Leave a Comment