Python 系列学习十七:descriptor 我的解读和总结

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 面向对象编程中有关 descriptor 的自我解读和总结的相关内容;

本文为作者的原创作品,转载需注明出处;

问题


在整理学完 相关内容以后,发现有一个问题;那就是类属性的调用规则根据类属性是方法还是对象的调用方式不同;按照 Descriptors 调用的说法,凡是 $b.x$ 的调用,若 $x$ 是 descriptor,那么都会被转换为 $type(b).__dict__[‘x’].__get__(b, type(b))$ 的调用方式,后来经过验证,这种说法并不完全正确,得看 $x$ 是什么了,如果 $x$ 是一个 $Function$,那是正确的,但是如果 $x$ 是一个对象实例,则是错误的;看下面这个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print('Retrieving', self.name, 'self: ', self, 'obj: ', obj)
return self.val
def __set__(self, obj, val):
print('Updating', self.name, 'self: ', self, 'obj: ', obj)
self.val = val


是的,这个例子正是官文中的所给出的例子;笔者只是稍作修改,打印的时候同时打印了 $self$ 和 $obj$ 参数,从上述问题出发,我们再来看看这个例子的执行情况,

1
2
3
4
5
6
7
8
9
10
11
>>> m = MyClass()
>>> m.x
Retrieving var "x" self: <__main__.RevealAccess object at 0x102ad1198> obj: <class '__main__.MyClass'>
10
>>> m.x = 20
Updating var "x" self: <__main__.RevealAccess object at 0x102ad1198> obj: <__main__.MyClass object at 0x102ad11d0>
>>> m.x
Retrieving var "x" self: <__main__.RevealAccess object at 0x102ad1198> obj: <class '__main__.MyClass'>
20
>>> m.y
5


是的,细心的你应该发现了什么,$self$ 指的是谁?是 $RevealAccess$,什么意思,也就是说,这里执行的 m.x 等价于执行的是 $type(m).__dict__(x).__get__(x, m)$,而不是官文中所说的$type(m).__dict__[‘x’].__get__(m, type(m))$;可见,如果对象的 descriptor 属性如果是一个对象实例,调用的方式并非按照官文所描述的方式进行的;这里要牢记;当然,如果对象的 descriptor 是一个 $Function$,那还是按照官文的方式进行调用的,这里作者就不再分析,有兴趣的读者可以自行研究;

Ok,后来发现,官文的内容没有错误,是我自己的理解有误,当在调用 $x.__get__(x, m)$ 传递的参数是后两个,也就是对应的是方法 $def __get__(self, obj, objtype):$ 的后两个参数既 $obj$ 和 $objtype$;$self$ 默认是实例本身;笔者并不打算删除本小节内容,以示给自己提醒;

调用流程

以下的调用流程相关的测试用例均以类 $B$ 和实例 $b$ 作为例子;

实例方法

实例 $b$ 的方法 $f$ 调用过程如上图所示,首先,由前文可知,$Function$ 对象用 Python 代码模拟如下,

1
2
3
4
5
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj)

下面我们来分析一下实例方法的调用流程,

  1. Step 1,$b.f()$ 开始实例方法的调用
  2. Step 1.1,Python 解释器将从对应的 $type$ 对象中的字典中去找 $f$ 方法对象
  3. Step 1.2,调用 $f$ 对象的 $__get__()$ 方法,将 $b$ 和 $type(b)$ 作为参数传递,这里既是调用上述的代码 $Function#__get__()$ 方法,特别注意的是,这里传递的参数 $b$ 和 $type(b)$ 对应的是其后两个参数;最后通过方法 $types.MethodType(self, obj)$ 构造出一个实例方法 $new\_f$ 的引用并返回,因此,这里的参数 $self$ 指的是 $Function$ 实例本身,而参数 $obj$ 是实例 $b$;
  4. 最后调用 $new\_f(b, *args)$ 以完成该实例方法的调用;

类方法

如图,便是类方法的调用过程,重温一下 $classmethod$ 类 Python 的模拟实现,

1
2
3
4
5
6
7
8
9
10
class classmethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc

很清晰的看到,通过 __get__() 返回了一个 $f$ 闭包函数的引用,闭包中始终引用了 $B$ 类;

静态方法

整个调用过程与类方法中所描述的过程基本一致,唯一的区别是这次调用的是 $staticmethod$;

1
2
3
4
5
6
class staticmethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f

可以看到,$staticmethod$ 的 __get__() 方法的实现非常的讨巧,什么也不做,直接返回方法的引用;