5.1.6. 属性引用基础
x.name形式的表达式就是属性引用,在这里x是任意的一个表达式,name是一个标识符,也叫属性名。许多Python对象都拥有属性,但是对于类或实例来说,属性引用还有着其它的丰富语义。一定要记住,方法也是属性,所以只要下面说到属性就同时指普通数据属性与可执行属性(如,方法对象属性、类对象属性)。
假设x是类C的实例,C的基类是B:
class B(object):
a = 23
b = 45
def f(self): print "method f in class B"
def g(self): print "method g in class B"
class C(B):
b = 67
c = 89
d = 123
def g(self): print "method g in class C"
def h(self): print "method h in class C"
x = C( )
x.d = 77
x.e = 88
一些属性名是专有的。如C.__name__的值是字符串“C”,表示类的名字。C.__bases__是一个值为(B,)的tuple,表示类的基类序列。x.__class__的值是类对象C,表示实例的类型。这些属性都是类与实例的专有属性,你不可以针对它们做解除绑定的操作,但是可以进行重绑定。所以,我们可以修改类名,一个类的基类,以及实例的类型,总的来说,很少需要使用这个高级技巧。
类C与实例x都拥有另一个专用属性:__dict__字典。除上面介绍的属性外,所有其它的属性都由此字典进行引用。
5.1.6.1. 获取类属性
当针对一个类C执行语句C.name来访问其属性,具体的查找操作有两个步骤:
1、若’name’属性是C.__dict__的一个合法键值,则取出C.__dict__[‘name’]的值v。然后,若v是一个描述符(也就是说,type(v)提供了__get__方法),则C.name的值是调用type(v).__get__(v,None,C)的返回值。否则,C.name的值就是v本身。
2、若’name’不是C.__dict__的合法键值,则C会将此操作委托给它的基类进行同样的处理。
5.1.6.2. 获取实例属性
当针对一个类C的实例对象x执行语句x.name来访问其属性,则具体的查找操作有三个步骤:
1、若’name’是类C或其祖先类的一个覆盖描述符(即type(v)提供了__get__与__set__方法)的名字,则结果值就是type(v).__get__(v,x,C)。(只有新型实例才有此步处理)
2、否则,若’name’是x.__dict__的一个合法键值,则x.name的值就是x.__dict__[‘name’]。
3、否则,x.name会将查找操作委托给x的类进行处理(即按照上一小节中介绍的两步操作进行查找处理)。若在操作过程中,发现v是一个描述符,则最终结果又会是type(v).__get__(v,x,C)。若v不是一个描述符,则最终结果就是v的值。
若所有以上步骤都没能找到相应的属性,则Python将会抛出AttributeError异常。然而,对于查找属性x.name,若类C或其基类定义有__getattr__方法,则Python不会直接抛出异常,而是调用C.__getattr__(x,’name’),而最终结果是返回一个值还是引发一个异常(通常是AttributeError),则由方法__getattr__进行处理。
考虑本节开始时的例子:
print x.e, x.d, x.c, x.b, x.a # prints: 88, 77, 89, 67, 23
x.e与x.d都存在于字典x.__dict__中,且没有相应的描述符参与处理,所以在获取实例属性的第二步就完成了,最后的值是88与77。
而另外三个属性,都要委托给x的类C进行处理。属性c与b在获取类属性的第一步就完成了,因为c与b都在C.__dict__中存在,最终数值是89与67。但是x.a属性需要进行到获取类属性的第二步处理,即从C类的基类B得到最终的数值23。
5.1.6.3. 设置属性
上述的查找步骤只在引用一个属性时发生,而不是在绑定属性时的处理。
当绑定一个非专有属性(类属性或实例属性)时,即绑定非专有属性名的属性时,只会影响到__dict__属性中的项目(当然,存在__setattr__方法,或覆盖描述符的__set__方法,出现拦截绑定操作的情况除外)。也就是说,在绑定这些属性时,除了检查是否存在对应的覆盖描述符外,不会出现任何查找操作。
5.1.7. 约束与非约束方法
一个函数对象的__get__方法返回一个非约束方法对象或用来包装此函数的约束方法对象。两种方法对象间的关键区别就是非约束方法对象不与某个特定的实例关联,而约束方法与某个实例对象存在关联。
在上一节的例子中,属性f、g、与h都是函数,指向它们任意一个的属性引用都返回一个包装了对应函数对象的方法对象。考虑以下代码:
print x.h, x.g, x.f, C.h, C.g, C.f
此语句的输出表示前三个方法是约束方法:
<bound method C.h of <_ _main_ _.C object at 0x8156d5c>>
另三个方法是非约束方法:
<unbound method C.h>
若引用实例x的属性,则得到约束方法,当引用类C的属性时,则得到非约束方法。
因为一个约束方法已经与一个特定的实例相关联,如果调用此方法:
x.h( ) # prints: method h in class C
要注意到,不需要按通常的函数参数传递语法传入方法的第一个参数self,因为实例x的约束方法会隐含地将实例对象x作为第一个参数传入方法进行处理。也就是说,即使没有显式地传入self参数,在方法体内的代码,还是可以通过self变量来访问此实例对象。
然而,一个非约束方法,它没有与某个特定实例关联,所以当调用此方法时,必须通过第一个self参数来指定对应的实例对象。如:
C.h(x) # prints: method h in class C
与绑定方法相比,只有较少的机会调用非约束方法。调用非约束方法的主要目的就是调用祖先类的覆盖方法。即使是这种情况,最好也是通过使用内置的super函数来进行调用。
5.1.7.1. 非约束方法
当类的属性引用指向一个函数,则针对此属性的引用将返回一个封装此函数的非约束方法。非约束方法拥有三个与被包装的函数对象相关的特定属性:im_class是提供此方法的类对象,im_func是被包装的函数,im_self总是None。这些属性都是只读的,所以不能重绑定或解除绑定。
你可以像调用im_func函数一样来调用一个非约束方法,只是任何调用的第一个参数必须是im_class(或其子类)的一个实例。换句话说,对非约束方法的调用至少需要有一个参数,也就是被封装的函数的第一个形参(此参数约定的名字为self)。
5.1.7.2. 约束方法
当需要引用一个实例对象的某个属性(当然,对于方法属性来说,它必然是由实例的类所定义并拥有的一个函数对象),在查找的过程中,查找操作会调用函数对象的__get__方法来获取属性值。在这种情况下,将会创建并返回一个封装此函数的约束方法。
要特别注意,如果在属性引用的查找操作中,在x.__dict__中发现了相应的函数对象,则这个属性引用操作并不会创建一个约束方法,它只会忠实地返回这个函数对象本身作为属性的值,因为在这种情况下,这个函数不会被当作是一个描述符,函数对象的__get__方法也不会被调用。
类似地,针对非普通函数,如Python内置函数,也不会有创建约束方法的处理,因为它们都不是描述符。
与非约束方法一样,约束方法也有三个只读属性,im_class是提供方法的类对象,im_func是被封装的函数对象,im_self则指向得到此方法的对象x,而针对非约束方法,im_self的值是None。
除了不需要提供第一个参数(即self参数)外,对约束方法的调用与对im_func所表示函数对象的调用完全一致。在调用约束方法时,im_self被作为im_func函数的第一个参数自动传入。
按照下面提供的代码,来详细看看当执行x.name(arg)语句时到底发生了什么:
def f(a, b):... # a function f with two arguments
class C(object):
name = f
x = C( )
针对语句x.name(arg),其中x是类C的一个实例对象,name是引用x的某个方法的标识符(在本例中是类C的一个值为函数对象的类属性,即函数f),arg是任意表达式。Python首先检查name是否是类C的一个覆盖描述符属性,在本例中它不是,其次Python会检测name是否存在于x.__dict__的键值中,在本例中它也不在此字典中。所以Python会检查x的类对象C(以及C的所有基类),并发现name是类C的一个属性。同时Python也会发现name属性的值,即函数对象f,是一个描述符,所以,Python会调用f.__get__(x,C)来创建一个约束方法对象,并将此方法对象的im_func设置为f,im_class设置为C,im_self设置为x。最后Python会使用参数arg调用这个约束方法对象,同时,自动将im_self(值为x)作为真正的第一个参数传入函数内。最终的效果就等同于:
x.__class__.__dict__[‘name’](x,arg)
当一个约束方法的函数体在运行时,并没有与任何特定的名字空间,如对应的实例对象或类对象,建立联系。所有引用到的变量要么是全局变量,要么是局部变量,与任何一个旁通函数运行时的情况完全一样。
要注意,对于函数体内的代码而言,并没有一个隐含的像JAVA语言中this这样的隐含作用域存在,如果你想要达到与JAVA中this同样的效果,你必须要自己在代码中通过对self参数的引用来获取相应的属性值。
可以在任何需要使用一个可执行对象的地方使用约束方法对象。因为约束对象拥有对被它封装的函数对象的引用,同时又拥有对执行此方法的对象的引用,所以利用约束方法也可以高效、灵活地实现封装器的功能(closure,在嵌套函数与嵌套作用域章节中有介绍)。同样,一个提供了__call__方法的类的实例对象也可以提供封装器的功能。除了受具体应用场景的限制外,这些构建方法都可以让你将某些行卫(代码)与某些状态(数据)捆绑到一个单一的可执行对象中。下面是在嵌套函数与嵌套作用域章节中介绍的封装器:
def make_adder_as_closure(augend):
def add(addend, _augend=augend):
return addend+_augend
return add
下面是使用约束方法的等效、且更灵活的实现:
def make_adder_as_bound_method(augend):
class Adder(object):
def __init__(self, augend): self.augend = augend
def add(self, addend): return addend+self.augend
return Adder(augend).add
下面是使用可执行实例(此实例的类提供了__call__方法)的等效实现:
def make_adder_as_callable_instance(augend):
class Adder(object):
def __init__(self, augend): self.augend = augend
def __call__(self, addend): return addend+self.augend
return Adder(augend)
从调用函数的代码的角度来看,所有这些工厂函数都是可以相互交换使用的,因为它们都返回一个可执行对象。从实现的角度来看,closure最简单,约束方法与可执行实例使用了更灵活、更普通、更强大的机制。
5.1.8. 继承
当你对类C的某个属性通过语句C.name进行引用时,如果name不是C.__dict__的键值,则查找操作会隐式地在C的基类中进行,即对C.__bases__中的类按一定顺序进行查找操作(此查找顺序称MRO,即Method Resolution Order,当然不光是对方法有效,对于所有的属性都是按些顺序进行查找操作)。之后,C的基类又会将查找操作代理给它们的基类,直到name被找到。
5.1.8.1. 方法解析顺序(MRO)
对一个类的属性名的查找顺序基本上就是对其所有的基类按从左到右,深度优先的原则进行处理。然而,对于多重继承的情况,这个原则可能会导致某个基类超过一次被查找。解决这个问题的方法就是,针对一个类,若它有多次出现在查找顺序中,则保留处理它处于查找序列中最右边的一次。
通过下面基于传统类模型的例子可以清楚地看到这个问题:
class Base1:
def amethod(self): print "Base1"
class Base2(Base1):
pass
class Base3(Base1):
def amethod(self): print "Base3"
class Derived(Base2, Base3):
pass
aninstance = Derived( )
aninstance.amethod( ) # prints: "Base1"
在上面的情况中,对于amethod方法的查找开始于Derived类,在找不到后,将对Base2进行处理,同样,在Base2中也找不到,则这个传统模式的查找将会处理到Base2的基类,即Base1类,并成功找到方法amethod。注意,传统模式的查找处理将会就此打住,它不会再去考虑同样也拥有属性amethod的Base3类。新型类模型中的MRO则会移除Base1类在查找序列中的第一次出现,那么这时,最后得到的amethod就是Base3类的属性了。
每个新型类与内置类型都拥有一个特定的只读类属性__mro__,这是个存储类的tuple,用来这个类的方法解析顺序。以下网址对于MRO有更详细的介绍:www.python.org/2.3/mro.html。
5.1.8.2. 覆盖属性
就像前面看到的,对属性的查找是按MRO顺序进行的,并在第一次找到后就结束查找操作。子类总是总是会在父类之前被检查,那么当子类定义了一个父类中已有的同名属性时,那么必定是子类的属性被首先找到,并结束整个查找操作。这就是子类覆盖父类的定义。如以下代码:
class B(object):
a = 23
b = 45
def f(self): print "method f in class B"
def g(self): print "method g in class B"
class C(B):
b = 67
c = 89
d = 123
def g(self): print "method g in class C"
def h(self): print "method h in class C"
类C覆盖了父类B的属性b与g。注意,在Python中,对于数据属性与可执行属性(方法),都可以进行覆盖操作。
5.1.8.3. 向父类方法的委托
当类C覆盖了父类B的一个方法f,一般情况下,在C.f的代码中都会需要将一部分实现委托给父类的实现去完成。这可以通过非约束方法来处理,如:
class Base(object):
def greet(self, name): print "Welcome ", name
class Sub(Base):
def greet(self, name):
print "Well Met and",
Base.greet(self, name)
x = Sub( )
x.greet('Alex')
在上面代码中,通过非约束方法Base.greet来获取对父类方法的引用,并手动传入包括self在内的所有调用参数。这也是最常使用的一种方式。
另一种经常需要使用委托的地方是特定方法__init__,与其它一些语言不同,当Python创建一个对象实例时,并不会自动调用父类的__init__方法。因而,在需要的情况下,要使用委托来进行实现。
class Base(object):
def __init__(self):
self.anattribute = 23
class Derived(Base):
def __init__(self):
Base.__init__(self)
self.anotherattribute = 45
如上面代码表示的,若子类不明确调用父类的__init__方法,则就会丢失由父类定义的所有属性。
5.1.8.4. 合作的父类方法调用
通过非约束方法的语法可以调用父类的方法,但对于多重继承的情况还存在问题,考虑以下代码:
class A(object):
def met(self):
print 'A.met'
class B(A):
def met(self):
print 'B.met'
A.met(self)
class C(A):
def met(self):
print 'C.met'
A.met(self)
class D(B,C):
def met(self):
print 'D.met'
B.met(self)
C.met(self)
当调用D().met()时,A.met()方法将会被调用二次,如何保证基类方法只被调用一次?答案就是使用内置类型super。super(aclass,obj)可以返回对象obj的一个特定的superobject。当我们在这个superobject上查找一个属性(如,一个方法)时,相应的查找操作会在MRO序列中的由aclass指定的类之后开始。下面重写这一段代码:
class A(object):
def met(self):
print 'A.met'
class B(A):
def met(self):
print 'B.met'
super(B,self).met( )
class C(A):
def met(self):
print 'C.met'
super(C,self).met( )
class D(B,C):
def met(self):
print 'D.met'
super(D,self).met( )
现在调用D().met()将只会调用所有基类的met方法一次。所以,当调用超类方法时全部都通过super来实现,是一个良好的习惯,且对程序没有任何其它的不良影响。
5.1.8.5. 移除类属性
继承与覆盖提供了非入侵式(入侵是指直接修改定义了属性的那个类本身)的,且简单而有效的方式,用来添加或修改类属性(特别是可以添加或修改类方法)。但继承没有提供移除父类属性的途径,若Python在子类中找不到某个属性,它会自动将查找操作委托给基类。所以,要想移除一个父类属性,我们必须要首先覆盖此方法,并引发异常。
避开继承,不在子类的__dict__中而在别处保持对属性的引用,然后再针对选中的委托定义__getattr__。
若使用新型对象模型,也可以直接覆盖__getattribute__方法。