본문 바로가기

Python

Python - Decorator

이번에는 파이썬에서 아주 중요하면서도 유용한 개념인 Decorator에 대해서 알아보도록 하겠습니다. :)

Decorator


Decorator란 무엇일까? 단어의 의미를 그대로 생각해보면 꾸며주는 것이다. 그렇다면 뭘 꾸며주는 걸까? decorator는 함수를 꾸며주는 역할을 한다. 그렇다면 꾸며준다는 게 어떤 의미일까? 우선 정의를 하자면, 함수의 객체와 함수를 변경하는 다른 객체를 wrapping해주는 역할을 하는 것으로 기존의 함수를 꾸며주는 역할을 한다. decorator의 단어의 의미를 살려서 기존의 함수에다가 어떤 기능을 첨가, 추가하거나 기존의 함수를 변형하여 새로운 함수를 만들어주는 것이다. 우리는 기존의 함수를 꾸며주거나, 기존의 함수에 뭔가를 첨가해준다는 느낌을 잘 살려야한다.

https://towardsdatascience.com/curious-case-of-decorators-in-python-9644a99e538

예를 들어보자. 아래와 같은 이모티콘 제작 프로그램이 있다고 해보자. 이렇게 이모티콘을 제작하는 프로그램을 만들었는데, 갑작스럽게 클라이언트의 요청으로 이 모든 이모티콘에 저작권 표시를 해줘야하는 상황이 되었다. 즉 각 이모티콘 함수에 print( @copyright )이 들어가야하는 것이다. 그렇다면 어떻게 해야할까?

def smile():
    print('😁')
    
def angry():
    print('🤯')
    
def love():
    print('😍')
    
... # ( 대략 수백개 정도의 이모티콘 존재 )

해답은 간단하다. 우리가 알고 있듯이 각 함수에 print문을 넣어주면 된다.

def smile():
    print('😁')
    print('@copyright12345')
    
def angry():
    print('🤯')
    print('@copyright12345')
    
def love():
    print('😍')
    print('@copyright12345')
    
... # ( 대략 수백개 정도의 이모티콘 존재 )

 

그런데 이렇게 한다면 우리는 수백개 정도의 이모티콘에 모두 추가적인 작업을 해줘야한다. 이러한 비효율적인 방법이 과연 프로그래머에게 적절한 방식일까? 그래서 등장하게 된 것이 바로 이 decorator다. decorator를 활용하게 되면 이 모든 함수들을 꾸며주는 것을 더 쉽게 할 수 있다. 물론 현재 예시와 같이 print문을 넣는 경우는 decorator를 쓰는 것과 다를바가 없다. 하지만, 훨씬 복잡한 어떤 기능을 여러 함수에 추가해줘야할 때 하나하나 다 복사하고 있을것인가?

이러한 관점에서 우리는 데코레이터를 통해 효율적이게 함수를 꾸며주게 된다. 지금부터 그렇다면 decorator를 어떻게 사용하고 어떻게 동작하는지를 알아보겠다. 그런데 decorator가 동작하는 바를 알기 위해서는 먼저 파이썬에서의 First Class Object 일급객체의 개념에 대해서 알아야한다.

First Class Object 

파이썬에서 "함수는 일급객체다." 라는 말을 많이 듣게 된다. 이 말은 파이썬에서 함수들은 인자로써 전달되거나 사용될 수 있다는 의미가 핵심이다. 물론 일급객체로써 함수가 가지는 특성들은 여러가지가 있다. 

  • 함수는 Object 타입의 인스턴스다. ( Object 클래스를 상속받아 객체로 정의된다. -> 파이썬에서 모든 것은 객체다.)
  • 함수들을 해시테이블이나 리스트 같은 자료구조에 저장할 수 있다.
  • 함수를 변수안에 저장할 수 있다.
    # Python program to illustrate functions
    # can be treated as objects
    def shout(text):
        return text.upper()
     
    print(shout('Hello'))
    >>> Hello
     
    yell = shout 
    print(yell('Hello'))
    >>> Hello
  • 다른 함수의 파라미터로 함수를 전달할 수 있다. ( 이건 사실 call by reference의 개념과 이어진다. 파라미터로 함수를 전달할 경우 call by reference 방식으로 동작한다. 그 이유는 함수의 위치, 주소를 가리키는 것은 함수의 이름이기 때문이다. )
    # Python program to illustrate functions
    # can be passed as arguments to other functions
    def shout(text):
        return text.upper()
     
    def whisper(text):
        return text.lower()
     
    def greet(func):
        # storing the function in a variable
        greeting = func("""Hi, I am created by a function passed as an argument.""")
        print (greeting)
     
    greet(shout)
    greet(whisper)
    >>> HI, I AM CREATED BY A FUNCTION PASSED AS AN ARGUMENT.
    >>> hi, i am created by a function passed as an argument.​
  • 함수의 return으로 함수를 받을 수 있다. 아래와 같이 중첨 함수에서 inner function을 return받을 수 있고, 따라서 해당중첩함수를 호출하게 될 경우 할당하는 변수는 함수가 될 것이다. 그래서 add_15()라는 함수는 create_adder()의 인자가 15이기 때문에 15를 더해주는 기능을 하는 함수가 리턴된 것이다.
    # Python program to illustrate functions
    # Functions can return another function
     
    def create_adder(x):
        def adder(y):
            return x+y
     
        return adder
     
    add_15 = create_adder(15)
     
    print(add_15(10))
    >>> 25​

포인트는 결국 모든 것이 객체인 파이썬에서 함수또한 객체이고( 따라서 변수에 할당할 수 있다. ) 함수의 파라미터로써도 반환되는 객체로써도 함수를 사용할 수 있다는 점일 것이다. 즉, 파이썬에서 함수는 일급객체인 것이다.

Closure

이 개념들은 파이썬에서의 Closure라는 개념과도 이어진다. 따라서 이에 대해 간단히 이야기 해보려고 한다. 우선 클로저의 정의를 먼저 간단히 살펴보자 "컴퓨터 언어에서 클로저는 일급 객체 함수의 개념을 이용하여 스코프에 묶인 변수를 바인딩 하기 위한 일종의 기술이다."( 위키백과 )

저번에 scoping rule에 대해 배우면서 scope에 대해 알아봤다. 그렇다면 위의 예제에 적용해보자. 위의 예제에서 중첩함수는 adder라는 inner function을 반환하고 있다. 이는 지금 언급하고 있듯이 함수가 일급객체이기 때문에 가능한 것이다. 그런데 주목해야할 점은 인자다. 우리는 create_adder15라는 인자를 넘겨줬다. 우리가 create_adder 함수를 호출해서 15라는 매개변수를 넘겨줬을 경우 이 create adder scope가 종료하면서 x라는 매개변수의 값인 15도 메모리상에서 사라진다고 생각할 수 있다. 그런데 실제로 실행을 해보면 create_adder가 호출된 이후로 반환되는 함수에 이 x라는 매개변수에 담긴 값 15가 그대로 기억되어 있는 것이다. 즉, enclosed scope에 묶인 x라는 변수를 바인딩해서 local scope의 함수에 두고 그걸 계속 바인딩해 두고있는 것이다. 즉, 내부함수에 쓰인 x에 우리가 호출할 때 외부함수로 넘겨줬던 값 15가 묶인다는 것이다. 이러한 행동을 하는 함수를 closure라고 부른다. 즉, 저러한 형식( 반환되는 것이 inner function + outer function의 매개변수를 inner function에서 사용 )의 중첩함수를 정의하고 이를 어떤 변수에 할당했다면 리턴되는 함수가 해당 변수에 저장될 것이다. 그럼 이 변수에 저장된 이 함수에는 중첩함수로 넘겨줬던 값을 바인딩해서 가지고 있을 것이고, 이 함수를 Closure함수라고 하는 것이다. 그런데 저 15라는 값은 도대체 어디에 기억되어 있는 것일까? 우리가 배웠던 scoping rule을 생각하면 아무리 봐도 이상하다. 알아보기위해서 closure함수를 더 자세히 파고들어보자.

def start_at(x):
    def increment_by(y):
        return x + y
    return increment_by
 
closure1 = start_at(1)
closure2 = start_at(2)
print("closure1:", closure1(3))
>>> closure1: 4
print("closure2:", closure2(4))
>>> closure2: 6

 

이렇게 closure함수를 생성되는 것을 확인할 수 있다. 위의 예시에서 볼 수 있듯이 저렇게 start_at함수에서 반환해주는 함수가 closure function인 것이다. 이렇게 clousre함수를 확인한 후에 __closure__ 매직메소드를 통해서 closure함수에 대해서 직접확인해볼 수 있다. (이렇게 클로져함수가 되었다면 __closure__라는 attribute가 객체에 있을 것이다. )

closure1.__closure__ # closure1 = start_at(1)
>>> (<cell at 0x0415AFB0: int object at 0x63F464B0>,)
closure2.__closure__ # closure2 = start_at(2)
>>> (<cell at 0x0415AEB0: int object at 0x63F464C0>,)

확인해보면 cell을 출력한다. cell이란 뭘까? 간단히 말해 cell은 여러 scope에서 참조할 수 있는 값을 저장할 수 있는 객체라고 한다. 그렇다면 이 cell에 어떤 값이 담긴 걸까? 우리가 지금까지 해온 것을 봤을 때 중첩함수에 인자로 넘겨주는 위의 예제에서 1이나 2와 같은 인자가 inner function에서 정의하지도 않았고, 전역변수도 아닌데 사용할 수 있다는 점에서 여러 scope에서 참조할 수 있는 값이라는 말에 가장 어울린다. 사실 이러한 코드영역에서 사용되지만, 그 local scope에서 정의하지도 않았고, 전역변수도 아닌데 사용할 수 있는 변수를 자유변수라고 부른다. 즉 closure1이라는 클로져에서는 1이라는 값이 자유변수일 것이다. 이를 cell 객체에 담고있는 값을 확인하기 위해서 cell_contents라는 attribute를 통해서 접근해보자.

closure1.__closure__[0].cell_contents
>>> 1
closure2.__closure__[0].cell_contents
>>> 2

이렇게 우리가 예상한대로 어디에 저장될지 모르는 것 같았던 저 인자가 자유변수라는 형태로 cell 객체에 저장되어 있던 것이다. 도대체 저 값을 어떻게 가져오는지 궁금했는데, 해결된 것 같다.

Decorator 

decorator의 개념을 본격적으로 알아보자. 우리는 두가지 주체를 기억해야한다. 꾸며질 기존의 함수 객체와 꾸며줄 중첨함수 객체이다. 이 때 꾸며주기 위한 함수가 중첩함수여야하는 이유는 기존의 함수를 꾸며서 함수를 return받기 위해서는 새롭게 꾸며진 함수가 있어야하고, 이것이 기존의 함수를 이용해서 중첩함수내에서 새로 정의되어야하기 때문이다. ( closure의 형식과 유사하다.) 사실 이러한 개념적인 것만 본다면 굳이 데코레이터를 쓰지 않고도 똑같은 기능을 할 수 있을 것처럼 느껴진다. 예를 들어서 위의 예시를 다시 가져와보자.

def smile():
    print('😁')
    
def angry():
    print('🤯')
    
def love():
    print('😍')

def copyright(func):
	def new_func():
		print('@copyright12345')
		func()
	return new_func
    
smile = copyright(smile)
love= copyright(love)
angry= copyright(angry)

smile()
angry()
love()
>>> @copyright12345
>>> 😁
>>> @copyright12345
>>> 🤯
>>> @copyright12345
>>> 😍

이렇게 closure의 개념만을 그대로 이용해서 위와같은 기능을 첨가할 수 있는 방법도 존재한다. 이를 좀 더 편리하게 바꿔놓은 기능이 decorator인 것이다.

decorator의 사용법은 꾸며주고자하는 기존의 함수를 정의할 때 바로 위에 @decorator_function을 적어줌으로써 기존의 함수에 추가적인 기능을 첨가할 수 있다. 이렇게 되면 기존의 함수가 decorator function으로 꾸며지게 되어 기존의 정의된 함수를 그대로 호출해서 사용할 경우 꾸며진 함수가 호출된다. 

@copyright
def smile():
    print('😁')
    
@copyright
def angry():
    print('🤯')
    
@copyright
def love():
    print('😍')

smile()
angry()
love()
>>> @copyright12345
>>> 😁
>>> @copyright12345
>>> 🤯
>>> @copyright12345
>>> 😍

이렇게 decorator를 사용함으로써 사람이 보기에도 이해하기에도 좋은 깔끔하고 간단한 코드처리가 되는 것이다. 이러한 점에서 decorator는 파이썬에서 함수나 클래스의 행동을 수정할 때 프로그래머들이 할 수 있는 강력하고 유용한 툴인 것이다.

 

Reference

https://www.geeksforgeeks.org/decorators-in-python/

 

Decorators in Python - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

https://whatisthenext.tistory.com/112?category=761276 

 

[파이썬] 클로져(Closure) 이해하기

클로저(Closure) 함수 내부함수 클로저(Closure) 함수를 배우기 전에, 내부 함수에 대해 먼저 알아야 한다. 일급 객체 의 특징중에서 함수 내에 함수를 정의 할 수 있다고 했다. 이를 내부 함수(inner fun

whatisthenext.tistory.com

https://blog.hexabrain.net/347

 

파이썬 강좌 번외편. 클로저(Closure)

이번 강좌는 클로저(Closure)에 대해 알아보도록 하겠습니다. 클로저는 위키백과의 정의를 빌어온자면 '컴퓨터 언어에서 클로저는 일급 객체 함수의 개념을 이용하여 스코프에 묶인 변수를 바인

blog.hexabrain.net

윤상석 지식제공자 님의 타입 파이썬! 강의내용 : https://www.inflearn.com/course/%ED%83%80%EC%9E%85-%ED%8C%8C%EC%9D%B4%EC%8D%AC 

 

타입 파이썬! 올바른 class 사용법과 객체지향 프로그래밍 - 인프런 | 강의

Python으로 생산성있는 개발만 아니라 견고하고 안전하게, 그리고 확장성있는 개발을 하세요! 🔥, - 강의 소개 | 인프런...

www.inflearn.com

 

'Python' 카테고리의 다른 글

Python - Magic Method  (0) 2022.02.13
Python OOP - Part 1 ( Object Oriented Programming)  (0) 2022.02.11
Python - Variable Length Arguments  (0) 2022.02.02
Python - Iterable / Iterator / Generator  (0) 2022.02.01
Python Scoping Rule ( by LEGB rule )  (0) 2022.02.01