Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1500142
  • 博文数量: 297
  • 博客积分: 10010
  • 博客等级: 上将
  • 技术积分: 3082
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-07 11:36
文章分类

全部博文(297)

文章存档

2011年(1)

2009年(45)

2008年(67)

2007年(184)

我的朋友

分类: Python/Ruby

2009-03-24 14:15:24

第九章 类

  Python是一个真正面向对象的语言,它只增加了很少的新语法就实现了类。它的类机制是C++ 和Modula-3的类机制的混合。Python的类并不严格限制用户对定义的修改,它依赖于用户自觉不去修改定义。然而Python对类最重要的功能都保持了完全的威力。类继承机制允许多个基类的继承,导出类可以重载基类的任何方法,方法可以调用基类的同名方法。对象可以包含任意多的私有数据。

  用C++术语说,所有类成员(包括数据成员)是公用的,所有成员函数是虚拟(virtual)的。没有特别的构建函数或销毁函数(destructor)。如同在Modula-3中一样,从对象的方法中要引用对象成员没有简捷的办法:方法函数的必须以对象作为第一个参数,而在调用时则自动提供。象在Smalltalk中一样,类本身也是对象,实际上这里对象的含义比较宽:在Python 中所有的数据类型都是对象。象在C++或Modula-3中一样,内置类型不能作为基类由用户进行扩展。并且,象C++但不象Modula-3,多数有特殊语法的内置函数(如算术算符、下标等)可以作为类成员重定义。

9.1 关于术语

  Python的对象概念比较广泛,对象不一定非得是类的实例,因为如同C++和Modula-3而不同于Smalltalk,Python的数据类型不都是类,比如基本内置类型整数、列表等不是类,甚至较古怪的类型如文件也不是类。然而,Python所有的数据类型都或多或少地带有一些类似对象的语法。

  对象是有单独身份的,同一对象可以有多个名字与其联系,这在其他语言中叫做别名。这样做的好处乍一看并不明显,而且对于非可变类型(数字、字符串、序表(tuple))等没有什么差别。但是别名句法对于包含可变对象如列表、字典及涉及程序外部物件如文件、窗口的程序有影响,这可以有利于程序编制,因为别名有些类似指针:比如,传递一个对象变得容易,因为这只是传递了一个指针;如果一个函数修改了作为参数传递来的对象,修改结果可以传递回调用处。这样就不必象Pascal那样使用两种参数传递机制。

9.2 Python作用域与名字空间

  在引入类之前,我们必须讲一讲Python的作用域规则。类定义很好地利用了名字空间,需要了解Python如何处理作用域和名字空间才能充分理解类的使用。另外,作用域规则也是一个高级Python程序员必须掌握的知识。

  先给出一些定义。

  名字空间是从名字到对象的映射。多数名字空间目前是用Python字典类型实现的,不过这一点一般是注意不到的,而且将来可能会改变。下面是名字空间的一些实例:Python中内置的名字(如abs()等函数,以及内置的例外名);模块中的全局名;函数调用中的局部变量名。在某种意义上一个对象的所有属性也构成了一个名字空间。关于名字空间最重要的事要知道不同名字空间的名字没有任何联系;例如,两个不同模块可能都定义了一个叫“maximize ”的函数而不会引起混乱,因为模块的用户必须在函数名之前加上模块名作为修饰。

  另外,在Python中可以把任何一个在句点之后的名字称为属性,例如,在表达式z.real中,real是一个对象z的属性。严格地说,对模块中的名字的引用是属性引用:在表达式modname.funcname 中,modname是一个模块对象,funcname是它的一个属性。在这种情况下在模块属性与模块定义的全局名字之间存在一个直接的映射:它们使用相同的名字空间!

  属性可以是只读的也可以是可写的。在属性可写的时候,可以对属性赋值。模块属性是可写的:你可以写“modname.the_answer = 42”。可写属性也可以用del语句闪出,如“del modname.the_answer”。

  名字空间与不同时刻创建,有不同的生存周期。包含Python内置名字的名字空间当Python 解释程序开始时被创建,而且不会被删除。模块的全局名字空间当模块定义被读入时创建,一般情况下模块名字空间也一直存在到解释程序退出。由解释程序的最顶层调用执行的语句,不论是从一个脚本文件读入的还是交互输入的,都属于一个叫做__main__的模块,所以也存在于自己的全局名字空间之中。(内置名字实际上也存在于一个模块中,这个模块叫做__builtin__ )。

  函数的局部名字空间当函数被调用时创建,当函数返回或者产生了一个不能在函数内部处理的例外时被删除。(实际上,说是忘记了这个名字空间更符合实际发生的情况。)当然,递归调用在每次递归中有自己的局部名字空间。

  一个作用域是Python程序中的一个文本区域,其中某个名字空间可以直接访问。“直接访问” 这里指的是使用不加修饰的名字就直接找到名字空间中的对象。

  虽然作用域是静态定义的,在使用时作用域是动态的。在任何运行时刻,总是恰好有三个作用域在使用中(即恰好有三个名字空间是直接可访问的):最内层的作用域,最先被搜索,包含局部名字;中层的作用域,其次被搜索,包含当前模块的全局名字;最外层的作用域最后被搜索,包含内置名字。

  一般情况下,局部作用域引用当前函数的局部名字,其中局部是源程序文本意义上来看的。在函数外部,局部作用域与全局作用域使用相同的名字空间:模块的名字空间。类定义在局部作用域中又增加了另一个名字空间。

  一定要注意作用域是按照源程序中的文本位置确定的:模块中定义的函数的全局作用域是模块的名字空间,不管这个函数是从哪里调用或者以什么名字调用的。另一方面,对名字的搜索却是在程序运行中动态进行的,不过,Python语言的定义也在演变,将来可能发展到静态名字解析,在“编译”时,所以不要依赖于动态名字解析!(实际上,局部名字已经是静态确定的了)。

  Python的一个特别之处是赋值总是进入最内层作用域。关于删除也是这样:“del x”从局部作用域对应的名字空间中删除x的名字绑定(注意在Python中可以多个名字对应一个对象,所以删除一个名字只是删除了这个名字与其对象间的联系而不一定删除这个对象。实际上,所有引入新名字的操作都使用局部作用域:特别的,import语句和函数定义把模块名或函数名绑定入局部作用域。(可以使用global语句指明某些变量是属于全局名字空间的)。

9.3 初识类

  类引入了一些新语法,三种新对象类型,以及一些新的语义。

9.3.1 类定义语法

  类定义的最简单形式如下:

class 类名:
    <语句-1>
    .
    .
    .
    <语句-N>

  如同函数定义(def语句)一样,类定义必须先执行才能生效。(甚至可以把类定义放在if 语句的一个分支中或函数中)。在实际使用时,类定义中的语句通常是函数定义,其它语句也是允许的,有时是有用的――我们后面会再提到这一点。类内的函数定义通常具有一种特别形式的自变量表,专用于方法的调用约定――这一点也会在后面详细讨论。

  进入类定义后,产生了一个新的名字空间,被用作局部作用域――于是,所有对局部变量的赋值进入这个新名字空间。特别地,函数定义把函数名与新函数绑定在这个名字空间。

  当函数定义正常结束(从结尾退出)时,就生成了一个类对象。这基本上是将类定义生成的名字空间包裹而成的一个对象;我们在下一节会学到类对象的更多知识。原始的局部作用域(在进入类定义之前起作用的那个)被恢复,类对象在这里被绑定到了类对象定义头部所指定的名字。

9.3.2 类对象

  类对象支持两种操作:属性引用和实例化。属性引用的格式和Python中其它的属性引用格式相同,即obj.name。有效的属性名包括生成类对象时的类名字空间中所有的名字。所以,如果象下面这样定义类:


class MyClass:
    "A simple example class"
    i = 12345
    def f(x):
        return 'hello world'

  则MyClass.i和MyClass.f都是有效的属性引用,分别返回一个整数和一个函数对象。也可以对类属性赋值,所以你可以对MyClass.i赋值而改变该属性的值。

  __doc__也是一个有效的属性,它是只读的,返回类的文档字符串:“A simple example class”。

  类实例化使用函数记号。只要把这个类对象看成是一个没有自变量的函数,返回一个类实例。例如(假设使用上面的类):

 x = MyClass()

  可以生成该类的一个新实例并把实例对象赋给局部变量x。 

9.3.3 实例对象

  我们如何使用实例对象呢?类实例只懂得属性引用这一种操作。有两类有效的属性。 

  第一类属性叫做数据属性。数据属性相当于Smalltalk中的“实例变量”,和C++中的“数据成员”。数据成员不需要声明,也不需要在类定义中已经存在,象局部变量一样,只要一赋值它就产生了。例如,如果x是上面的MyClass类的一个实例,则下面的例子将显示值16而不会留下任何痕迹:


x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print x.counter
del x.counter

  类实例能理解的第二类属性引用是方法。方法是“属于”一个对象的函数。(在Python中,方法并不是只用于类实例的:其它对象类型也可以有方法,例如,列表对象也有append、insert 、remove、sort等方法。不过,在这里除非特别说明我们用方法来特指类实例对象的方法)。

  类对象的有效方法名依赖于它的类。按照定义,类的所有类型为函数对象属性定义了其实例的对应方法。所以在我们的例子y,x.f是一个有效的方法引用,因为MyClass是一个函数;x.i 不是方法引用,因为MyClass.i不是。但是x.f和MyClass.f不是同一个东西――x.f是一个方法对象而不是一个函数对象。

9.3.4 方法对象

  方法一般是直接调用的,例如:

 x.f()

  在我们的例子中,这将返回字符串‘hello world’。然而,也可以不直接调用方法:x.f 是一个方法对象,可以把它保存起来再调用。例如:


xf = x.f
while 1:
    print xf()

  会不停地显示“hello world”。 

  调用方法时到底发生了什么呢?你可能已经注意到x.f()调用没有自变量,而函数f在调用时有一个自变量。那个自变量是怎么回事?Python如果调用一个需要自变量的函数时忽略自变量肯定会产生例外错误――即使那个自变量不需要用到……

  实际上,你可以猜出答案:方法与函数的区别在于对象作为方法的第一个自变量自动传递给方法。在我们的例子中,调用x.f()等价于调用MyClass.f(x)。一般地,用n个自变量的去调用方法等价于把方法所属对象插入到第一个自变量前面以后调用对应函数。

  如果你还不理解方法是如何工作的,看一看方法的实现可能会有所帮助。在引用非数据属性的实例属性时,将搜索它的类。如果该属性名是一个有效的函数对象,就生成一个方法对象,把实例对象(的指针)和函数对象包装到一起:这就是方法对象。当方法对象用一个自变量表调用时,它再被打开包装,由实例对象和原自变量表组合起来形成新自变量表,用这个新自变量表调用函数。

9.4 一些说明

  在名字相同时数据属性会覆盖方法属性;为了避免偶然的名字冲突,这在大型程序中会造成难以查找的错误,最好按某种命名惯例来区分方法名和数据名,例如,所有方法名用大写字母开头,所有数据属性名前用一个唯一的字符串开头(或者只是一个下划线),或方法名用动词而数据名用名词。

  数据属性可以被方法引用也可以被普通用户(“客户”)引用。换句话说,类不能用来构造抽象数据类型。实际上,Python中没有任何办法可以强制进行数据隐藏——这些都是基于惯例。(另一方面,Python的实现是用C写的,它可以完全隐藏实现细节,必要时可以控制对象存取;用C写的Python扩展模块也有同样特性)。

客户要自己小心使用数据属性——客户可能会因为随意更改类对象的数据属性而破坏由类方法维护的类数据的一致性。注意客户只要注意避免名字冲突可以任意为实例对象增加新数据属性而不需影响到方法的有效性——这里,有效的命名惯例可以省去许多麻烦。

  从方法内要访问本对象的数据属性(或其它方法)没有一个简写的办法。我认为这事实上增加了程序的可读性:在方法定义中不会混淆局部变量和实例变量。

  习惯上,方法的第一自变量叫做self。这只不过是一个习惯用法:名字self在Python中没有任何特殊意义。但是,因为用户都使用此惯例,所以违背此惯例可能使其它Python程序员不容易读你的程序,可以想象某些类浏览程序会依赖于此惯例)。

  作为类属性的任何函数对象都为该类的实例定义一个方法。函数的定义不一定必须在类定义内部:只要在类内把一个函数对象赋给一个局部变量就可以了。例如:


# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)
 
class C:
    f = f1
    def g(self):
        return 'hello world'
    h = g

  现在f、g和h都是类C的属性且指向函数对象,所以它们都是C的实例的方法——其中h与g 完全等价。注意我们应该避免这种用法以免误导读者。

  方法可以用代表所属对象的self自变量来引用本类其它的方法,如:


class Bag:
    def empty(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

  实例化操作(“调用”一个类对象)生成一个空对象。许多类要求生成具有已知初识状态的类。为此,类可以定义一个特殊的叫做__init__()的方法,如:


    def __init__(self):
        self.empty()

  一个类定义了__init__()方法以后,类实例化时就会自动为新生成的类实例调用调用__init__() 方法。所以在Bag例子中,可以用如下程序生成新的初始化的实例:

x = Bag()

  当然,__init__()方法可以有自变量,这样可以实现更大的灵活性。在这样的情况下,类实例化时指定的自变量被传递给__init__()方法。例如:


>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
... 
>>> x = Complex(3.0,-4.5)
>>> x.r, x.i
(3.0, -4.5)

  方法可以和普通函数一样地引用全局名字。方法的全局作用域是包含类定义的模块。(注意类本身并不被用作全局作用域!)虽然我们很少需要在方法中使用全局数据,全局作用域还是有许多合法的用途:例如,导入全局作用域的函数和模块可以被方法使用,在同一模块中定义的函数和方法也可以被方法使用。包含此方法的类一般也在此全局作用域中定义,下一节我们会看到一个方法为什么需要引用自己的类!

9.5 继承

  当然,一个语言如果不支持继承就谈不到“类”。导出类的定义方法如下:

  class 导出类名(基类名):


    <语句-1>
    .
    .
    .
    <语句-N>

  其中“基类名”必须在包含导出类定义的作用域中有定义。除了给出基类名外,还可以给出一个表达式,在基类定义于其它模块中时这是有用的,如:

  class 导出类名 (模块名.基类名):

  导出类定义的运行和基类运行的方法是一样的。生成类对象是,基类被记忆。这用于解决属性引用:如果类中未找到要求的属性就到基类中去查找。如果基类还有基类的话这个规则递归地应用到更高的类。

  导出类在实例化时没有任何特殊规则。“导出类名()”产生该类的一个新实例。方法引用这样解决:搜索相应类属性,如果必要的话逐级向基类查找,如果找到了一个函数对象就是有效的方法引用。

  导出类可以重写基类的方法。因为方法在调用同一对象的其它方法时并无任何特殊权限,如果基类中某一方法调用同一基类的另一方法,在导出类中该方法调用的就可能是已经被导出类重写后的方法了。(对C++程序员而言:Python中所有方法都是“虚拟函数”)。

  导出类中重写的方法可能是需要扩充基类的同名方法而不是完全代替原来的方法。导出类调用基类同名方法很简单:“基类名.方法名(self, 自变量表)”。对类用户这种做法偶尔也是有用的。(注意只有基类在同一全局作用域定义或导入时才能这样用)。

8.5.1 多重继承

  Python也支持有限的多重继承。有多个基类的类定义格式如下:

  class 导出类名 (基类1, 基类2, 基类3):


    <语句-1>
    .
    .
    .
    <语句-N>

  关于多重继承只需要解释如何解决类属性引用。类属性引用是深度优先,从左向右进行的。所以,如果在导出类定义中未找到某个属性,就先在基类1中查找,然后(递归地)在基类1 的基类中查找,如果都没有找到,就在基类2中查找,如此进行下去。

  (对某些人来说宽度优先——先在基类2和基类3中查找再到基类1的基类中查找——看起来更自然。然而,这需要你在确定基类1与基类2的属性冲突时明确知道这个属性是在基类1本身定义还是在其基类中定义。深度优先规则不区分基类1的一个属性到底是直接定义的还是继承来的)。

很显然,如果不加约束地使用多重继承会造成程序维护的恶梦,因为Python避免名字冲突只靠习惯约定。多重继承的一个众所周知的问题是当导出类有两个基类恰好从同一个基类导出的。尽管很容易想到这种情况的后果(实例只有一份“实例变量”或数据属性被共同的基类使用),但是这种做法有什么用处却是不清楚的。

9.6 私有变量

  Python对私有类成员有部分支持。任何象__spam这样形式的标识符(至少有两个前导下划线,至多有一个结尾下划线)目前被替换成_classname__spam,其中classname是所属类名去掉前导下划线的结果。这种搅乱不管标识符的语法位置,所以可以用来定义类私有的实例、变量、方法,以及全局变量,甚至于保存对于此类是私有的其它类的实例。如果搅乱的名字超过255个字符可能会发生截断。在类外面或类名只有下划线时不进行搅乱。

  名字搅乱的目的是给类一种定义“私有”实例变量和方法的简单方法,不需担心它的其它类会定义同名变量,也不怕类外的代码弄乱实例的变量。注意搅乱规则主要是为了避免偶然的错误,如果你一定想做的话仍然可以访问或修改私有变量。这甚至是有用的,比如调试程序要用到私有变量,这也是为什么这个漏洞没有堵上的一个原因。(小错误:导出类和基类取相同的名字就可以使用基类的私有变量)。

  注意传递给exec,eval()或evalfile()的代码不会认为调用它们的类的类名是当前类,这与global语句的情况类似,global的作用局限于一起字节编译的代码。同样的限制也适用于getattr() ,setattr()和delattr(),以及直接访问__dict__的时候。

  下面例子中的类实现了自己的__getattr__和__setattr__方法,把所有属性保存在一个私有变量中,这在Python的新旧版本中都是可行的:


class VirtualAttributes:
    __vdict = None
    __vdict_name = locals().keys()[0]
     
    def __init__(self):
        self.__dict__[self.__vdict_name] = {}
    
    def __getattr__(self, name):
        return self.__vdict[name]
    
    def __setattr__(self, name, value):
        self.__vdict[name] = value

9.7 补充

  有时我们希望有一种类似Pascal的“record”或C的“struct”的类型,可以把几个有名的数据项组合在一起。一个空类可以很好地满足这个需要,如:


class Employee:
    pass
 
john = Employee() # 生成一个空职员记录
 
# 填充记录的各个域
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

  一段需要以某种抽象数据类型作为输入的Python程序经常可以接受一个类作为输入,该类只是模仿了应输入的数据类型的方法。例如,如果你有一个函数是用来格式化一个文件对象中的数据,就可一个定义一个具有方法read()和readline()的类,该类可以不从文件输入而是从一个字符串缓冲区输入,把这个类作为自变量。

  实例方法对象也有属性:m.im_self是方法所属的实例,m.im_func是方法对应的函数对象。 

9.7.1 例外可以是类

  用户自定义的例外除了可以是字符串对象以外还可以是类。这样可以定义可扩充的分层的类例外结构。

  raise语句有两种新的有效格式:


raise 类, 实例

raise 实例

  在第一种形式中,“实例”必须是“类”的实例或“类”的导出类的实例。第二种形式是

raise instance.__class__, instance

  的简写。except语句除了可以列出字符串对象外也可以列出类。execpt子句中列出的类如果是发生的例外类或基类则是匹配的(反过来不对——except中如果是导出类而发生的例外属于基类时是不匹配的)。例如,下面的程序会显示B、C、D:


class B:
    pass
class C(B):
    pass
class D(C):
    pass
 
for c in [B, C, D]:
    try:
        raise c()
    except D:
        print "D"
    except C:
        print "C"
    except B:
        print "B"

  注意如果把except子句的次序颠倒过来的话(“except B”放在最前),程序将显示B,B ,B——因为第一个匹配的except子句被引发。

  当没有处理的例外是类的时候,类名显示在错误信息中,后面跟着一个冒号和一个空格,最后是实例用内置函数str()转换成字符串的结果。

阅读(1033) | 评论(0) | 转发(0) |
0

上一篇:Python入门(7)

下一篇:Python入门(9)

给主人留下些什么吧!~~