相信很多人在初次接触python中的装饰器时,会跟我一样有个疑问,这跟设计模式中的装饰器模式有什么区别吗?本质上是一样的,都是对现有对象,包括函数或者类的一种扩展。这篇文档将进行对比分析。
python的装饰器
装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。
使用一个性能测试的例子来说明。当我们需要测试一段训练或者推理的时长时,可能会写出类似这样的代码,直接在一个训练函数里加入时间统计
def train(X, y):
start = time.time()
knn = KNeighborsTimeSeriesClassifier(n_neighbors=2)
knn.fit(X, y)
end = time.time()
train_time = end - start
print('train time cost : %.5f sec' %train_time)
return knn
这是一段真实的代码片段,选自在某次训练任务过程中。当然如果不知道python的装饰器,在这个比较简短的函数体内,加入三行代码,其实也无伤大雅。但是既然山在那里,就要去攀登呀,因此强行上装饰器。
def getDuration(func):
def wrapper(*args, **kwargs):
print("%s is running" % func.__name__)
start = time.time()
result = func(*args, **kwargs)
end = time.time()
train_time = end - start
print('train time cost : %.5f sec' %train_time)
return result
return wrapper
@getDuration
def train(X, y):
# start = time.time()
knn = KNeighborsTimeSeriesClassifier(n_neighbors=2)
knn.fit(X, y)
# end = time.time()
# train_time = end - start
# print('train time cost : %.5f sec' %train_time)
return knn
在train上方加入@+函数名,即将train函数加上一层装饰器。train函数中原本记录时间的start和end,移到wrapper中。train的函数通过wrapper中的*args、**kwargs转发。
面向对象的装饰器模式
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。其结构图为
这个通用的结构看起来略显抽象,结合一个咖啡豆的例子来说明,其UML图为:
咖啡是由咖啡豆制作来的。而咖啡又根据糖分含量(或奶含量?)的比例不同分为美式、拿铁、摩卡、卡布奇洛等等。那不同的咖啡当然可以有咖啡这个类继承而来,但更灵活的方式是用装饰器模式。这里贴出这几个类的设计实现。
// 咖啡豆,抽象类,定义接口,可以显示咖啡类型和价格
class CoffeeBean
{
public:
virtual void ShowCoffeeName() = 0;
virtual void ShowPrice() = 0;
public:
std::string m_sCoffeeName;
int m_iPrice;
};
//对咖啡豆类的一份实现
class Coffee:public CoffeeBean
{
public:
Coffee(std::string name,int price)
{
m_sCoffeeName = name;
m_iPrice = price;
}
~Coffee() {}
void ShowCoffeeName()
{
std::cout << "CoffeeBean name:" << m_sCoffeeName << std::endl;
}
void ShowPrice()
{
std::cout << "CoffeeBean Price:" << m_iPrice << std::endl;
}
};
// 对咖啡豆抽象类的扩展,这个扩展类相当于在caffeebean类型上加上一个装饰器
// 的效果,不同的装饰器成为美式、拿铁和摩卡
class ExtendCoffee :public CoffeeBean
{
public:
ExtendCoffee(CoffeeBean* pBean)
{
m_pBean = pBean;
}
~ExtendCoffee(){}
virtual void ShowCoffeeName() = 0;
virtual void ShowPrice() = 0;
protected:
CoffeeBean* m_pBean;
};
// 美式的实现版本(通过装饰器的方式)
class Americano :public ExtendCoffee
{
public:
Americano(CoffeeBean* pBean):ExtendCoffee(pBean){}
~Americano() {}
void ShowCoffeeName()
{
std::cout << "I am Americano Coffee,Coffee name:" << m_pBean->m_sCoffeeName + " from American" << std::endl;
}
void ShowPrice()
{
m_pBean->m_iPrice = 48;
std::cout << "Americano Coffee price:" << m_pBean->m_iPrice << std::endl;
}
};
// 拿铁的实现版本(通过装饰器的方式)
class Latte :public ExtendCoffee
{
public:
Latte(CoffeeBean* pBean) :ExtendCoffee(pBean) {}
~Latte() {}
void ShowCoffeeName()
{
std::cout << "I am Latte Coffee,Coffee name:" << m_pBean->m_sCoffeeName + " from Italy" << std::endl;
}
void ShowPrice()
{
m_pBean->m_iPrice = 58;
std::cout << "Latte Coffee price:" << m_pBean->m_iPrice << std::endl;
}
};
// 摩卡的实现版本(通过装饰器的方式)
class Mocha :public ExtendCoffee
{
public:
Mocha(CoffeeBean* pBean) :ExtendCoffee(pBean) {}
~Mocha() {}
void ShowCoffeeName()
{
std::cout << "I am Mocha Coffee,Coffee name:" << m_pBean->m_sCoffeeName + " from Franch" << std::endl;
}
void ShowPrice()
{
m_pBean->m_iPrice = 68;
std::cout << "Mocha Coffee price:" << m_pBean->m_iPrice << std::endl;
}
};
这里的ExtendCoffee相当于抽象装饰,由此基础上实现了不同的装饰效果,即不同的咖啡类型。
优点
-
不改动原有代码,动态增加功能。
-
对象间不会相互依赖、松耦合。
-
符合开闭原则,扩展性好,便于维护。
缺点
-
装饰器环节过多的话,导致装饰器类膨胀。
-
装饰器层层嵌套比较复杂,可能导致排查问题流程繁琐。
参考文档
Python 函数装饰器
理解 Python 装饰器看这一篇就够了
C++装饰器模式
设计模式装饰器模式