Computer Engineering/Fluent Python 정리

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

jordan.bae 2022. 1. 18. 21:00

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. 일급 함수

 


Chapter6 Introduction

6장은 일급 함수를 사용함으로써 디자인 패턴을 보다 효율적으로 구현하는 방법을 소개합니다. Design Patterns책의 저자인 Peter Norvig은 '동적 언어에서의 디자인 패턴' 발표에서 Design Patterns에서 정의한 23개의 패턴 중 16개는 동적 언어에서 '보이지 않거나 더 단순하다'고 설명했다고 합니다.

 

특히 일급 함수를 지원하는 언어에서는 Strategy, Command, Template, Visitor Pattern은 다시 생각해보라고 권고했습니다. Pattern에 참여하는 일부 클래스의 객체를 간단한 함수로 교체하면, 획일적으로 반복되는 코드의 상당 부분을 줄일 수 있다라고 소개했습니다.



Strategy Pattern 리팩토링

Strategy Pattern은 파이썬에서 Strategy(행위, 알고리즘)을 일급 객체인 함수로 사용하면 더욱 간단해질 수 있는 디자인 패턴의 대표적인 사례입니다.

 

Strategy Pattern은 Context와 Strategy(행위, 알고리즘)을 분리해서 만들어서 Client에 따라서 알고리즘을 독립적으로 변경할 수 있습니다. 책에서 나온 예제를 살펴보면 아래와 같습니다.

 

온라인 상점이 다음과 같은 할인 규칙을 갖고 있다.

- 충성도 포인트가 1,000점 이상인 고객은 전체 주문에 대해 5%할인을 적용.

- 하나의 주문에서 20개 이상의 동일 상품을 구입하면 해당 상품에 대해 10%할인을 적용.

- 서로 다른 상품을 10종류 이상 주문하면 전체 주문에 대해 7%할인을 적용.

 

원래 Strategy Pattern 방식으로 구현한 코드

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.discount(self)
        return self.total() - discount

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


class Promotion(ABC):  # the Strategy: an Abstract Base Class

    @abstractmethod
    def discount(self, order):
        """Return discount as a positive dollar amount"""


class FidelityPromo(Promotion):  # first Concrete Strategy
    """5% discount for customers with 1000 or more fidelity points"""

    def discount(self, order):
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):  # second Concrete Strategy
    """10% discount for each LineItem with 20 or more units"""

    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * .1
        return discount


class LargeOrderPromo(Promotion):  # third Concrete Strategy
    """7% discount for orders with 10 or more distinct items"""

    def discount(self, order):
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * .07
        return 0

 

함수 지향 전략 (strategy 부분을 함수로 구현)

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())


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


def bulk_item_promo(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


def large_order_promo(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

 

전략이 하나의 메서드를 가진 클래스고 속성을 가지지 않기 때문에 이런 경우 함수로 대체하면 더 적은 코드로 동일한 기능을 구현할 수 있다.

 

 

Strategy Pattern 리팩토링에서 가장 좋은 전략을 선택하는 '메타 전략' 만들기

여러 Strategy 중에 가장 할인율이 높은 Meta Strategy를 만들 때 다양한 방법으로 Strategy들의 리스트를 가져오는 방법을 소개한다.

# 제일 단순한 버전 -> Promotion이 추가될 때 마다 promos list에 추가가 필요함.

promos = [fidelity_promo, bulk_item_promo, large_order_promo]  # <1>

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

# globals 내장 함수를 이용해서 구현.
promos = [globals()[name] for name in globals()  # <1>
            if name.endswith('_promo')  # <2>
            and name != 'best_promo']   # <3>

def best_promo(order):
    """Select best discount available
    """
    return max(promo(order) for promo in promos)  # <4>
    
    
# 모듈화해서 모듈 내부를 조사해서 promos 리스트 만들기

import promotions

promos = [func for name, func in
                inspect.getmembers(promotions, inspect.isfunction)]

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

 

 

Command Pattern

함수를 인수로 전달하는 기법을 사용하면 Command Pattern도 구현을 단순하게 만들 수 있다.

Command Pattern은 Invoker(실행자)에게 Command를 전달해서 Invoker는 단순히 해당 Command가 가진 Receiver의 동작을 실행시킬 수 있게 된다.(invoker는 어떤걸 실행하는지는 몰라도 된다.) 즉, 호출자는 수신자의 인터페이스를 알 필요가 없으며, 명령의 Subclass를 통해 서로 다른 수신자를 추가할 수 있다.

 

파이썬에서 command.execute()를 호출하는 대신 Command class의 __call__() 매직메서드를 구현해서 command()를 호출하면 되도록 구현할 수 있다.

 

class MacroCommand:
    """ 명령 시트를 실행하는 명령"""
    def __init__(self, commands):
        self.commands = list(commands)
    
    def __call__(self):
        for command in self.commands:
            command()

 

 

정리

두 가지 Pattern을 리팩토링 한 부분을 상위 수준에서 살펴보면 단일 메서드 인터페이스를 구현하는 클래스의 객체를 callable 로 대체한 것이다. 파이썬의 함수가 일급함수라는 성질과 class의 객체를 callable로 사용할 수 있는 것을 이용해서 디자인 패턴을 조금 더 간결하게 구현할 수 있다는 것을 소개하고 싶었던 것 같다. 파이썬은 자바나 C#같은 언어와 비교해서 design pattern 책이 많이 적다고 한다. 최근에 Design Pattern책이 하나 출간된 것 같은데 살펴봐야 겠습니다.



 

반응형