Python标准库-包装Callable对象

  |  

摘要: Python 标准库 functools 中的 partial,包装 Callable 对象

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


functools 模块应用于高阶函数,即参数或返回值为其他函数的函数。 通常来说,此模块的功能适用于所有可调用对象。

本文我们来学习其中的 partial 对象,涉及到的知识点如下。

参考书(中英文版):

functools-partial 组件

functools 提供的 partial 类可以包装有默认参数的 callable 对象。

(1) partial 对象

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

  1. partial.func: 一个可调用对象
  2. partial.args: 最左边的未知参数将放置在提供给 partial 对象调用的位置参数之前。
  3. partial.keywords: 当调用 partial 对象时将要提供的关键字参数。

partial 对象与函数对象区别和联系:

  • 联系:两者都是可调用,可弱引用的对象,并拥有属性。
  • 区别:partial 对象不会自动创建 namedoc 属性;在类中定义的 partial 对象的行为类似于静态方法,不会在实例属性查找期间转换为绑定方法。

(2) partial(func, /, args, *keywords)

返回一个新的 partial 对象,它调用时相当于 func 附带未知参数 args 和关键字参数 keywords 被调用。

如果为调用提供了更多参数,会被附加到 args,如果提供额外的关键字参数,会扩展并重载 keywords,大致的实现方式如下

1
2
3
4
5
6
7
8
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

(3) partialmethod(func, /, args, *keywords)

返回一个新的 partialmethod 描述器,其行为类似 partial 但它被设计用作方法定义而非直接用作可调用对象。

描述器:一个类中定义了__get____set____delete__中的一个或几个,这个类的实例就可以叫做一个描述器。

func 必须是描述器(descriptor)或可调用对象,如果同时属于两者,比如函数,则按描述器处理。

  • 当 func 为描述器时,例如:
1
2
3
4
5
Python函数
classmethod()
staticmethod()
abstractmethod()
其它partialmethod实例

get 的调用会被委托给底层的描述器,并返回一个适当的 partial 对象作为结果。

  • 当 func 为一个非描述器的可调用对象,则会动态创建一个适当的绑定方法。当用作方法时,其行为类似普通 Python 函数:将会插入 self 参数作为第一个位置参数。

(4) update_wrapper

1
2
3
4
5
update_wrapper(wrapper
,wrapped
,assigned=WRAPPER_ASSIGNMENTS
,updated=WRAPPER_UPDATES
)

将 wrapped 中的某些属性直接赋值给 wrapper 函数的匹配属性的元组。

赋值的属性默认为 (‘module‘, ‘name‘, ‘qualname‘, ‘doc‘, ‘annotations‘),可以在模块级常量 functools.WRAPPER_ASSIGNMENTS 中查看

为了访问原始函数(例如绕过 lru_cache() 之类的缓存装饰器),此函数会自动为 wrapper 添加一个指向被包装函数的 wrapped 属性。

此函数的主要目的是在 decorator 函数中用来包装被装饰的函数并返回包装器。 如果包装器函数未被更新,则被返回函数的元数据将反映包装器定义而不是原始函数定义

(5) wraps 函数装饰器

1
2
3
4
functools.wraps(wrapped
,assigned=WRAPPER_ASSIGNMENTS
,updated=WRAPPER_UPDATES
)

这是一个便捷函数,用于在定义装饰器函数时发起调用 update_wrapper() 作为函数装饰器。它等价于 partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

wraps 装饰器会对所装饰的装饰器函数应用 update_wrapper(),获得装饰器的函数属性。

(6) total_ordering 类装饰器

给定一个【声明一个或多个全比较排序方法】的类,这个类装饰器实现剩余的方法。

此类必须包含以下方法之一:lt() 、le()、gt() 或 ge()。另外,此类必须支持 eq() 方法。

(7) cmp_to_key

将旧式的比较函数转换为新式的 key function

sorted() , min() , max() , heapq.nlargest() , heapq.nsmallest() , itertools.groupby() 等函数的 key 参数中使用。

(8) lru_cache

一个为函数提供缓存功能的装饰器

1
2
functools.lru_cache(user_function)
functools.lru_cache(maxsize=128, typed=False)
  • 使用字典存储缓存,所以该函数的固定参数和关键字参数必须是可哈希的
  • 被包装的函数配有一个 cache_parameters() 函数,该函数返回一个新的 dict 用来显示 maxsize 和 typed 的值。
  • 为了衡量缓存的有效性以便调整 maxsize 形参,被装饰的函数带有一个 cache_info() 函数。当调用 cache_info() 函数时,返回一个具名元组,包含命中次数 hits,未命中次数 misses ,最大缓存数量 maxsize 和 当前缓存大小 currsize。在多线程环境中,命中数与未命中数是不完全准确的。
  • 该装饰器也提供了一个用于清理/使缓存失效的函数 cache_clear() 。
  • 原始的未经装饰的函数可以通过 wrapped 属性访问。它可以用于检查、绕过缓存,或使用不同的缓存再次装饰原始函数。

(9) cache

  • 简单轻量级未绑定函数缓存。
  • 返回值与 lru_cache(maxsize=None) 相同,创建一个查找函数参数的字典的简单包装器。因为它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快。

(10) cached_property

将一个类方法转换为特征属性,一次性计算该特征属性的值,然后将其缓存为实例生命周期内的普通属性。类似于 property() 但增加了缓存功能。

property()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C:
def __init__(self):
self._x = None

def getx(self):
return self._x

def setx(self, value):
self._x = value

def delx(self):
del self._x

x = property(getx, setx, delx, "I'm the 'x' property.")

c 是 C 的实例, c.x 将触发 getter,c.x = value 将触发 setter , del c.x 触发 deleter。

property 装饰器

下面的代码将 voltage() 方法转化成同名只读属性的 getter 方法。

1
2
3
4
5
6
7
8
class Parrot:
def __init__(self):
self._voltage = 100000

@property
def voltage(self):
"""Get the current voltage."""
return self._voltage

property 的 getter, setter 和 deleter 方法同样可以用作装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C(object):
def __init__(self):
self._x = None

@property
def x(self):
"""I'm the 'x' property."""
return self._x

@x.setter
def x(self, value):
self._x = value

@x.deleter
def x(self):
del self._x

functools-partial 例子

partial 用默认参数包装函数

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
import functools

def my_func(a, b=2):
"Docstring for myfunc()."
print(" called myfunc with: ({}, {})".format(a, b))

def show_details(name, f, is_partial=False):
"Show details of a callable object"
print("{}:.format(name)")
print(" object: {}".format(f))
if not is_partial:
print(" __name__: {}".format(f.__name__))
else:
print(" func: {}".format(f.func))
print(" args: {}".format(f.args))
print(" keywords: {}".format(f.keywords))

def main():
show_details("my_func", my_func)
my_func("a", 3)
print()

p1 = functools.partial(my_func, b=4)
show_details("partial with named default", p1, True)
p1("passing a")
p1("override b", b=5)
print()

p2 = functools.partial(my_func, "default a", b=99)
show_details("partial with default", p2, True)
p2()
p2(b="override b")
print()


if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{}:.format(name)
object: <function my_func at 0x7f32c94b52f0>
__name__: my_func
called myfunc with: (a, 3)

{}:.format(name)
object: functools.partial(<function my_func at 0x7f32c94b52f0>, b=4)
func: <function my_func at 0x7f32c94b52f0>
args: ()
keywords: {'b': 4}
called myfunc with: (passing a, 4)
called myfunc with: (override b, 5)

{}:.format(name)
object: functools.partial(<function my_func at 0x7f32c94b52f0>, 'default a', b=99)
func: <function my_func at 0x7f32c94b52f0>
args: ('default a',)
keywords: {'b': 99}
called myfunc with: (default a, 99)
called myfunc with: (default a, override b)

partial 获得函数属性

函数的属性可以在定义函数时同时定义函数属性, 也可以在函数声明外定义函数属性。

函数对象的 dict 特殊属性包含了函数对象的属性和属性值。

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
import functools

def my_func(a, b=2):
"Docstring for myfunc()."
print(" called myfunc with: ({}, {})".format(a, b))

def show_details(name, f):
"Show details of a callable object"
print("{}:.format(name)")
print(" object: {}".format(f))
print(" __name__: ", end="")
try:
print(f.__name__)
except AttributeError:
print("(no __name__)")
print(" __doc__: {}".format(repr(f.__doc__)))
print()

def main():
show_details("my_func", my_func)

p1 = functools.partial(my_func, b=4)
show_details("raw wrapper", p1)

print("Updating wrapper:")
print(" assign: {}".format(functools.WRAPPER_ASSIGNMENTS))
print(" update: {}".format(functools.WRAPPER_UPDATES))
print()

functools.update_wrapper(p1, my_func)
show_details("updated wrapper", p1)
print()


if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{}:.format(name)
object: <function my_func at 0x7f8f79626e18>
__name__: my_func
__doc__: 'Docstring for myfunc().'

{}:.format(name)
object: functools.partial(<function my_func at 0x7f8f79626e18>, b=4)
__name__: (no __name__)
__doc__: 'partial(func, *args, **keywords) - new function with partial application\n of the given arguments and keywords.\n'

Updating wrapper:
assign: ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
update: ('__dict__',)

{}:.format(name)
object: functools.partial(<function my_func at 0x7f8f79626e18>, b=4)
__name__: my_func
__doc__: 'Docstring for myfunc().'

partial 用默认参数包装定义了 call 的类实例

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
import functools

class MyClass:
"Demonstration class for functools"
def __call__(self, e, f=6):
"Docstring for MyClass.__call__"
print(" called object with: {}, {}, {}".format(self, e, f))

def show_details(name, f):
"Show details of a callable object."
print("{}:".format(name))
print(" object: {}".format(f))
print(" __name__: ", end="")
try:
print(f.__name__)
except AttributeError:
print("(no __name__)")
print(" __doc__", repr(f.__doc__))


o = MyClass()

show_details("instance", o)
o("e goes here")
print()

p = functools.partial(o, e="default for e", f=8)
functools.update_wrapper(p, o)
show_details("instance wrapper", p)
p()
1
2
3
4
5
6
7
8
9
10
11
instance:
object: <__main__.MyClass object at 0x7fba1d267358>
__name__: (no __name__)
__doc__ 'Demonstration class for functools'
called object with: <__main__.MyClass object at 0x7fba1d267358>, e goes here, 6

instance wrapper:
object: functools.partial(<__main__.MyClass object at 0x7fba1d267358>, e='default for e', f=8)
__name__: (no __name__)
__doc__ 'Demonstration class for functools'
called object with: <__main__.MyClass object at 0x7fba1d267358>, default for e, 8

用 partial 对象定义类的方法

在类定义中,partial() 返回的可调用对象作为类的非绑定方法(行为类似静态方法)使用

在类定义中,partialmethod() 返回的可调用对象作为类的绑定方法。

关于绑定方法与非绑定方法

绑定方法: 通过实例访问的方法会绑定到那个实例上。方法其实是描述符,访问方法时,会返回一个包装自身的对象,把方法绑定到实例上。那个对象就是绑定方法。调用绑定方法时,可以不传入self的值。例如,像my_method=my_obj.method这样赋值之后,可以通过 my_method() 调用绑定方法。
非绑定方法: 直接通过类访问的实例方法没有绑定到特定的实例上,因此把这种方法称为“非绑定方法”。若想成功调用非绑定方法,必须显式传入类的实例作为第一个参数。那个实例会赋值给方法的self参数。

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
import functools

def my_method(self, a=1, b=2):
"my_method"
print(" called my_method with: {}, {}, {}".format(self, a, b))
if self is not None:
print(" self.attr = {}".format(self.attr))

class MyClass:
"Demonstration class for functools"

def __init__(self):
self.attr = "instance attribute"

method1 = functools.partialmethod(my_method)
method2 = functools.partial(my_method)

o = MyClass()

print("my_method")
my_method(None)
print()

print("method1 as my_method")
o.method1()
print()

print("method2 as my_method")
try:
o.method2(o)
except TypeError as err:
print("ERROR: {}".format(err))

method1 可以从 MyClass 的实例中调用,此实例作为第一个参数传入。

method2 为非绑定方法,所以必须显式传递 self 参数。

1
2
3
4
5
6
7
8
9
my_method
called my_method with: None, 1, 2

method1 as my_method
called my_method with: <__main__.MyClass object at 0x7fa4e8de9240>, 1, 2
self.attr = instance attribute

method2 as my_method
ERROR: my_method() missing 1 required positional argument: 'self'

获取装饰器的函数属性

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
43
44
45
46
47
48
49
import functools

def show_details(name, f):
"Show details of a callable object."
print('{}:'.format(name))
print(' object: {}'.format(f))
print(' __name__: ', end='')
try:
print(f.__name__)
except AttributeError:
print('(no __name__)')
print(' __doc__', repr(f.__doc__))
print()

def simple_decorator(f):
@functools.wraps(f)
def decorated(a='decorated defaults', b=1):
print(' decorated: ({}, {})'.format(a, b))
print(' ', end=' ')
return f(a, b=b)
return decorated

def myfunc(a, b=2):
"myfunc() is not complicated"
print(' myfunc: ({}, {})'.format(a, b))
return

# The raw function
show_details('myfunc', myfunc)
myfunc('unwrapped, default b')
myfunc('unwrapped, passing b', 3)
print()

# Wrap explicitly.
wrapped_myfunc = simple_decorator(myfunc)
show_details('wrapped_myfunc', wrapped_myfunc)
wrapped_myfunc()
wrapped_myfunc('args to wrapped', 4)
print()

# Wrap with decorator syntax.
@simple_decorator
def decorated_myfunc(a, b):
myfunc(a, b)
return

show_details('decorated_myfunc', decorated_myfunc)
decorated_myfunc()
decorated_myfunc('args to decorated', 4)
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
myfunc:
object: <function myfunc at 0x7f5505f24378>
__name__: myfunc
__doc__ 'myfunc() is not complicated'

myfunc: (unwrapped, default b, 2)
myfunc: (unwrapped, passing b, 3)

wrapped_myfunc:
object: <function myfunc at 0x7f5505f24400>
__name__: myfunc
__doc__ 'myfunc() is not complicated'

decorated: (decorated defaults, 1)
myfunc: (decorated defaults, 1)
decorated: (args to wrapped, 4)
myfunc: (args to wrapped, 4)

decorated_myfunc:
object: <function decorated_myfunc at 0x7f5505f24510>
__name__: decorated_myfunc
__doc__ None

decorated: (decorated defaults, 1)
myfunc: (decorated defaults, 1)
decorated: (args to decorated, 4)
myfunc: (args to decorated, 4)

Share