幕后的故事:descriptor (zz)

转自newsmth bbs的Python版,很强很精彩

发信人: ilovecpp (cpp), 信区: Python
标  题: 幕后的故事:descriptor (1)
发信站: 水木社区 (Sat Dec  8 22:12:51 2007), 转信

注:本文只针对CPython。比较CPython和IronPython将是很有意思的。

2 __dict__, __class__, __bases__

Python对象说到底就是一个dict(__dict__)和一个指向类的指针(__class__)[注]。
__dict__包含这个对象的特有属性(通常是成员变量),而类包含该类对象的共性
(通常是方法)。

[注]:对于built-in type和有__slots__的对象,情况有所不同。

类也是对象,也有__dict__和__class__。那么,类的__class__是什么?我们称之
为metaclass。new-style class的metaclass是type;old-style class的metaclass
是types.ClassType,而后者的metaclass--你猜对了,还是type。从任何对象开始
沿__class__上朔,一定会回到type。type自己也不例外,它是自己的实例。

>>> class C(object): pass
...
>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> C.__class__
<type 'type'>
>>> int.__class__
<type 'type'>
>>> object.__class__
<type 'type'>
>>> type.__class__
<type 'type'>

另一方面,new-style class都继承自一个或多个new-style class,所以类比普通
对象多一个属性:__bases__。从任何new-style class开始沿__bases__上朔,一
定会回到object -- new-style class层次结构的根。object是唯一的例外,它的
__bases__是空的。

>>> C.__bases__
(<type 'object'>,)
>>> int.__bases__
(<type 'object'>,)
>>> object.__bases__
()

小测验:object.__class__是什么?type.__bases__又是什么?

>>> object.__class__
<type 'type'>
>>> type.__bases__
(<type 'object'>,)

我们似乎遇到了先有鸡还是先有蛋的问题?

    +--------+                      +------+
    |        | <=== __bases__ ====  |      | <====|
    | object |                      | type |  __class__
    |        | ==== __class__ ===>  |      | =====|
    +--------+                      +------+

如果type和object像用户定义类型那样动态创建,交叉引用的确是个问题。但
built-in type的内存都是预先静态分配的,Python runtime初始化时就设置好了
这些指针。

如果class, metaclass和base class看起来很晕,这样想可能清楚一些:如果两个
类有共同的metaclass,那么这两个类有共性;(例如,new-style class的
metaclass都是type,所以它们都有__bases__成员,Cls()都可以创建Cls类型的对
象,等等。)如果两个类有共同的base class,那么这两个类的实例有共性。(例
如,object是所有new-style class的(间接)base,所以即使没有重载__str__,
str(obj)也能工作,因为object提供了缺省的实现。)解析obj.attr时只涉及obj,
obj.__class__和obj.__class__.__bases__,不会用到metaclass对象。

3 属性解析

还是拿MyInt作例子。

>>> 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。

(待续)

--
民主不能大,
自由不能化,
政府不能骂,
小平不能下。
--- 四项基本原则


※ 修改:·ilovecpp 于 Dec  8 22:21:48 修改本文·[FROM: 222.129.42.*]
※ 来源:·水木社区 newsmth.net·[FROM: 222.129.42.*]
发信人: ilovecpp (cpp), 信区: Python
标  题: 幕后的故事:descriptor (2)
发信站: 水木社区 (Mon Dec 17 00:15:31 2007), 转信

让我们再次召唤出本文万年不变的例子:

>>> class MyInt(int):
...     def square(self):
...             return self*self
...    
>>> n = MyInt(2)
>>> n.square()
4

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

>>> def hello():
...     print 'hello'
...
>>> n.hello = hello
>>> n.hello()
hello

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

>>> type(MyInt.__dict__['square'])
<type 'function'>
>>> type(n.__dict__['hello'])
<type 'function'>

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

>>> n.square
<bound method MyInt.square of 2>
>>> type(n.square)
<type 'instancemethod'>

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

这个机制也算简单有效。可是当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(self, obj, cls)

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

(待续)

--
民主不能大,
自由不能化,
政府不能骂,
小平不能下。
--- 四项基本原则


※ 修改:·ilovecpp 于 Feb 13 12:23:38 修改本文·[FROM: 207.46.92.*]
※ 来源:·水木社区 newsmth.net·[FROM: 222.129.54.*]
发信人: ilovecpp (cpp), 信区: Python
标  题: 幕后的故事:descriptor (3)
发信站: 水木社区 (Tue Feb 12 22:15:01 2008), 转信

在上节中,我们通过descriptor API,把"绑定self参数"这一逻辑从class下放
到了具体的属性中:

def instance_getattr(obj, name):
    ...
    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 types.MethodType(v, obj, cls)

现在可以有丰富多采的绑定方式。比如,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):
...             print 'in f'
...    
>>> c = C()
>>> c.f()
f called
in f

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

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

>>> class C(object):
...     n = 0
...
>>> 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来
说"性能"似乎从来不是牺牲"功能"(以及其他各种美德)的理由,这次也不例外。

(待续)

--
Java is an application specific language, still looking for an
application to be specific to.
--- Per Abrahamsen, in advogato.org


※ 修改:·ilovecpp 于 Feb 12 22:19:29 修改本文·[FROM: 222.129.45.*]
※ 来源:·水木社区 newsmth.net·[FROM: 222.129.45.*]
发信人: ilovecpp (cpp), 信区: Python
标  题: 幕后的故事:descriptor (4)
发信站: 水木社区 (Wed Feb 13 00:57:24 2008), 转信

4 Magic method

Python中有不少dis.dis, timeit.timeit这样的名字。你有没有想过,其实这些
module需要的是__call__呢?很遗憾,这样是不行的:

C:/Python>cat mymodule.py
def __call__():
    print 'called!'

C:/Python>python
Python 2.5 (r25:51908, Sep 19 2006, 09:52:17) [MSC v.1310 32 bit (Intel)]
on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import mymodule
>>> mymodule()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'module' object is not callable

与普通方法不同,__call__这种两边是双下划线的magic method只在类(和基类)
中查找。

(也不尽然:

>>> mymodule.__call__()
called!

准确地说,magic method在隐式调用(比如像mymodule()调用__call__)时有特
殊的查找规则;显式调用还是使用普通规则。)

为什么magic method要特殊对待。我不是Guido,所以只能猜测了。

原因a:这可是magic method!实例中需要定义它们的可能性接近于0。不查找实
例对性能总会有些帮助吧,magic method调用可是相当频繁的。

可能有些道理,但我认为还有更重要的原因:

如果我要让类C的实例callable,当然是定义C.__call__:

>>> class C(object):
...     def __call__(self):
...             print 'called'
...
>>> obj = C()
>>> obj()
called

很显然,C本身也是callable,因为C的metaclass--type也定义了__call__。那
么,根据查找顺序,C()应该调用谁,type.__call__还是C.__call__?实例优先
于类,应该是C.__call__。那可真是个不幸的结果。不仅调错了函数(C.__call__
是为C的实例,而不是C自己准备的),连参数个数都错了(回忆一下,因为
C.__call__是在实例C,而不是类type中找到,self是不会绑定的)。就因为这
个,magic method也非要有特殊的查找规则(不查找实例)不可。

话虽如此,"特殊"两字听起来总是不爽啊。能不能把"特殊"变成"一般",规定所
有方法调用都只在类中查找呢?不是有descriptor么,如果在实例中查找到的是
function-like descriptor,直接忽略掉它,其他查找规则不变,这样不就使得
方法只在类中查找,而数据成员还是实例中的优先了么?

很抱歉,还是不行。对象/类里的函数可不都是方法。记得我们的StaticMethod
么:

class StaticMethod(object):
    def __init__(self, f):
        self.f = f

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

按照这个规则,self.f根本查找不到。f可不是StaticMethod的方法,它是个函
数类型的数据成员。你打算禁止函数作为数据成员吗?我不确定这个和特殊查找
规则哪个听起来更糟。

说到底,问题在于方法和数据成员用法上不同:方法通常定义在类里,而数据成
员通常定义在实例里。这本来也没什么,用实例-类的查找顺序就行了。可加上
metaclass,再加上function as first-class object就糟糕了:你怎么知道
C.f()是要调用metaclass里定义的方法f,还是C里的数据成员f呢?

看看其他流行的面向对象语言:JavaScript没有类;C++/Java/C#没有metaclass;
Smalltalk调用方法和访问数据成员语法不同,而且方法(有名字)和函数(block,
无名字)都不是一个东西。惟独Python拥有metaclass和method=function这一不幸
的结合?

如果Python给方法调用换种语法也行,比如用->。C->f()是调用方法type.f,C.f()
是调用数据成员C.f,没有歧义。不过现在说这个太晚了。

像前面说的,禁止函数作为数据成员也不是不可以。真那样的话大家总可以把函
数放在list里蒙混过关。不过,Python实际上采用的对magic method区别对待的
做法还是更好:metaclass本来就少有人用,用的话也就定义__init__等少数几
个magic method,特殊处理一下就能解决绝大部分问题。也许你用了好几年也不
会注意到有这么个特殊规则。函数不能作为数据成员的话,那可是人人都要受影
响的。

有C语言的背景的话,可能会有"什么message passing这么玄乎,方法不就是个有
隐含参数的函数嘛"这样的想法。亲爱的Guido,没这么简单吧?

(待续)

--
Java is an application specific language, still looking for an
application to be specific to.
--- Per Abrahamsen, in advogato.org


※ 修改:·ilovecpp 于 Feb 13 01:06:45 修改本文·[FROM: 222.129.45.*]
※ 来源:·水木社区 newsmth.net·[FROM: 222.129.45.*]
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值