Python 装饰器应用

一、创建装饰器时保留函数元信息

当装饰器作用在某个函数上时,这个函数的重要元信息:名字,文档,注解和参数签名都会丢失。
可以使用functools库中的@wraps装饰器来注解底层包装函数。
eg:

import time
from functools import wraps
def timethis(func):
	@wraps(func)
	def wrapper(*args,**kwargs):
		start=time.time()
		result=func(*args,**kwargs)
		end=time.time()
		print(func.__name__,end-start)
		return result
	return wrapper

@wraps有一个重要的特征是它能让你通过属性__wrapped__直接访问被包装函数。
如果有多个装饰器,那么访问__wrapped__属性的行为是不可预知的,应避免这样做。

二、带参数的装饰器

例如:想写一个装饰器添加日志功能,同时允许用户指定日志的级别和其他选项,

from functools import wraps
import loging

def logged(level,name=None,message=None):
	def decorate(func):
		logname=name if name else func.__module__
		log=logging.getLogger(logname)
		logmesg=message if message else func.__name
		@wraps(func)
		def wrapper(*args,**kwargs):
			log.log(level,logmesg)
			return func(*arg,**kwargs)
		return wrapper
	return decorate

这段代码看上去很复杂,其实核心思想很简单:
最外层函数logged()接受参数并将他们作用在内部的装饰器函数上面,内层函数decorate()接受一个函数作为参数,然后在函数上面放置一个包装器,

三、可自定义属性的装饰器

场景:
如果想写一个装饰器来包装函数,并且允许用户提供参数在运行时控制装饰器行为。
解决方案:
引入访问函数,使用nonlocal来修改内部变量,然后这个访问函数被作为一个属性复制给包装函数。

from functools import wraps,partial
import logging

def attach_wrapper(obj,func=None):
	if func is None:
		return partial(attach_wrapper,obj)
	setattr(obj,func.__name__,func)
	return func
def logged(level,name=None,message=None):
	def decorate(func):
		logname=name if name else func.__module__
		log=logging.getLogger(logname)
		logmsg=message if message else func.__name__
		
		@wraps(func)
		log.log(level,logmsg)
		return func(*args,**kwargs)
		
		@attach_wrapper(wrapper)
		def set_level(newlevel):
			nonlocal level
			level=newlevel
		@attach_wrapper(wrapper)
		def set_message(newmsg):
			nonlocal logmsg
			logmsg=newmsg
		return wrapper
	return decorate		

其中set_level/set_message作为属性赋给包装器,每个访问函数允许使用nonlocal来修改函数内部的变量。

四、带可选参数的装饰器

场景:
如果想写一个装饰器,既可以不传参数比如:@decorator,也可以传递可选参数,比如:@decorator(x,y,z)
解决方案:

from functools import wraps,partial
import logging

def logged(func=None,*,level=loging.DEBUG,name=None,message=None):
	if func is None:
		return partial(logged,level=level,name=name,message=message)
	logname=name if name else func.__module__
	log =logging.getLogger(logname)
	logmsg=message if message else func.__name__
	@wraps(func)
	def wrapper(*args,**kwargs):
		log.log(level,logmsg)
		return func(*args,**kwargs)
	return wrapper
	
#example
@logged
def add(x,y):
	return x+y
@logged(level=loggin.CRITICAL,name='example')
def spam():
	print ('test')

一般当我们使用装饰器的时候,要么不给他们传递参数,要么传递确切的参数。
要理解代码是如何工作的,得非常熟悉装饰器是如何作用到函数上以及它的调用规则。
上面的add函数,可以等价于:add=logged(add)
这时候函数会作为第一个参数直接传递给logged装饰器。因此logged()中的第一个参数就是被包装函数本身,所有其他参数都必须有默认值。
上面的spam函数,可以等价于:spam=logged(level=loging.CRITICAL,name='example)(func)
初始调用logged函数时,被包装函数并没有被传递进来,因此在装饰器中,它必须是可选的,这反过来会迫使其他参数必须使用关键字来指定。并且,这些参数被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。为了这样做,可以使用functools.partial,它会返回一个未完全初始花的自身。除了被包装函数外其他参数都已经确定下来了。

五、利用装饰器强制函数上的类型检查

应用场景:
如果想对函数参数进行强制类型检查。
解决方案:

from inspect import signature
from functools import wraps

def typeassert(*ty_args,**ty_kwargs):
	def decorate(func):
		if not __debug__:
			return func
		sig=signature(func)
		bound_types=sig.bind_partial(*ty_args,**ty_kwargs).arguments
		@wraps(func)
		def wrapper(*args,**kwargs):
			bound_values=sig.bind(*args,**kwargs)
			for name,value in bound_values.arguments.items():
				if name in bound_types:
					if not isinstance(value,bound_types[name]):
						raise TypeError('argument {} must be {}'.format(name,bound_types[name]))
			return func(*args,**kwargs)
		return wrapper
	return decorate

装饰器只会在函数定义时被调用一次,有时候你想去掉装饰器的功能,那么只需要简单的返回被装饰函数即可,如果全局变量__debug__被设置为False那么就会直接返回函数本身。

示例中还对被装饰函数的参数签名进行了检查,使用inspect.signature()函数。

六、装饰器为被包装函数增加参数

应用场景:
如果想在装饰器中给被包装函数增加额外的参数,但不能影响这个函数现有的调用。
解决方案
可使用关键字参数来给被包装函数增加额外参数。

from functools import wraps

def optional_debug(func):
	@wraps(func)
	def wrapper(*args,debug=False,**kwargs):
		if debug:
			print('calling',func.__name__)
		return func(*args,**kwargs)
	return wrapper
	

为了避免关键字参数与被包装函数的关键字参数重复,可以这样检查关键字参数:

from functools import wraps
import inspect

def optional_debug(func):
	if 'debug' in inspect.getargspec(func).args:
		raise TypeError('debug argument already defined')
	@wraps(func)
	def wrapper(*args,debug=False,**kwargs):
		if debug:
			print('calling',func.__name__)
		return func(*args,**kwargs)
	return wrapper

这么做之所以可以,是因为强制关键字参数很容易被添加到接受*args和kwargs参数的函数中。通过使用强制关键字参数,它被作为一个特殊情况被挑选出来,并接下来仅仅使用剩余的位置和关键字参数去调用这个函数时,这个特殊参数会被排除在外,也就是说它不会被纳入kwargs中去。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值