魏忠的Space发布 ::-- WeiZhong [2006-01-09 23:58:38] ::-- ZoomQuiet [2006-01-09 06:12:38]

1. python中的新型类及其实例详解

(原文见《Python In a Nutshell(2003)》5.2节)

1.1. 写在前面

刚刚接触python不久,对python中的Classic Class 及 New-Style Class 的区别一头雾水中,在啃《Python In a Netshell》时发现第五章第二节讲得非常好,于是边读边译,就有了这篇东西。对于原文中已经过时的东西,我做了删减,对python2.4中新增的东西,我做了部分补充。由于对python术语不是很熟悉,有翻译错误之处,还请大家指正。

常见术语:

Classic 被翻译为 传统, 如 Classic Class(传统类), classic object model (传统对象模型) New-Style 在视上下文被翻译为 新型 或者 新的, 如 New-Style Class(新型类),New-Style object model (新的对象模型)

可以考虑不翻译,更容易理解 -- Limodou

1.2. 5.2 新型类及其实例

前面我已经讲了python 2.2中引入的 new-style 对象模型. 新型类及其实例象传统类一样,都是第一层次对象,都可以拥有任意的属性,通过调用一个类生成该类的一个实例等等. 在这一小节,我来向大家揭示新的对象模型及传统对象模型的不同.

从python2.2起,如果一个类继承自内建的object类型(或者它是任何内建类型如list,dict,file的子类),那它就是一个新型类。在这之前,不允许通过继承内建类别生成新类,也根本没有object这个对象。在本章5.4节的后半部分,我介绍了将一个传统类改造成新型类的方法。

在本章的开头,我建议每个人从现在开始养成只使用新型类的编程习惯(当然你得用Python2.2以上版本)。新的对象模型与传统对象模型相比有小但却很重要的优势,可以说接近完美. 很简单,认准新的对象模型,好好学没错.

1.3. 5.2.1 内建的object类型

object类是所有内建类型及新型类的祖先。 object类定义了一系列特殊方法(本章5.3节后半部分有它们的文档)来实现所有对象的默认行为。

__new__,__init__方法 你可以创建 object 的直接子类,静态方法 __new__()用来创建类的实例,实例的__init__()方法用来初始化自己。默认的__init__()方法会忽略你传递过来的任何参数。(2.4版好象做了改动,__new__和__init__方法默认都不接收任何参数,除非你重新实现这两个方法)

__delattr__ , __getattribute__, __setattr__ 方法 对象用这些方法来处理属性引用。本章前半部分已经做了详细介绍。

__hash__ , __repr__, __str__ 方法 一个对象可以作为参数传递给这些方法.

允许object的子类重载这些方法,或添加新方法。

1.4. 5.2.2 类方法

新的对象模型提供了两种类方法(传统对象模型没有这些方法):静态方法和类方法。只有python2.2及更新版本才支持类方法. 需要提一下的是,在python2.2及更新版本中,传统类也实现了类方法。新的对象模型提供的诸多新特性中,有且仅有类方法这一特性被传统对象模型全功能实现。

1.4.1. 5.2.2.1静态方法

静态方法可以直接被类或类实例调用。它没有常规方法那样的特殊行为(绑定、非绑定、默认的第一个参数规则等等)。完全可以将静态方法当成一个用属性引用方式调用的普通函数来看待。任何时候定义静态方法都不是必须的(静态方法能实现的功能都可以通过定义一个普通函数来实现). 某些程序员认为,当有一堆函数仅仅为某一特定类编写时,这种方式可以提供使用上的一致性。 根据python2.4提供的新的语法,你可以象下面这样来创建一个静态方法,

   1 class AClass(object):
   2     @staticmethod       #静态方法修饰符,表示下面的方法是一个静态方法
   3     def astatic(  ): print 'a static method'
   4 anInstance = AClass(  )
   5 AClass.astatic(  )                    # prints: a static method
   6 anInstance.astatic(  )                # prints: a static method

注:staticmethod是一个内建函数,用来将一个方法包装成静态方法,在2.4以前版本,要用下面的方式定义一个静态方法(不再推荐使用):

   1 class AClass(object):
   2     def astatic(  ): print 'a static method'
   3     astatic=staticmethod(astatic)

这种方法在函数定义本身比较长时经常会忘记后面这一行.

1.4.2. 5.2.2.2 类方法

一个类方法就是你可以通过类或它的实例来调用的方法, 不管你是用类调用这个方法还是类的实例调用这个方法,python只会将实际的类对象做为该方法的第一个参数.记住:方法的第一个参数都是类对象而不是实例对象. 按照惯例,类方法的第一个形参被命名为 cls. 任何时候定义类方法都不是必须的(静态方法能实现的功能都可以通过定义一个普通函数来实现,只要这个函数接受一个类对象做为参数就可以了).某些程序员认为这个特性当有一堆函数仅仅为某一特定类编写时会提供使用上的一致性.

你可以象下面这样来生成一个类方法:

   1 class ABase(object):
   2     @classmethod        #类方法修饰符
   3     def aclassmet(cls): print 'a class method for', cls.__name__
   4 class ADeriv(ABase): pass
   5 bInstance = ABase(  )
   6 dInstance = ADeriv(  )
   7 ABase.aclassmet(  )               # prints: a class method for ABase
   8 bInstance.aclassmet(  )           # prints: a class method for ABase
   9 ADeriv.aclassmet(  )              # prints: a class method for ADeriv
  10 dInstance.aclassmet(  )           # prints: a class method for ADeriv

注:staticmethod是一个内建函数,用来将一个方法封装成类方法,在2.4以前版本,你只能用下面的方式定义一个类方法:

   1 class AClass(object):
   2     def aclassmethod(cls): print 'a class method'
   3     aclassmethod=staticmethod(aclassmethod)

并没有人要求必须封装后的方法名字必须与封装前一致,但建议你总是这样做(如果你使用python2.4版本以下时). 这种方法在函数定义本身比较长时经常会忘记后面这一行.

1.5. 5.2.3 新型类

除了拥有传统类的全部特性之外,新型类当然还具有一些新特性.__init__特殊方法的行为与传统类相比有了一些变化,另外还新增了一个名为 __new__的静态方法

1.5.1. 5.2.3.1 __init__方法

新型类C中,从 object 继承来的原始 __init__方法,可以认为就是一个 pass 语句,因为它几乎什么都不做,建议你在所有的新型类中重载 __init__方法.

   1 class C(object):
   2     def __init__(self): pass
   3     # rest of class body omitted

示例中的的类只允许无参数调用,硬要传递一个参数给它会产生异常(如用C('xyz')). 如果C没有重载__init__方法,调用C('xyz')会象'xyz'根本不存在一样忽略参数继续执行. 注意: (根据我的试验,2.4版中这点发生了变化,即使没有重载__init__方法,象C('xyz')这样调用一样会引发异常)

1.5.2. 5.2.3.2 __new__方法

每一个新型类都有一个名为__new__的静态方法. 当你调用 C(*args,**kwds)创建一个C实例时,python内部调用的是 C.__new__(C,*args,**kwds). new方法的返回值 x 就是该类的实例. 在确认 x 是C的实例以后, python调用C.__init__(x,*args,**kwds)来初始化这个实例. 也就是说,对新类C来讲,语句 x=C(23)等同于:

   1 x = C.__new__(C, 23)
   2 if isinstance(x, C): C.__init__(x, 23)

object.__new__创建一个新的,未初始化的类实例,它接收传递过来的第一个参数(也就是类对象本身),忽略其它的参数.当你重载__new__方法时,你不必使用函数修饰符@staticmethod, python解释器根据上下文会认出__new__()方法是一个静态方法. 如果你需要重绑定 C.__new__方法,你只需要在类外面执行 C.__new__=staticmethod(你想使用的新方法)就可以了.(极少有这样的需求)

__new__方法拥有函数工厂的绝大部分弹性. 根据实际需求,我们可以让__new__返回一个已有的实例或者创建一个新的实例.
下面举一个通过重载__new__方法实现独身对象的设计模式的例子:

   1 class Singleton(object):
   2     _singletons = {}
   3     def __new__(cls, *args, **kwds):
   4         if not cls._singletons.has_key(cls):            #若还没有任何实例
   5             cls._singletons[cls] = object.__new__(cls)  #生成一个实例
   6         return cls._singletons[cls]                             #返回这个实例

Singleton的所有子类(当然是没有重载__new__方法的子类)都只可能有一个实例. 如果该类的子类定义了一个__init__方法,那么它必须保证它的__init__方法能够安全的对同一实例进行多次调用.

1.6. 5.2.4新型类实例

新型类实例除了拥有传统类实例的全部特性之外,还拥有一种称为property的新属性及一个叫作__slots__的特殊属性,该属性会对实例其它属性的访问产生重要影响. 新的对象模型同样添加了一个新的方法 __getattribute__ 比原有的 __getattr__ 方法更通用. 不同的实例可以拥有这些特殊方法的不同实现.

1.6.1. 5.2.4.1 Properties

property 是实例中具有特殊功能的属性. 你可以使用常规语法对property进行引用,绑定或解除绑定.如:

   1 print x.prop
   2 x.prop=23
   3 del x.pro

然而,property如果只有这点功能那就和普通属性没什么两样了,property有它的独到之处,请往下读. 下面介绍如何定义一个只读property:

   1 class Rectangle(object):
   2     def __init__(self, width, heigth):
   3         self.width = width
   4         self.heigth = heigth
   5     def getArea(self):
   6         return self.width * self.heigth
   7     area = property(getArea, doc='area of the rectangle')

矩形类的每一个实例 r 均拥有一个只读属性 r.area ,该属性由 r.getArea()方法实时计算得来. Rectangle.area.__doc__是'area of the rectangle',这个属性是只读的(试图对它进行重绑定或解除绑定的企图都注定会失败),这是因为我们在property定义中只指定了该属性 get 方法.

properties 干的活与那些特殊方法__getattr__,__setattr__,__delattr__等是极其相似的,不过同样的活它干起来更简单更快捷. 内建property类别(我倒宁愿把当成一个函数来看)用来生成一个property,并将其返回值绑定为一个类属性.如同绑定类的常规属性,一般在定义类时就创建property,当然也有其它选择.假设在定义新型类 C 时,使用以下语法:

attrib = property(fget=None, fset=None, fdel=None, doc=None)

x是C的一个实例,当你引用 x.attrib 时,python调用 fget方法取值给你. 当你为x.attrib赋值: x.attrib=value 时,python调用 fset方法,并且value值做为fset方法的参数,当你执行del x.attrib 时,python调用fdel方法,你传过去的名为doc的参数即为该属性的文档字符串. 在矩形类中,因为我们没有为area属性指定fset和fdel参数,所以该属性只能读取.

1.6.2. 5.2.4.2 __slots__属性

通常,每个实例对象 x 都拥有一个字典 x.__dict__. python通过此字典允许你绑定任意属性给 x 实例. 定义一个名为__slots__的类属性可以有效减少每个实例占用的内存数量.__slots__是一个字符串序列(通常是一个tuple).  当类 C 拥有 __slots__属性, x的直接子类就没有 x.__dict__属性. 如果试图绑定一个__slots__中不存在属性给实例的话,就会引发异常. __slots__属性虽然令你失去绑定任意属性的方便,却能有效节省每个实例的内存消耗,有助于生成小而精干的实例对象.  
注: 当一个类会生成很多很多实例时(有些类同时拥有数百万而不是几千个实例),即使一个实例节省几十个字节都可节省一大笔内存时,就值得使用__slots__属性.只有在类定义中可以使用 __slots__=aTuple 语句来为一个类添加__slots__属性,其它任何位置对一个类或其父类的__slots__属性的修改,重新绑定或解除绑定都是无效的.
下面介绍如何通过添加 __slots__属性给刚才定义的 Rectangle 类,以得到瘦身的类实例:

   1 class OptimizedRectangle(Rectangle):
   2     __slots__ = 'width', 'heigth'

__slots__里不能包含 properties, 只能包含常规实例属性.我们不需也不允许给area property也定义一个slot.  若不定义 __slots__属性,常规属性则保存在实例的__dict__属性中.

__slot__ 只是用来占位,因此对于__slot__定义的属性名,你首先要赋值,然后才可以使用。直接使用是会报错的。 -- Limodou

1.6.3. 5.2.4.3 __getattribute__方法

对新型类的实例来说, 所有的属性引用都是通过特殊方法__getattribute__()完成的.该方法由基类对象提供,负责实现对象属性引用的全部细节.在本章的前面有该方法详细的文档. 如果有特殊需求,你也可以重载 __getattribute__属性(比如你打算在子类实例中隐藏父类的某些属性或方法).下面的例子演示了实现一个没有append方法的list类:

   1 class listNoAppend(list):
   2     def __getattribute__(self, name):
   3         if name == 'append': raise AttributeError, name
   4         return list.__getattribute__(self, name)

除了功能不全以外,该类的实例与内建list对象完全相同.任何调用该类实例append方法的企图都会引发一个异常.

1.6.4. 5.2.4.4个体实例方法

传统与新的对象模型都允许一个实例拥有私有的属性和方法(通过绑定或重绑定) . 实例的私有属性会屏蔽掉类定义中的同名属性.举例来说:

   1 class abc(object):
   2         def attrib_a(self):
   3                 print 'aMethod defined in class abc'
   4 b=abc()
   5 def afunc():
   6         print 'hello,world!'
   7 b.attrib_a=afunc
   8 b.attrib_a()

该例子将打印 'hello,world!'

在python隐式调用实例的私有(后绑定)特殊方法时,新的对象模型改变了传统对象模型的行为.在传统对象模型中,无论是显示调用,还是隐式调用,都会调用这个实例的后绑定特殊方法.而在新的对象模型中,除非显式调用实例的特殊方法,否则python总是去调用在类中定义的特殊方法.下面这个例子可以说明这一点:

   1 def fakeGetItem(idx): return idx
   2 class Classic: pass
   3 c = Classic(  )
   4 c.__getitem__ = fakeGetItem
   5 print c[23]                       # prints: 23
   6 class NewStyle(object): pass
   7 n = NewStyle(  )
   8 n.__getitem__ = fakeGetItem
   9 print n[23]                       # 程序执行到这步会出错. 如果将代码改为 print n.__getitem__(23) 则正常运行
  10 # Traceback (most recent call last):
  11 #   File "<stdin>", line 1, in ?
  12 # TypeError: unindexable object

调用n[23],将产生一个隐式的__getitem__方法调用,因为新型类 n 中并未定义该方法,所以引发了异常.不过如果你使用n.__getitem__(23)这种方式来显式调用特殊方法时,它还是可以工作的.

1.7. 5.2.5 新的对象模型中的继承

在新的对象模型中,继承的使用方式与传统模型大致相同.一个关键的区别就是新型类能从一个内建类型中继承而传统类不能. 新型类仍然支持多继承,若要从多个内建类型继承生成一个新类,则这些内建类型必须是经过特殊设计能够相互兼容. python不支持随意的从多个内建类型进行多继承,通常情况都是通过至多从一个内建类型继承得到新类.这意味着在多继承时,除object以外,至多有一个内建类型可以是其它内建类型和新型类的超类.

1.7.1. 5.2.5.1方法解析顺序:

在传统对象模型中,方法和属性按 从左至右 深度优先 的顺序查找.显然,当多个父类继承自同一个基类时,这会产生我们不想要的结果. 举例来说, A是B和C的子类,而B和C继承自D,传统对象模型的的属性查找方法是 A-B-D-C-D. 由于Python先查找D后查找C,即使C对D中的方法进行了重定义,也只能使用D中定义的版本.由于这个继承模式固有的问题,在实际应用中会造成一些麻烦.

在新的对象模型中,所有类均直接或间生成子类对象. python改变了传统对象模型中的解析顺序,使用上面的例子,当D是一个新型类(比如D是object 的直接子类),新的对象模型的搜索顺序就变为 A-B-C-D.

每个内建类型及新型类均内建一个特殊的只读属性 __mro__ ,这是一个tuple,保存着方法解析类型. 只允许通过类来引用 __mro__(不允许通过实例).

1.7.2. 5.2.5.2 协作式调用超类方法

前面我们提到,当一个子类重载了父类中一个方法,子类中的方法通常要调用父类中的同名方法来做一些事.这也是python传统对象模型惯用的方式,即使用非绑定方法语法调用父类的同名方法.当多继承时,这种方法是有缺限的,见下例:

   1 class A(object):
   2     def met(self):
   3         print 'A.met'
   4 class B(A):
   5     def met(self):
   6         print 'B.met'
   7         A.met(self)
   8 class C(A):
   9     def met(self):
  10         print 'C.met'
  11         A.met(self)
  12 class D(B,C):
  13     def met(self):
  14         print 'D.met'
  15         B.met(self)
  16         C.met(self)

在上面的代码中,当我们调用 D().met()方法时, A.met()方法被调用了两次. 我们怎样才可以保证每个父类的实现均被顺序调用且仅仅调用一次呢?不采取点特殊措施这个问题很难解决.从python2.2起,提供了这样一个特殊手段. 那就是 super类型. super(aclass,obj)返回对象obj的一个特殊的超对象(superobject). 当我们调用该超对象的一个属性或方法时,就保证了每个父类的实现均被调用且仅仅调用一次了. 改写后的代码如下:

   1 class A(object):
   2     def met(self):
   3         print 'A.met'
   4 class B(A):
   5     def met(self):
   6         print 'B.met'
   7         super(B,self).met(  )
   8 class C(A):
   9     def met(self):
  10         print 'C.met'
  11         super(C,self).met(  )
  12 class D(B,C):
  13     def met(self):
  14         print 'D.met'
  15         super(D,self).met(  )

现在就可以得到期望的结果了. 如果你养成了总是使用superclass调用父类方法,你的类就能适应无论多复杂的继承结构.

PyNewStyleClass (last edited 2006-01-11 12:07:09 by limodou)