案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import time import functools
class DelayFunc: def __init__(self, duration, func): self.duration = duration self.func = func
def __call__(self, *args, **kwargs): print(f'Wait for {self.duration} seconds...') time.sleep(self.duration) return self.func(*args, **kwargs)
def eager_call(self, *args, **kwargs): print('Call without delay') return self.func(*args, **kwargs)
def delay(duration): """ 装饰器:推迟某个函数的执行。 同时提供 .eager_call 方法立即执行 """ return functools.partial(DelayFunc, duration)
@delay(duration=2) def add(a, b): return a + b
print(add)
print(add(3, 5))
print(add.eager_call(1, 2))
|
提问:为啥这个 add 函数名会给自动传给 DelayFunc 的 init 函数中的 func 而不是 duration?duration 和 add 传过去时,python 是怎么区分的?
分析
首先,@delay(duration=2) 这个装饰器表达式会被首先执行:
delay(duration=2) 返回了一个偏函数:
1 2
| functools.partial(DelayFunc, 2)
|
然后这个偏函数会作为装饰器作用在 add 函数上:
1 2 3 4
| add = functools.partial(DelayFunc, 2)(add)
add = DelayFunc(2, add)
|
所以完整的执行顺序是:
1 2 3 4 5
| delayed_func = delay(duration=2)
add = delayed_func(add)
|
用一个更直观的例子来说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import functools import time
def debug_decorator(): print("1. 装饰器函数被调用") class Wrapper: def __init__(self, func): print("3. Wrapper.__init__被调用,参数是:", func.__name__) self.func = func def __call__(self, *args, **kwargs): print("4. 调用被装饰的函数") return self.func(*args, **kwargs) print("2. 返回Wrapper类") return Wrapper
@debug_decorator() def hello(): print("Hello!")
print("5. 开始调用hello()") hello()
|
输出将是:
1 2 3 4 5 6
| 1. 装饰器函数被调用 2. 返回Wrapper类 3. Wrapper.__init__被调用,参数是: hello 5. 开始调用hello() 4. 调用被装饰的函数 Hello!
|
回到开始的例子,我们可以加入调试信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import time import functools
class DelayFunc: def __init__(self, duration, func): print(f"DelayFunc.__init__被调用:duration={duration}, func={func.__name__}") self.duration = duration self.func = func def __call__(self, *args, **kwargs): print(f'Wait for {self.duration} seconds...') time.sleep(self.duration) return self.func(*args, **kwargs)
def delay(duration): print(f"delay()被调用,参数duration={duration}") partial_delay = functools.partial(DelayFunc, duration) print("返回偏函数partial_delay") return partial_delay
@delay(duration=2) def add(a, b): return a + b
print("开始调用add(1, 2)") result = add(1, 2) print(f"结果:{result}")
|
执行这段代码,输出会类似:
1 2 3 4 5 6
| delay()被调用,参数duration=2 返回偏函数partial_delay DelayFunc.__init__被调用:duration=2, func=add 开始调用add(1, 2) Wait for 2 seconds... 结果:3
|
这就清晰地展示了整个过程:
delay(duration=2) 被调用,返回一个偏函数
- 这个偏函数被用来装饰
add,此时创建了 DelayFunc 实例
- 当我们调用
add(1, 2) 时,实际上是在调用 DelayFunc 实例的 __call__ 方法
Python 之所以能正确区分参数,是因为:
delay(duration=2) 创建了一个偏函数,这个偏函数已经固定了 DelayFunc 的第一个参数为 2
- 当这个偏函数被用作装饰器时,被装饰的函数
add 自动作为第二个参数传入
DelayFunc 的 __init__ 方法接收这两个参数,完成初始化
这就是为什么尽管看起来有点混乱,但参数还是能被正确地传递和识别。
偏函数在这里的应用:DelayFunc 类的初始化,需要两个参数,可以用偏函数来暂时固定一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import functools
class Person: def __init__(self, age, name): self.age = age self.name = name print(f"创建了一个{age}岁的{name}")
CreateChild = functools.partial(Person, 3)
child1 = CreateChild("小明") child2 = CreateChild("小红")
|
回到 DelayFunc 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import time import functools
class DelayFunc: def __init__(self, duration, func): self.duration = duration self.func = func print(f"初始化DelayFunc: duration={duration}, func={func.__name__}")
def print_some(self): print("print_some")
normal_delayed = DelayFunc(2, print)
Delay2Seconds = functools.partial(DelayFunc, 2) delayed_print = Delay2Seconds(print)
print("演示1: 创建等待3秒的偏函数") Delay3Seconds = functools.partial(DelayFunc, 3) print("Delay3Seconds是:", Delay3Seconds)
print("\n演示2: 使用这个偏函数创建实例") def greet(): print("Hello!")
delayed_greet = Delay3Seconds(greet) print("delayed_greet是:", delayed_greet) print("isinstance(delayed_greet, DelayFunc):", isinstance(delayed_greet, DelayFunc))
delayed_print.print_some()
|
输出会类似:
1 2 3 4 5 6 7 8
| 演示1: 创建等待3秒的偏函数 Delay3Seconds是: functools.partial(<class '__main__.DelayFunc'>, 3)
演示2: 使用这个偏函数创建实例 初始化DelayFunc: duration=3, func=greet delayed_greet是: <__main__.DelayFunc object at 0x...> isinstance(delayed_greet, DelayFunc): True print_some
|
可以再深入一点,看看偏函数是如何工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import functools
def demo_func(a, b, c): print(f"a={a}, b={b}, c={c}")
partial_demo = functools.partial(demo_func, 1)
print("偏函数的属性:") print(f"- func: {partial_demo.func}") print(f"- args: {partial_demo.args}") print(f"- keywords: {partial_demo.keywords}")
print("\n不同的调用方式:") partial_demo(2, 3) partial_demo(b=2, c=3) partial_demo(c=3, b=2)
partial_demo_kw = functools.partial(demo_func, c=3) print("\n固定关键字参数:") partial_demo_kw(1, 2)
|
这样设计的好处是:
- 参数复用
- 代码清晰
- 通过固定某些参数,使函数调用更简洁
- 代码意图更明确
- 灵活性
- 可以固定任意位置的参数
- 可以固定位置参数或关键字参数
- 工厂模式
- 可以用来创建特定配置的函数或类实例
- 很适合用在装饰器模式中
在你的装饰器例子中,partial(DelayFunc, 2) 创建了一个新的可调用对象,它:
- 已经固定了
DelayFunc 的第一个参数 (duration) 为 2
- 等待接收第二个参数 (
func)
- 当装饰器应用到函数上时,这个函数就自动成为第二个参数
这就是为什么装饰器模式和偏函数配合得如此好的原因!