装饰器

高阶函数

高阶函数满足下面一个条件

  • 接受一个或多个函数作为参数

  • 输出一个函数

  • 函数也是对象,是可调用对象

  • 函数可以作为普通变量,也可以作为函数的参数、返回值

def counter(base):
    def inc(step=1):
        base += step
        return base
    return inc
def counter(base):
    def inc(step=1): # 有没有闭包?
        nonlocal base # 形参base也是外部函数counter的local变量
        base += step
        return base
    return inc
c1 = counter(5)
print(c1())
print(c1())
print(c1())
f1 = counter(5)
f2 = counter(5)
print(f1 == f2) # 相等吗? 不相等
柯里化
  • 指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数
  • z = x(x,y) 转换成z = f(x)(y)的形式

例如:

def add(x,y)
    return x+y

原来函数调用为 add(4, 5) ,柯里化目标是 add(4)(5) 。如何实现?

每一次括号说明是函数调用,说明 add(4)(5) 是2次函数调用。

add(4)(5)
等价于
t = add(4)
t(5)

也就是说add(4)应该返回函数。

def add(x):
    def _add(y):
        return x + y
    return _add
add(100, 200)

通过嵌套函数就可以把函数转成柯里化函数。

def add(x, y, z):
    return x + y + z
# 练习,对add柯里化后,可以分别得到下面三种调用方式
#
add(4)(5, 6)
def add(x):
    def mm(y,z):
        return x+y+z
    return mm
#
add(4, 5)(6)
def add(x,y):
    def dd(z):
        return x+y+z
    return dd
#
add(4)(5)(6)
def add(x):
    def dd(y):
        def mm(z):
            return x+y+z
        return mm
    return dd

装饰器

由来

需求:为一个加法函数增加记录实参的功能

def add(x, y):
    print('add called. x={}, y={}'.format(x, y)) # 增加的记录功能
    return x + y
add(4, 5)

上面的代码满足了需求,但有缺点:

记录信息的功能,可以是一个单独的功能。显然和add函数耦合太紧密。加法函数属于业务功能,输出信息属于非功能代码,不该放在add函数中

1、提供一个函数logger完成记录功能

def add(x,y):
    return x+y
def logger(fn):
    print('调用前增强')
    ret = fn(4,5)
    print('调用后增强')
    return ret
print(logger(add))

2、改进传参

def add(x, y):
    return x + y
def logger(fn, *args, **kwargs):
    print('调用前增强')
    ret = fn(*args, **kwargs) # 参数解构
    print('调用后增强')
    return ret
print(logger(add, 4, 5))

3、柯里化

def add(x, y):
    return x + y
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper

调用

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

或者

inner = logger(add)
x = inner(4, 5)
print(x)

再进一步

def add(x, y):
    return x + y
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper
add = logger(add)
print(add(100, 200))

4、装饰器语法

def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add) #等价于add = logger(add)   ,logger相当于把下面的函数add以参数的形式传到logger函数中,并且覆盖add
def add(x, y):
    return x + y
print(add(100, 200))

@logger就是装饰器语法

无参装饰器
  • 上例的装饰器语法,称为无参装饰器
  • @符号后是一个函数
  • 虽然是无参装饰器,但是@后的函数本质上是单参函数
  • 上例的logger函数是一个高阶函数
日志装饰器实现
import time
import datetime
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    time.sleep(2)
    return x + y
print(add(100, 200))
文档字符串
  • Python文档字符串Documentation Strings
  • 在函数(类、模块)语句块的第一行,且习惯是多行的文本,所以多使用三引号
  • 文档字符串也算是合法的一条语句
  • 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
  • 可以使用特殊属性__doc__访问这个文档
def add(x, y):
    """这是加法函数的文档"""
    return x + y
print("{}'s doc = {}".format(add.__name__ , add.__doc__))
import time
import datetime
def logger(fn):
    def wrapper(*args, **kwargs):
        "wrapper's doc"
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add's doc"""
    time.sleep(0.1)
    return x + y
print("name={}, doc={}".format(add.__name__ , add.__doc__))

被装饰后,函数名和文档都不对了。如何解决?

functools模块提供了一个wraps装饰器函数,本质上调用的是update_wrapper,它就是一个属性复制函数。

wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

  • wrapped就是被包装函数
  • wrapper就是包装函数
  • 用被包装函数的属性覆盖包装函数的同名属性
  • 元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性
    • __module__ , __name__ , __qualname__ , __doc__ , __annotations__
    • 模块名、名称、限定名、文档、参数注解
import time
import datetime
from functools import wraps
def logger(fn):
    @wraps(fn) # 用被包装函数fn的属性覆盖包装函数wrapper的同名属性
    def wrapper(*args, **kwargs): # wrapper = wraps(fn)(wrapper)
        "wrapper's doc"
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add's doc"""
    time.sleep(0.1)
    return x + y
print("name={}, doc={}".format(add.__name__ , add.__doc__))
带参装饰器
  • @之后不是一个单独的标识符,是一个函数调用
  • 函数调用的返回值又是一个函数,此函数是一个无参装饰器
  • 带参装饰器,可以有任意个参数
    • @func()
    • @func(1)
    • @func(1, 2)
import datetime
from functools import wraps
def logger(fn):
    @wraps(fn) # 用被包装函数fn的属性覆盖包装函数wrapper的同名属性
    def wrapper(*args, **kwargs): # wrapper = wraps(fn)(wrapper)
        "wrapper's doc"
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add function"""
@logger
def sub(x, y):
    """sub function"""
print(add.__name__, sub.__name__)
  • logger什么时候执行?
  • logger执行过几次?
  • wraps装饰器执行过几次?
  • wrapper的 __name__ 等属性被覆盖过几次?
  • add.__name__ 打印什么名称?
  • sub.__name__ 打印什么名称?