데코레이터를 이해하기 위해서는 다음과 같은 개념들을 이해하여야 한다.
1. 일급 객체(First-Class-Citizen)
2. Closure
객체지향프로그래밍에서 자주 사용되는 개념 중 하나로 아래의 조건을 만족시키는 객체를 말한다.
1. 변수혹은 DataStructure(리스트, 튜플, 등등..)에 객체를 담을 수 있어야 한다.
2. 매개변수로 전달할 수 있어야 한다.
3. 리턴값으로 사용될 수 있어야 한다.
쉽게 말해서 객체를 변수처럼 사용할 수 있냐? 라는 말인데, 파이썬에서 함수는 일급객체의 Condition을 만족시킨다. 아래의 예시를 확인해보자.
def print_info(students: StudentInfo):
print(students.id)
print(students.password)
print(students.address)
def print_type(func: Union['function', None]):
print(type(func))
student = StudentInfo()
student.id = 201900278
student.password = 1
student.address = "경기도 광명"
print_info(student)
func = print_info
print_type(func)
new_list = [print_info,print_type]
print(new_list)
이전 포스팅에서 사용한 print_info
함수를 사용해서 함수를 변수로 받아서 매개변수로 전달해주었다.
print_type
이라는 함수를 선언해서 Function의 type을 출력할 수 있도록 하였고
맨 아래의 두 Line에서 볼 수 있듯이 각각을 변수로써도 사용할 수 있음을 알 수 있다.
위의 예시에서 파이썬의 함수가 일급객체임을 관찰하였고(즉, 변수로써 사용가능함을)
이제 파이썬에서 함수가 중첩돼서 사용되는 예시를 관찰해보자
def calculate(a: int, b: int): # 1
def add(): # 2
print(a+b) # 3
return add # 4
func = calculate(2, 3) # 5
print('--------') # 6
func() # 7
뭔가 이상하지 않은가? 분명 calculate(2, 3)은 실행이 끝났는데 2랑 3 즉, 지역변수들이 살아남아서 func()출력시에 출력이 된다.
이것이 바로 closure()의 개념이다.
함수의 구조가 다음과 같을때 내부함수는 closure 함수이다.
위에 함수를 다시한번 봐보자.
def calculate(a: int, b: int): # 1
def add(): # 2
print(a+b) # 3
return add # 4
func = calculate(2, 3) # 5
print('--------') # 6
func() # 7
#1(외부함수)가 #2(내부함수)를 갖고 있기 때문에 1번조건을 만족한다.
#2(내부함수)에서 외부함수의 변수 a와 b를 참조한다.
#4 Line은 외부함수의 return Line으로써 내부함수를 return하는 것을 볼 수 있다.
dir함수
dir 함수는 built-in함수로써, 인자로 객체를 넣어주면 그 객체가 갖고있는 변수와 함수를 나열하여 보여준다. 만약 인자를 넣어주지 않고 사용할 경우에는 현재 메모리에 할당된 변수들의 리스트를 보여준다.
위에서 언급한 dir함수를 이용해서 우리의 function의 인자를 확인해보자.
func = calculate(2, 3)
print('--------')
func()
print('--------')
print(dir(func))
3번 인덱스에 __closure__
튜플이 존재하는 것을 알 수 있다.
여기서 잠깐
구글링을 해봤을때, 모든 설명이 전부__closure__
가 함수라는 식으로 말하기에 혼동이 일어났다. 다시 말하자면 위의 컨디션을 만족할때의 함수를 Closure 함수라고 하고 모든 함수는__closure__
속성을 갖지만 closure 함수의 경우에만__closure__
속성에 Tuple로써 외부 함수의 변수(프리변수)가 저장된다.
만약 closure함수가 아니라면__closure__
의 값이 None으로 고정된다.
print(dir(func))
print(type(func.__closure__))
print(func.__closure__)
print(func.__closure__[0].cell_contents,func.__closure__[1].cell_contents)
위의 코드를 실행했을때, 다음과 같은 결과를 얻을 수 있다.
그럼 Closure함수를 왜 정의하여 사용해야할까?
장점으로썬 어떤게 있을까?
일단, 다른 블로그에서 언급한 장점은 다음과 같은게 있다.
클로저 함수를 사용하는 대신 전역변수를 선언해 사용하면, 변수가 섞일수도 있고 변수의 책임 범위를 명확하게 할 수 없는 문제가 생긴다.
클로저 함수를 사용함으로써, 전역변수를 사용하지 않아 위와 같은 문제가 생기지 않는다.
위의 예시로써, 다음과 같은 상황을 고려해 볼 수 있다. 리스트에 숫자를 넣고 출력을 하는 과정을 생각해보자.
numbers = []
def enter_number(x):
numbers.append(x)
print(numbers)
enter_number(3)
enter_number((7))
enter_number(4)
# using Closure
def enter_number_outer():
numbers = []
def enter_number_inner(x):
numbers.append(x)
print(numbers)
return enter_number_inner
enter_num = enter_number_outer()
enter_num(3)
enter_num(7)
enter_num(4)
위에서 언급한 장점대로 numbers를 전역변수로 선언할 경우 코드가 복잡해지면서 numbers를 건드릴 가능성이 존재한다고 보인다. 이러한 면에서 책임을 구분할 수 있다고 하는 것이다.
추가적으로, 파이썬 홈페이지를 찾아보니깐 장점으로써 언급하는 것이 있었다.
(→ 클로저를 사용함으로써 전역변수의 사용을 피할 수 있고, 데이터를 숨길 수 있다. 또한, Decorator를 사용할 수 있게 해준다.)
데코레이터는 말그대로 꾸며주는 함수를 의미한다. 언제 꾸며주는 함수를 사용할까? 바로 다음과 같은 상황을 생각해볼수 있다.
def first(*args):
#line1
#line2
#line3
#line4
pass
def second(*args):
# line1
# line2
# line5
# line4
pass
두함수에서 #line3와 #line5만이 다름을 알 수 있다. 이렇게 두개의 함수에 대해서는 복사해서 그렇게 쓸 수 있다고 해보자. 하지만 여러 곳에서 매우 많이 사용될 경우에는 코드가 복잡해지고 알아보기 어려울 것이다. 이럴때 Decorator를 만들어서 사용하면 된다.
우린는 파이썬에서 함수가 일급객체, 즉 변수로써 활용될 수 있음을 앞에서 살펴보았다. 또한 우리는 closure함수를 알아보았고, closure함수가 되기 위한 condition또한 알아봤다. 이 두개의 개념을 합쳐 closure함수인데 변수를 함수로써 받는 것이 바로 Decorator Function이다.
from typing import Tuple
def calculate(func): # 1. calculate function 선언
def print_result(*args):
print("계산 결과는", end=' ')
# print("args",args) # 튜플이 출력
func(args)
print("입니다.")
return print_result
@calculate
# 3. add 함수를 calculate(=decorator) function 으로 꾸며준다.
# 이 부분을 add = calculate(add)로 이해하면 된다.
def add(tup: Tuple): # 2. add function 선언
summation = 0
for i in tup:
summation = summation + i
print(summation, end='')
add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) # 현재 튜플이 입력된 상황
#4 사실상 add가 아닌 print_result가 실행됨. closure개념이 적용된다
위에 처럼 데코레이터를 사용할 수 있다.
여기서 하나 짚고 넘어가야할 개념이 바로 *(asterisk)
이다.
args와kwargs말고 다른 이름을 사용해도 괜찮다.
*args
와**kwargs
?
*args
는 *argument의 줄임말로써 복수개의 인자를 함수로 받고자할때 사용한다. 이때 args를 출력해보면 tuple형태로 값을 받음을 알 수 있다. 이는 위에 코드에서#print("args",args)
에서 확인할 수 있다.
**kwargs
는**keyword argument
의 줄임말로써 키워드를 제공한다. kwargs를 출력해보면 딕셔너리 형태로 값을 받음을 알 수 있다.def kwargs_test(**kwargs): for key,values in kwargs.items(): # Dictionary의 item 메소드 print("key:",key, end=' ') print("value:",values) print("kwargs", kwargs) kwargs_test(a=1, b=2)
이때 주의할점은 우리가 딕셔너리를 생성할때는{'a':1}
와 같은 형식으로 생성하지만 여기서 함수에 파라미터를 넣어줄때는키워드 = 특정값
으로 넣어주어야 한다.
다시 돌아와서 데코레이터는 클래스로도 사용가능하다.
다음과 같이 calculator를 만들 수 있다.
from typing import Tuple
class Calculator:
def __init__(self, func):
self.func = func
def operation(self,*args):
print("연산시작")
self.func(args)
print("연산종료")
@Calculator
def add(tup: Tuple): # 2. add function 선언
summation = 0
for i in tup:
summation = summation + i
print(summation, end='')
add.operation(1,2,3)
__call__
을 사용해서 다음과 같이 사용할 수도 있다.
class Calculator:
def __init__(self, func):
self.func = func
def __call__(self,*args,**kwargs):
print("연산시작")
self.func(args)
print("연산종료")
@Calculator
# add = Calculator(add)
def add(tup: Tuple): # 2. add function 선언
summation = 0
for i in tup:
summation = summation + i
print(summation)
add(1,2)
__call__
이란?
__call__
은 클래스의 객체도 호출하게 만들어주는 메소드로써 인스턴스가 호출 됐을때 실행이된다.
위에서는 클로저함수와 다르게 함수를 직접 리턴하는 문법이 없기 때문에, add가 Calculator인스턴스가 됐을때 호출을 하기 위해서 작성하였다.