主题
Python装饰器
闭包
闭包是一种特殊的函数结构,它允许一个函数访问并操作其外部作用域中的变量。闭包的核心在于它能够“记住”并保留外部函数的局部变量,即使外部函数已经执行完毕,这些变量仍然可以被内部函数访问和使用。闭包通常由嵌套函数构成,即一个函数定义在另一个函数内部。
python
def line(a, b):
def inner(x):
return a*x+b
return inner
# 相当于直线y=ax+b,设置a和b后,改变x即可求出y
l1 = line(2, 4)
print(l1(3))
需要注意的是:返回的函数并没有立刻执行,而是直到调用了才执行。
python
def build():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = build()
print(f1(), f2(), f3()) # 猜猜输出是什么?
查看答案
并不是"1, 4, 9",而是"9, 9, 9"。调用的时候i的值已经是3了。
常规装饰器
装饰器的作用是给函数添加额外的功能而无需改动原函数。
python
def decorator(func):
print("此部分在装饰的时候就会运行")
def inner(*args, **kwargs):
print('---函数执行前---')
res = func(*args, **kwargs)
print("---函数执行后---")
return res
return inner
@decorator # 装饰器装饰之后:func1 = decorator(func1)
def func1():
return 'func1'
@decorator
def func2():
return 'func2'
print(func1())
print(func2())
输出如下:
此部分在装饰的时候就会运行
此部分在装饰的时候就会运行
---函数执行前---
---函数执行后---
func1
---函数执行前---
---函数执行后---
func2
函数func1
经过@decorator
装饰之后就变成了inner
函数,所以func1()
实际执行的是inner()
,原先的func1
函数内容作为参数func
传递给函数内部,根据python闭包的特性,此参数将会被保留下来,即使decorator函数运行完,此参数仍然可以被内部函数访问和使用。
带参装饰器
python
def wrap(param):
def decorator(func):
def inner(*args, **kwargs):
print('inner: param=%s' % param)
res = func(*args, **kwargs)
return res
return inner
return decorator
# f1 = wrap('abc')(f1)
# 先运行wrap('abc')返回decorator函数,@decorator(f1)就相当于之前的常规装饰器了
@wrap('abc')
def f1():
return 'f1'
python
from functools import partial
def decorator(func=None, *, name='aaa', age=18):
if func is None:
return partial(decorator, name=name, age=age)
def inner(*args, **kwargs):
print(f"inner: {name=}, {age=}")
res = func(*args, **kwargs)
return res
return inner
# f1 = decorator(f1) 相当于常规装饰器
@decorator
def f1():
print("f1")
# f2 = decorator(name='bbb', age=20)(f2)
# 先执行decorator(name='bbb', age=20),参数func为None,所以返回decorator函数
# 最后f2=decorator(f2) 相当于常规装饰器
@decorator(name='bbb', age=20)
def f2():
print("f2")
装饰类的方法
装饰类的方法和装饰一般函数没有太大区别,唯一要注意装饰器和类方法装饰器的顺序不能弄混。
python
def stat_time(func):
def inner(*args, **kwargs):
ins = args[0]
print(ins.name)
start = time.time()
res = func(*args, **kwargs)
end = time.time()
print("cost time: ", end - start)
return res
return inner
class Test:
name = "xxx"
@stat_time
def instance_method(self, n):
while n > 0:
n -= 1
time.sleep(0.1)
@classmethod
@stat_time
def class_method(cls, n):
while n > 0:
n -= 1
time.sleep(0.1)
@staticmethod
@stat_time
def static_method(n):
while n > 0:
n -= 1
time.sleep(0.1)
提示
第三方库wrapt在处理带参装饰器和装饰类的方法上更加简单,有兴趣可以阅读其文档了解。
关于函数签名问题
python
from functools import wraps
def decorator(func):
@wraps(func)
def inner(*args, **kwargs):
res = func(*args, **kwargs)
return res
return inner
@decorator
def func1():
"""函数文档"""
return 'func1'
# 不使用functools.wraps装饰时: inner None
# 使用functools.wraps装饰时:func1 函数文档
print(func1.__name__, func1.__doc__)
从上面的代码可以得出,装饰器会改变函数签名,需要使用functools.wraps
装饰器来修正函数签名。事实上,functools.wraps
也没完全修正函数签名,函数的参数签名仍然与原函数不一致,但对于大部分场景已经够用了。如果你想完全保持一致,可以使用第三方库👉decorator。
python
from decorator import decorator
from inspect import getfullargspec
from functools import wraps
def use_wraps(func):
@wraps(func)
def inner(*args, **kwargs):
print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
return func(*args, **kwargs)
return inner
@decorator
def use_decorator(func, *args, **kwargs):
print("calling %s with args %s, %s" % (func.__name__, args, kwargs))
return func(*args, **kwargs)
def func1(a, *, b=None):
pass
@use_wraps
def func2(a, *, b=None):
pass
@use_decorator
def func3(a, *, b=None):
pass
print(getfullargspec(func1))
print(getfullargspec(func2))
print(getfullargspec(func3))
func2()
# 输出结果如下:
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
# FullArgSpec(args=['a'], varargs=None, varkw=None, defaults=None, kwonlyargs=['b'], kwonlydefaults={'b': None}, annotations={})
# calling func2 with args (1,), {}
从结果可以看出,decorator
的参数签名与原函数一致。
装饰器结合函数签名的相关方法,可以验证函数的参数类型,给函数增加参数等,这里就不扩展了。一般开发搞不到这个层度。
类装饰器
python
class WrapClass:
def __init__(self, class_name):
self.cls = class_name
def __call__(self, *args, **kwargs):
print("你已经被装饰了")
return self.cls(*args, **kwargs)
# Person = WrapClass(Person) 装饰之后Person变为WrapClass类的实例对象wrap_ins,
# 当其被实例化时,实际是wrap_ins(name, age),会调用__call__方法,返回是Person类的实例
@WrapClass
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 实例化的时候进行装饰
p1 = Person("wgj", 27)
python
class WrapClass:
def __init__(self, level=1):
self.level = level
def __call__(self, cls):
@wraps(cls)
def wrap(*args, **kwargs):
if self.level > 1:
print(f"当前等级:{self.level}")
return cls(*args, **kwargs)
return wrap
# Person = WrapClass(2)(Person)
# 本质和上面的无参形式没啥区别,只是实例对象有初始化参数。
@WrapClass(2)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
__call__
方法可以让类的实例像函数一样被调用。
下面看一个类装饰器实现的给函数扩充功能的例子:
python
import types
from functools import partial, update_wrapper
import time
class DelayFunc:
def __init__(self, func, duration=1):
self.duration = duration
self.__func = func
update_wrapper(self, func) # == wraps(func)(self)
def __call__(self, *args, **kwargs):
print(f'Wait for {self.duration} seconds... {args}')
time.sleep(self.duration)
return self.__func(*args, **kwargs)
# 让此装饰器可以装饰类的函数
def __get__(self, instance, owner):
if instance is None:
return self
# 返回一个函数,此函数的第一参数是实例instance(相当于实例方法)。
# 由于self成为可调用对象是通过__call__实现的,
# 所以当调用__call__时,其args的第一个参数就是这个实例instance
method = types.MethodType(self, instance)
return method
def eager_call(self, *args, **kwargs):
return self.__func(*args, **kwargs)
def delay(duration):
"""
装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行。
为了避免定义额外函数,直接使用 functools.partial 帮助构造 DelayFunc 实例
:param duration:
:return:
"""
return partial(DelayFunc, duration=duration)
# delay(duration=2) -> @DelayFunc(duration=2) -> add = DelayFunc(add, duration=2)
@delay(2)
def add(a, b):
print(a + b)
class Spam:
# add = DelayFunc(add, duration=2) add是DelayFunc的实例对象df了
# Spam的实例调用add方法时,由于add是非数据描述器DelayFunc的实例,
# 根据描述器协议:描述器的实例被另一个类访问时,__get__会被调用。
# df() 将调用__call__,其args=(self, a, b),就是下面的三个参数
@delay(2)
def add(self, a, b):
print(a + b)
# add已经是DelayFunc的实例,但它也有函数的部分属性(归功于update_wrapper)
print(add, add.__name__)
add(1, 2) # 这次调用将会延迟 2 秒
add.eager_call(1, 2) # 这次调用将会立即执行,用类装饰为函数添加了新的功能
s = Spam()
s.add(1, 2)