📄 9.html
字号:
<HTML><!-- Mirrored from www.math.pku.edu.cn/teachers/lidf/docs/Python/9.html by HTTrack Website Copier/3.x [XR&CO'2005], Fri, 08 Jul 2005 11:49:15 GMT --><HEAD><TITLE>第九章 类</TITLE></HEAD><BODY LINK="#0000ff"><H1>第九章 类</H1><P>Python是一个真正面向对象的语言,它只增加了很少的新语法就实现了类。它的类机制是C++和Modula-3的类机制的混合。Python的类并不严格限制用户对定义的修改,它依赖于用户自觉不去修改定义。然而Python对类最重要的功能都保持了完全的威力。类继承机制允许多个基类的继承,导出类可以重载基类的任何方法,方法可以调用基类的同名方法。对象可以包含任意多的私有数据。</P><P>用C++术语说,所有类成员(包括数据成员)是公用的,所有成员函数是虚拟(virtual)的。没有特别的构建函数或销毁函数(destructor)。如同在Modula-3中一样,从对象的方法中要引用对象成员没有简捷的办法:方法函数的必须以对象作为第一个参数,而在调用时则自动提供。象在Smalltalk中一样,类本身也是对象,实际上这里对象的含义比较宽:在Python中所有的数据类型都是对象。象在C++或Modula-3中一样,内置类型不能作为基类由用户进行扩展。并且,象C++但不象Modula-3,多数有特殊语法的内置函数(如算术算符、下标等)可以作为类成员重定义。</P><H2>9.1 关于术语</H2><P>Python的对象概念比较广泛,对象不一定非得是类的实例,因为如同C++和Modula-3而不同于Smalltalk,Python的数据类型不都是类,比如基本内置类型整数、列表等不是类,甚至较古怪的类型如文件也不是类。然而,Python所有的数据类型都或多或少地带有一些类似对象的语法。</P><P>对象是有单独身份的,同一对象可以有多个名字与其联系,这在其他语言中叫做别名。这样做的好处乍一看并不明显,而且对于非可变类型(数字、字符串、序表(tuple))等没有什么差别。但是别名句法对于包含可变对象如列表、字典及涉及程序外部物件如文件、窗口的程序有影响,这可以有利于程序编制,因为别名有些类似指针:比如,传递一个对象变得容易,因为这只是传递了一个指针;如果一个函数修改了作为参数传递来的对象,修改结果可以传递回调用处。这样就不必象Pascal那样使用两种参数传递机制。</P><H2>9.2 Python作用域与名字空间</H2><P>在引入类之前,我们必须讲一讲Python的作用域规则。类定义很好地利用了名字空间,需要了解Python如何处理作用域和名字空间才能充分理解类的使用。另外,作用域规则也是一个高级Python程序员必须掌握的知识。</P><P> 先给出一些定义。<P>名字空间是从名字到对象的映射。多数名字空间目前是用Python字典类型实现的,不过这一点一般是注意不到的,而且将来可能会改变。下面是名字空间的一些实例:Python中内置的名字(如abs()等函数,以及内置的例外名);模块中的全局名;函数调用中的局部变量名。在某种意义上一个对象的所有属性也构成了一个名字空间。关于名字空间最重要的事要知道不同名字空间的名字没有任何联系;例如,两个不同模块可能都定义了一个叫“maximize”的函数而不会引起混乱,因为模块的用户必须在函数名之前加上模块名作为修饰。</P><P>另外,在Python中可以把任何一个在句点之后的名字称为属性,例如,在表达式z.real中,real是一个对象z的属性。严格地说,对模块中的名字的引用是属性引用:在表达式modname.funcname中,modname是一个模块对象,funcname是它的一个属性。在这种情况下在模块属性与模块定义的全局名字之间存在一个直接的映射:它们使用相同的名字空间!</P><P>属性可以是只读的也可以是可写的。在属性可写的时候,可以对属性赋值。模块属性是可写的:你可以写“modname.the_answer = 42”。可写属性也可以用del语句闪出,如“del modname.the_answer”。</P><P>名字空间与不同时刻创建,有不同的生存周期。包含Python内置名字的名字空间当Python解释程序开始时被创建,而且不会被删除。模块的全局名字空间当模块定义被读入时创建,一般情况下模块名字空间也一直存在到解释程序退出。由解释程序的最顶层调用执行的语句,不论是从一个脚本文件读入的还是交互输入的,都属于一个叫做__main__的模块,所以也存在于自己的全局名字空间之中。(内置名字实际上也存在于一个模块中,这个模块叫做__builtin__)。</P><P>函数的局部名字空间当函数被调用时创建,当函数返回或者产生了一个不能在函数内部处理的例外时被删除。(实际上,说是忘记了这个名字空间更符合实际发生的情况。)当然,递归调用在每次递归中有自己的局部名字空间。</P><P>一个作用域是Python程序中的一个文本区域,其中某个名字空间可以直接访问。“直接访问” 这里指的是使用不加修饰的名字就直接找到名字空间中的对象。</P><P>虽然作用域是静态定义的,在使用时作用域是动态的。在任何运行时刻,总是恰好有三个作用域在使用中(即恰好有三个名字空间是直接可访问的):最内层的作用域,最先被搜索,包含局部名字;中层的作用域,其次被搜索,包含当前模块的全局名字;最外层的作用域最后被搜索,包含内置名字。</P><P>一般情况下,局部作用域引用当前函数的局部名字,其中局部是源程序文本意义上来看的。在函数外部,局部作用域与全局作用域使用相同的名字空间:模块的名字空间。类定义在局部作用域中又增加了另一个名字空间。</P><P>一定要注意作用域是按照源程序中的文本位置确定的:模块中定义的函数的全局作用域是模块的名字空间,不管这个函数是从哪里调用或者以什么名字调用的。另一方面,对名字的搜索却是在程序运行中动态进行的,不过,Python语言的定义也在演变,将来可能发展到静态名字解析,在“编译”时,所以不要依赖于动态名字解析!(实际上,局部名字已经是静态确定的了)。</P><P>Python的一个特别之处是赋值总是进入最内层作用域。关于删除也是这样:“del x”从局部作用域对应的名字空间中删除x的名字绑定(注意在Python中可以多个名字对应一个对象,所以删除一个名字只是删除了这个名字与其对象间的联系而不一定删除这个对象。实际上,所有引入新名字的操作都使用局部作用域:特别的,import语句和函数定义把模块名或函数名绑定入局部作用域。(可以使用global语句指明某些变量是属于全局名字空间的)。</P><H2>9.3 初识类</H2><P> 类引入了一些新语法,三种新对象类型,以及一些新的语义。<H3>9.3.1 类定义语法</H3><P> 类定义的最简单形式如下:<PRE>class 类名: <语句-1> . . . <语句-N></PRE><P>如同函数定义(def语句)一样,类定义必须先执行才能生效。(甚至可以把类定义放在if语句的一个分支中或函数中)。在实际使用时,类定义中的语句通常是函数定义,其它语句也是允许的,有时是有用的――我们后面会再提到这一点。类内的函数定义通常具有一种特别形式的自变量表,专用于方法的调用约定――这一点也会在后面详细讨论。</P><P>进入类定义后,产生了一个新的名字空间,被用作局部作用域――于是,所有对局部变量的赋值进入这个新名字空间。特别地,函数定义把函数名与新函数绑定在这个名字空间。</P><P>当函数定义正常结束(从结尾退出)时,就生成了一个类对象。这基本上是将类定义生成的名字空间包裹而成的一个对象;我们在下一节会学到类对象的更多知识。原始的局部作用域(在进入类定义之前起作用的那个)被恢复,类对象在这里被绑定到了类对象定义头部所指定的名字。</P><H3>9.3.2 类对象</H3><P>类对象支持两种操作:属性引用和实例化。属性引用的格式和Python中其它的属性引用格式相同,即obj.name。有效的属性名包括生成类对象时的类名字空间中所有的名字。所以,如果象下面这样定义类:</P><PRE>class MyClass: "A simple example class" i = 12345 def f(x): return 'hello world'</PRE><P>则MyClass.i和MyClass.f都是有效的属性引用,分别返回一个整数和一个函数对象。也可以对类属性赋值,所以你可以对MyClass.i赋值而改变该属性的值。</P><P>__doc__也是一个有效的属性,它是只读的,返回类的文档字符串:“A simple example class”。</P><P>类实例化使用函数记号。只要把这个类对象看成是一个没有自变量的函数,返回一个类实例。例如(假设使用上面的类):</P><PRE> x = MyClass()</PRE><P>可以生成该类的一个新实例并把实例对象赋给局部变量x。 <H3>9.3.3 实例对象</H3><P> 我们如何使用实例对象呢?类实例只懂得属性引用这一种操作。有两类有效的属性。 <P>第一类属性叫做数据属性。数据属性相当于Smalltalk中的“实例变量”,和C++中的“数据成员”。数据成员不需要声明,也不需要在类定义中已经存在,象局部变量一样,只要一赋值它就产生了。例如,如果x是上面的MyClass类的一个实例,则下面的例子将显示值16而不会留下任何痕迹:</P><PRE>x.counter = 1while x.counter < 10: x.counter = x.counter * 2print x.counterdel x.counter</PRE><P>类实例能理解的第二类属性引用是方法。方法是“属于”一个对象的函数。(在Python中,方法并不是只用于类实例的:其它对象类型也可以有方法,例如,列表对象也有append、insert、remove、sort等方法。不过,在这里除非特别说明我们用方法来特指类实例对象的方法)。</P><P>类对象的有效方法名依赖于它的类。按照定义,类的所有类型为函数对象属性定义了其实例的对应方法。所以在我们的例子y,x.f是一个有效的方法引用,因为MyClass是一个函数;x.i不是方法引用,因为MyClass.i不是。但是x.f和MyClass.f不是同一个东西――x.f是一个方法对象而不是一个函数对象。</P><H3>9.3.4 方法对象</H3><P> 方法一般是直接调用的,例如:<PRE> x.f()</PRE><P>在我们的例子中,这将返回字符串‘hello world’。然而,也可以不直接调用方法:x.f是一个方法对象,可以把它保存起来再调用。例如:</P><PRE>xf = x.fwhile 1: print xf()</PRE><P>会不停地显示“hello world”。 <P>调用方法时到底发生了什么呢?你可能已经注意到x.f()调用没有自变量,而函数f在调用时有一个自变量。那个自变量是怎么回事?Python如果调用一个需要自变量的函数时忽略自变量肯定会产生例外错误――即使那个自变量不需要用到……</P><P>实际上,你可以猜出答案:方法与函数的区别在于对象作为方法的第一个自变量自动传递给方法。在我们的例子中,调用x.f()等价于调用MyClass.f(x)。一般地,用n个自变量的去调用方法等价于把方法所属对象插入到第一个自变量前面以后调用对应函数。</P><P>如果你还不理解方法是如何工作的,看一看方法的实现可能会有所帮助。在引用非数据属性的实例属性时,将搜索它的类。如果该属性名是一个有效的函数对象,就生成一个方法对象,把实例对象(的指针)和函数对象包装到一起:这就是方法对象。当方法对象用一个自变量表调用时,它再被打开包装,由实例对象和原自变量表组合起来形成新自变量表,用这个新自变量表调用函数。</P><H2>9.4 一些说明</H2><P>在名字相同时数据属性会覆盖方法属性;为了避免偶然的名字冲突,这在大型程序中会造成难以查找的错误,最好按某种命名惯例来区分方法名和数据名,例如,所有方法名用大写字母开头,所有数据属性名前用一个唯一的字符串开头(或者只是一个下划线),或方法名用动词而数据名用名词。</P><P>数据属性可以被方法引用也可以被普通用户(“客户”)引用。换句话说,类不能用来构造抽象数据类型。实际上,Python中没有任何办法可以强制进行数据隐藏——这些都是基于惯例。(另一方面,Python的实现是用C写的,它可以完全隐藏实现细节,必要时可以控制对象存取;用C写的Python扩展模块也有同样特性)。</P><P>客户要自己小心使用数据属性——客户可能会因为随意更改类对象的数据属性而破坏由类方法维护的类数据的一致性。注意客户只要注意避免名字冲突可以任意为实例对象增加新数据属性而不需影响到方法的有效性——这里,有效的命名惯例可以省去许多麻烦。</P><P>从方法内要访问本对象的数据属性(或其它方法)没有一个简写的办法。我认为这事实上增加了程序的可读性:在方法定义中不会混淆局部变量和实例变量。</P><P>习惯上,方法的第一自变量叫做self。这只不过是一个习惯用法:名字self在Python中没有任何特殊意义。但是,因为用户都使用此惯例,所以违背此惯例可能使其它Python程序员不容易读你的程序,可以想象某些类浏览程序会依赖于此惯例)。</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -