函数装饰器(function decorator)可以对函数进行“标注”,给函数提供更多的特性。
在理解装饰器之前需要理解闭包(closure)。python3.0 引入了保留关键字 nonlocal,使用闭包同样也离不开 nonlocal。顺便说一句,闭包除了用在装饰器上,对于异步编程也是很重要的概念。
装饰器(decorator)是一个可调用的装饰函数,它接收另一个函数作为参数。
假设已经定义好了一个装饰器 decorate(decorate 实际上是一个接收函数并且返回函数的函数),那么以下两段代码是等价的。
@decoratedef target():print('running target()')
和
')target = decorate(target)
可以看到,@标注这种语法实际上是一个语法糖。target 经过标注后已经成为了另一个函数 decorate(target)。
我们再看看一个实际定义 decorate 的例子:
decorator(func): inner():running inner())return inner@decorator target1():running target1()) target2():running target2())inner_func1 = target1()print(inner_func1)-' * 10(target1)*)inner_func2 = decorator(target2)()(inner_func2)print(decorator(target2))
输出:
running inner()None----------<function decorator.<locals>.inner at 0x10ae3f598>**********running inner()None----------<0x10aee7510>
根据代码和结果进一步验证我们的理解。通过装饰器函数的标注,一个函数可以变为另一个函数。至于怎么转换的,是根据装饰器函数本身定义的。装饰器函数的输入和输出都是函数,它定义了函数的变换。
装饰器的执行顺序当定义一个函数 A 时,如果它用了装饰标注 B,那么 A 在定义时就已经执行了装饰器 B 的代码,而不是在调用函数 A 时执行。即:
running decorator(func)target()before calling target.)target()
等价于
inner)target = decorator(target))target()
其结果都是:
running decorator(func)before calling target.running inner()
因为这个原因,装饰器往往在导入模块的时候就会执行(import time),而被装饰函数(装饰器返回的函数)是在显式调用的时候执行(runtime)。
不改变原函数的装饰器大多数装饰器往往都会改变原函数,但也有一些应用场景不会改变原函数。例如:
registry = [] register(func):registry.append(func)return func
这种装饰器会收集使用过它的函数。
变量范围规则以下代码会因为变量 b 没有定义而报错:
f1(a):(a)(b)f1(3)
3---------------------------------------------------------------------------nameError Traceback (most recent call last)<ipython-input-28-25d665eb58d1> in <module>3 print(b)4----> 5 f1(3)<ipython-input-28-25d665eb58d1> in f1(a)1 def f1(a):2 print(a)----> 3 print(b)45 f1(3)nameError: name 'b' is not defined
而下面代码因为全局变量 b 的存在而不会报错。
b = 6f1(3)
36
接下来才是重点,以下代码会报错:
b = 6 f2(a):(b)b = 9f2(3)
3---------------------------------------------------------------------------UnboundLocalError Traceback (most recent call last)<ipython-input-30-55c0dd1a1ffb> in <module>5 b = 96----> 7 f2(3)<ipython-input-30-55c0dd1a1ffb> in f2(a)2 def f2(a):3 print(a)----> 4 print(b)5 b = 96UnboundLocalError: local variable 'b' referenced before assignment
这是因为 Python 在编译函数时,发现了 b 在函数内进行了赋值,因此它认为 b 是一个局部变量。于是生成的字节码会认为 b 是局部变量,会试图在局部环境中找到 b,于是报错了。
如果需要修复这个问题,需要进行显式地全局声明:
b = 6 f3(a):global bf3(3)
36闭包
由于匿名函数经常在函数内定义一个函数,而闭包也用到了嵌套函数,所以两者经常混淆。
但是实际上,闭包的关注点不在于匿名与否。闭包是这样一个函数,它在函数内绑定了一个函数外的非全局变量。
我们看看这样一个计算平均数的闭包。
make_averager():serIEs = averager(new_value):serIEs.append(new_value)total = sum(serIEs)return total/len(serIEs) averageravg = make_averager()avg(10)avg(15)avg(20)
输出:
10.012.515.0
这里的 serIEs 称之为自由变量(free variable),尽管 make_averager 已经调用完了,但是 serIEs 依然包含在闭包中而没有销毁,averager 函数依然可以使用它。
查看 avg 的变量:
avg.__code__.co_varnamesavg.__code__.co_freevars
输出:
('new_value','total')('serIEs',)
所以,闭包就是包含了自由变量的一个函数。
为了便于类比,我们再看看一个基于类的实现:
class Averager():def __init__(self):self.serIEs =__call__(self,new_value):self.serIEs.append(new_value)total = sum(self.serIEs)len(self.serIEs)avg = Averager()avg(10)avg(20)
输出:
10.012.515.0nonlocal
一个更优雅的闭包,避免存储一个列表:
make_averager():count = 0total = 0 averager(new_value):nonlocal count,totalcount += 1total += new_valuereturn total / count)avg(20)
这里使用了 nonlocal,表示该变量不是局部变量。这个关键字是用于处理前面提到的变量范围规则:当函数有赋值语句时,Python 会认为这个变量是局部变量。显然我们应该让 count 和 total 作为 averager 外的自由变量,于是需要加上 nonlocal 关键字。
实现一个简单的装饰器以下是一个计时装饰器的示例:
import time clock(func):def clocked(*args,**kwargs):"""clocked doc"""t0 = time.perf_counter()result = func(*args,1)">kwargs)elapsed = time.perf_counter() - t0name = func.__name__arg_str = ,'.join(repr(arg) for arg in args)[%0.8fs] %s(%s) -> %r' % (elapsed,name,arg_str,result)) result clocked@clock snooze(seconds):snooze doctime.sleep(seconds)@clock factorial(n):factorial doc"""return 1 if n < 2 else n*factorial(n-1)' * 40,Calling snooze(.123))snooze(.123Calling factorial(6)6! =',factorial(6))
输出:
**************************************** Calling snooze(.123)[0.12775655s] snooze(0.123) -> None**************************************** Calling factorial(6)[0.00000100s] factorial(1) -> 1[0.00006883s] factorial(2) -> 2[0.00012012s] factorial(3) -> 6[0.00016687s] factorial(4) -> 24[0.00022555s] factorial(5) -> 120[0.00030625s] factorial(6) -> 7206! = 720
其结果是不言而喻的,装饰器对原函数进行了包装,变为一个新函数,增加了计时信息。以上的装饰器有一些小缺陷:
snooze.snooze.__doc__factorial.__doc__
'clocked''\n clocked doc\n ''clocked''\n clocked doc\n
可以看到装饰器“污染”了被装饰函数的一些属性。
使用 functools.wrap:一种更优雅的做法:
time functools clock(func):@functools.wraps(func)kwargs):t0 = time.time()result = func(*args,1)">kwargs)elapsed = time.time() -arg_lst = []if args:arg_lst.append( args)) kwargs:pairs = [%s=%r' % (k,w) for k,w sorted(kwargs.items())]arg_lst.append(.join(pairs))arg_str = .join(arg_lst)[%0.8fs] %s(%s) -> %r ))snooze.__doc__
输出:
**************************************** Calling snooze(.123)[0.12614298s] snooze(0.123) -> None**************************************** Calling factorial(6)[0.00000119s] factorial(1) -> 1[0.00012970s] factorial(2) -> 2[0.00022101s] factorial(3) -> 6[0.00031495s] factorial(4) -> 24[0.00039506s] factorial(5) -> 120[0.00047684s] factorial(6) -> 7206! = 720'snooze''\n snooze doc\n ''factorial''\n factorial doc\n标准库中的装饰器
Python 有 3 种用于装饰方法的内置函数:
propertyclassmathodstaticmethod另一种常见的装饰器是 functools.wraps。
标准库还有两个有意思的装饰器(在 functools 中定义)是:
lru_cachesingledispatchfunctools.lru_cachefunctools.lru_cache 实现了记忆功能。LRU 表示 Least Recently Used。我们看看这个装饰器如何加速 fibonacci 的递归。
普通方法:
@clock fibonacci(n):if n < 2: nreturn fibonacci(n-2) + fibonacci(n-1print(fibonacci(6))
输出:
[0.00000000s] fibonacci(0) -> 0[0.00000310s] fibonacci(1) -> 1[0.00028276s] fibonacci(2) -> 1[0.00000095s] fibonacci(1) -> 1[0.00000000s] fibonacci(0) -> 0[0.00000167s] fibonacci(1) -> 1[0.00007701s] fibonacci(2) -> 1[0.00015092s] fibonacci(3) -> 2[0.00051212s] fibonacci(4) -> 3[0.00000095s] fibonacci(1) -> 1[0.00000000s] fibonacci(0) -> 0[0.00000095s] fibonacci(1) -> 1[0.00007415s] fibonacci(2) -> 1[0.00014782s] fibonacci(3) -> 2[0.00000095s] fibonacci(0) -> 0[0.00000095s] fibonacci(1) -> 1[0.00007510s] fibonacci(2) -> 1[0.00000119s] fibonacci(1) -> 1[0.00000095s] fibonacci(0) -> 0[0.00000000s] fibonacci(1) -> 1[0.00007606s] fibonacci(2) -> 1[0.00015116s] fibonacci(3) -> 2[0.00030208s] fibonacci(4) -> 3[0.00052595s] fibonacci(5) -> 5[0.00111508s] fibonacci(6) -> 88
使用 lru_cache:
@functools.lru_cache()@clockprint(fibonacci(6))
[0.00000095s] fibonacci(0) -> 0[0.00000191s] fibonacci(1) -> 1[0.00041223s] fibonacci(2) -> 1[0.00000215s] fibonacci(3) -> 2[0.00048995s] fibonacci(4) -> 3[0.00000215s] fibonacci(5) -> 5[0.00056982s] fibonacci(6) -> 88泛型函数:singledispatch
泛型函数:一组用不同方式(依据第一个参数的类型)执行相似 *** 作的函数。
代码感受一下:
from functools singledispatchfrom collections abc numbers HTML@singledispatch HTMLize(obj):content = HTML.escape(repr(obj))return <pre>{}</pre>.format(content)@HTMLize.register(str) _(text):content = HTML.escape(text).replace(\n<br>\n<p>{0}</p>.format(content)@HTMLize.register(numbers.Integral) _(n):<pre>{0} (0x{0:x})</pre>.format(n)@HTMLize.register(tuple)@HTMLize.register(abc.MutableSequence) _(seq):inner = </li>\n<li>'.join(HTMLize(item) for item seq)<ul>\n<li>' + inner + </li>\n</ul>HTMLize({1,2,3})HTMLize(abs)HTMLize(Heimlich & Co.\n- a game)HTMLize(42print(HTMLize([Alpha@d1@d2 f():f')
等价于
)f = d1(d2(f))
含参的装饰器我们知道,当函数被装饰器装饰的时候,实际上是作为参数传入给了装饰器。要实现含参装饰器,需要构建一个装饰器工厂函数,这个函数接收参数,返回一个装饰器。说白了,就是又多了一层函数嵌套,写一个返回装饰器的函数。
下面提供一些例子作为参考。
注册器registry = set()def register(active=True): decorate(func):running register(active=%s)->decorate(%s)'% (active,func)) active:registry.add(func)else:registry.discard(func) func decorate@register(active=False) f1():running f1())@register() f2():running f2() f3():running f3())registryregister()(f3)registryregister(active=False)(f2)registry
输出:
running register(active=False)->decorate(<function f1 at 0x10aef8510>)running register(active=True)->decorate(<function f2 at 0x10aef8950>){<function __main__.f2()>}running register(active=True)->decorate(<function f3 at 0x10ad791e0>)<function __main__.f3()>{<function __main__.f2()>,<function __main__.f3()>}running register(active=False)->decorate(<function f2 at 0x10aef8950>)<function __main__.f2()>{<function __main__.f3()>}计时器
timeDEFAulT_FMT = [{elapsed:0.8f}s] {name}({args}) -> {result}'def clock(fmt=DEFAulT_FMT):def clocked(*_args):t0 = time.time()_result = func(*_args)elapsed = time.time() -args = _args)result = repr(_result)print(fmt.format(**locals())) _result clocked decorate @clock() snooze(seconds):time.sleep(seconds)for i in range(3):snooze(.123)
[0.12674093s] snooze(0.123) -> None[0.12725592s] snooze(0.123) -> None[0.12320995s] snooze(0.123) -> NoneUDP 客户端/服务器
UDP clIEnt/server decoratorUDP clIEnt: to send data.UDP server: to perform operation on the frame it receives. functools socket Json timedef process_udp_server(ip=0.0.0.0):UDP server decorator:param ip::param port::param data_size::return:server = socket.socket(socket.AF_INET,socket.soCK_DGRAM)server.bind((ip,port))print(fUDP server started at {str(ip) + ":" + str(port)}. start_server(func):@functools.wraps(func)def processed(*args,1)">kwargs):while True:data = server.recv(data_size)data = Json.loads(data.decode())res = func(data,*args,1)">kwargs)if res == -1break processed start_server camera_udp_clIEnt(ip,port):UDP clIEnt decorator:param ip::param port::return:clIEnt = start_clIEnt(func):@functools.wraps(func)def send_data(*args,1)">kwargs):data = func(*args,1)">kwargs)clIEnt.sendto(str.encode(Json.dumps(data)),(ip,port)) send_data start_clIEntif __name__ == __main__ argparseparser = argparse.ArgumentParser()parser.add_argument(-m--modeserver/clIEnt modeserver)parser.add_argument(-i--ipIP address-p--portUDP port-c--cameracamera number0)args = parser.parse_args()if args.mode == :@process_udp_server(args.ip,args.port,1024 * 1024 multiply(x):time.sleep(1print(x * 2)multiply()elif args.mode == clIEnt:@camera_udp_clIEnt(args.ip,args.port) send_single_data(x): x True:send_single_data(8)time.sleep(1python udp_decorator.py -m [server|clIEnt]')
参考《Fluent Python》by Luciano Ramalho
总结
以上是内存溢出为你收集整理的回顾Python装饰器全部内容,希望文章能够帮你解决回顾Python装饰器所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)