데코레이터(Decorator)

이성준·2023년 6월 26일
0

데코레이터를 이해하기 위해서는 다음과 같은 개념들을 이해하여야 한다.
1. 일급 객체(First-Class-Citizen)
2. Closure

1. 일급 객체

객체지향프로그래밍에서 자주 사용되는 개념 중 하나로 아래의 조건을 만족시키는 객체를 말한다.
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()의 개념이다.

2. Closure

함수의 구조가 다음과 같을때 내부함수는 closure 함수이다.

  1. 어떤 함수의 내부 함수일 것
  2. 내부함수가 외부함수의 변수를 참조할 것
  3. 외부 함수가 내부 함수를 리턴할 것

위에 함수를 다시한번 봐보자.

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를 사용할 수 있게 해준다.)

3. 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}와 같은 형식으로 생성하지만 여기서 함수에 파라미터를 넣어줄때는 키워드 = 특정값으로 넣어주어야 한다.



3.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인스턴스가 됐을때 호출을 하기 위해서 작성하였다.

참고자료
블로그
블로그
동영상

profile
Time-Series

0개의 댓글