转自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.*]