浅谈Python中的装饰器

Python的装饰器,是Python函数(或者类)功能增强的一种方式。在了解装饰器之前,有必要先了解两个概念:闭包和柯里化。

闭包

Python的装饰器,实际上的闭包的应用。了解闭包是什么及其特性,请浏览这篇文章《理解Python闭包概念》。

柯里化

所谓的柯里化,先来看看它在维基百科上的解释:

柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

例子如下:

# add 函数,接收两个参数并返回和
def add(x,y):
    return x + y


# 将add函数柯里化
def add(x):
    def _add(y):
        return x + y
    return _add

# 调用:
t = add(4)
t(5) # 输出 9

通过嵌套函数,把函数柯里化了。柯里化函数,其实就是闭包的应用。或者反过来说也成立,利用柯里化机制的函数,就是闭包函数。二者相辅相成。

了解了柯里化和闭包的概念,我们就可以来看看装饰器的概念了。

装饰器

Python的装饰器是为函数进行功能增强的一种方式。试想下有这么一个需求:

实际业务中,某些关键性函数必须增加打印日志的功能。

为了降低代码的耦合性,打印功能不应该糅合到其他业务功能的代码中,而是作为一个增强性的功能而存在。此时,就不应该侵入到指定的函数增加输出日志的代码,而应该把它抽象出来,作为一个功能增强的装饰而存在。

我们知道,为了实现代码重用,可以把代码抽象为函数的形式。那装饰器是不是也是函数?是的。

本质上,装饰器就是一个函数(或者类),它接收函数作为参数,并返回另一个函数对象。因此,装饰器是一种高阶函数,它实现了对传入函数的功能的增强(装饰)。

无参装饰器

下面来看看例子,是如何使用装饰器:

需求:一个加法函数,想增强它的功能,能够输出被调用过以及调用的参数信息。

# 原函数
def add(x,y):
	return x + y
	
# 增加信息输出功能 v1
def add(x,y):
	print("call add, x + y") # 日志输出到控制台
	return x + y

上述的v1版加法函数满足了需求,但存在以下缺点:

  • 打印语句的耦合度太高
  • 加法函数属于业务功能,而输出信息的功能则属于非业务功能代码,不应该放到业务函数中。否则改变了业务功能函数的属性。

我们只需要,在调用add函数前,把打印语句先打印出来就可以了。比如:

def add(x,y):
    return x + y

def logger(fn,x,y):
    print('args: {}.{}'.format(x,y))
    ret = fn(x,y)
    print('call finished!')
    return ret

print(logger(add,4,5))

更加通用的实现:

def add(x,y):
    return x + y

def logger(fn,*args,**kwargs): # 这里存在多个参数,可以做柯里化
    print('args: {},{}'.format(*args,*kwargs))
    ret = fn(*args,**kwargs)
    print('call finished!')
    return ret

print(logger(add,4,5))

对logger柯里化:

def add(x,y):
    return x + y

def logger(fn):
    def _logger(*args,**kwargs):
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    print('call finished!')
    return _logger


print(logger(add)(4,5))

注意:logger(add)返回的是_logger,而_logger函数返回的是fn,也就是add的调用结果。因此,实际上_logger函数指向了新的add函数。也就是说logger(add) -> _logger -> add

因此,可以如此表示add = logger(add),使用语法糖@,变成了以下形式:

def logger(fn):
    def _logger(*args,**kwargs):
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    print('call finished!')
    return _logger

@logger  # 语法糖,用于装饰器函数。它相当于执行了add = logger(add)
def add(x,y):
    return x + y

print(add(4,5))

业务函数好比一幅画,而装饰器则比作画外部的画框,以实现不同的装饰。比如,前置功能增强或者后置功能增强。

装饰器的本质是使用非侵入式代码进行功能增强。当然,此功能即使不使用,也不应该影响原来的业务功能。

副作用

经过logger装饰的add函数,实际上已经不是原来的add函数了,它已经变成了装饰器函数。因此带来一些副作用。

看下面的例子:

def logger(fn):
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    print('call finished!')
    return wrapper

@logger
def add(x,y):
    'This is a function for add'
    return x + y

print("name={},doc={}".format(add.__name__,add.__doc__))	

输出:

call finished!
name=wrapper,doc=I am wrapper 

add函数的文档字符串改变了。原对象的属性都被替换了。实际上,我们关心的是被包装函数的属性(原add函数),而无关装饰器。

因此,需要考虑将需要的属性,从被包装函数,覆盖到包装函数中。

def copy_properties(src,dest):
    dest.__name__ = src.__name__
    dest.__doc__ = src.__doc__
    # ... 还有许多属性

def logger(fn):
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    copy_properties(fn,wrapper)
    print('call finished!')
    return wrapper

@logger
def add(x,y):
    'This is a function for add'
    return x + y

print("name={},doc={}".format(add.__name__,add.__doc__))

输出:

call finished!
name=add,doc=This is a function for add

我们应该知道,上述的copy_properties函数,实际上就是wrapper函数功能的增强,那是否可以把它改造成装饰器?

带参装饰器

def copy_properties(src):
    def _inner(dest):
        dest.__name__ = src.__name__
        dest.__doc__ = src.__doc__
        return dest
    return _inner

def logger(fn):
    @copy_properties(fn)  # wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    print('call finished!')
    return wrapper

@logger
def add(x,y):
    'This is a function for add'
    return x + y

print("name={},doc={}".format(add.__name__,add.__doc__))

因为装饰器携带参数,因此成为带参装饰器。上述的例子,是一个应用在装饰器里的装饰器。

functools模块

functools.update_wrapper

上述带参装饰器的例子中,写一个copy_properties函数来指定原函数需要传递的属性值,这种方式不够优雅。functools.update_wrapper正好解决了这个问题。

先看看它的构造函数:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    for attr in assigned: # 以元组的形式
        try:
            value = getattr(wrapped, attr) # 相当于value = wrapped[attr]
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value) # 把原属性值,追加到wrapper. wrapper[attr] = value
    for attr in updated: # 以字典的形式,attr为key
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    wrapper.__wrapped__ = wrapped
    return wrapper
  • wrapper是包装函数,wrapped是被包装函数
  • 元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性
  • 元组WRAPPER_UPDATES是要被更新的属性,__dict__属性字典
  • 增加一个__wrapped__属性,保留着wrapped被包装函数

因此,可以如此使用它:

import functools

def logger(fn):

    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    functools.update_wrapper(wrapper,fn) # 更新属性
    print('call finished!')
    return wrapper

@logger
def add(x,y):
    'This is a function for add'
    return x + y

print("name={},doc={}".format(add.__name__,add.__doc__))

是否可以把functools.update_wrapper改造成装饰器?

functools的另一个工具可以解决这个问题。

functools.wraps

源码:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

这里简单说明下偏函数的作用

functools.partial把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

上述例子中,wraps 相当于update_wrapper(wrapped,assigned,updated)。当然它需要传递wrapped函数作为参数,因此它是一个带参装饰器。

偏函数把wrapped=wrapped,assigned=assigned, updated=updated这些参数固定住了,并传递给update_wrapper.

因此,可以如此使用它:

def logger(fn):

    @functools.wraps(fn)
    def wrapper(*args,**kwargs):
        'I am wrapper'
        print('args: {},{}'.format(*args, *kwargs))
        ret = fn(*args,**kwargs)
        return ret
    print('call finished!')
    return wrapper

此时,add.__wrapped__实际上就是原add,也就是fn了。这保留了wrapped函数。

你可能感兴趣的:(python初识)