标题中的接口名称是函数的统称,指的是函数。
在开发业务中如果遇到流程类场景,通过会有多个分支流程。一般我们都是为每个分支写一个流程代码,实现功能。但是这样的写法在每个分支中会存在一些重复代码,不利于维护。
上面这类的场景我们使用挂钩函数来开发是一个不错的选择,代码结构简单,支持左开右闭原则,易扩展和维护。
装饰器是一个面向切面编程的方式,通常加载目标函数上,当运行目标函数前每次都会自动调用装饰器函数。特别适合为目标函数执行前后增加额外辅助功能。
装饰器特点:
钩子函数是作为参数传入目标函数,当运行目标函数时只有符合预期条件时才会调用钩子函数,通过钩子函数改变目标函数执行逻辑,输出结果。
钩子函数特点:
下面通过一些使用示例先带大家体验下使用挂钩函数开发的好处,然后在下面会介绍如何在在开发中创建符合业务的挂钩函数,将它的便利带到业务中。
python有许多内置的函数,都支持挂钩函数,允许我们传入一个函数来定制它的行为。作为参数的函数称为挂钩函数。
大家对挂钩函数的叫法也不同,比如回调函数,钩子函数,其实本质上指的都是同一个实质。
下面用python内置的defaultdict函数来介绍下挂钩函数使用例子。
这个示例是通过挂钩为defaultdict函数定制不同的行为,默认情况下defaultdict函数遇到不存在的key会返回一个默认值,我们为默认值提供一个挂钩函数。
defaultdict函数挂上挂钩函数后,遇到不存在的键就输出log_missing字符串。
from collections import defaultdict
# 定义挂钩函数
def log_missing():print('log_missing')return 0current = {'green':12, 'blue':13}
increments = [('red', 1),('blue', 2),('orange', 3),]# defaultdict方法第一个参数是key不存在时返回一个默认值,第二个参数是向字典添加数据
result = defaultdict(log_missing, current)
print(f'Before: {dict(result)}')
# 遍历列表,获取元组数据 key=red, amount=1
for key, amount in increments:# 当访问字典中key不存在时候调用调用log_missing函数返回结果,log_missing就是一个钩子函数。result[key] += amount
print(f'after:{dict(result)}')
运行上面的代码,在访问result字典key时red和orange两个键不存在,执行了挂钩函数,输出了两次log_missing字符串
Before: {'green': 12, 'blue': 13}
log_missing
log_missing
after:{'green': 12, 'blue': 15, 'red': 1, 'orange': 3}
上面的挂钩函数是一个简单没有状态的函数,如果下面需求希望统计defaultdict函数一共遇到了多少次键缺失的情况,实现这个功能,可以采用闭包来实现有状态的挂钩函数。
def increment_with_report(current, increments):added_count = 0# 闭包处理键不存在的逻辑def missing():nonlocal added_countadded_count += 1return 0# 创建字典,第一个参数调用闭包处理键缺失逻辑,第二个参数向字典添加数据result = defaultdict(missing, current)for key, amount in increments:# 访问字典中的key,如果不存在就调用闭包函数result[key] += amountreturn result, added_countresult, count = increment_with_report(current, increments)
print(f'result:{dict(result)}, count:{count}')
运行上面的代码,统计出了key缺失的次数。
result:{'green': 12, 'blue': 15, 'red': 1, 'orange': 3}, count:2
上面使用闭包实现了有状态的挂钩函数,increment_with_report函数由于嵌套了闭包代码也复杂了,同时不易扩展。
下面使用类来设计有状态挂钩函数
把有状态的闭包所具备的行为,改用辅助类来实现,要比前面increment_with_report函数更加清晰。
在类中使用了call魔法方法,可使这个类的对象能够像函数那样调用(defaultdict(counter, current)),即便初次看到这段代码也能明白这个类的主要目标。因为你应该会注意到这比较明显的call方法,暗示这个类可以项有状态闭包那样使用。
当我们使用类创建挂钩函数时,通过call函数实现了让接口(defaultdict函数)接收函数而不是类的实例。
# 通过类构建有状态的挂钩函数
class BetterCountMissing:def __init__(self):self.added = 0# 挂钩函数核心代码def __call__(self):self.added +=1return 0counter = BetterCountMissing()# call方法使得类的对象能够像函数那样调用,直接将对象传入,他会自动执行call方法内容。
result = defaultdict(counter, current)
for key, amount in increments:result[key] += amountprint(f'result:{dict(result)}, count:{count}')
前面通过几个小示例展示了如何使用钩子函数,并且体会到了通过钩子函数改变了defaultdict的行为,当defaultdict需要不同的行为时,我们只需要为他传入对应的挂钩函数即可,不需要修改业务代码。实现了左开右闭原则,同时代码维护变得简单了许多。
当我们在开发过程中如果遇到分支流程时就可以考虑使用钩挂函数解决,下面通过一个示例介绍如何将挂钩设计思想带入业务。
下面创建一个getOddNumber函数,它的作用和python内置的defaultdict函数角色是一样的接收一个挂钩参数。为该函数传入不同的挂钩函数实现定制它的行为。
# 回调函数1
# 生成一个2k形式的偶数
def double(x):return x * 2# 回调函数2
# 生成一个4k形式的偶数
def quadruple(x):return x * 4# 目标函数:相当于defaultdict函数角色
# 接受一个挂钩函数作为参数
# 返回一个奇数
def getOddNumber(k, getEvenNumber):return 1 + getEvenNumber(k)k = 1
# 当需要生成一个2k+1形式的奇数时
i = getOddNumber(k, double)
print(f'2k+1形式的奇数:{i}')
# 当需要一个4k+1形式的奇数时
i = getOddNumber(k, quadruple)
print(f'4k+1形式的奇数:{i}')
# 当需要一个8k+1形式的奇数时
i = getOddNumber(k, lambda x: x * 8)
print(f'匿名钩子函数扩展功能:{i}')
运行上面的代码
2k+1形式的奇数:3
4k+1形式的奇数:5
匿名钩子函数扩展功能:9