파이썬 공부를 계속하다가 데코레이터(Decorator)를 공부하게 되었고, 그 내용을 정리하고자 포스팅한다.


여기 아주 엄청난 일을 하는 함수가 있다.

1
2
def HelloName(name):
    print "Hello, %s!" % (name)
cs

 무려 인자로 이름을 입력받아 인사를 해주는 무척 대단한 함수이다. 아무 문제 없이 함수를 사용하다 보니, 이제 만났을때 하는 인사 외에도 헤어질 때 하는 인사를 만들고 싶어졌다. 정말 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
cs

 만났을 때 인사하고, 헤어질 때 인사하고 나니 안부를 묻는 말도 만들고 싶어졌다. 또 열심히 공을 들여서 다음과 같은 함수를 만들어냈다.


1
2
def HowAreYouName(name):
    print "How are you, %s?" % (name)
cs

 뿌듯하게 바라보는 와중, 문득 머릿속에서 이 함수들의 기능만으로는 너무 밋밋하다는 생각이 들어서 출력하기 전에 위아래에 경계선을 출력하도록 만들고 싶어졌다. 그래서 만들어진 결과가 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
def HelloName(name):
    print "============================"
    print "Hello, %s!" % (name)
    print "============================"
 
def GoodbyeName(name):
    print "============================"
    print "Goodbye, %s!" % (name)
    print "============================"
 
def HowAreYouName(name):
    print "============================"
    print "How are you, %s?" % (name)
    print "============================"
cs

 딱 봐도 무엇인가 많이 반복되고 있고, 불편함이 느껴진다. 이 함수들은 모두 다른 함수들이지만, 공통적으로 위아래에 경계선을 출력한다는 행위는 동일하다.

 파이썬에서 함수는 다른 함수의 인자로 넘길 수도 있고, 리턴할 수도 있는 특징을 가지고 있다. 이를 이용해서 일단 데코레이터를 사용하지 않고 원시적인 방법을 사용하여 이 문제를 해결해 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def AddLine(targetFunction, args):
    print "============================"
    targetFunction(args)
    print "============================"
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    AddLine(HelloName, "5kyc1ad")
    AddLine(GoodbyeName, "5kyc1ad")
    AddLine(HowAreYouName, "5kyc1ad")
cs

 함수와 그 인자를 넘겨 함수 시작 전, 함수가 끝난 후에 경계선을 출력하도록 하는 새로운 함수를 만들고, 그것을 이용해 각자의 함수를 실행하도록 변경해 보았다. 원하던 대로 결과가 나오기는 하지만, 무엇인가 불편한 기분이 드는 것은 여전하다. 호출하고 싶은 함수는 HelloName, GoodbyeName, HowAreYouName 인데 실제 호출하는 함수는 AddLine이고, 인자의 전달도 직관적으로 눈에 들어오지 않는다.

직관적으로 HelloName 함수를 실행하는 것이 보이면서 인자의 전달도 깔끔하게 정리할 수 있는 방법이 없을까?

답은 '함수를 새로 만들어서 사용하는 것' 이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def AddLine(targetFunction):
    def wrapper(args):
        print "============================"
        targetFunction(args)
        print "============================"
    return wrapper
 
def HelloName(name):
    print "Hello, %s!" % (name)
 
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    newHelloName     = AddLine(HelloName)
    newGoodbyeName   = AddLine(GoodbyeName)
    newHowAreYouName = AddLine(HowAreYouName)
 
    newHelloName("5kyc1ad")
    newGoodbyeName("5kyc1ad")
    newHowAreYouName("5kyc1ad")
cs

 AddLine 함수 내에 wrapper라는 이름으로 다른 함수 하나를 선언하고, 내부에서 인자로 전달받은 함수를 실행하기 전, 실행이 끝난 후에 줄을 추가로 출력하도록 한 후, AddLine 함수가 끝날 때 wrapper 함수 자체를 반환하도록 만들었다. 이제 main 에서 AddLine 함수에 원하는 출력 함수를 넣은 후 리턴된 함수를 받아와서 사용해주면 된다.

 정답에 아주 가까워졌지만 여전히 불편한 점이 몇가지 있다. 내가 선언한 함수는 HelloName인데 새로 newHelloName을 만들어서 사용해주어야 한다는 점, 함수마다 전부 저 작업을 추가로 해주어야 한다는 점 등이다. 선언한 함수를 그냥 사용할 수 없다는 점에서 또 직관성이 떨어지는 코드이기도 하다.

 이 모든 문제점을 해결하기 위해 사용하는 것이 데코레이터(Decorator)이다. 다른 함수들의 시작 전이나 후에 특정한 장식(Decoration)을 붙여 주는 것이다. 여기의 예제에서는 위아래에 붙은 경계선이 '장식' 되시겠다.

사용법은 생각보다 무척이나 간단하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def AddLine(targetFunction):
    def wrapper(*args, **kwargs):
        print "============================"
        targetFunction(*args, **kwargs)
        print "============================"
    return wrapper
 
@AddLine
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

wrapper 에서 받는 인자가 *args, **kwargs 가 되어서 당황할 수 있지만, 사실 이는 가변 인자를 넘겨주기 위한 것으로 생각보다 간단한 개념이다. 여기에 잘 설명되어 있으므로 참고.

 AddLine 함수는 달라진 점이 전혀 없고, 각 함수 위에 @AddLine 한 줄씩이 추가된 것을 볼 수 있다. @AddLine은 데코레이터 AddLine을 이 함수에 적용시키겠다는 의미이다. 각 함수들은 선언 순간 데코레이터 AddLine이 적용되어 리턴된 wrapper 함수를 자기 자신으로서 갖게 되고, 따라서 사용할 때는 별도의 작업 없이 선언한 함수를 그대로 사용하면 된다.

이것이 가장 기초적인 데코레이터의 사용법이다.


이제 여기서 한 계단만 더 올라가보자.

 데코레이터를 사용하다 보면 데코레이터 자체가 인자를 받는 모습을 볼 수 있다. 예를 들면 Flask를 이용한 REST API 개발을 해본 사람이라면, 다음과 같은 코드가 함수 앞에 붙는 것을 봤을 것이다.


1
2
3
@app.route("/file", methods=["GET"])
def blah_blah():
    pass
cs

 딱 봐서는 blah_blah 라는 함수에 app.route라는 데코레이터를 적용하는 코드로 보이는데, 지금까지 보지 못한 부분이 추가되었다.

바로 app.route에 ("/file", methods=["GET"]) 로 인자를 받는 부분이 추가되었다는 것이다.


 생각해 보면 지금까지 작성한 예시에서 데코레이터가 하는 일을 나타내어 봤을 때 '함수 시작 전과 끝난 후에 특정 줄을 하나씩 출력한다'라고 표현할 수 있다. 그런데, 모든 함수가 다 같은 줄만을 출력하는 것이 아닌 특정 문자열을 출력하고 싶은 것일 수도 있다. 즉, 같은 데코레이터를 이용하여 다른 내용을 출력할 수 있도록 만들고 싶을 수 있는 것이다.

 이렇게 데코레이터에 인자를 넣기 위해서는 데코레이터 함수를 한번 더 감싸주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def AddLine(targetStr):
    def RealDecorator(targetFunction):
        def wrapper(*args, **kwargs):
            print targetStr
            targetFunction(*args, **kwargs)
            print targetStr
        return wrapper
    return RealDecorator
 
@AddLine("===HelloName===")
def HelloName(name):
    print "Hello, %s!" % (name)
 
@AddLine("===GoodbyeName===")
def GoodbyeName(name):
    print "Goodbye, %s!" % (name)
 
@AddLine("===HowAreYouName===")
def HowAreYouName(name):
    print "How are you, %s?" % (name)
 
def main():
    HelloName("5kyc1ad")
    GoodbyeName("5kyc1ad")
    HowAreYouName("5kyc1ad")
cs

이런 식으로 말이다.

함수 위에 @AddLine으로 명시된 함수들은 선언 순간 AddLine 데코레이터를 적용시키게 되며, AddLine 데코레이터에는 출력할 문자열을 인자로 넘기도록 되어 있다. 위 예시의 실행 결과는 다음과 같다.


각각의 함수에서 AddLine 데코레이터에 인자로 넘긴 문자열이 함수의 시작과 끝에서 정상적으로 출력되는 모습을 확인할 수 있다.


 데코레이터를 잘만 이용하면 Repeat 등의 이름을 가진 데코레이터를 만들어서 한 함수를 여러 번 반복시키는 것도 가능하다.

대표적인 문제가 codefights.com 의 Arcade - Python의 77번째 문제인 'Merging Vines' 이다.

상당히 재미있고 데코레이터를 익히는 데에 적절한 문제이므로 한번씩 풀어보는 것도 좋을 것 같다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요