09.python高阶函数、柯里化、装饰器、无参装饰器、有参装饰器


高阶函数

一等公民

  • 函数在Python是一等公民(First-Class Object)
  • 函数也是对象,是可调用对象
  • 函数可以作为普通变量,也可以作为函数的参数、返回值

高阶函数

高阶函数(High-order Function)

  • 数学概念 y = f(g(x))
  • 在数学和计算机科学中,高阶函数应当是至少满足下面一个条件的函数
    • 接受一个或多个函数作为参数
    • 输出一个函数

观察下面的函数定义,回答问题

def counter(base):
    def inc(step=1):
        nonlocal base
        base += step
        return base
    return inc

if __name__ == '__main__':
    f=counter(12)
    print(f())
  • 请问counter是不是高阶函数?是
  • 上面代码有没有问题?如果有,如何改?有
  • 如何调用以完成计数功能?
  • f1 = counter(5)和f2=counter(5),请问f1和f2相等吗?
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 = f(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)
add(4, 5)(6)
add(4)(5)(6)

装饰器

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

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)
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 打印什么名称?