Scribbling

Python: Design Patterns 본문

Computer Science/Python

Python: Design Patterns

focalpoint 2022. 3. 25. 15:34

 

Some traditional design patterns can be simplified by making good use of first-order functions in python.

Let's see the below example.

 

1. Goal: Calculate total price and discount for each order

2. There are three discount strategies;

- fidelitypromo: if the customer's fidelity >= 1000, then 5% of the total

- bulkitempromo: if q >= 20 for an item, then 10% of the item

- largeorderpromo: if type >= 10, then 7% of the total

 

We can implement such strategies as below in a traditional sense.

from abc import ABC, abstractmethod
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.quantity * self.price


class Order:

    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 == None:
            discount = 0
        else:
            discount = self.promotion.discount(self)
        return self.total() - discount

    def __repr__(self):
        return f"Order total: {self.total():.2f} due: {self.due():.2f}"


class Promotion(ABC):

    @abstractmethod
    def discount(self, order):
        """ returns discount """


class FidelityPromo(Promotion):

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


class BulkItemPromo(Promotion):

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


class LargeOrderPromo(Promotion):

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


joe = Customer('John', 0)
ann = Customer('Ann', 1100)

cart = [LineItem('banana', 4, 0.5), LineItem('apple', 10, 1.5), LineItem('watermellon', 5, 5.0)]
print(Order(joe, cart, FidelityPromo()))
print(Order(ann, cart, FidelityPromo()))
print(Order(joe, cart, BulkItemPromo()))


 

Above code works fine. However, we can make it concise with python functions.

+ When initiating 'Order object', we use 'Promotion objects'. It means we have to create 'Promotion object' everytime we initiate 'order object', which is a waste. We can reuse 'Promotion object', but it will cost us a longer and complicated code.

 

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.quantity * self.price


class Order:

    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 == None:
            discount = 0
        else:
            discount = self.promotion(self)
        return self.total() - discount

    def __repr__(self):
        return f"Order total: {self.total():.2f} due: {self.due():.2f}"


def fidelity_promo(order):
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

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

def large_order_promo(order):
    distinct_items = {item.product for item in order.cart}
    return order.total() * 0.1 if len(distinct_items) >= 10 else 0



joe = Customer('John', 0)
ann = Customer('Ann', 1100)

cart = [LineItem('banana', 4, 0.5), LineItem('apple', 10, 1.5), LineItem('watermellon', 5, 5.0)]
print(Order(joe, cart, fidelity_promo))
print(Order(ann, cart, fidelity_promo))
print(Order(joe, cart, bulk_item_promo))

 

Above code is more concise and readable. On top of that, it is more flexible. Imagine if we should add a new function "best_promo" which returns the largest discount for an order. In the previous code, it would be much difficult. On the other hand, it can be implemented as simple as below.

def best_promo(order):
    promos = [fidelity_promo, bulk_item_promo, large_order_promo]
    return max((promo(order) for promo in promos))



joe = Customer('John', 0)
ann = Customer('Ann', 1100)

cart = [LineItem('banana', 4, 0.5), LineItem('apple', 10, 1.5), LineItem('watermellon', 5, 5.0)]
print(Order(joe, cart, best_promo))
print(Order(ann, cart, best_promo))

 

Another tip for the above code is that we can automatically update 'promos list' as below and by doing so it will less likely cause bugs when we implement additional promotion strategies. 

def best_promo(order):
    promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promo']
    return max((promo(order) for promo in promos))

 

The same can be achieved with modules as below.

def best_promo(order):
    import inspect
    import promotions
    promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
    return max((promo(order) for promo in promos))

 

Decorator may provide a nicer solution.

promos = []

def promotion(promo_func):
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity_promo(order):
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulk_item_promo(order):
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * 0.1
    return discount

@promotion
def large_order_promo(order):
    distinct_items = {item.product for item in order.cart}
    return order.total() * 0.1 if len(distinct_items) >= 10 else 0

def best_promo(order):
    return max(promo(order) for promo in promos)

 

 

'Computer Science > Python' 카테고리의 다른 글

Python: Object References  (0) 2022.04.04
Python: Decorator & Closure  (0) 2022.03.29
Python: Memoryview function  (0) 2022.03.23
Python Immutable Dictionary, Set  (0) 2022.03.22
Python Data Structures: Sequences  (0) 2022.03.21