Dev/Python

[Python 기초] Closure & Decorator

sincerely10 2020. 6. 30. 00:23
반응형

이번 포스트에서는 Closure와 Decorator에 대해 알아보겠습니다.

포스트 항목은 다음과 같습니다.
1. Nested Function(중첩함수)
2. Closure
3. Decorator
    3.1 기능 및 정의

    3.2 형태와 예제
    3.3 *args, **kwargs 받기
    3.4 @ 사용하기
    3.5 Decorator 함수에 변수넣기


1. Nested Function(중첩함수)

Closure와 Decorator를 이해하기 전에 Nested Function(중첩함수)에 대한 이해가 필요합니다.

def greeting():
    def hello():
        print("Hello!")
        
    hello()


>>> greeting()
Hello!

위의 코드와 같이 간단한 중첩함수를 만들어 보았습니다.
greeting이라는 함수 안에서 hello 라는 함수를 호출 했기 때문에
gretting 함수만 Call을 해도 'Hello!' 라는 문구가 정상적으로 출력 됩니다.

또한, 중첩함수(hello)는 부모함수(greeting) 안 에서만 사용할 수 있습니다.
만약 greeting 함수 밖에서 hello를 사용했다가는 가차 없이 에러가 뜰 것 입니다.

그렇다면, 중첩 함수를 쓰는 이유는 무엇일까요?
크게 두 가지의 이유가 있습니다.

  • 가독성

  • Closure

가독성은 이 문단에서 설명하고, Closure는 아래에서 설명하겠습니다.

Code Block 형태로 코드가 짜여지는데, 반복되는 액션을 함수 안에 정의하면 코드 길이를 줄일 수 있습니다.

2. Clousure

두 번째 이유는 Closure 입니다.
Closure는 국문으로 '폐쇄 또는 울타리' 라는 뜻입니다.
어떤 것을 가두는 걸까요?
바로 부모함수의 변수나 정보를 가둡니다.
그리고 이렇게 사용하는 것을 'closure'라고 합니다.

위 내용을 다시 정리하면서, Closure의 세 가지 조건을 확인 하겠습니다.

조건1. Nested 구조를 갖춰야 한다.(중첩이 되어야 한다)
조건2. 중첩함수가 부모함수의 변수나 정보를 중첩함수 내에서 사용해야 한다.
조건3. 부모함수는 return 값으로 중첩함수를 return 해야 한다.

이러한 조건을 보았을 때, Closure는 언제 사용되는걸까요?
어떤 정보를 기반으로 연산을 실행하고 싶으나 그 정보의 접근을 제한하여 노출이나 수정을 막고자 할 때 사용됩니다.

주로 factory 패턴을 구현할 때 사용됩니다. factory => '공장(만드는 곳)' 입니다.
무엇인가를 생성해내는 패턴입니다. 주로 함수나 Object를 생성하는데 사용됩니다.
factory에서 뭔가를 생성하기 위해서는 설정값이 필요합니다.
결국, 설정값을 노출하지 않아서 수정이 불가능하게(closure의 개념) 하면서 해당 설정값을 기반으로한 연산을 할 수 있는 함수를 만들 때, closure를 사용할 수 있습니다.

설명이 길어졌는데 이제 예제코드를 확인하겠습니다.

def generate_power(base_number):    #line1
    def nth_power(power):           #line2
        return base_number ** power #line3
    return nth_power                #line4
    
# closure 생성 및 출력
>>> calculate_power_of_two = generate_power(2)
>>> calculate_power_of_two(7)
128

이 함수는 승(,power) 또는 지수라고 하는 값을 구하는 함수입니다.
생성 및 출력 부분을 보면,
generate_power에 arguments 2를 넣은 값(즉 함수)을 calculate_power_of_two라는 변수에 대입합니다.
이제 calculate_power_of_two는 함수가 되었습니다.
이 함수에 2가 대입되어서
-> nth_power까지 return 하고(4번 Line)
-> 여기서 base_number만 담고 있고, power라는 변수를 받는 것을 기다리는 상태입니다.

calculate_power_of_two = generate_power(2) 이 라인에서 코드는 1 -> 2 -> 4 순으로 실행됩니다.

그리고 calculate_power_of_two(7)에서는 7이 들어오고, 2의 7 승인 128이 return 됩니다.
여기서 코드는 2 -> 3 으로 끝이 납니다.

3. Decorator

드디어 포스트의 작성 목적인 Decorator에 대한 내용을 작성합니다.
여러번 보아도 100% 이해가 되지 않은 부분이었는데 작성하며 저의 것으로 되길 희망합니다.

3.1 기능 및 정의

Decorator는 국문으로 '꾸미는 사람' 정도로 해석할 수 있습니다.
Decorator의 역할은 '함수를 인자로 받아 새로운 함수를 만들어 반환하는 함수' 입니다.
'함수 실행 전 특정동작을 하게 하는걸 간단하게 할 수 있게 만드는 것'이라고 할 수 있겠습니다.

실제 어떻게 적용되는지 확인하겠습니다.

3.2 형태와 예제

가장 기쁜 메세지 중 하나인 '배송완료' 문구를 출력하는 함수를 생각 해보겠습니다.
그리고 이 함수에 배송된 시간을 기록 하겠습니다.
사용한 경우 datetime과 배송완료라는 문구가 정상적으로 출력됩니다.

import datetime				#Line1
def delivery_ok():			#Line2
    print(datetime.datetime.now())	#Line3
    print("배송완료")			#Line4
    
>>> delivery_ok()
2020-06-25 23:10:40.553720
배송완료

이런 함수가 여러개이거나 delivery_ok 라는 함수에 기능이 많을 수록 가독성은 떨어지고, 에러 찾기도 어려워집니다.
이런 이유 때문에 조금 전 설명한 Decorator를 사용합니다.

우선 중첩함수(function)을 만들어야 합니다.

import datetime				#Line1
	
def decorator(func):			#Line3
    def wrapper():			#Line4
    	print(datetime.datetime.now())  #Line5
        return func()			#Line6
    return wrapper			#Line7

 def delivery_ok():			#Line9
      print("배송완료")			#Line10

delivery_ok = decorator(delivery_ok)	#Line12
delivery_ok()				#Line13

위와 같이 decorator의 뼈대 기능이 구현 됐습니다.
Line12에서 Closure와 같이 decorator 함수가 실행됩니다.
decorator라는 함수는 함수를 인자로 받기 때문입니다.
그리고 이렇게 새로운 함수가 delivery_ok로 재정의 되는 것입니다.
이렇게 꾸며지는 형태가 decorator 입니다.
실행순서를 적자면 12 -> 3 -> 4 -> 7 입니다.

그리고 Lin13에서 새로운 delivery_ok라는 함수가 실행됩니다.
이 때, Line4 아래의 부분이 비로소 실행되는 것입니다.
4 -> 5 -> 6 -> 9 ->10으로 실행되어서
Line5에서 시간출력을 하고 Line7에서 본 함수(line9)를 return 했기 때문에
Line 10이 실행됩니다. 

그래서 화면 출력은 아래와 같이 나옵니다.

2020-07-01 11:22:22.302859
배송완료

 

3.3 *args, **kwargs 받기

이제 시간과 배송완료 메세지가 나왔지만, 아직 만족하기에 이릅니다.
추가기능을 더 포함시켜 보겠습니다.
배송지까지 나타내고 싶습니다.

우선, delivery_ok(본 함수)에 배송지라는 정보가 Variable Length Keyword-only Arguments로 올 것임을 약속하고**kwargs로 받겠습니다.
그리고 배송지를 출력해주는 내용을 본 함수에 기입합니다.
코드내용을 보면서 확인하겠습니다.

import datetime				#Line1
	
def decorator(func):			#Line3
    def wrapper(*args, **kwargs):	#Line4
        print(datetime.datetime.now())	#Line5
        return func(**kwargs)		#Line6
    return wrapper			#Line7
	
def delivery_ok(**kwargs):		#Line9
    print("배송완료")			#Line10
    if 'where' in kwargs:		#Line11
    	print(f"배송지는 {kwargs['where']} 입니다.")	#Line12

delivery_ok = decorator(delivery_ok)	#Line14
delivery_ok(where='송파',company='한진')	#Line15

이제 decorator 함수도 이에 맞게 수정해야 합니다.
우선 함수를 받는다는건 동일하겠네요.
다음으로 wrapper 입니다.
Line15에서 함수가 실행될 때, 형태가
wrapper(where='송파')와 같습니다.

그렇다면 wrapper 함수를 만들 때, 애초에 where라는 positional arguments를 wrapper에 받아도 되지 않을까요?
wrapper(address, *kwargs)와 같이요.
사실 이 이유에 대한 의문은 완전히 풀리지 않았습니다.
그러나 대부분 wrapper(*args,**kwargs)의 형태로 사용한다는 것을 확인하였습니다.


따라서 wrapper 함수는 where라는 kwargs를 받고, 시간을 출력하고,
최종적으로 delivery_ok(where='송파',company='한진')을 실행시킵니다.

그리고 Line10과 Line12에서 해당 정보를 출력합니다.
결과는 다음과 같습니다.

2020-07-01 12:02:29.804221
배송완료
배송지는 송파 입니다.

3.4 @사용하기

매번 '본함수 = decorator_function(본함수)' 형태로 만들기는 번거로운 일입니다.
이를 한 문장으로 표현하는 방식이 있습니다.

import datetime				#Line1
	
def decorator(func):			#Line3
    def wrapper(*args, **kwargs):	#Line4
        print(datetime.datetime.now())	#Line5
        return func(**kwargs)		#Line6
    return wrapper			#Line7
	
@decorator				#Line9
def delivery_ok(**kwargs):		#Line10
    print("배송완료")			#Line11
    if 'where' in kwargs:		#Line12
    	print(f"배송지는 {kwargs['where']} 입니다.")	#Line13

delivery_ok(where='송파',company='한진')	#Line15

바로 위의 예제코드와 동일한 코드입니다.
다만 delivery_ok = decorator(delivery_ok)
Line10 위인 Line9에 @decorator로 치환 됐습니다.

3.5 decorator 함수에 변수넣기

마지막으로 decorator 함수에 변수넣기 입니다.
이 레벨의 구현을 위해 앞의 과정을 하나씩 거쳤습니다.

바로 위에 있는 코드에서 일반/등기의 구분이 너무 중요해서 함수에 넣는다고 가정해보겠습니다.
먼저 구현된 코드를 보면서 하나씩 이해 해보겠습니다.

import datetime					#Line1
	
def type_decorator(type):			#Line3
    def decorator(func):			#Line4
        def wrapper(*args, **kwargs):		#Line5
            whereis = func(**kwargs) + "인"	#Line6
            print(datetime.datetime.now())	#Line7
            print(f"{whereis} {type} 택배입니다.")	#Line8
        return wrapper				#Line9
    return decorator				#Line10

@type_decorator("등기")				#Line12
def delivery_ok(**kwargs):			#Line13
    print("배송완료")				#Line14
    if 'where' in kwargs:			#Line15
    	return "배송지는 "+ kwargs['where']	#Line16

delivery_ok(where='송파',company='한진')		#Line18

먼저 Line12의 type_decorator를 만나면서 함수를 정의하게 됩니다.
3 -> 4 -> 10 -> 5 -> 8 을 통해 wrapper가 마찬가지로 Variable Length Keyword-Only Arguments 변수를 받을 준비를 합니다.
특이한건 Line3이 새로 생겨서 type 즉, 택배 종류를 받을 준비를 한다는 것입니다.
달라진건 decorator와 wrapper가 한 칸씩 들여써졌고, decorator도 return을 해준다는 것 뿐입니다.

이제 Line18에서 keyword arguments를 받습니다.
즉, wrapper(where='송파',company='한진)라고 할 수 있습니다.
whereis는 delivery_ok라는 함수를 실행시켜, '배송완료'라는 메세지를 출력하고
"배송지는 송파"라는 문자열을 return 합니다.
그리고 Line6에서 이를 받아오는 것이죠.
'최종적으로 배송지는 송파인 등기 택배입니다.'라는 문자열이 출력됩니다.

다시 한 번, 최종 출력을 확인하면,

배송완료
2020-07-01 20:58:32.590988
배송지는 송파인 등기 택배입니다.

 

여기까지가 Nested Function, Closure, Decorator에 대해 학습하였습니다.
처음에 블로그 타이틀만 만들고 2주 동안 이해를 하지 못 해 작성 시작을 못 하였는데,
이제는 어느정도 이해를 할 수 있어서 다행입니다.

반응형