본문 바로가기
Coding/Python

Python 일급함수/클로저/Decorator에 관해서

by Hide­ 2019. 6. 4.
반응형

파이썬에서의 일급함수, 데코레이터를 구현하는 방법은 알고 있었지만 그것이 무엇인지 명확하게 하나의 문장으로 설명할 수 있을까? 라는 질문을 나에게 던져본다면 잠시 망설일 것 같다. 평소에 이론적인 지식보다는 실무적으로 실제 구현 능력을 키우는 것에 좀 더 집중했기 때문인데 기본기를 다시 한번 다지자라는 마음으로 이렇게 포스팅으로나마 정리해본다. (최대한 간단한 예제를 통해 설명할 예정이다)첫 번째로 일급 함수에 대해 설명할 것이며 두 번째로 클로저에 대해서, 최종적으로 이 모든 특성을 결합하여 데코레이터를 만들어본다.


일급 함수(First citizen function)

파이썬에서의 함수는 일급 함수이다. 일급 함수는 다음과 같은 조건에 만족해야한다.


1. 하나 또는 여러개의 함수를 인자로 받을 수 있다.

2. 리턴 값으로 함수를 줄 수 있다.

3. 변수에 함수를 담을 수 있다.


첫 번째로 "하나 또는 여러개의 함수를 인자로 받을 수 있다" 에 대해 살펴본다. 


def add_two_nums(a, b):
return a + b


def main(f, *args):
return f(*args)


if __name__ == '__main__':
print(main(add_two_nums, 1, 2))


위 코드를 실행시키면 3이라는 결과가 나온다. 위처럼 main의 인자로 add_two_nums 함수를 건내줬고 main에서는 건내받은 함수를 인자와 함께 실행시켰다. 


두 번째로 "리턴 값으로 함수를 줄 수 있다"  와 "변수에 함수를 담을 수 있다" 에 대해 살펴본다.


def add_two_nums(a, b):
return a + b


def main():
return add_two_nums


if __name__ == '__main__':
result = main()
print(result(1, 2))

main()(1, 2)


위 코드를 실행시키면 3이라는 결과가 나온다. result에 main() 함수의 실행 결과를 담아줬다. main() 함수에서는 add_two_nums라는 함수 자체를 리턴해줬으므로 result(1, 2)를 통해 리턴받은 함수를 실행시킨 결과다. 이렇게 리턴 값으로 함수를 줄 수 있으며, 변수에 함수를 담을 수 있다. 변수에 담지 않고 사용하려면 가장 마지막 라인에 있는 것 처럼 main()(1, 2)처럼 사용해도 된다.


클로저(Clojure)

다음으로 클로저란 무엇인가 간략하게 알아본다. 예제 코드부터 살펴보자.


def outer():
temp = 'temp variable'

def inner():
print(temp)
return inner


o = outer()
o()


위 코드는 outer함수 내부에 inner라는 또다른 중첩 함수가 들어있다. 보통 지역 변수의 Lifecycle은 함수의 종료까지인데, 위처럼 중첩으로 함수를 적용하면 중첩 함수 내부에서도 상위 함수의 변수에 접근할 수 있다. 파이썬은 클로저의 특성 때문에 inner함수에서 outer함수에 정의되어있는 temp라는 변수에 정상적으로 접근할 수 있다.


def outer():
temp = 'temp variable'

def inner():
print(temp)
return inner


o = outer()
del outer
o()


만약 위처럼 outer함수를 네임스페이스에서 삭제한다고 하더라도 inner함수는 temp변수를 가지고 있다.


데코레이터(Decorator)

이제 위에서 학습한 내용을 토대로 데코레이터를 만들어보면 다음과 같다.


def is_admin(function):
def wrapper(*args, **kwargs):
if kwargs.get('user_id') != 'admin':
print('[*] ADMIN ONLY')
return
else:
print('[*] SUCCESS')
function(*args, **kwargs)
return wrapper


@is_admin
def test(user_id):
print(f'test function user_id: {user_id}')


test(user_id='admin')


간단한 예제로 관리자인지 체크하는 코드를 작성해봤다. is_admin함수부터 살펴본다. 함수를 인자로 받고 내부 함수로 정의되어있는 wrapper를 실행시킨다. 해당 함수에서는 인자로 넘어온 user_id값이 admin인지 검사한다. 그리고 최종적으로 인자로 받은 함수를 실행시킨다. 이렇게 정의한 함수를 데코레이터로써 실행시키려면 @함수이름 을 원하는 함수의 바로 상단에 명시해주면 된다. 


class IsAdmin:
def __init__(self, function):
self.function = function

def __call__(self, *args, **kwargs):
if kwargs.get('user_id') != 'admin':
print('[*] ADMIN ONLY')
return
else:
print('[*] SUCCESS')
self.function(*args, **kwargs)


@IsAdmin
def test(user_id):
print(f'test function user_id: {user_id}')


test(user_id='admin')


데코레이터는 위처럼 클래스 형태로도 작성할 수 있다. 개인적으로 함수형태로 작성하는것보단 위처럼 클래스로 작성하는것이 더 깔끔한 것 같다. 예를 들어서 위 코드에서 test함수에 Docstring을 추가한 이후 __doc__를 출력해보자.


class IsAdmin:
def __init__(self, function):
self.function = function

def __call__(self, *args, **kwargs):

if kwargs.get('user_id') != 'admin':
print('[*] ADMIN ONLY')
return
else:
print('[*] SUCCESS')
self.function(*args, **kwargs)


@IsAdmin
def test(user_id):
"""Test Docstring"""
print(f'test function user_id: {user_id}')


test(user_id='admin')
print(test.__doc__)

결과


[*] SUCCESS test function user_id: admin None


예상대로라면 Test Docstring이 나와야하지만 이상하게도 None이 나온다. test함수 단독적으로 실행되는것이 아닌 데코레이터로 한번 Wrapping된 이후 실행되기 때문인데, 이는 functools의 update_wrapper를 통해 쉽게 해결할 수 있다.


import functools


class IsAdmin:
def __init__(self, function):
self.function = function
functools.update_wrapper(self, function)

def __call__(self, *args, **kwargs):

if kwargs.get('user_id') != 'admin':
print('[*] ADMIN ONLY')
return
else:
print('[*] SUCCESS')
self.function(*args, **kwargs)


@IsAdmin
def test(user_id):
"""Test Docstring"""
print(f'test function user_id: {user_id}')


test(user_id='admin')
print(test.__doc__)

결과 [*] SUCCESS test function user_id: admin Test Docstring


functools.update_wrapper(self, 인자명) 을 통해 wrapper를 한번 업데이트 해주면 위처럼 쉽게 해결이 가능하다.