python装饰器模式

横切关注点指的是一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效的模块化的一类特殊关注点。

装饰器提供了不用和继承结构绑定的定义功能的方法。可以用装饰器实现程序中的某个方面,让后将装饰器应用于类、方法或者函数。

横切关注点的设计通常见于日志、审计和安全相关。因为这些行为需要横跨多个模块。我们可以使用不同的装饰器来实现不同的横切需求。

内置装饰器

常用的用于标注类方法的内置装饰器有 @property 、 @classmethod 、@staticmethod。

@property用于方法上时,会额外创建一些属性。他们可以控制赋值、删除和获取的动作。

@classmethod 和 @staticmethod可以将一个方法函数转换成一个类级函数。被装饰的方法可以用类调用,而不是实例对象。被@classmethod 装饰的方法第一个传入的参数时当前的类,而@staticmethod没有显示的引用。

class Area:
    @staticmethod
    def from_width_height(w,h):
        return Area(w,h)
    def __new__(cls,w,h):
        self = super().__new__(cls)
        self._area = w*h
        return self
    @property
    def area(self):
        return self._area

        
>> a = Area.from_width_heght(10,20)
>> a.area
200

标准库中也提供了很多装饰器。比如contextlib、functools、unittest等模块都包含了横切方面的经典范例装饰器

例如functools中的 @total_ordering装饰器,它定义了一些列比较运算符,可以让我们不用定义全部的比较运算函数就可以实现一套完整的比较运算。

import functools

@functools.total_ordering
class Person:
    def __init__(self,age):
        self.age = age
        
    def __eq__(self, other):
        return self.age == other.age
        
    def __lt__(self,other):
        return self.age < other.age
    
>> xiaoming = Person(10)
>> xiaohong = Person(20)
>> xiaoming == xiaohong
Out[33]: False
>> xiaoming > xiaohong
Out[34]: False
>> xiaoming <= xiaohong
Out[36]: True

mixin类

如果使用多重继承来创建横切面,可以使用一个基类加上一个mixin类的方式来引入新的功能。通常使用mixin类来创建横切面。

mixin类实际上是抽象基类,其中实现了一些通用的方法,使用mixin类可以使代码易读并且使横切面以一致的方式定义。

比如我们想要实现一个上下文管理器,那么可以使用 contextlib 中的 ContextDecorator mixin。

上下文管理器即是实现了 __enter__和__exit__方法的类 ,可以使用 with 语句来管理上下文。

这个例子仅为理解。

from contextlib import ContextDecorator
class Person(ContextDecorator):
    def __init__(self,age,name):
        self.age = age
        self.name = name
    def __enter__(self):
        return self.__dict__
    def __exit__(self, exc_type, exc_value, traceback):
        pass
    
>> with Person(10,'xiaoming') as d:
>>     print(d)
{'age': 10, 'name': 'xiaoming'}

创建装饰器

函数装饰器

当一个函数被装饰过后,原有的函数名和doc会被装饰器中的相应信息所替代。要避免这个问题,可以使用functools.wraps装饰器。

假设我们有一些函数,需要在执行前打印参数信息,执行后打印结果信息,那么正常代码是这样的:

loggin.debug("function(",args,kw,")")
result = func(*args,**Kw)
loggin.debug("result="result)
return result

如果有很多这样的函数,使用这样的方式会出现很多重复的代码,并且会让逻辑不清晰,使用装饰器可以很好的解决这个问题。

import functools
import logging
def debug(func):
    @functools.wraps(func)
    def logged(*args,**kwargs):
        # 为每个函数使用特定的日志记录器
        log = logging.getLogger(func.__name__)
        log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        result = func(*args,**kwargs)
        log.debug("%s = %r",func.__name__,result)
        return result
    return logged

@debug
def my_sum(x,y):
    return x+y

带参数的装饰器

带参数的装饰器意在修改外边的封装函数。

@decorator(arg)
def func():
    pass

# 上面的代码的正常版本是这样的
def func():
    pass
func = decorator(arg)(func)

所以带参数的装饰器是这样的结构:

def decorator(config):
    def concrete_decorator(func):
        @functools.wraps(func)
        def wrapped(*args,**kwargs):
            return func(*args,**kwargs)
        return wrapped
    return concrete_decorator

下面是一个上面日志装饰器的进阶版,支持设置日志记录器名称:

def debug_named(log_name):
    def concrete_decorator(func):
    	@functools.wraps(func)
    	def wrapped(*args,**kwargs):
        	# 为每个函数使用特定的日志记录器
        	log = logging.getLogger(log_name)
        	log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        	result = func(*args,**kwargs)
        	log.debug("%s = %r",func.__name__,result)
        	return result
        return wrapped
    return concrete_decorator

方法函数装饰器

方法函数装饰器和单独的函数装饰器是一样的,只是在不同的上下文中使用,所以需要显式的声明self。

假设我们将上面的函数用作与方法函数上,只需要进行一点更改:

def debug(func):
    @functools.wraps(func)
    # 添加self
    def logged(self,*args,**kwargs):
        # 为每个函数使用特定的日志记录器
        log = logging.getLogger(func.__name__)
        log.debug("%s(%r, %r)",func.__name__,args,kwargs)
        # 添加self
        result = func(self,*args,**kwargs)
        log.debug("%s = %r",func.__name__,result)
        return result
    return logged

类装饰器

和函数装饰器类似,也可以写一个类装饰器,用来添加功能。他接受一个类作为参数,返回一个类作为返回值。

从技术上说,创建一个封装了原始类的类是可以的,但包装类本身必须是非常通用的;也可以创建一个被装饰类的子类,但是这样会导致使用者非常困惑;而删除类中的一些功能是最不可取的做法。

@functools.total_ordering装饰器是创建lambda对象并将他们赋值给类属性。

假如我们现在希望每个类有自己的日志记录器,那么我们会这么做:

class MyClass:
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__qualname__)

这样的作法缺点是他创建了一个对象,他不属于类操作的一部分但却是类的一个独立方面的logger实例。这样的额外方法会污染类,并且每次实例化时都会有额外的消耗。

我们可以把logger从实例方法中拿出来,放到类属性中,这样可以避免每次实例化的消耗:

class MyClass:
    logger = logging.getLogger('MyClass')
    def __init__(self):
        self.logger.info('start...')

但这样依旧缺少灵活性。因为logger在类级属性中,这时类还没有被创建,无法获取到我们需要的类名信息(self.__class__.__qualname__),没办法自动设置类名。

但是用类装饰器可以解决这个问题:

def logged(class_):
    class_.logger = logging.getLogger(class_.__qualname__)
    return class_

@logged
class C:
    def __init__(self):
        self.logger.info('start...')

假设我们现在需要通过装饰器向类中创建新的方法函数,那么实际上是分为两步的:

  1. 创建方法
  2. 将他插入到类中

通常情况下,mixin类比装饰器更好用。但是存在特殊情况,比如total_ordering装饰器,它可以根据类中已经提供了什么方法,来灵活的插入方法。

假如我们想定义一个标准的功能,并把这个功能添加到多个不同的类中,那么用装饰器的方法可以这样:

# 装饰器即闭包,闭包是一个独立命名空间,所以下面的这种命名方式不会导致命名冲突

def print_class_name(class_):
    def print_class_name(self):
        print "class name is {0.__class__.__qualname__}".format(self)
    class_.print_class_name = print_class_name
    return class_

@print_class_name
class C:
    pass

但是这样有个问题,我们不能通过重载print_class_name()方法来处理特殊的情况。如果我们要扩展它,那么必须要将他升级为可调用对象。

如果我们决定要升级它,那么倒不如使用一个mixin类向类中添加方法。这是使用继承的机制,所以我们可以灵活的更改他。

class PrintClassName:
    def print_class_name(self):
        print "class name is {0.__class__.__qualname__}".format(self)

class C(PrintClassName):
    pass

装饰器和mixin的目的是为了分离业务相关的功能和通用的功能,例如安全、审计、日志。这其实也是一种设计模式。

继承和不属于继承的横切关注点(装饰器和mixin),在设计上是有却别的:

  1. 继承的功能是显式设计的一部分,他们是实现业务的一部分
  2. mixin或装饰器他们是定义一个对象如何工作,实现的是通用功能的部分。

继承和横切关注点(装饰器和mixin)之间的分别有时并不明显。由于大家看待问题的切入点不同,在一些情况下通常是根据个人的习惯或者审美来决定的。

方法通常是从类中定义创建的,他们是主类或者mixin类的一部分。我们可以通过封装来引入新的功能,但在一些情况下,我们会发现需要暴露一些只是简单委托给底层类的方法,这种情况使用装饰器或者mixin可能是更好的选择。