Python装饰器与partial

最近看到一些Python装饰器相关的库都使用到了 functools.partial 函数,不是很清楚使用的原因,探求一二.

functools.partial

偏函数,这个好理解,一般用于固定函数的部分参数,返回一个 partial 对象. 不过写出这个定义之后,有点能理解一定的关联性了,都是做函数的再次包装. 先来看看函数的使用

	import functools
	def mul(a, b):
		return a * b
	mul3 = functools.partial(mul, b=3)
mul(1, 2)
mul3(1)

我们看一下函数的定义 functools.partial(func, /, *args, **keywords) 当被调用时,行为类似于 func 附带位置参数 args 和关键字参数 keywords 被调用如果为调用提供了更多的参数,它们会被附加到 args 如果提供了额外的关键字参数,它们会扩展并重载 keywords

偏函数的优势

空间分离:可以在代码的不同地方指定参数,从结构化的角度或者词法作用域的角度来说可能会很方便. 时间分离:参数可以在不同阶段被应用,表现上看来就是有些参数是包括在代码里的,另外一些参数是在运行时决定的.

partial对象

查看官方文档的解释,非常的详细了

partial 对象是由 partial() 创建的可调用对象。 它们具有三个只读属性:

partial.func

一个可调用对象或函数。 对 partial 对象的调用将被转发给 func 并附带新的参数和关键字。

partial.args

最左边的位置参数将放置在提供给 partial 对象调用的位置参数之前。

partial.keywords

当调用 partial 对象时将要提供的关键字参数。

partial 对比 function

partial 对象与 function 对象的类似之处在于它们都是可调用、可弱引用的对象并可拥有属性。但两者也存在一些重要的区别。例如前者不会自动创建 __name____doc__ 属性。 而且,在类中定义的 partial 对象的行为类似于静态方法,并且不会在实例属性查找期间转换为绑定方法。

装饰器Decorator

装饰器其实只是一个语法糖,它本身就是一个函数,然后将函数作为参数传给它,并返回一个新的函数. 对于装饰器语法,第一个参数会默认传入被包装的函数关于装饰器本身就不多解释,这里主要说一下和 partial 相关的部分.

带参数的装饰器

我们先看下面的例子

@decorator_with_args('a', 'b')
def test(*args, **kwargs):
	pass

上述包装过的函数实际上会被翻译成 test = decorator_with_args(arg1, arg2)(test) decorator_with_args 是一个接受自定义参数的函数,并且返回实际的装饰器.

from functools import wraps
def decorator_with_args(arg1, arg2):
	print "arg1 is %s, arg2 is %s" % (arg1, arg2)

	@wraps(func)
	def real_decorator(func):
		def wrapper(*args, **kwargs):
			print "call real function"
			return func(*args, **kwargs)
		return wrapper
	return real_decorator

在实际的执行逻辑上,在装饰器引入的时候,便会先执行最外层的参数函数 decorator_with_args 然后返回装饰器,等待调用执行.

partial出场

partial在装饰器场景算是一个小的 trick, 可以实现一个带可选参数的装饰器,并且符合编程习惯,能在不传参数的时候通过 @decorator 也可以传递参数给它,比如 @decorator(a, b)

这里选用 Python Cookbook 里面的一个示例:

from functools import wraps, partial
import logging

def logged(func=None, *, level=logging.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 use
@logged
def add(x, y):
	return x + y

@logged(level=logging.CRITICAL, name='example')
def spam():
	print('Spam!')

可以看到, @logged 装饰器可以同时不带参数或带参数。

展开第一个装饰器得到

add = logged(add)

展开第二个装饰器得到

spam = logged(level=logging.CRITICAL, name='example')(spam)

接着根据 logged 的定义继续展开

spam = partial(logged, level=logging.CRITICAL, name='example')(spam)

这时候 partial(logged, level=logging.CRITICAL, name='example') 会返回一个装饰器, 这个装饰器的作用就是 logged 的真正装饰逻辑

这里使用 partial 的逻辑是,先将 logged(level=logging.CRITICAL, name='example') 视为一个函数调用,然后返回真实的装饰器,走后面的装饰器流程.

partial 会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了。

我们通过自己实现这个装饰器来对比 partial 的作用

from functools import wraps, partial
import logging

def my_logged(level=logging.DEBUG, name=None, message=None):
	def decorator(func):
		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
	return decorator

partial 带来的好处是这个装饰器是既可以带参数调用,也可以不带参数调用的,同时也减少了装饰器的包装层级,代码看起来更加清晰.

这里给出一个 partial 装饰器模板

import functools
def decorator_with_args(func=None, *, arg=None):

	# 将参数传给自身返回带参数的装饰器
	if func is None:
		return functools.partial(decorator_with_args, arg=arg)

	# 处理参数的逻辑

	# 实际的装饰器
	@functools.wraps(func)
	def wrapper(*args, **kwargs):
		# 编写装饰器的逻辑
		# 处理真实函数调用前的逻辑
		# ...
		result = func(*args, **kwargs)
		# 处理真实函数调用后的逻辑
		# ...
		return result
	return wrapper

参考链接

https://stackoverflow.com/questions/48098569/use-of-functools-partial-in-a-decorator-that-attaches-function-as-attribute-of-o https://python3-cookbook.readthedocs.io/zh%5FCN/latest/c09/p06%5Fdefine%5Fdecorator%5Fthat%5Ftakes%5Foptional%5Fargument.html

现代化javascript拾遗

Django日志关联Jeager信息