如何理解python_如何理解 Python 的 Descriptor?

旧文搬运:

>>> class MyInt(int):

... def square(self):

... return self*self

...

>>> n = MyInt(2)

>>> n.name = 'two'

>>> n.square()

4

>>> n.name

'two'

小测验:上面代码的最后4行,n.square和n.name分别在几个对象的__dict__中查找'square'或'name'?

1个?2个?答案是2和4。n.square需要查找MyInt和n,n.name需要查找MyInt, int, object和n,查找顺序就如我列出来这样。

我们知道对象的属性覆盖类的属性:

>>> MyInt.name = 'MyInt'

>>> n.name

'two'

既然如此为什么查找顺序不反过来:先查找n,既然n.__dict__['name']存在就不需要再查找3个类?原因在于Python 2.2引入的新API: descriptor。

Python中不仅是类,实例也可以有自己的方法:

>>> def hello():

... print 'hello'

...

>>> n.hello = hello

>>> n.hello()

hello

MyInt.square比hello多一个self参数,为什么都可以用n.foo()形式来调用?因为前者是"方法"类型而后者是"函数"类型?不,我们已经知道class关键字不会改变def的语义:

>>> type(MyInt.__dict__['square'])

>>> type(n.__dict__['hello'])

Python在这里耍了个小花招:当在n中找不到属性square,而在n.__class__(即MyInt)中找到,而且MyInt.square是函数时,不直接返回这个函数,而是创建 一个wrapper:

>>> n.square

>>> type(n.square)

Wrapper中包含了n的引用,或者说,square的self参数被绑定到n。在有new-style class之前(如Python 1.5.2),这个查找过程大概是这样(实际的代码是C语言):

def instance_getattr(obj, name):

'Look for attribute /name/ in object /obj/.'

v = obj.__dict__.get(name)

if v is not None:

# found in object

return v

v, cls = class_lookup(obj.__class__, name)

# found v in class cls

if isinstance(v, types.FunctionType): # Note this line

# function type. build method wrapper

return BoundMethod(v, obj, cls)

if v is not None:

# data attribute

return v

raise AttributeError(obj.__class__, name)

def class_lookup(cls, name):

'Look for attribute /name/ in class /cls/ and bases.'

v = cls.__dict__.get(name)

if v is not None:

# found in this class

return v, cls

# search in base classes

for i in cls.__bases__:

v, c = class_lookup(i, name)

if v is not None:

return v, c

# not found

return None, None

这个机制也算简单有效。可是当Python开发者们准备用new-style class整理类型系统时,下面这几行代码就显得有些扎眼:

if isinstance(v, types.FunctionType):

# function type. build method wrapper

return BoundMethod(v, obj, cls)

Python的风格是不太鼓励用isinstance的,因为它不符合duck typing的精神:不要问我是什么,问我能做什么。函数属性需要创建wrapper而数据属性不需要,这是Python的基本设计,不需要改动也不能改动。但是我们可以把这个规则一般化:

给"像函数的"属性创建wrapper,而不给"像数据的"属性创建。Any software problem can be solved by adding another layer of indirection.

if v.like_a_function():

# function-like type. build method wrapper

return BoundMethod(v, obj, cls)

一不做,二不休,为什么不让对象自己决定怎样创建wrapper?

...

if hasattr(v, '__get__'):

# anything with a '__get__' attribute is

# a function-like descriptor

return v.__get__(obj, obj.__class__)

...

class FunctionType(object):

...

def __get__(self, obj, cls):

return BoundMethod(v, obj, cls)

好了,我们得到了descriptor的雏形。现在任何对象都可以模仿函数的行为,即使作为方法也没有问题。但是,潘多拉的盒子已经打开,开发者们不会就此止步的。对灵活性的追求永无止境。。。

比如,staticmethod把函数的绑定方式变为"不绑定":

class StaticMethod(object):

def __init__(self, f):

self.f = f

def __get__(self, obj, cls):

return self.f

class C(object):

@StaticMethod

def f(): # no self param

pass

或者,log每次函数调用:

>>> import types

>>> class Log(object):

... def __init__(self, f):

... self.f = f

... def __get__(self, obj, cls):

... print self.f.__name__, 'called'

... return types.MethodType(self.f, obj, cls)

...

>>> class C(object):

... @Log

... def f(self):

... pass

...

>>> c = C()

>>> c.f()

f called

Descriptor也不仅限于用在函数上。立即想到的是用它来做property。可是用__get__只能做出readonly property,那就再加个__set__吧:

>>> class Property(object):

... def __init__(self, fget, fset):

... self.fget = fget

... self.fset = fset

... def __get__(self, obj, cls):

... return self.fget(obj)

... def __set__(self, obj, val):

... self.fset(obj, val)

...

>>> class C(object):

... def fget(self):

... print 'fget called'

... def fset(self, val):

... print 'fset called with', val

... f = Property(fget, fset)

...

>>> c = C()

>>> c.f

fget called

>>> c.f = 1

fset called with 1

且慢,上面这段代码要能正常工作,还要克服一个困难:赋值总是作用于实例,根本不会去类中查找:

>>> c = C()

>>> c.n

0

>>> c.n = 1

>>> c.n

1

>>> C.n

0

这样一来,c.f = 1这个操作根本不会查找到我们在类中定义的property f,__set__方法也无从发挥作用。所以,我们只能改变赋值操作的语义,让类里定义的descriptor能够拦截对实例的属性赋值。现在要先在类和基类中查找名为'f',而且定义了__set__方法的descriptor,只有找不到时,才在实例中进行赋值。

可是,我们之前为函数设计的__get__方法,查找顺序是在实例属性之后的;而__set__方法查找顺序又必须在实例属性之前。如果同一个descriptor的两个方法查找顺序竟然不一样,那看上去可不太美。怎么解决descriptor用于函数和property时,对查找顺序的不同要求呢?

Python的解决方法说也简单:如果一个descriptor只有__get__方法(如FunctionType),我们就认为它是function-like descriptor,适用"实例-类-基类"的普通查找顺序;如果它有__set__方法(如Property),就是data-like descriptor,适用"类-基类-实例"的特殊查找顺序。但是找到descriptor之前又怎么可能知道它的类型呢?所以无论如何都得先查找类和基类,再根据是否找到descriptor,和descriptor的类型,来决定是否需要查找实例。现在的查找算法成了这样:

def object_getattr(obj, name):

'Look for attribute /name/ in object /obj/.'

# First look in class and base classes.

v, cls = class_lookup(obj.__class__, name)

if (v is not None) and hasattr(v, '__get__') and hasattr(v, '__set__'):

# Data descriptor. Overrides instance member.

return v.__get__(obj, cls)

w = obj.__dict__.get(name)

if w is not None:

# Found in object

return w

if v is not None:

if hasattr(v, '__get__'):

# Function-like descriptor.

return v.__get__(obj, cls)

else:

# Normal data member in class

return v

raise AttributeError(obj.__class__, name)

现在我们可以回答第一节末尾的问题了。接触descriptor之前,每个人概念里的查找顺序大概都是"实例-类-基类",而实际的查找过程却是"类-基类-实例"。概念上实例属性应该只需一次查找,实际上却是查找次数最多的(需要查找全部基类);查找次数最少的是方法(2次:类-找到function-like descriptor,实例-未找到)。另一个意外的结果是基类越多,查找实例属性越慢,尽管这个查找看上去和基类不相干。好在Python是动态类型,类层次一般不深。

这一切都是为了支持property。值不值得呢?能在类上拦截对实例属性的访问,由此可以引出很多有趣的用法,和metaclass结合起来更是如此。对于Python来说"性能"似乎从来不是牺牲"功能"(以及其他各种美德)的理由,这次也不例外。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值