새소식

인기 검색어

개발일기

Python Decorator 선언시 호출하지 않으면 TypeError 발생했던 이유

  • -

들어가면서

사내에서 사용하는 함수 중 @redis_cache라는 함수가 있다. 함수의 FQCN과 input을 기억해놓고 캐싱하는 역할의 함수이다. 이 함수를 사용하는데, 자꾸만 TypeError가 발생했다. TypeError라고 해서 인자 타입이 잘못된 것인줄 알았는데 Python의 TypeError는 호출하려는 함수의 시그니처가 다를 경우 발생하기도 한다.

@redis_cache
def get_feeds(feed_pk: int) {
	...
}

 

Python에서는 시그니처 검사시 타입을 검사하지 않고 인수의 수만 검사한다. 동적 언어기 때문에 식별자에 담긴 타입이 언제든지 변할 수 있고, 참조값을 저장하기 때문에 타입 검사가 의미 없기 때문이다. 실제로 feed_pk에서 타입 힌팅을 int로 주고 있지만 str 타입이 들어와도 무사히 돌아간다.

 

그렇다면 위 함수에서 문제는 무엇일까? 바로 @redis_cache를 선언하기만 했지, 호출하지 않았기 때문이다. 다음과 같이 변경했더니 무사히 통과했다.

@redis_cache()
def get_feeds(feed_pk: int) {
	...
}

 

나는 Spring AOP를 사용할 때 처럼 너무 당연하게 함수를 호출하든 안 하든 동일하게 동작할 것이라 예상했는데 아니었다. 이 문제를 해결하면서 Python Decorator가 어떻게 동작하는지 알아봤고, Python Closure도 함께 공부했다.

Spring AOP과 Python Decorator

출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

 

Spring AOP는 프록시 방식의 AOP로 구현되어 있다. Spring에서 원하는 객체를 사용할 때 Bean Context에서 Bean을 꺼내다 쓰는데, 이때 Bean을 꺼낼 때 프록시가 적용된 Bean을 호출하도록 한다.

 

Python의 Decorator도 비슷하다. 위 함수에서 get_feeds()를 호출하면 get_feeds()를 바로 호출할 것 같으나, 사실은 redis_cache()의 local 함수의 내부함수를 먼저 호출하고 그 내부에서 get_feeds()를 호출하는 식으로 동작한다. (그래서 오류가 발생했을 때도 traceback에 redis_cache.<local>.wrapper() 이런 식으로 메시지가 찍혀 있었다)

 

# 출처: https://medium.com/@dmytro.ch/pythons-decorators-vs-java-s-annotations-same-thing-2b1ef12e4dc5
def cached(func):
    def wrapper(*args, **kwargs):
        global cached_item
        if func.__name__ not in cached_items:
            cached_items[func.__name__] = func(*args, **kwargs)
        return cached_items[func.__name__]
    return wrapper
    
@cached
def intensive_task():
    time.sleep(1.0)
    return 10

근데 위 코드에서는 함수 호출해서 넘기지  않았는데?

사실 사내에서 사용하는 redis_cache의 내부에는 이런 식으로 코드가 되어 있다. generate_cache_key(func, args, kwargs, ...)를 보면 func를 넘기고 있다.

def redis_cache():
    def cache_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            ...

            return cached_data

        return func_wrapper

 

자바 개발할 때는 보기 힘든 문법이다. 위에서 TypeError가 발생한 이유는 내부에 cache_decorator, func_wrapper 이런식으로 내부 함수들이 존재하기 때문이다.

 

이걸 이해하려면 일급 객체 개념과 클로저에 대해 이해해야 한다. 파이썬의 모든 것은 객체라고 여겨지는데, 만약 a = redis_cache 이런 식으로 담으면 redis_cache라는 객체를 담은 것이다. 그 객체를 호출해서 함수처럼 사용할 수 있다. 이때 호출되는 것은 내부에 선언된 __call__ 함수이다.

 

이런 식으로 __call__이 정의된 객체를 Callable Object라고 부른다. 이러한 특성은 Closoure라는 것에서 비롯된다. 자바 사용할 때 내가 이해한 클로저는, 데이터를 Stack이 아닌 Heap에 담아서 다른 Scope에서도 공유해서 사용할 수 있도록 하는 것이었다.

 

위키백과에서는 일급 객체 언어에서 lexical scope에 네이밍 바인딩을하도록 만드는 기술이라고 하는데, lexical binding이 runtime stack에서 독립적으로 만드는 기술이라고 한다

 

너무 어렵게 말한 것 같은데, 쉽게 말하자면 상태를 heap에 저장해 놓고 참조 해제 전까지는 언제든지 가져다 쓸 수 있도록 만드는 기술이라고 생각할 수 있을 것 같다

 

이 특성으로 인해 함수를 만들어만 놓고 나중에 호출할 수 있다. 위에 있던 문제가 발생했던 코드 구조를 다시 보면 이렇다

 

def redis_cache():
    def cache_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            ...

            return cached_data

        return func_wrapper

 

함수 내부에 함수가 있고, 또 함수가 있는 구조다. 만약 @redis_cache 이렇게만 선언하고 호출하지 않았으면 redis_cache(foo) 이런 식으로 넘어가게 되는 것이다. 따라서 함수를 호출해서 내부에 있는 함수를 호출해주도록 해야 한다

 

 

위 사진에서 real force!가 찍히지 않은 이유는? 저 내부에서 함수 호출을 하지 않았기 때문이다. func을 받았지만 호출하지 않았기 때문이다

마치면서

위 문제 때문에 2시간 넘게 씨름한 것 같은데, 덕분에 예전 JS할 때 경험이 없었더라면 이해하지 못해 훨씬 힘들었을 것 같다. 그래도 이번 기회에 Python Callable Object라던가, decorator 원리 같은 것들을 공부했고 동적 언어에 대해 조금 더 이해할 수 있는 계기가 되었다. 아직은 자바가 훨씬 편하지만... 내공을 빨리 쌓아서 파이써닉한 코드를 짜고 싶다!

 

참조

- https://blog.naver.com/PostView.naver?blogId=devinfo_today&logNo=220761552259&parentCategoryNo=&categoryNo=1&viewDate=&isShowPopularPosts=false&from=postView

 

Python의 Decorator에 대해 깊게 알아보기 - 작동 원리 편

 이전 포스트에서 Python의 Decorator라고 해서, 이런 게 있다~ 정도로 알아봤는데, 이번 포스트에...

blog.naver.com

- https://en.wikipedia.org/wiki/Closure_(computer_programming)

 

Closure (computer programming) - Wikipedia

From Wikipedia, the free encyclopedia Technique for creating lexically scoped first class functions Not to be confused with the programming language Clojure. In programming languages, a closure, also lexical closure or function closure, is a technique for

en.wikipedia.org

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.