timerring

Organizing Code with Design Patterns

August 17, 2023 · 20 min read
Tutorial
Python
If you have any questions, feel free to comment below. Click the block can copy the code.
And if you think it's helpful to you, just click on the ads which can support this site. Thanks!

好的设计模式方便对代码进行功能扩展/修改,降低耦合性,提高复用性。

from abc import abstractmethod, ABC
from typing import Callable, final, Tuple, List, Sequence, Dict
from random import choices, randrange
from functools import wraps
from numpy import ndarray
from pandas import DataFrame
from time import perf_counter, sleep
from datetime import datetime, timedelta

代码设计模式:合理组织代码 #

这部分为进阶内容,需要注意的是设计模式往往在复杂的项目中最有效。

为什么需要设计模式 #

  1. 方便对代码进行功能扩展/修改,降低耦合性
    更改某一处功能,不会(较小)地影响其他代码逻辑,最小范围内修改代码

  2. 提高复用性
    好的设计模式可以较好的解耦代码,因此拆解出来的功能模块可以方便的迁移,提高编程效率

  3. 同样提升代码可读性
    好的设计模式,使得不同功能模块间的关系更加清晰,方便理解代码

什么是设计模式 #

一句话概括:为了解决某种问题,或者实现某种目的,而探索出的特定的编程规范(套路/预案),将代码/功能模块组织起来的特定的编程形式。

设计模式如何使用 #

设计模式有可能比较难以理解,但学习设计模式主要在于领悟其思想、了解使用场景,而不是机械地记忆所有模式的实现。具体的方法可以在需要的时候在查看相关资料。

单例模式 #

使类只能实例化一次;避免反复实例化浪费资源和运行时间,比如:

  • 数据库的链接池,往往一个就够了
  • 加载资源: 读取模型文件/加载数据 ,加载一次就够了

避免在不知情的情况下同一份数据被加载多次

单例模式的实现有三种,这里介绍最简单,使用最广泛的装饰器方法实现单例。

def singletonDecrator(cls, *args, **kwargs): # 如果还不是很理解装饰器,可以先用起来;单例装饰器的写法是定死的
    '''
    将类单例化
    '''

    ins_dict = {} # 定义一个key为类,value为实例的dict;wrapper_singleton在全局被调用时,成为函数闭包
    
    @wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if cls not in ins_dict: # 如果类不在dict里,则实例化后添加入dict
            ins_dict[cls] = cls(*args, **kwargs)

        return ins_dict[cls] # 从ins_dict里按类取对应的实例对象

    return wrapper_singleton


@singletonDecrator
class ModelServing:

    def __init__(self):
        self.model = self._load_model()
        self.random = randrange(1, 10) # 用于检测类是否进行了多次实例化

    def _load_model(self):
        ...

    def serving(self):
        ...
model_serve = ModelServing()
model_serve.random

模板模式 #

先确定类的职责,以及类之间的联系,在此之后再实现类的具体逻辑
将 思考类的职责以及类的交互方式 这件事抽离出来,作为一个单独的任务去做

先制作各零件的模具,确定各零件组装接口 (联系),然后再开始正式制造 工程逻辑越复杂,模板模式越重要;纯数据科学有可能用不上,但是涉及到工程落地就需要了

场景:

  • 有多个模型需要部署,制作一个模型服务类封装了一些方法: 加载训练好的模型,预测,储存预测结果的方法
  • 传统机器学习和深度学习模型加载方有区别(sklearn, tensorflow)
  • 先写一个模板类ModelServing确定类职责,以及各接口名称和调用方式;
  • 然后针对 sklearn, tensorflow 模型,分别继承模板类重写 _load_model 方法
# 主要展示,模板模式的使用方式
# 模板模式,用于确定类的职责边界以及交互关系,会在后续内容中提到
# from abc import abstractmethod, ABC
# from typing import final


class ModelServing(ABC):
# class ModelServing:
    def __init__(self):
        self.model = self._load_model()
    
    @abstractmethod # 对于虚化的方法,必须继承后重写,不重写无法实例化
    def _load_model(self): # 不同的框架下产生模型文件,加载方式不同
        ...

    @abstractmethod
    def serving(self, feature:ndarray):
        ...

    @final
    def SaveToDb(self, res): # 将推断结果储存到数据库
        ...

    
class SklearnModelServing(ModelServing):
    
    def _load_model(self): 
        # 直接读取文件
        ...

    def serving(self, feature: ndarray):
        print('sklearn模型推断中')
        # return self.model.predict(feature)


class TfModelServing(ModelServing):
    
    def _load_model(self): 
        # 加载计算图
        # 加载图上的常量和变量的值
        ...

    def serving(self, feature: ndarray):
        print('tf模型推断中')
     

model_serving_ins = SklearnModelServing()
model_serving_ins.serving(...)
  1. 模板类是用于确定类职能边界和类之间的交互方式的;因此模板类只被继承,不能被直接实例化使用,将类虚化达成此目的
  2. abstractmethod修饰方法,用于提示这个方法的具体实现会有变化,每个继承模板类的实现类必须重写该方法;
  3. 继承 ABC 类, 使得 abstractmethod 的限制得以生效

工厂模式 #

  • 简单工厂
  • 工厂方法
  • 抽象工厂

将对象的构造,和实例化过程封装起来;用户只需要传入简单的参数/简单的操作,即可完成原本复杂的对象实例化操作,操作和复杂的构造进行解耦,二者独立发展,当构造的逻辑更改了,使用方无需任何变动。

工厂模式是比较难以理解的,且在数据科学里用的不多,但是为了完整性还是要介绍下工厂模式。

简单工厂 #

以咖啡机举例:按下相应的按钮(传入简单参数/简单操作) -> 咖啡机内部工作(对象复杂的构造) -> 得到相应咖啡(得到实例化对象)。

# 一个咖啡机的例子

class Coffee(ABC): # 咖啡模板

    def __init__(self, name: str):
        self.name = name

    @final
    def get_name(self):
        return self.name

    @abstractmethod
    def get_taste(self):  # 每种咖啡的味道不一样
        ...

class Latte(Coffee):
    
    def __init__(self):
        super().__init__('拿铁咖啡')

    def get_taste(self):
        print('轻柔香醇')


class Mocha(Coffee):
    
    def __init__(self):
        super().__init__('摩卡咖啡')    
    
    def get_taste(self):
        print('丝滑醇厚')


class CoffeeFactory: # 工厂类

    @staticmethod
    def make_coffee(coffe_name: str):
        
        if coffe_name == '摩卡':
            coffe = Mocha()

        elif coffe_name == '拿铁':
            coffe = Latte()

        else:
            raise Exception(f'咖啡机暂无法制作{coffe_name}!')

        print('处理中...')
        print(f'{coffe.get_name()}制作好了')

        return coffe

        
coffe = CoffeeFactory.make_coffee('拿铁')
coffe.get_taste()
# 处理中...
# 拿铁咖啡制作好了
# 轻柔香醇

简单工厂: 专门定义一个类来构造需要的类,通过传入参数来判断构造哪一个种类的实例,简单工厂不易扩展,当需求较为固定/实例化对象数量较少时可以使用。

工厂方法 #

类的种类/个数变动过快,使用简单工厂难以扩展;(有的口味咖啡机上没有,添加一个咖啡机) 每一个对象对应一个自己的工厂方法(相当于每个咖啡机只制作一种咖啡,购置多个咖啡机)

class Coffee(ABC):

    def __init__(self, name: str):
        self.name = name

    @final
    def get_name(self):
        return self.name

    @abstractmethod
    def get_taste(self):
        ...
        

class Macchiatto(Coffee):
    
    def __init__(self):
        super().__init__('玛奇朵')    
    
    def get_taste(self):
        print('玛奇朵的味道')


class Cappuccino(Coffee):
    
    def __init__(self):
        super().__init__('卡布奇诺')    
    
    def get_taste(self):
        print('卡布奇诺的味道')


class CoffeeFactory(ABC):

    @staticmethod
    @abstractmethod
    def make_coffee(coffe_name: str):
        ...


class MacchiattoFactory:

    @staticmethod
    def make_coffee():
        print('处理中...')
        coffe = Macchiatto()
        print(f'{coffe.get_name()}制作好了')

        return coffe


class CappuccinoFactory:

    @staticmethod
    def make_coffee():
        print('处理中...')
        coffe = Cappuccino()
        print(f'{coffe.get_name()}制作好了')

        return coffe
coffee = MacchiattoFactory.make_coffee()  
coffee.get_taste()  
coffee = CappuccinoFactory.make_coffee()  
coffee.get_taste()

# 处理中...
# 玛奇朵制作好了
# 玛奇朵的味道
# 处理中...
# 卡布奇诺制作好了
# 卡布奇诺的味道

工厂方式的优劣式比较明显

优势:

  1. 功能增加,无需修改原有代码(保持稳定性),而是增加一个新的工厂方法来实现类的实例化 (新购置一个咖啡机)
  2. 封装复杂的实例化过程,有时一个类的实例化比较复杂(制作咖啡的过程比较复杂)

劣势:
每增加一个不同的类,就要增加一个对应的工厂方法(每一种咖啡都新增一个咖啡机);这一点由抽象工厂解决。

抽象工厂方法 #

当构造的类差异过大、类的种类/个数变动过快;

  • 将类分群,同样类型的类各自拥有自己的工厂方法;(现磨的和速溶的分别对应两个咖啡机)。
  • 而工厂方法是每个类都对应一个工厂方法 (每一种咖啡对应一个咖啡机)。
class Coffee(ABC): # 咖啡模板类

    def __init__(self, name: str):
        self.name = name

    @final
    def get_name(self):
        return self.name

    @abstractmethod
    def get_taste(self):
        ...

class InstantLatte(Coffee): # 速溶拿铁
    
    def __init__(self):
        super().__init__('速溶拿铁咖啡')

    def get_taste(self):
        print('轻柔香醇,但是速溶')


class InstantMocha(Coffee): # 速溶摩卡
    
    def __init__(self):
        super().__init__('速溶摩卡咖啡')    
    
    def get_taste(self):
        print('丝滑醇厚,但是速溶')


class GroundLatte(Coffee): # 现磨拿铁

    def __init__(self):
        super().__init__('现磨拿铁咖啡')
        self.bean = '拿铁专用咖啡豆'

    def get_taste(self):
        print('轻柔香醇,不亏是现磨')    


class GroundMocha(Coffee): # 现磨摩卡
    
    def __init__(self):
        super().__init__('现磨摩卡咖啡')    
        self.bean = '摩卡专用咖啡豆'

    def get_taste(self):
        print('丝滑醇厚,不亏是现磨')
class CoffeeFactory(ABC): # 咖啡机模板

    @staticmethod
    @abstractmethod
    def make_coffee(coffe_name: str):
        ...


class InstantCoffeeFactory(CoffeeFactory):

    @staticmethod
    def make_coffee(coffe_name: str):
        if coffe_name == '摩卡':
            coffe = InstantMocha()

        elif coffe_name == '拿铁':
            coffe = InstantLatte()

        else:
            raise Exception(f'咖啡机暂无法制作{coffe_name}!')

        print('处理中...')
        print(f'{coffe.get_name()}制作好了')

        return coffe


class GroundCoffeeFactory(CoffeeFactory):

    @staticmethod
    def make_coffee(coffe_name: str):
        if coffe_name == '摩卡':
            coffe = GroundMocha()

        elif coffe_name == '拿铁':
            coffe = GroundLatte()

        else:
            raise Exception(f'咖啡机暂无法制作{coffe_name}!')

        print('处理中...')
        print(f'研磨{coffe.bean}...') # 增加了研磨的处理流程
        print(f'{coffe.get_name()}制作好了')

        return coffe

不同的工厂类(咖啡机)对应不同的类(现磨和速溶咖啡),有不同的构造方式(make_coffee 的逻辑不同)。

coffee = InstantCoffeeFactory.make_coffee('摩卡')
coffee.get_taste()

# 处理中...
# 速溶摩卡咖啡制作好了
# 丝滑醇厚,但是速溶
coffee = GroundCoffeeFactory.make_coffee('摩卡')
coffee.get_taste()

# 处理中...
# 研磨摩卡专用咖啡豆...
# 现磨摩卡咖啡制作好了
# 丝滑醇厚,不亏是现磨

只看咖啡的例子是不够的,还需要放到数据科学场景下观察。

场景: 用户分类模型,对不同类别的用户进行不同的处理(有些类别是重点客户,信息要及时推送;有些类型的客户要喂入另外的piepeline)

特点:

  1. 同一类用户的处理方式可能会有变化(原来是存入数据库的,现在还需要发邮件通知运营人员)
  2. 同一类用户,对应着多个的处理方式(有可能既要通知,还需要对此类用户进行进一步分析/预测)
  3. 所有的方式可以分为两类;
    一类为简单处理,例如邮件通知,数据储存;
    另一类为复杂处理,例如获取用户相关数据进行异常检测等
  4. 简单处理逻辑简单且不易改变;复杂处理逻辑复杂且容易产生变化,难以维护容易出bug
  5. 用户类别有可能变化,但概率不大;即使变化,用户类别数量也不会很多

设计:

  1. 两个工厂类,分别用于简单处理和复杂处理;
  2. 每一个用户处理方法(邮件通知得等等)构造一个类,提供统一的接口,供工厂方式调用
  3. 不再以 字符串关键字来选择对应的类进行实例化(输入咖啡名称,得到咖啡);而是:
    1. 在工厂类实例化时,将处理方法实例注入;
    2. 每个用户类别的处理提供固定的接口

Related readings


<< prev | Data Structures... Continue strolling Data... | next >>

If you want to follow my updates, or have a coffee chat with me, feel free to connect with me: