파이썬 데코레이터

파이썬 데코레이터

개요

파이썬 데코레이터에 대한 간단한 설명과 예제

Udacity 에서 Designing RESTful APIs 강의를 듣다가

@app.route('/token')
@auth.login_required
def get_auth_token():
    token = g.user.generate_auth_token()
    return jsonify({'token': token.decode('ascii')})

이런 코드를 보게 됐다. route 의 경우, flask 에서는 이렇게 사용하는구나 하고 사실 아무생각없이 사용하고 있었다. 그런데 @auth.login_required 는 좀 낯설어서 뭔지 찾아보니 decorator 라는 녀석이었다. 자바의 어노테이션 같은 것이라 생각했는데, 메타데이터(데이터에 대한 데이터)의 역할을 하는 자바의 어노테이션과는 비슷하지만 좀 다르다.


데코레이터

파이썬의 데코레이터는 함수를 decoration(장식) 하는 함수다. 데코레이터는 어떤 함수가 실행되기 전에 먼저 실행되는 함수를 말한다. 어떤 함수를 다른 함수로 감쌀 수 있는 방법인데, 감싸는 것에 장식한다는 의미를 붙여서 decorator 라고 부르는 것 같다. @staticmethod, @classicmethod 와 같은 것들이 데코레이터다.


데코레이터 만들기 1 - 기초

먼저 import functools 혹은 from functools import wraps 를 import 해줘야 한다. 아이러니 하지만 데코레이터를 만드는 데에도 데코레이터가 사용된다. wraps 라는 함수는 데코레이터를 만드는데 사용되는 데코레이터이다.

데코레이터의 정의는 아래와 같은 과정으로 한다.

  • 데코레이터로 쓰일 함수를 정의한다.
  • 내부에 @wraps 라는 데코레이터를 사용하여, 파라미터로 받은 함수를 감쌀 함수를 정의한다.
  • 파라미터로 받은 함수를 실행하기 전과 후에 필요한 로직을 넣는다.
  • wrap 한 함수를 리턴한다.

아래는 데코레이터를 정의하고 사용하는 예제 코드이다.

from functools import wraps


def my_decorator(func):
    @wraps(func)
    def runs_func():
        print("This is ")
        func()
        print("blog")
    return runs_func


@my_decorator
def my_func():
    print("yaboong's ")


my_func()

동작하는 과정은 아래와 같다.

  • my_func() 실행! 하려다가 하지않고
  • 데코레이터 함수인 my_decorator 에 my_func() 함수를 인자로 넘겨줌
  • my_decorator() 함수 실행
  • return runs_func 에 의해 runs_func() 함수 실행
  • runs_func() 함수가 실행 되면서 인자로 받은 func 즉, my_func() 가 실행됨


데코레이터 만들기 2 - 약간의 응용

위에서 작성한 데코레이터를 적용하는 함수 my_func() 는 파라미터가 없었다. 파라미터가 있는 함수에 데코레이터를 적용해보자. 그리고, 어딘가에 저장되어 있는 권한에 대한 정보를 가져와서 ‘admin’ 이면 어떤 함수를 실행하고 ‘admin’ 이 아닌 경우 권한이 없음을 나타내는 login_required 데코레이터를 만들어보자.

from functools import wraps

AUTH_INFO = 'admin'
DB = {}


def login_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if AUTH_INFO == 'admin':
            f(*args, **kwargs)
            print('update success')
        else:
            print('no permission')
    return decorated


@login_required
def update_data(data1, data2):
    DB['data1'] = data1
    DB['data2'] = data2


update_data(1, 2)
print(DB)

AUTH_INFO 는 어딘가에서 가지고 온 것이라 가정하고 보면, 데이터베이스의 갱신을 수행하는 update_data() 함수는 login_required 데코레이터를 적용하여 권한이 admin 인 경우에만 갱신을 할 수 있도록 만들 수 있다.

이때, 데코레이터를 적용하여 데코레이터 내부에서 실행되는 update_data() 함수의 파라미터는 *args, **kwargs 로 전달한다.

동작하는 과정은 데코레이터 만들기 1 에서 했던 것과 거의 비슷하다.

  • update_data() 실행! 하려다가 하지않고
  • 데코레이터 함수인 login_required 에 update_date() 함수를 인자로 넘겨줌, f 라는 파라미터로 받음.
    • 이때, 인자 1, 2 는 *args 로 전달됨
    • 호출시 update_data(x=1, y=2) 형태라면 **kwargs 에 {‘x’:1, ‘y’:2} 형태로 전달됨
    • 위 코드의 경우 *args 는 {tuple} <class 'tuple'>: (1, 2), **kwargs 는 {dict} {}
  • login_required() 함수 실행
  • return decorated 에 의해 decorated() 함수 실행
  • decorated() 함수가 실행 되면서 인자로 받은 f 즉, update_data() 가 실행됨


실제 Flask 에서 (HTTPAuth 를 상속받아 구현한 HTTPBasicAuth 클래스의) login_required 함수는 데코레이터로 아래와 같은 방법으로 사용된다.

@app.route('/token')
@auth.login_required
def get_auth_token():
    token = g.user.generate_auth_token()
    return jsonify({'token': token.decode('ascii')})


Flask 의 login_required() 함수는 아래와 같이 생겼다.

def login_required(self, f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if auth is None and 'Authorization' in request.headers:
            # Flask/Werkzeug do not recognize any authentication types
            # other than Basic or Digest, so here we parse the header by
            # hand
            try:
                auth_type, token = request.headers['Authorization'].split(
                    None, 1)
                auth = Authorization(auth_type, {'token': token})
            except ValueError:
                # The Authorization header is either empty or has no token
                pass

        # if the auth type does not match, we act as if there is no auth
        # this is better than failing directly, as it allows the callback
        # to handle special cases, like supporting multiple auth types
        if auth is not None and auth.type.lower() != self.scheme.lower():
            auth = None

        # Flask normally handles OPTIONS requests on its own, but in the
        # case it is configured to forward those to the application, we
        # need to ignore authentication headers and let the request through
        # to avoid unwanted interactions with CORS.
        if request.method != 'OPTIONS':  # pragma: no cover
            if auth and auth.username:
                password = self.get_password_callback(auth.username)
            else:
                password = None
            if not self.authenticate(auth, password):
                # Clear TCP receive buffer of any pending data
                request.data
                return self.auth_error_callback()

        return f(*args, **kwargs)
    return decorated


정리

데코레이터는 작성하기에는 조금 헷갈릴 수 있지만 다양한 방식으로 유용하게 쓰일 수 있다. (권한처리, 로깅, 유효성검사 등등)

login_required 함수 같은 것을 굳이 데코레이터를 사용하지 않고 어떤 함수 내부에서 호출하여 권한이 있는지 없는지를 판단 할 수도 있지만 데코레이터를 사용하면 좀 더 우아하게 관심사를 분리한 코드를 만들 수 있다.


참고한 자료

Yaboong's Picture

Yaboong

Oskar Schindler was a mere opportunist and a corrupt businessman. Yet, when it seemed that great evil was taking over the world, it was not nobles, intellectuals, or religious leaders who rose to defy it and save lives—it was a corrupt opportunist, Oskar Schindler.

Massachusetts, US linkedin github