본문 바로가기

개발 언어/Python

[Python] 클로저, 데코레이터, wraps, contextmanager

안녕하세요.

 

오늘은 클로저데코레이터에 대한 설명을 하고,

파이썬 라이브러리에서 자주 쓰이는 데코레이터 중 하나인 wrapscontextmanager에 대한 설명을 해보겠습니다.

 

 

클로저

클로저는 겉함수가 호출할 함수를 파라메터로 받아오고,

def outer(func: callable):

겉함수 안에서 속함수를 정의하고,

def outer(func: callable):
    print("outer 시작")
    
    def wrapper(*args, **kwargs):
        print("Wrapper 시작")
        print(func.__name__)
        func(*args, **kwargs)
        print("Wrapper 끝")

속함수를 반환하는 형태의 겉함수를 클로저라고 합니다.

def outer(func: callable):
    print("outer 시작")
    
    def wrapper(*args, **kwargs):
        print("Wrapper 시작")
        print(f"func 호출: {func(*args, **kwargs)}")
        print("Wrapper 끝")
    
    print("outer 끝")
    return wrapper

 

예시 코드입니다.

def outer(func: callable):
    print("outer 시작")
    
    def wrapper(*args, **kwargs):
        print("Wrapper 시작")
        print(f"func 호출: {func(*args, **kwargs)}")
        print("Wrapper 끝")
    
    print("outer 끝")
    return wrapper

def add(a, b):
    return a + b

closure_add = outer(add) ## 얘는 함수임!

closure_add(1, 2)

실행 결과는 아래와 같습니다.

 

겉함수에 함수를 넣어 반환되는 값이 함수라는 것에 주목해주세요. 그 함수는 wrapper입니다.

 

위 코드에서는 이 wrapper에서 a와 b를 받으면, 튜플이기 때문에 *args로 unpacking되어 wrapper 내부에서 실제 함수인 add가 *args를 파라미터로 받아 실행되고 있는 것입니다.

 

클로저 사용 이유

1. 겉함수에서 호출할 함수 자체에 대한 변화가 없이 추가적인 로직을 수행할 수 있습니다.

2. 클로저는 겉함수의 변수에 접근할 수 있습니다.

 

예시 중 하나로, memoization을 하는 경우입니다.

def outer(func: callable):
    cache = {}
    
    def wrapper(*args, **kwargs):
        if args in cache:
            print(f"Cache hit: {cache[args]}")
            return cache[args]
        
        result = func(*args, **kwargs)
        cache[args] = result
        print(f"Not cache hit: {result}")
        return result

    return wrapper

def add(a, b):
    return a + b

closure_add = outer(add)

print(closure_add(1, 2))
print(closure_add(1, 2))
print(closure_add(1, 3))

결과는 아래와 같습니다.

 

cache 딕셔너리에 그 값이 저장되어서 동일한 파라미터를 받은 경우라면 실제 함수를 호출하지 않고도 바로 결과를 낼 수 있습니다.

 


데코레이터

 

데코레이터는 클로저를 쉽게 사용할 수 있게끔 한 문법입니다.

 

클로저를 만드는 outer 함수가 있다고 했을 때,

 

add = outer(add)
@outer
def add(a, b):
    return a + b

위의 두 코드는 정확히 동일합니다. 첫번째 코드보다 두번째 코드가 더욱 직관적으로 다가오죠.

 


wraps

 

functools 라이브러리에서 wraps는 데코레이터로 감싸진 함수에 대한 메타데이터 값을 보존하기 위한 목적입니다.

 

바로 예시로 설명해보도록 하죠.

from functools import wraps


def my_decorator(func: callable):    
    ...
    # @wraps(func) # wraps가 없을 때...
    def wrapper(a, b):            
        res = func(a, b)        
        return res               
    
    return wrapper   
 
@my_decorator
def add(a, b):
    '''
    Add two numbers
    '''
    return a + b

print(add.__name__)
print(add.__doc__)

결과:

 

데코레이터와 클로저의 원리를 이해했다면 add에 대한 __name__과 __doc__은 return 값은 wrapper이기 때문에 wrapper의 name과 wrapper의 doc이 나온다는 것을 유추하실 수 있을 것입니다.

즉, 우리가 원하는 동작인 add 자체에 대한 메타데이터가 나오지 않는다는 것이죠.

 

하지만 @wraps(func)를 적용하면 메타데이터의 보존이 잘 됩니다.

 

바로 이렇게 말이죠.

 

 

이렇게 메타데이터를 보존하여 디버깅을 수월하게 할 수 있습니다.

 

왜 @wraps에 func 파라미터를 넣어야 되는지는 추가로 생각해보시기 바랍니다.

 

wraps 또한 클로저라는 사실을 생각해보시면 알 수 있습니다(데코레이터가 아닌 클로저라고 생각해보시기 바랍니다).

 

힌트: 

def outer(func, x):

 

add = outer(add, x)

 


contextmanager

 

contextlib 라이브러리의 contextmanager데코레이터로 사용하고,

특정 코드 패턴을 내부 함수에 적용해주면 함수 시행 전 해야 할 것함수 종료 후 해야 할 것을 실행해줍니다.

코드 패턴은 아래와 같습니다.

예시 코드를 보겠습니다.

from contextlib import contextmanager

@contextmanager
def custom_open(*args, **kwargs):
    f = open(*args, **kwargs)    
    try:        
        yield f                
    finally:
        f.close()
        
with custom_open('test.txt', 'w') as f:
    f.write('Hello, world!')

 

우리는 파일을 열고 나면 항상 close를 해줘야 합니다.

 

그렇기 때문에 f = open()을 미리 준비해주고, yield로 파일을 넘겨주었습니다.

 

이후 최종적으로 finally 때문에 try문 로직에 에러가 있던 없던 f.close()는 반드시 해주는 모습입니다.

 

위 코드는 내장된 with open문과 동일한 기능을 합니다.

 

as 뒤에 값이 바로 yield로 나오는 값이라는 것만 숙지하시면 되겠습니다.

 

다만, 흥미로운 사실은 저 with는 scope와는 전혀 상관 없습니다. 

 

아래 예시 코드를 보시죠.

@contextmanager
def default_list(*args, **kwargs):    
    lst = [*args, *kwargs.keys(), *kwargs.values()]
    try:        
        yield lst                
    finally:
        lst.clear()
        
with default_list('a', 'b', c='d') as d_lst:
    print(d_lst)

d_lst.append('e')
print(d_lst)

결과는 아래와 같습니다.

with 안에서는 default_list에 따라 모든 파라미터를 리스트로 만들었다면, with 바깥에서는 clear가 되니 다시 빈 리스트에서 append로 추가가 되는 것을 보실 수 있습니다.

 

이는 파이썬에서 if 문이 scope를 만들지 않는 것과 동일한 맥락이라고 보시면 되겠습니다.

 


 

감사합니다.

 

 

참고:

 

Python의 Closure에 대해 알아보자

Python에서 유용한 Closure에 대해 살펴봅니다.

shoark7.github.io

 

파이썬 클로저 : 개념, 사용 이유, 사용 법, 장단점 - H-A

Python의 클로저는 함수가 해당 스코프 외부에서 호출되는 경우에도 자유 변수에 액세스할 수 있는 함수 개체를 나타냅니다. 클로저 개념에서 중요한 것은 두 가지 입니다. 1. 함수가 종료되어도

hangbok-archive.com

 

파이썬 - decorator

 

www.youtube.com