도비LOG(跳飛錄)

도비의 AI 엔지니어 도전기

Python

[전문가를 위한 파이썬] 7장 함수 데커레이터와 클로저 내용 정리

나쁜도비 2024. 6. 10. 16:27

7.1. 데커레이터 기본 지식

- 데커레이터: 다른 함수를 인수로 받는 콜러블(데커레이트된 함수)

- 데커레이터는 데커레이트된 함수에 어떤 처리를 수행하고, 함수를 반환하거나 함수를 다른 함수나 콜러블 객체로 대체한다.

 

# decorate 라는 이름의 데커레이터가 있다고 가정하자.
@decorate
def target():
    print("running target()")
    
# 위 코드는 아래 코드와 동일하게 동작한다.
def target():
    print("running target()")
target = decorate(target)

 

- 두 코드를 실행한 후 target은 꼭 원래의 target() 함수를 가리키는 것이 아니며, decorate(target)이 반환한 함수를 가리키게 된다.

- 데커레이터는 데커레이트된 함수를 다른 함수로 대체하는 능력이 있다.

- 데커레이터는 모듈이 로딩될 때 바로 실행된다.

 

 

7.2. 파이썬이 데커레이터를 실행하는 시점

- 데커레이터는 데커레이트된 함수가 정의된 직후에 실행된다. 일반적으로 임포트 타임에 실행된다.

- 함수 데커레이터는 모듈이 임포트 되자마자 실행되지만, 데커레이트된 함수는 명시적으로 호출될 때(런타임)만 실행된다.

 

 

7.3. 데커레이터로 개선한 전략 패턴

- promotion 데커레이터로 채운 promos 리스트

promos = []

def promotion(promo_func):
    promos.append(promo_func)
    return promo_func
    
@promotion
def fidelity(order):
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0
    
@promotion
def bulk_item(order):
    discount = 0
    for item in order.cart:
        discount += item.total() * .1
    return discount
    
@promotion
def large_order(order):
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0
    
def best_promo(order):
    return max(promo(order) for promo in promos)

- promotion() 데커레이터는 promo_func를 promos 리스트에 추가한 후 그대로 반환한다.

- @promotion으로 데커레이트한 함수는 모두 promos 리스트에 추가된다.

 

 

7.4. 변수 범위 규칙

- 함수 내에서 전역 변수에 값을 할당하려면 global을 사용해야 한다.

 

 

7.5. 클로저

- 클로저: 함수 본체에서 정의하지 않고 참조하는 비전역(nonglobal) 변수를 포함한 확장 범위를 가진 함수

# 이동평균을 계산하는 고위 함수
def make_averager():
    
    #### 클로저 ####
    series = [] # 자유변수
    
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)
    #### 클로저 ####
    
    return averager

- make_averager()를 호출하면 average() 함수 객체를 반환한다. average() 함수는 호출될 때마다 받은 인수를 series 리스트에 추가하고, 현재까지의 평균을 계산해서 출력한다.

- make_averager() 함수 본체 안에서 series = [] 로 초기화하고 있으므로, series는 이 함수의 지역범수이다. 그렇지만 avg(10)을 호출할 때 make_averager() 함수는 이미 반환햇으므로 지역 범위도 이미 사라진 후다.

- average 안에 있는 series는 자유 변수(free variable)다. 즉, 지역 범위에 바인딩되어 있지 않은 변수이다.

- 클로저는 함수를 정의할 때 존재하던 자유 변수에 대한 바인딩을 유지하는 함수다. 따라서 함수를 정의하는 범위가 사라진 후에 함수를 호출해도 자유 변수에 접근할 수 있다.

 

 

7.6. nonlocal 선언

- 변수를 nonlocal로 선언하면 함수 안에서 변수에 새로운 값을 할당하더라도 그 변수는 자유 변수임을 나타낸다.

- 새로운 값을 nonlocal 변수에 할당하면 클로저에 저장된 바인딩이 변경된다.

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        nonlocal count, total
        count += 1 # nonlocal 선언을 하지 않고 count += 1을 하게 되면 에러 발생
        total += new_value
        return total / count
        
    return averager

 

 

 

7.7. 간단한 데커레이더 구현하기

- 함수의 실행 시간을 출력하는 데커레이터

import time

def clock(func):
    def clocked(*args): # 내부함수 clocked()가 임의 개수의 위치 인수를 받을 수 있도록 정의한다.
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ", ".join(repr(arg) for arg in args)
        print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, arg_str, result))
        return result
    return clocked # 내부함수를 반환해서 데커레이트된 함수를 대체한다.
    
    ########
    # clockdeco_demo.py
    import time
    from clockdeco import clock
    
    @clock
    def snooze(seconds):
        time.sleep(seconds)
        
    @clock
    def factorial(n):
        return 1 if n < 2 else n*factorial(n-1)

 

 

7.7.1. 작동 과정

@clock 
def factorial(n):
    return 1 if n < 2 else n*factorial(n-1)

위 코드는 실제로 다음 코드로 실행된다.

factorial = clock(factorial)

- clock() 은 factorail() 함수를 func 인수로 받는다. 그 후 clocked() 함수를 만들어서 반환하는데, 파이썬 인터프리터가 내부적으로 clocked()를 factorial에 할당한다.

- clocked() 함수는 다음과 같은 연산을 수행한다.

1. 초기 시각 t0를 기록한다.

2. 원래의 factorail() 함수를 호출하고 결과를 저장한다.

3. 흘러간 시간을 계산한다.

4. 수집한 데이터를 출력한다.

5. 2번 단계에서 저장한 결과를 반환한다.

 

- 위 clock() 데커레이터의 단점

    - 키워드 인수를 지원하지 않는다.

    - 데커레이트된 함수의 __name__과 __doc__ 속성을 가린다.

- 아래의 clock() 데커레이터는 functools.wraps() 데커레이터를 이용해서 clocked로 관련된 속성을 복사해준다.

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
        return result
    return clocked

 

 

 

7.8. 표준 라이브러리에서 제공하는 데커레이터

7.8.1. functools.lru_cache()를 이용한 메모이제이션

- 메모이제이션은 이전에 실행한 값비싼 함수의 결과를 저장함으로써 이전에 사용된 인수에 대해 다시 계산할 필요가 없게 해준다. 

- lru는 Least Recently Used 를 의미한다. 

 

from clockdeco import clock
import functools

@functools.lru_cache() # lur_cache() 데커레이터를 일반 함수처럼 호출해야 한다는 점에 주의.
@clock # 데커레이터 누적. clock() 에 의해 반환된 함수에 lru_cache()가 적용된다.
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

 

 

7.8.2. 단일 디스패치를 이용한 범용 함수

- 파이썬 객체의 자료형마다 HTML 코드를 생성하는 함수

- functools.signledispatch() 데커레이터로 일반 함수를 데커레이트하면, 이 함수는 범용 함수(generic function)가 된다. 즉, 일련의 함수가 첫 번재 인수의 자료형에 따라 서로 다른 방식으로 연산을 수행하게 된다.

from functools import signedispatch
from collections import abc
import numbers
import html

@singledispatch #객체형을 다룰 기반 함수를 표시한다.
def html(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)
    
@htmlize.register(str) # 각각의 특화된 함수는 @<기반함수>.register(<객체형>) 으로 데커레이트된다.
def _(text): # 특화된 함수의 이름은 필요없으므로 언더바로 처리한다.
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)
    
@htmlize.register(numbers.Integral) # 특별하게 처리할 자료형을 추가할 때마다 새로운 함수를 등록한다.
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)
    
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence) # 동일한 함수로 여러 자료형을 지원하기 위해 register 데커레이터를 여러 개 쌓아올릴 수 있다.
def _(seq):
    inner = '</li>]n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n<ul>'

 

 

 

7.9. 누적된 데커레이터

- 하나의 함수 f() 에 대해서 두 데커레이터 @d1과 @d2를 차례대로 적용하면, 결과는 f = d1(d2(f))와 동일하다.

 

 

7.10. 매개변수화된 데커레이터

- 소스 코드에서 데커레이터를 파싱할 때 파이썬은 데커레이트된 함수를 가져와서 데커레이터 함수의 첫 번째 인수로 넘겨준다. 

- 다른 인수를 받는 데커레이터를 만들기 위해서는, 인수를 받아 데커레이터를 반환하는 데커레이터 팩토리를 만들고 나서, 데커레이트될 함수에 데커레이터 팩토리를 적용하면 된다.

 

 

7.10.1. 매개변수화된 등록 데커레이터

- 데커레이터 팩토리 만들기

registry = set() # 함수의 추가와 제거를 빠르게 하기 위해 집합형으로 정의

def register(active=True): # 선택적 인수 키워드를 받는다.
    def decorate(func): # decorate() 내부함수가 실제 데커레이터
        print('running register(active=%s)->decorate(%s)' % (active, func))
        if active: 
            registry.add(func)
        else:
            registry.discard(func)
        
        return func
    return decorate
    
@register(active=False)
def f1():
    print('running f1()')
    
@register() # 인수를 전달하지 않더라도 함수로 호출해야 하므로 @register() 형태로 호출한다.
def f2():
    print('running f2()')

- 핵심은 register()가 decorate()를 반환하고, 데커레이트될 함수에 decorate()가 적용된다는 것이다.

 

 

728x90