ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 파이썬 Closure 및 Decorator 설명
    Programming/Python 2022. 8. 12. 15:56

     

     

      데코레이터를 이해하려면 먼저 클로저를 알아야 합니다. 클로저를 이해하려면 파이썬에서 변수 범위가 어떻게 작동하는지 알고 있어야 합니다. 파이썬 변수 범위부터 살펴보겠습니다. 

     

    파이썬의 변수 범위 규칙

    전역변수는 프로그램의 어떤 부분에서든 접근 가능한 변수입니다. 보통 함수 외부에 정의하거나 함수 내부에서 global 키워드를 통해 전역변수를 조작합니다.

    b = 6 # 전역변수
    def my_func_local():
        a = 3 # 지역변수
        b = 3
        print(f"local a: {a}")
        print(f"local b: {b}")
    
    def my_func_global():
        global b # b라는 지역변수를 전역변수로 사용한다는 의미
        a = 3 # 지역변수
        b = 3
        print(f"local a: {a}")
        print(f"local b: {b}")
    
    my_func_local()
    print(f"global b: {b}")
    
    my_func_global()
    print(f"global b: {b}")
    
    # Results (local)
    # 3
    # 3
    # 6
    
    # Results (global)
    # 3
    # 3
    # 3

    위에서 my_func_global 함수의 경우 global 키워드로 인해 b 전역변수의 값이 변경된 것을 확인할 수 있습니다. 

     

     

    Closure 의 작동원리

     

    파이썬의 클로저는 함수 속에 내부 함수를 구현하고 내부함수를 반환하는 함수를 의미합니다. 중첩된 함수가 해당 범위 내의 변수를 참조할 때 정의됩니다. 이때 참조된 변수는 '자유 변수' 라고 합니다. 클로저는 함수를 정의할 때 존재하던 자유 변수에 대한 바인딩을 유지하고, 함수호출이 종료된 시점에도 자유변수에 접근할 수 있습니다. 

    기억할 것은 함수가 non local 인 변수를 다루는 경우는 중첩된 함수의 경우 뿐이라는 것입니다. 

    def make_averager():
    
        # Closure -----------------------------
        series = []
        
        def averager(new_value):
            series.append(new_value) # 중첩된 함수에서 외부변수를 참조 -> 자유변수
            total = sum(series)
            avg = total/len(series)
            print(avg)
            return avg
        
        # Closure -----------------------------
    
        return averager
    
    avg = make_averager()
    avg(10)
    avg(11)
    avg(12)
    print(avg.__code__.co_varnames)
    print(avg.__code__.co_freevars)
    print(avg.__closure__[0].cell_contents)

     

    nonlocal 키워드

    클로저의 예시에서는 리스트에 모든 값들을 저장하면서 평균값을 계산했습니다. 극단적으로 생각해서 값이 1억개가 되면 메모리를 낭비하게 되어 매우 비효율적인 코드가 됩니다. 아래 예시에서는 nonlocal 키워드를 통해 더 효율적으로 구현합니다.

    def make_averager():
        count = 0
        total = 0
        
        def averager(new_value):
            nonlocal count, total 
            count += 1
            total += new_value
            return total / count
        
        return averager

    int, float, str, tuple 같은 자료형들은 불변형이기 때문에 값을 갱신할 수 없습니다. count += 1 같은 코드는 변수를 다시 바인딩하기 때문에 count라는 지역변수를 만들어서 계산하게 됩니다. 그러면 count가 함수 외부에서 참조되는 자유변수가 아니게 되므로 클로저에 저장되지 않게 됩니다. 이를 해결하기 위해 nonlocal 키워드를 사용합니다. 변수에 새로운 값을 할당해도 그 변수가 자유변수임을 나타냅니다.

     

     

    Basic of Decorator

      데코레이터는 함수 자체를 인자로 받는 callable 함수입니다. decorate된 어떤 함수에 작성한 코드를 수행하고, 함수를 리턴하거나 함수를 다른 함수나 callable 객체로 대체합니다. Decorator 문법은 골뱅이 기호 @ 를 통해 사용합니다. 소스 코드의 함수를 표시해서 함수의 작동을 개선할 수 있게 해줍니다.

     

    1. 일반적으로 데코레이터는 함수를 다른 함수로 대체합니다. 

    예제를 살펴보겠습니다. 

    def deco(func): # 함수 객체를 인자로 받습니다. 
        func() 
        def inner():
            print("inner() 함수를 실행중입니다.")
        return inner # 내부의 inner 함수 객체를 리턴합니다. 
    
    @deco
    def target():
        print('target() 함수를 실행중입니다. ')
    
    target()
    print(target)
    
    # Results
    # target() 함수를 실행중입니다. 
    # inner() 함수를 실행중입니다.
    # <function deco.<locals>.inner at 0x000002877E757948>

    그림과 같이 target 함수 객체를 받아서 호출을 하고 target() 함수를 실행한다는 메세지가 출력합니다. 

    그 후, deco 함수 내부에서 inner()를 정의하고 함수 객체를 리턴합니다. 

    마지막으로 inner가 호출되어 inner() 함수를 실행중이라는 메세지가 출력됩니다. 

    그래서 target을 print해보 deco 내부의 inner method를 가리키고 있음을 알 수 있습니다. 함수가 대체된 것이죠!! 

     

    Decorater의 실행시점

      데코레이터는 함수가 정의되고 바로 실행됩니다. 즉, 파이썬이 모듈을 로딩하는 시점인 임포트 타임에 실행됩니다. 이 때문에 일반적으로 실제 코드에서는 데코레이터를 정의하는 모듈과 적용하는 모듈을 분리해서 구현합니다. 

     

    예제: 함수실행시간 측정 데코레이터

    코딩테스트의 문제를 풀고 제출하게 되면 함수 호출부터 종료까지의 시간을 측정하게 됩니다. 예를 들어 피보나치 배열 계산 함수와 팩토리얼 계산 함수의 시간을 측정하는 코드를 아래와 같이 작성할 수 있습니다. 

     

    import time
    
    # value 까지의 피보나치 배열을 구하는 함수
    def fibo(value):
        fibo_dict = dict()
        a, b = 0, 1
        for item in range(1, value+1):
            a, b, = b, a + b
            fibo_dict[item] = a
        return fibo_dict
    
    # n! 을 구하는 함수
    def factorial(n):
        i = n
        while i > 1:
            i -= 1
            n *= i
        return n
    
    start = time.time()
    print(fibo(50))
    finish = time.time()
    print(f"fibo execution: {finish-start}")
    
    
    start2 = time.time()
    print(factorial(100))
    finish2 = time.time()
    
    print(f"factorial execution: {finish2-start2}")

     

    그런데 함수를 작성할 때마다 저렇게 time을 호출해서 쓰는 것은 귀찮고 가독성이 떨어집니다. 이때 데코레이터를 쓰면 코드를 더 좋게 작성할 수 있습니다. 그저 함수 상단에 데코레이터를 붙이면 끝입니다. 

     

    def clock(func):
        def clocked(*args):
            start = time.perf_counter()
            result = func(*args)
            finish = time.perf_counter()
            elapsed = finish - start
            func_name = func.__name__
            arg_str = ', '.join(repr(arg) for arg in args)
            print(f"[{elapsed:.5f}] {func_name}({arg_str}) -> {result}")
        return clocked
    
    @clock
    def fibo(value):
        fibo_dict = dict()
        a, b = 0, 1
        for item in range(1, value+1):
            a, b, = b, a + b
            fibo_dict[item] = a
        return fibo_dict
    
    
    @clock
    def factorial(n):
        i = n
        while i > 1:
            i -= 1
            n *= i
        return n
    
    
    @clock
    def snooze(seconds):
        time.sleep(seconds)
    
    
    fibo(50)
    factorial(100)
    snooze(0.5)

     

     

    References

    [1] Fluent Python, 2016

     

     

    'Programming > Python' 카테고리의 다른 글

    리스트 컴프리헨션 사용시 주의점  (0) 2022.08.18
    [Python] __all__ 의 역할  (0) 2022.08.09

    댓글

Designed by Tistory.