decorator

teal·2023년 8월 10일
0

Python

목록 보기
3/8

python을 통해서 프로그래밍을 하다보면 수많은 데코레이터를 사용하게 된다.

간단한 예를 들면
django에서의

@login_required

flask에서는

@app.route('/')

fast api에서는

@app.post("/")

등 웹프레임워크에서도 많이 사용하고 그냥 파이썬으로도

@staticmethod
@classmethod
@property
@functools.wraps(f)

등 프로그래밍을 할 때나 다른 사람들이 짠 코드를 볼때 수없이 많은 데코레이터를 마주치게 된다.

이런 데코레이터는 왜 사용하는 걸까?

  1. 모듈화
    • 딱 생긴것만 봐도 모듈화를 통해 코드의 중복 작성없이 재사용하기 좋아보인다. 또한 가독성도 증진된다(데코레이터의 이름을 잘지어야 하는 이유)
  2. 로직 분리
    • 실제 비즈니스 로직과 인증, 로깅, 캐싱과 같은 로직을 분리시켜 개발할 수 있다.

모듈화와 로직 분리를 두고 데코레이터를 생각해보자.

@login_required를 두고 생각하면 django에서 view를 작성할때 별도로 유저가 로그인한 상태인지를 판단해서 로그인페이지로 리다이렉트시키는 코드를 작성할 필요가 없어진다. 그리고 남이 잘 만들어둔 데코레이터를 통해서 로그인이 필요한 여러 view에서 decorator하나만 붙이면 로그인 체크가 가능해지는 것이다.

대부분의 경우 이미 만들어진 데코레이터를 사용하겠지만 만들어 사용할 수도 있다.

간단하게 피보나치수열을 계산하는 경우를 생각해보자. 피보나치 수열을 계산하는 여러 방법이 있겠지만 가장 무식하게 만들어 보자.

def fib(n) -> int:
    if n < 3:
        return 1
    return fib(n-2) + fib(n-1)
print(fib(100))

엄청나게 무식한 코드가 완성되었다. 이 경우는 O(2^N)의 엄청난 시간 복잡도를 가진 코드이다. 이 코드의 시간복잡도를 줄여보자

cache: Dict[int, int] = {}
def fib(n: int) -> int:
    if n < 3:
        return 1

    if cache.get(n):
        return cache[n]

    cache[n] = fib(n-2) + fib(n-1)
    return cache[n]
print(fib(100))

메모이제이션 기법을 통해서 시간복잡도를 O(N)까지 줄일수 있었다.
그런데 만약 이런 기법을 다른 함수에도 쓰고 그러면 이제 cache의 이름을 어떻게 지어야하나 고민이 시작된다(fib_cache ?) 그리고 전역 변수의 남발 + 사람의 실수로 인해 저 캐시 딕셔너리를 다른 함수에서 사용해서 엄청난 오류가 발생할 가능성이 생기게 된다.

이를 데코레이터를 통해 해결할 수 있다.

def cache(f: Callable) -> Callable:
    cache = {} # 이 경우는 Dict[Any, Any]로 해야하나?

    def inner(*arg, **kwargs):
        n = arg[0]

        if cache.get(n):
            return cache[n]

        cache[n] = f(*arg, **kwargs)
        return cache[n]
    return inner


@cache
def fib(n: int) -> int:
    if n < 3:
        return 1
    return fib(n-2) + fib(n-1)

print(fib(100))

데코레이터 함수로 함수의 인자 첫번째를 통해 해당 값이 저장된 적이 있으면 곧바로 해당값을 반환해주는 함수를 만들었다. 이를 통해 cache 딕셔너리가 전역 변수로 선언될 필요도 없어지고 외부에서 참조도 힘든 상태가 되어 안전해진다.

그런데 잘 생각해보면 데코레이터를 붙이는 것은

fib = cache(fib)

위와 마찬가지인 행동이 아닌가?
그리고 cache 데코레이터 함수는 반환값이 Callable 즉 함수인 상황이다.
cache는 inner 함수를 반환하게되고 결국 fib는 inner 함수가 된다.

실제로 함수 이름을 출력하면

print("fuction name", fib.__name__)

fuction name inner

위와 같이 나온다.

그러면 대체 cache 함수에서 선언된 이 cache 딕셔너리는 대체 어디에 남아있길래 사용될 수 있는걸까?

del cache
print("del atfer", fib(100))

del atfer 354224848179261915075

심지어 위처럼 cache함수를 삭제해서 함수 네임스페이스에서 삭제하고 어떤 객체와도 연결되지 않은 unbound로 만들어도 동작한다. 대체 cache 딕셔너리는 뭘까?

여기서는 이제 클로저(closure)라는 개념이 나오게 된다. 아니 클로저는 함수형 프로그래밍 이야기를 들을때나 나오는 것이 아니었나? 다음편에서는 클로저에 관련해서 이야기를 해보겠다.

profile
고양이를 키우는 백엔드 개발자

0개의 댓글