yield-bytes

沉淀、分享与无限进步

深入functools.wraps、partial

  在装饰器的定义中,经常引用functools.wraps,本篇文章将深入其内部源码,由于该方法的定义中,还引入其他重要的函数或者类,因此根据其调用链,对每个函数或者方法或者类进行单独分析,所以文章的结构大致如下:

第一部分内容:

根据其调用链:functools.wraps——>partial——>update_wrapper
functools.wraps需要调用partial,因此需要解析partial的源码
partial调用了update_wrapper函数,因此需要解析update_wrapper的源码

第二部分内容:

patial作为关键函数,在第三方库造轮子里,使用频率较高。这里以borax第三方库里面的fetch方法说明partial的使用场合。考虑到fetch还使用的python内建的attrgetter和itemgetter,故还对这两个类进行解析。
其调用链为:fetch—->partial/attrgetter/itemgetter

第三部分内容:

在python的内建方法中,attrgetter/itemgetter类,里面用了__slots__方法,本文也给出关于该方法作用的内容

考虑到以上有多个知识点混合,本文采用倒叙方式,文章的内容组织如下

  • 1、python的魔法方法__slots__的作用
  • 2、attrgetter/itemgetter类的解析
  • 3、borax.fetch的用法
  • 4、partial的解析和用法
  • 5、update_wrapper的解析和用法
  • 6、functools.wraps的解析和用法

1、python的魔法方法__slots__的作用

首先看看attrgetter/itemgetter的源代码定义(这里仅给出方法名)

1
2
3
4
5
6
class attrgetter:
__slots__ = ('_attrs', '_call')
def __init__(self, attr, *attrs):
def __call__(self, obj):
def __repr__(self):
def __reduce__(self):

该类里面引用了__slots__方法,仅有两个私有属性:('_attrs', '_call')

对于attrgetter,其作用:

  • 给类指定一个固定大小的空间存放属性,用于极致减少对象的内存占用,例如当十几万个小类(数据类),对象占用内存利用率将更有效。
  • 更快的属性访问速度
  • 实例后限制绑定新属性

为何_slots_方法有以上作用?

  这是因为,在定义个对象时(定义类),Python默认用一个字典来保存一个该对象实例属性。然而,对于有着已知属性的小对象类来说(例如一个坐标点类,仅有几个属性即可),当创建几十万个这些实例时,将有几十万个这样的字典占用大量内存,因此可通过slots方法告诉Python不使用字典,使用一个元组作为这几个属性的存放位置,以节省每个小对象的存储空间。
==slots这里不建议使用列表,因为列表占用空间比元组大。==

1.1、 slots方法保证实例不会创建__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
class Point(object):
def __init__(self,x,y):
self._x=x
self._y=y

def __str__(self):
return 'Point<{0},{1}>'.format(self._x,self._y)

a=Point(1,2)
print(a)
print(a.__dict__)
a.test=3
print(a.test)
# 输出:
Point<1,2>
{'_x': 1, '_y': 2}
3

# 加入slot之后
class Point(object):
__slots__ = ('_x','_y')
def __init__(self,x,y):
self._x=x
self._y=y

def __str__(self):
return 'Point<{0},{1}>'.format(self._x,self._y)
a=Point(1,2)
print(a)
print(a.__dict__)

# 输出:
Point<1,2>
#AttributeError: 'Point' object has no attribute '__dict__'
#可见实例没有使用dict字典存放属性

不过需要注意的是:slots魔法方法定义的属性仅对当前类实例起作用,对继承的子类是无效的

1.2、为何列表占用空间比元组大?
  • ==内存占用有区别==

首先列表和元组最重要的区别就是,列表是动态的、可变的对象、可读可写,而元组是静态的、不可变的对象,可读不可写。

下面通过实例看看它们存储以及占用空间的区别

查看列表的内存占用

1
2
3
l=[]
l.__sizeof__()
# 40

加入4个字符,每个字符为8字节空间

1
2
3
list_obj=['a','b','c','d']
list_obj.__sizeof__()
# 结果为72字节=列表自身40+4个字符*8

此外列表的空间是动态增加的,在数据结构与算法里,大家在设计列表这种数据结构应该知道,当调用append方法时,内部会判断当前列表预留空间是否满足用于存放新元素,若空间不足,会再动态申请新内存,申请的逻辑为:

(1)当原底层数组存满时,list类会自动请求一个空间为原列表两倍的新列表

(2)原列表的所有元素将被一次存入新列表里

(3)删除原列表,并初始化新列表

例如下面测试,原list_obj有4个字符元素,总计为72个字节,再加一个字符,是等于80个字节吗?

1
2
3
list_obj.append('e')
list_obj.__sizeof__()
# 结果为104字节=列表自身42+原4个字符*8+新1个字符*8+原4个字符*8的新申请预留空间

这里列表自身从40变为42字节,是因为增加了一个1索引。

查看元组的内存占用

1
2
3
t=()
t.__sizeof__()
# 24

加入4个字符,每个字符为8字节空间

1
2
3
tuple_obj=('a','b','c','d')
tuple_obj.__sizeof__()
# 结果为56字节=元组自身24+4个字符*8

可以看到,存储5个字符,列表用了72个字节,元组只用了56个字节。

  • ==对象创建时间有区别==
1
2
3
4
import timeit
t1 = timeit.Timer('list_obj=["a", "b", "c", "d"]')
t1.timeit()
# 0.0844487198985604
1
2
3
t2 = timeit.Timer('tuple_obj=("a", "b", "c", "d")')
t2.timeit()
# 0.01631815598959463

因为列表数据结构初始化需要方法逻辑比元组负责,而且需要预占空间,可以看到它们之间创建时间差别大,列表创建时间是元组的5倍左右。

2、attrgetter/itemgetter类的解析

2.1 attrgetter的使用场景

attrgetter主要用于快速获取对象的keys或者属性。
以下以数据类型为BlogItem对象为例,该数据对象有三个attribute,分别博客网址、作者、博客文章数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BlogItem(object):
def __init__(self, website, author, blog_nums):
self.website = website
self.author = author
self.blog_nums = blog_nums

def __str__(self):
return "{0}:{1}".format(self.__class__.__name__,self.website)

__repr__ = __str__


blog_object_list = \
[BlogItem("www.aoo.cn", 'aoo', 10),
BlogItem("www.boo.cn", 'boo', 5),
BlogItem("www.coo.cn", 'coo', 20)
]
print(blog_object_list)
# 输出:
[BlogItem:www.aoo.cn, BlogItem:www.boo.cn, BlogItem:www.coo.cn]

现要获取每行数据对象的blog属性,并对其实施排序,通常会使用lambda表达式实现

1
2
3
print (sorted(blog_object_list, key=lambda item: item.blog_nums))
#输出:
[BlogItem:www.boo.cn, BlogItem:www.aoo.cn, BlogItem:www.coo.cn]

有了attrgetter方法后,更方便调用获取对象的属性,例如下面的用法

1
2
3
4
print (sorted(blog_object_list,key=attrgetter('blog_nums')))
#输出:
[BlogItem:www.boo.cn, BlogItem:www.aoo.cn, BlogItem:www.coo.cn]

也可以传入多个属性,按多个属性进行排序,例如这里先根据blog排序、再根据author排序

1
2
3
print (sorted(blog_object_list,key=attrgetter('blog_nums','author')))
# 输出:
[BlogItem:www.boo.cn, BlogItem:www.aoo.cn, BlogItem:www.coo.cn]

2.2 attrgetter的内部实现:

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
class attrgetter:
"""
Return a callable object that fetches the given attribute(s) from its `.
After f = attrgetter('name'), the call f(r) returns r.name.
After g = attrgetter('name', 'date'), the call g(r) returns (r.name, r.date).
After h = attrgetter('name.first', 'name.last'), the call h(r) returns (r.name.first, r.name.last).
"""
__slots__ = ('_attrs', '_call')

def __init__(self, attr, *attrs):
if not attrs:
if not isinstance(attr, str):
raise TypeError('attribute name must be a string')
self._attrs = (attr,)
names = attr.split('.')
def func(obj):
return getattr(obj, name)
self._call = func
else:
# 如果获取多个属性,使用map对每个属性实施attrgetter
self._attrs = (attr,) + attrs
getters = tuple(map(attrgetter, self._attrs))
def func(obj):
return tuple(getter(obj) for getter in getters)
self._call = func

def __call__(self, obj):
return self._call(obj)

def __repr__(self):
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__qualname__,
', '.join(map(repr, self._attrs)))

def __reduce__(self):
return self.__class__, self._attrs

attrgetter在内部定义了一个闭包函数func,该函数其实就是getattr(obj, name)的功能。
attrgetter内有个特殊的魔法方法__reduce__,该方法用于pickle反序列化后可以找到该对象绑定的类及其入参,若要想对某对象进行pickle,那么该对象要定义__reduce__方法,否则在序列化时(使用pickle.load)无法持久化存储,查看其源码pickle.dumps可以看到相关逻辑

1
2
3
4
5
6
7
8
9
10
11
reduce = getattr(obj, "__reduce_ex__", None)
if reduce is not None:
rv = reduce(self.proto)
else:
# 获取被处理的对象的__reduce__方法,若不存在提示无法pickle持久化(或者称为无法被序列化),所以attrgetter对象或者其实例是可以被序列化
reduce = getattr(obj, "__reduce__", None)
if reduce is not None:
rv = reduce()
else:
raise PicklingError("Can't pickle %r object: %r" %
(t.__name__, obj))

attrgetter的doc文档说:f = attrgetter('name'), the call f(r) returns r.name。 __call__方法是为了实现f(r)用法,等价于getattr(r, name),用于获取给定对象的属性值

与attrgetter不同的是:getattr只能获取单个属性值而且getattr也是python工厂函数,在builtins.py内部定义;而attrgetter可以获取多个属性值,是对getattr的再次封装和加强,只不过它的封装逻辑写得还不错,即清晰易懂。
==对比getattr与attrgetter的区别==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class R:
def __init__(self,name,age):
self.name=name
self.age=age

# getattr用法
r=R('foo',10)
print(getattr(r,'name'))
#输出:
foo
# attrgetter用法
f=attrgetter('name')
print(f(r))
#输出:
foo

这里给出f=attrgetter('name')==>f(r)==>'foo'的调用过程,有两种情况

A、只获取对象的一个属性
f=attrgetter('name')==>f(r)==>触发__call__==>self._call(obj),因为self._call = func==>func(obj),根据func的定义,就是返回getattr(obj, name)==>getattr(r,"name")==>“foo”

B、获取对象两个属性以上的情况

例如attrgetter(‘name’,‘age’),那么其传递过程为
f=attrgetter('name',‘age')==>f(r)==>触发__call__==>self._call(obj),因为self._call = func==>func(obj)==>因为要获取两个属性,故递归调用attrgetter: getters = tuple(map(attrgetter, self._attrs))==>(attrgetter('name'),attrgetter('age'))==>对元组里面的元素按照步A的传递路线==>(getattr(r,"name"),getattr(r,"age"))==>('foo',10)

从上面的分析可知,attrgetter的实现有点绕了,所以python的闭包机制虽然可以基于原始函数上封装出具备更强功能的函数,但其代价就像符合函数,层层封装。
例如复合函数h,h=f(g(e(x))),复合函数h作为加强版方法,通过封装f方法,f方法封装g方法,g方法封装e方法,从而使得h方法的功能比最初始e方法具备更强大的功能,但你需要一路往内部追踪,才知道h函数最里面的函数为e函数。

2.3 itemgetter的使用场景

attrgetter可以理解给定单个key或者多个key,返回这些key的值,而对应另外一个类itemgetter,它用来实现对数据对象在给定索引号后,返回索引号对应的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
r=[
("www.boo.cn", 'boo', 20),
("www.aoo.cn", 'aoo', 10),
("www.coo.cn", 'coo', 15),
]

f=itemgetter(1)
print(f(r))
# 输出:
('www.aoo.cn', 'aoo', 10)

f=itemgetter(02) # 注意这里不是范围,而是索引0和索引2
print(f(r))
# 输出:
(('www.boo.cn', 'boo', 20), ('www.coo.cn', 'coo', 15))

其源代码也简单,实现跟attrgetter逻辑一直,attrgetter使用getter(obj,key)获取值,itemgetter使用obj[index]的方式获得值,但要求obj内部必须实现__getitem__(self, item)方法。

2.4 itemgetter的源码分析

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
class itemgetter:
"""
Return a callable object that fetches the given item(s) from its operand.
After f = itemgetter(2), the call f(r) returns r[2].
After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3])
"""
__slots__ = ('_items', '_call')

def __init__(self, item, *items):
if not items:
self._items = (item,)

def func(obj):
# 若只提供一个索引号,则直接按getitem的写法获取该对象的值
return obj[item]
self._call = func
else:
self._items = items = (item,) + items

def func(obj):
# 若获取多个索引号,则遍历这些索引号,获取每个对象的值
return tuple(obj[i] for i in items)
self._call = func

def __call__(self, obj):
return self._call(obj)

def __repr__(self):
return '%s.%s(%s)' % (self.__class__.__module__,
self.__class__.__name__,
', '.join(map(repr, self._items)))

def __reduce__(self):
return self.__class__, self._items

取值传递过程有两种情况

A、当给定1个索引号

f=itemgetter(1)==>self._call(obj)==>因为self._call = func,等价于func(obj)==>根据func的定义,obj[item]==>r[1]==>也即是列表索引取值的方式,('www.aoo.cn', 'aoo', 10)

B、给定多个索引号

f=itemgetter(0,2)==>self._call(obj)==>因为self._call = func,等价于func(obj)==>根据func的定义以及入参大于1 ==>tuple(obj[i] for i in items)==>(obj[0],obj[1])==>(r[0],r[1])==>(('www.boo.cn', 'boo', 20), ('www.coo.cn', 'coo', 15))

3、broax.fetch的用法

borax是一个python第三库轻量库,里面有一些基本中国农历函数、choice、数据结构、设计模式以及fetch函数。它的doc

Borax is a utils collections for python3 development, which contains
some common data structures and the implementation of design patterns

主要的module:

  • borax.calendars : A Chinese lunar calendar package, which contains lunar,festivals, birthday.
  • borax.choices : choices a enhance module using class-style define for const choices.
  • borax.fetch : A function sets for fetch the values of some axises.
  • borax.structures : A useful data structure for dictionary/list/set .
  • borax.patterns : A implementation for the design patterns.

fetch函数功能:从数据序列中选择一个或多个字段的数据,它很好展示了partial函数的实际项目的用法。

在这里插播之后会写一篇blog的通告:
fetch是从已有的数据序列中,根据指定key或者属性对应的记录行,而records库则是从各类关系型数据库取出数据记录行(当然可完成增删查改),发现records源代码清晰简单,但实现功能确如此强大,所以接下来会单独给出一篇blog用于解析records源码,records不到550行,封装逻辑通俗易懂。

records是kennethreitz的for Humans™系列的库,用于近乎易懂的方式操作数据库,kennethreitz是requests库的作者—github地址),kennethreitz总能把底层较为繁琐的逻辑封装成易用的逻辑,其开源的项目的源代码具有不错的学习价值。

3.1 fetch模块的源码简析

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import operator
from itertools import tee
from functools import partial
# 当使用from fetch import * 时,通过__all__属性来限制import *的导出范围
__all__ = ['fetch', 'ifetch', 'fetch_single', 'ifetch_multiple', 'ifetch_single']

class Empty(object):
pass
EMPTY = Empty()
# 以下iterable就是要处理的数据序列例如以下数据对象Person序列
# [Person('aoo',10'),Person('boo',21),....,Person('coo',13)]

def ifetch_single(iterable, key, default=EMPTY, getter=None):
"""
getter() g(item, key):pass
# 给定单个key或者属性或者索引号,用于获取数据序列对象的值,例如获取每个Person数据对象name的值
"""
def _getter(item):
if getter:
# 这里就是第三库如何把partial引用到自己的代码实现里面的一个示例,这里留到后面章节给出其解释
custom_getter = partial(getter, key=key)
return custom_getter(item)
else:
try:
# 用了第2章节内容提到的attrgetter获取属性值
# 其实就是return getattr(item,key),即获取单个数据项key对应的值
attrgetter = operator.attrgetter(key)
return attrgetter(item)
except AttributeError:
pass

try:
# 用了第2章节内容提到的itemgetter,给定索引号,获取值
# 其实就是return item[key],即获取给定索引号的单个数据项对应的值
itemgetter = operator.itemgetter(key)
return itemgetter(item)
except KeyError:
pass
if default is not EMPTY:
return default
raise ValueError('Item %r has no attr or key for %r' % (item, key))
return map(_getter, iterable)

def fetch_single(iterable, key, default=EMPTY, getter=None):
# 因为ifetch_single返回的map对象,因此需要list化后,才能得到整个列表数据值
return list(ifetch_single(iterable, key, default=default, getter=getter))

def ifetch_multiple(iterable, *keys, defaults=None, getter=None):
# 用于处理给定的key或者属性或者索引号的入参大于1个的情况,例如要获取每个Person数据对象的name属性的值、age属性的值、phone属性的值,所以有三个key:name、age、phone
defaults = defaults or {}
if len(keys) > 1:
# 根据给定key的个数n,生成对应n个数据序列的迭代器,其实就是把要处理的数据序列变成迭代器后,并复制了多份,显然存在设计不合理的地方,拷贝多份数据,占用空间。
#例如3个key对应生成3个迭代器 iters = (iterable,iterable,iterable)
iters = tee(iterable, len(keys))
else:
iters = (iterable,)
#结果为:[ifetch_single(data_list,'name'),ifetch_single(data_list,'age'),ifetch_single(data_list,'phone')]
iters = [ifetch_single(it, key, default=defaults.get(key, EMPTY), getter=getter) for it, key in zip(iters, keys)]
return iters

def ifetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None):
# 该函数就是通过判断需要获取1个属性还是多个属性来决定调用ifetch_single还是ifetch_multiple
if len(keys) > 0:
keys = (key,) + keys
return map(list, ifetch_multiple(iterable, *keys, defaults=defaults, getter=getter))
else:
return ifetch_single(iterable, key, default=default, getter=getter)


def fetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None):
# 这个fetch其实多此一举,可以直接在ifetch返回处加入list方法即可。
return list(ifetch(iterable, key, *keys, default=default, defaults=defaults, getter=getter))

3.2 fetch的使用示例

示例1

1
2
3
4
5
6
7
8
9
10
11
12
data_list = [
{'name': 'aro', 'age': 10,'phone':131},
{'name': 'bro', 'age': 13,'phone':132},
{'name': 'cro', 'age': 15,'phone':143},
]
result = fetch(data_list,'name')
print(result)
#['aro','bro','cro']

result = fetch(data_list,'name','age')
print(result)
#[['aro','bro','cro'],[10,13,15]]

当了解fetch里面的调用了itemgetter的内部逻辑后,其实就是字典的取值:data[‘name’],data[‘age’]

示例2

1
2
3
4
5
6
7
8
9
10
11
class R:
def __init__(self,name,age):
self.name=name
self.age=age
data_list=[
R('aro',10),
R('bro',12),
R('cro',19)
]
print(fetch(data_list,'name'))
#['aro', 'bro', 'cro']

当了解fetch里面的调用了attrgetter的内部逻辑后,其实就是使用内建方法getattr获取对象属性的值:[getattr(data1,’name’),getattr(data2,’name’),getattr(data3,’name’)]

示例3
自定义getter,这里用到了partial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person:
def __init__(self, id, name, age, phone):
self.id = id
self._data = {'name': name, 'age': age, 'phone': phone}
def get(self, key):
return self._data.get(key)
data_item = [
Person(1001,'Aerk', 22, 141),
Person(1002, 'Berk', 25, 151),
Person(1003, 'Derk', 21, 181)
]
def my_getter(item,key):
return item.get(key)
values = fetch(data_item, 'name', getter=my_getter)
print(values)
#输出
# ['Aerk', 'Berk', 'Derk']

4、本文核心内容

经过前面3个章节多个知识点的铺垫后,再来看本章节内容,则会更容易理解。本节内容对应前言第一部分:
wrap装饰器的调用过程:functools.wraps——>partial——>update_wrapper
先看看functools.wraps的示例。

4.1 login_require的装饰器例子

在Django的app开发中,一般会使用装饰器鉴权,大致如下结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def login_require(func):
# 未使用functools.wraps
def inner_wrap(*args, **kwargs):
"""用于对外部的request做是否已经登录请求鉴权"""
return func(*args, **kwargs)
return inner_wrap

@login_require
def get_blog_list(request):
"""获取blog列表的function"""
return 200

print(get_blog_list.__doc__)
print(get_blog_list.__name__)
print(get_blog_list.__qualname__)

输出
1
2
3
#  用于对外部的request做是否已经登录请求鉴权
# inner_wrap
# login_require.<locals>.inner_wrap

这里显然不符合需求,getbloglist的__doc____name____qualname__被改成loginrequire里面闭包函数innerwrap对应属性的值
这里如何保证被装饰函数getbloglist的属性值不被改动呢?加入functools.wraps(func)
1
2
3
4
5
6
7
8
9
def login_require(func):
@functools.wraps(func)
def inner_wrap(*args, **kwargs):
"""用于对外部的request做是否已经登录请求鉴权"""
return func(*args, **kwargs)
return inner_wrap
print(get_blog_list.__doc__)
print(get_blog_list.__name__)
print(get_blog_list.__qualname__)

输出
1
2
3
获取blog列表的function
get_blog_list
get_blog_list

这次,get_blog_list的`__doc
__name
__qualname
`属性保持不变。

不使用functools.wraps(func),也可以实现get_blog_list被装饰后,其属性值保持不变,通过setattr处理即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def login_require(func):
def inner_wrap(*args, **kwargs):
"""用于对外部的request做是否已经登录请求鉴权"""
return func(*args, **kwargs)
inner_wrap.__doc__=func.__doc__
inner_wrap.__name__=func.__name__
inner_wrap.__qualname__=func.__qualname__
return inner_wrap

#或者使用setattr设置属性值

def login_require(func):
def inner_wrap(*args, **kwargs):
"""用于对外部的request做是否已经登录请求鉴权"""
return func(*args, **kwargs)
setattr(inner_wrap,'__doc__',func.__doc__)
setattr(inner_wrap,'__name__',func.__name__)
setattr(inner_wrap,'__qualname__',func.__qualname__)
return inner_wrap
print(get_blog_list.__doc__)
print(get_blog_list.__name__)
print(get_blog_list.__qualname__)

输出
1
2
3
获取blog列表的function
get_blog_list
get_blog_list

update_wrapper正是使用上述setter方式实现对原函数被装饰后,新函数属性和原函数属性和保持一致。
但在软件工程中,这种写法是过程式设计,初级的写法,无法被重用,因此需要使用更优雅的方式将这些逻辑封装打包,对外可以重用。

4.2 functools.wraps的定义

1
2
3
4
5
6

def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)

wraps里面调用partial,partial里面调用update_wrapper,应用在get_blog_list也就是:partial(update_wrapper, wrapped=get_blog_list)
接下来,先看看update_wrapper到底实现的什么功能

4.3 update_wrapper的源码分析

以login_require的装饰器例子,wrapper就是inner_wrap函数,而wrapped则是get_blog_list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):

for attr in assigned:
# 将原函数指定的5个属性(__name__、__doc__等)更新到(注册到/覆盖到)新函数,使得原函数被装饰后,指定的5个属性也保不变。
try:
# 获取原函数的属性值
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
# 将原函数的属性值更新到相应的新函数属性中
setattr(wrapper, attr, value)
for attr in updated:
# 原函数的__dict__属性更新到(注册到/覆盖到)新函数的__dict__属性,从而实现原函数被装饰后其__dict__属性保持不变。
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper

所以update_wrapper设计简约,实现了4.1章节内容所提如下内容的功能
1
2
3
setattr(inner_wrap,'__doc__',func.__doc__)
setattr(inner_wrap,'__name__',func.__name__)
setattr(inner_wrap,'__qualname__',func.__qualname__)

4.3 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
def partial(func, *args, **keywords):
#对于login_require的装饰器例子,这里的func参数就是update_wrapper,args参数为空为:keywords就是wrapped=get_blog_list被装饰函数,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES
# 因为update_wrapper没有`func`属性,所以跳过这部分处理
if hasattr(func, 'func'):
args = func.args + args
tmpkw = func.keywords.copy()
tmpkw.update(keywords)
keywords = tmpkw
del tmpkw
func = func.func

# 这里newfunc是整个partial设计为最巧妙的地方!!!乃至functools.wraps里面设计最为巧妙的环节。将新加入的位置参数和关键字参数追加到func里
def newfunc(*fargs, **fkeywords):
#wrapped=get_blog_list被装饰函数,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES拷贝到newkeywords
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
# 对于login_require例子,这里fkeywords为空
return func(*(args + fargs), **newkeywords)

newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

当使用以下写法时
1
2
@functools.wraps(func)
def inner_wrapper

就会触发partial调用newfunc,而newfunc被定义Wie函数,是可以被__call__的,它返回func((args + fargs), *newkeywords),func就是update_wrapper函数,args为空参数,fargs就是inner_wrapper函数,newkeywords就是wrapped=get_blog_list被装饰函数,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES
所以以下这一小段代码
1
2
@functools.wraps(func)
def inner_wrapper

就是转换为以下语句的调用。

1
update_wrapper(wrapper=inner_wrapper,wrapped=get_blog_list,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES)

4.3章节已经详细指出updatewrapper的作用:
将原函数getbloglist指定的5个属性(`_name__doc
`等)更新到(注册到/覆盖到)新函数inner_wrapper,使得原函数被装饰后,指定的5个属性也保不变。
这就是functools.wraps(func)的内部基于partial的实现逻辑

4.4 再论partial

partical的官方说明:

functools.partial(func, args, *keywords)
Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords.

从英文的解释来看(不建议翻译为中文,直接理解英文更加准确),partial不应该翻译成“偏函数”,partial词意中:有不完整的意思,翻译成“待补全外部参数”的类函数对象貌似更贴切,
以下面例子说明

1
2
3
4
5
6
def add(x,y,z=1):
return x+y+z
# 先给定1个位置参数和1个关键字参数,对于add函数,参数是不完整的,需要待后面补全多一个外部位置参数
add_15=partial(add,5,z=10)
#这里add_15如果直接调用,会提示缺少一个位置参数,因此对于add_15,3就是待补全外部参数,
print(add_15(3)) # 输出18

以上的实际执行过程如下:
partial返回func((args + fargs), **newkeywords),这里的func为add函数,args为(5,10)两个位置参数,3为后面补全的参数,就是fargs的值,newkeywords定义为:
1
2
newkeywords = keywords.copy()
newkeywords.update(fkeywords)

add的z=10,就是keywords,fkeywords为空,因此newkeywords为{‘z’:10}
所以有以下等价链
`partial(add,5,z=10)<===>func(
(args + fargs), *newkeywords)<===>add(((5,)+(3,)),z=10)<===>add(5,3,z=10)`

从上面分析可以看出,paritial最大的用处就是基于某个原函数和原函数参数基础上生成一个”待补全外部参数新函数“,提供给该新函数的入参个数比原函数少了,得到高效简洁地调用指定函数,例如新函数add_15只需要提供1个参数即可,而原函数add需要提供3个参数。