Computer Engineering/Fluent Python 정리

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

jordan.bae 2022. 1. 23. 20:49

Chapter1의 Introduction 부분에서 이야기 한 것 처럼 지난 5년간 다양한 언어나 소프트웨어를 공부하고 이용하여 소프트웨어를 개발했는데 이것 저것 하다보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올 해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한해를 만들고 싶은 소망을 가지고 있습니다. 21장 까지 정리를 성공하고 맛있는걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요!

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

 

 

Chapter7 Introduction

Chapter7 에서는 Decorator와 Closure를 살펴봅니다. Decorator를 이해하려면 아래와 중요한 개념들을 이해해야 해서 많은 것들을 생각하고 공부할 수 있었던 Chapter였던 것 같습니다.

 

- import time과 runtime

- 변수 범위

- Closure

- nonlocal

 

해당 chapter에서는 decorator가 무엇이고 위에 개념들이 decorator와 어떻게 관련되어있는지를 잘 설명해주고 있어서 이 부분을 잘 정리해 보겠습니다.

 

Meta Programming (메타 프로그래밍)

Decorator를 살펴보기에 앞서 메타프로그래밍이 무엇이길래 decorator가 메타프로그래밍을 구현하는 방법 중 하나인지를 살펴보겠습니다.

위키에서 설명하는 메타 프로그래밍에 대한 정의를 보면 다음과 같습니다. (개인적으로 한글 내용이 잘 이해가 되지 않아서 영어를 번역해봤습니다.)

메타 프로그래밍은 컴퓨터 프로그램이 다른 컴퓨터 프로그램을 데이터와 같이 처리하는 능력을 가지도록 
프로그래밍하는 방법(technique)이다.
이것은 프로그램이 프로그램이 수행하는 도중에 다른 프로그램을 읽고, 생성하고, 분석하고 또는 변형 및 수정하는
것을 의미한다.

- 영문 해석

출처: https://en.wikipedia.org/wiki/Metaprogramming

 

Decorator는 함수를 인자(데이터)로 받아서 runtime에 프로그램을 변형합니다. 그래서 메타프로그래밍 방법 중에 하나라고 할 수 있습니다. (혹시, 제가 잘못 생각하고 있다면 편하게 댓글로 알려주세요!) 

 

참고로 Fluent Python 책에서는 메타 프로그래밍을 아래와 같이 설명합니다.

자신에 대한 런타임 정보를 이용해서 자신의 행동을 변경하는 프로그램을 작성하는 기법.
예를 들어 ORM은 모델 클래스 선언을 조사해서 데이터베이스 레코드 필드를 검증할 방법을 결정하고 데이터
베이스 자료형을 파이썬 자료형으로 변환할 수 있다.

 결과적으로 어떤 프로그램(함수나 클래스 메서드등)을 runtime에 변경하는 프로그램(자기가 자기 자신을 변경할 수도 있음.)을 작성하여 프로그래밍하는 방법이라고 이야기 할 수 있을 것 같습니다.

 

 

Decorator

Decorator를 한 문장으로 정의하면 저는 다음과 같이 정리해보고 싶습니다.

다른 함수를 인수로 받는 callable의 syntatic sugar(편리 구문)

 

코드로 살펴보겠습니다.

# 함수를 인자로 받는 데코레이터 함수
def deco(func):
    def inner():
        print("running inner()")
    return inner

# 데코레이터 표현식
@deco
def target():
    print('running taget()')

# runtime에 target함수의 동작을 변경. (메타프로그래밍)
target()
running inner()

# target이 inner() 함수를 가리키고 있음.
target
<function __main__.deco.<locals>.inner()>

 

 

import time 과 runtime

Decorator의 가장 큰 특징 중 하나는 파이썬이 모듈을 로딩하는 시점인 import time에 실행된다는 것입니다.

Decorator함수는 모듈이 임포트 되지마자 실행되지만  Decorate 된 함수는 명시적으로 호출될 때 실행됩니다.(runtime)

이런 특징을 이용해서 지난 6장에서 Strategy Pattern에서 best promo list를 생성하는 코드를 수정할 수 있습니다.

from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')


class LineItem:

    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price

    def total(self):
        return self.price * self.quantity


class Order:  # the Context

    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = list(cart)
        self.promotion = promotion

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion(self)
        return self.total() - discount

    def __repr__(self):
        fmt = '<Order total: {:.2f} due: {:.2f}>'
        return fmt.format(self.total(), self.due())

# BEGIN STRATEGY_BEST4

promos = []  # <1>

def promotion(promo_func):  # <2>
    promos.append(promo_func)
    return promo_func

@promotion  # <3>
def fidelity(order):
    """5% discount for customers with 1000 or more fidelity points"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item(order):
    """10% discount for each LineItem with 20 or more units"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount

@promotion
def large_order(order):
    """7% discount for orders with 10 or more distinct items"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0

def best_promo(order):  # <4>
    """Select best discount available
    """
    return max(promo(order) for promo in promos)

promotion decorator함수가 세 개의 함수를 decorate하고 있으므로 import를 하게되면 3번이 바로 실행이 된다. 그렇기 때문에 각 함수가 실행이 되지 않더라도 best_promo()를 실행했을 때 promos에 모든 promo strategy들이 append되어 있다.

 

 

변수 범위

파이썬은 함수 내에 지역 변수가 있으면 지역 변수를 참조하고 없으면 전역 변수에서 찾습니다. 그리고 함수 내에서 전역변수를 읽을 수는 있지만 선언 없이 변경할 수 없습니다. (파이썬은 지역변수를 선언하는 것으로 생각하기 때문)

# 1
b = 6

def func2(a):
    print(a)
    print(b)

func2(2)
2
6

# 2

b = 6

def func2(a):
    print(a)
    print(b)
    b = 1

func2(2)
2
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
/var/folders/_k/w0m1fxfx2nngqbf5r4sp65lr0000gn/T/ipykernel_8966/4034409858.py in <module>
      6     b = 1
      7 
----> 8 func2(2)

/var/folders/_k/w0m1fxfx2nngqbf5r4sp65lr0000gn/T/ipykernel_8966/4034409858.py in func2(a)
      3 def func2(a):
      4     print(a)
----> 5     print(b)
      6     b = 1
      7 

UnboundLocalError: local variable 'b' referenced before assignment

# 3
b = 6

b = 6

def func2(a):
    global b
    print(a)
    print(b)
    b = 1

func2(2)
b

2
6
1

첫 번째 코드에서는 global 변수 b를 참조하지만, 두 번재 코드에서 파이썬은 함수를 컴파일 할 때 함수 안에 b가 있기 때문에 지역 변수를 가져오려고 하고 할당 전에 참조하려고 해서 에러가 발생합니다. 글로벌 변수를 함수 내에서 변경해주기 위해서는 global keyword를 사용하면 됩니다.

 

Closure 

책에서는 closure의 정의를 혼동하는 사람들이 많다고 소개 하면서 설명합니다. 이번에도 정리해서 한 문장으로 얘기해보면 Closure는 함수 내에 정의하지 않은 nonglobal변수를 포함하여 일반 함수보다 확장된 함수라고 정의할 수 있을 것 같습니다.

아래 averager라는 함수를 보면 어떻게 nonglobal 변수를 포함하는지 이해할 수 있습니다.

def make_averager():
    series = []
    
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager
    
    
avg = make_averager()
avg(10)
10.0

avg(11)
10.5

 

averager라는 함수는 어디에 series에 대한 정보를 가지고 있는지를 살펴보겠습니다ㅏ. 먼저, series와 같이 지역 변수 범위에 포함되지 않은 변수는 free variable이라고 합니다. 그리고 __closure__ 라는 속성에서 free variable에 접근할 수 있습니다.

avg.__code__.co_varnames
('new_value', 'total')

avg.__code__.co_freevars
('series',)

avg.__closure__
(<cell at 0x106d06b80: list object at 0x106da5a00>,)

avg.__closure__[0].cell_contents
[10, 11]

 

nonlocal 선언

closure의 free variable을 변경하기 위해서는 global 변수를 함수 내에서 update할 때 사용한 것 처럼 nonlocal keyword를 사용해야합니다. nonlocal keyword를 사용하지 않으면 할당 전에 참조를 했다는 에러가 발생합니다.

# nonlocal keyword 없을 때
def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        count +=1
        total += new_value
        return total/count
    return averager
    
avg = make_averager()
avg(10)

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
/var/folders/_k/w0m1fxfx2nngqbf5r4sp65lr0000gn/T/ipykernel_1211/4007330935.py in <module>
      1 avg = make_averager()
----> 2 avg(10)

/var/folders/_k/w0m1fxfx2nngqbf5r4sp65lr0000gn/T/ipykernel_1211/1904653741.py in averager(new_value)
      4 
      5     def averager(new_value):
----> 6         count +=1
      7         total += new_value
      8         return total/count

UnboundLocalError: local variable 'count' referenced before assignment


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

avg = make_averager()
avg(10)
10

 

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

파이썬에서는 메서드 decorator를 위한 3개의 내장 함수를 제공합니다.

- property()

- classmethod()

- staticmethod()

 

그리고 또 하나의 자주 볼수 있는 데커레이터로는 functools.wraps()가 있습니다. 이 함수는 제대로 동작하는 decorator를 만들기 위한 헬퍼함수입니다. (decorator된 함수의 __name__과 __doct__속성을 복사해준다.)

 

또, 굉장히 유용한 decorator로 functools.lru_cache()가 있다. 메모이제이션을 구현 한 decorator로 함수의 결과를 저장함으로써 이전에 사용된 인수에 대해 다시 계산을 할 필요가 없게 해준다.

Ipython의 %time command를 이용해서 성능을 측정했다.

def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

%time fibonacci(35)
CPU times: user 2.93 s, sys: 9.29 ms, total: 2.94 s
Wall time: 2.95 s
9227465


import functools

@functools.lru_cache()
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

%time fibonacci(35)
CPU times: user 24 µs, sys: 1 µs, total: 25 µs
Wall time: 27.9 µs
9227465

 

파이썬에서는 메서드나 함수의 overriding을 지원하지 않으므로, 서로 다르게 처리하고자 하는 자료형별로 서로 다른 로직을 가진 함수를 만들 수 없다. 하지만, generic function을 만들 수 있도록 지원해주는 functools.singledispatch() decorator이 있다.

from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch  # <1>
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)  # <2>
def _(text):            # <3>
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)  # <4>
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

@htmlize.register(tuple)  # <5>
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

 

누적된 decorator

두 개의 decorator를 사용하고 싶으면 다음과 같이 표현할 수 있다.

@d1
@d2
def f():
	print('f')
    
# 위의 함수는 아래와 같다.
f = d1(d2(f))

 

매개 변수를 가지고 있는 decorator

매개 변수를 가지고 있는 decorator를 만들기 위해서는 decorator를 만드는 factory 함수를 만들면 된다.

함수 자체가 decorator를 return한다.

registry = set()  # <1>

# decorator를 만드는 factory함수
def register(active=True):  # <2>
    def decorate(func):  # <3>
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:   # <4>
            registry.add(func)
        else:
            registry.discard(func)  # <5>

        return func  # <6>
    return decorate  # <7>

@register(active=False)  # <8>
def f1():
    print('running f1()')

@register()  # <9>
def f2():
    print('running f2()')

def f3():
    print('running f3()')

 

정리

introduction에서 이야기 한 것 처럼 decorator를 이해하기 위해서 많은 개념들을 공부했다.

다시 한 번 살펴 보면 아래와 같다.

- import time과 runtime

- 변수 범위

- Closure

- nonlocal

이 외에도 내장 decorator와 functools package에서 지원하는 decorator도 살펴봤다.

 

 

참조

- Fluent Python Chapter7

- https://github.com/fluentpython/example-code/

- https://en.wikipedia.org/wiki/Metaprogramming

 

제가 정리하는 글이 도움이 된다면 응원 댓글이나 좋아요를 해주시면 큰 응원이 됩니다!

읽어주셔서 감사합니다.

 

 

반응형