9. 类
Python在尽可能不增加新的语法和语义的情况下加入了类机制。这种机制是C++和Python's Modula-3的混合。Python中的类没有在用户和定义之间建立一个绝对的屏障,而是依赖于用户自觉的不去“破坏定义”。然而,类机制最重要 的功能都完整的保留下来。类继承机制允许多继承,派生类可以覆盖(override)基类中的任何方法,方法中可以调用基类中的同名方法。对 象可以包含任意数量的私有成员。
用C++术语来讲,所有的类成员(包括数据成员)都是公有(public)的,所有的成员函数都是虚拟 (virtual)的。用Modula-3的术语来讲,在成员方法中没有什么简便的方式(shorthands)可以引用对象的成员:方法函数在定义时需要以引 用的对象做为第一个参数,以调用时则会隐式引用对象。这样就形成了语义上的引入和重命名。( This provides semantics for importing and renaming. )但是,像C++而非Modula-3中那样,大多数带有特殊语法的内置操作符(算法运算符、下标等)都可以针对类的需要重新定义 。
9.1 有关术语的话题
由于没有什么关于类的通用术语,我从Smalltalk和C++中借用一些(我更希望用Modula-3的,因为它的面向对象机制 比C++更接近Python,不过我想没多少读者听说过它)。
我要提醒读者,这里有一个面向对象方面的术语陷阱,在Python中“对象”这个词不 一定指类实例。Python中并非所有的类型都是类:例如整型、链表这些内置数据类型就不是,甚至某些像文件这样的外部类型也不是,这一点 类似于C++和Modula-3,而不像Smalltalk。然而,所有的Python类型在语义上都有一点相同之处:描述它们的最贴切词语是“对象”。
对象是 被特化的,多个名字(在多个作用域中)可以绑定同一个对象。这相当于其它语言中的别名。通常对Python的第一印象中会忽略这一点,使用 那些不可变的基本类型(数值、字符串、元组)时也可以很放心的忽视它。然而,在Python代码调用字典、链表之类可变对象,以及大多数涉 及程序外部实体(文件、窗体等等)的类型时,这一语义就会有影响。这通用有助于优化程序,因为别名的行为在某些方面类似于指针。例如 ,很容易传递一个对象,因为在行为上只是传递了一个指针。如果函数修改了一个通过参数传递的对象,调用者可以接收到变化--在Pascal 中这需要两个不同的参数传递机制。
9.2 Python 作用域和命名空间
在介绍类之前,我首先介绍一些有关Python作用域的规则:类的定义非常 巧妙的运用了命名空间,要完全理解接下来的知识,需要先理解作用域和命名空间的工作原理。另外,这一切的知识对于任何高级Python程序 员都非常有用。
我们从一些定义开始。
命名空间是从命名到对象的映射。当前命名空间主要是通过Python字典实现的,不过通常不关心具体 的实现方式(除非出于性能考虑),以后也有可能会改变其实现方式。以下有一些命名空间的例子:内置命名(像 abs() 这样的函数,以及内 置异常名)集,模块中的全局命名,函数调用中的局部命名。某种意义上讲对象的属性集也是一个命名空间。关于命名空间需要了解的一件很 重要的事就是不同命名空间中的命名没有任何联系,例如两个不同的模块可能都会定义一个名为“maximize”的函数而不会发生混淆--用户 必须以模块名为前缀来引用它们。
顺便提一句,我称Python中任何一个“.”之后的命名为属性--例如,表达式z.real中的real是对象z的一 个属性。严格来讲,从模块中引用命名是引用属性:表达式 modname.funcname中,modname 是一个模块对象, funcname 是它的一个属性。因 此,模块的属性和模块中的全局命名有直接的映射关系:它们共享同一命名空间! 9.1
属性可以是只读过或写的。后一种情况下,可以对属 性赋值。你可以这样作:“modname.the_answer = 42”。可写的属性也可以用del语句删除。例如:“del modname.the_answer”会从 modname对象中删除the_answer 属性。
不同的命名空间在不同的时刻创建,有不同的生存期。包含内置命名的命名空间在Python解释器启动时 创建,会一直保留,不被删除。模块的全局命名空间在模块定义被读入时创建,通常,模块命名空间也会一直保存到解释器退出。由解释器在 最高层调用执行的语句,不管它是从脚本文件中读入还是来自交互式输入,都是__main__模块的一部分,所以它们也拥有自己的命名空间。( 内置命名也同样被包含在一个模块中,它被称作__builtin__。)
当函数被调用时创建一个局部命名空间,函数反正返回过抛出一个未在函数 内处理的异常时删除。(实际上,说是遗忘更为贴切)。当然,每一个递归调用拥有自己的命名空间。
作用域是指Python程序可以直接访问到 的命名空间。“直接访问”在这里意味着访问命名空间中的命名时无需加入附加的修饰符。
尽管作用域是静态定义,在使用时他们都是动态的 。每次执行时,至少有三个命名空间可以直接访问的作用域嵌套在一起:包含局部命名的使用域在最里面,首先被搜索;其次搜索的是中层的 作用域,这里包含了同级的函数;最后搜索最外面的作用域,它包含内置命名。
如果一个命名声明为全局的,那么所有的赋值和引用都直接针 对包含模全局命名的中级作用域。另外,从外部访问到的所有内层作用域的变量都是只读的。
从文本意义上讲,局部作用域引用当前函数的命 名。在函数之外,局部作用域与全局使用域引用同一命名空间:模块命名空间。类定义也是局部作用域中的另一个命名空间。
作用域决定于源 程序的文本:一个定义于某模块中的函数的全局作用域是该模块的命名空间,而不是该函数的别名被定义或调用的位置,了解这一点非常重要 。另一方面,命名的实际搜索过程是动态的,在运行时确定的——然而,Python语言也在不断发展,以后有可能会成为静态的“编译”时确定 ,所以不要依赖于动态解析!(事实上,局部变量已经是静态确定了。)
Python的一个特别之处在于其赋值操作总是在最里层的作用域。赋值 不会复制数据——只是将命名绑定到对象。删除也是如此:“del x ”只是从局部作用域的命名空间中删除命名x。事实上,所有引入新命名的 操作都作用于局部作用域。特别是import语句和函数定将模块名或函数绑定于局部作用域。(可以使用global语句将变量引入到全局作用域。 )
9.3 初识类
类引入了一点新的语法,三种新的对象类型,以及一些新的语义。
9.3.1 类定义语法
最简单的类定义形式如下:
class ClassName:
<statement-1>;
.
.
.
<statement-N>;
类的定义就像函数定义(def语句),要先执行才能生效。(你当然可以把它放进if语句 的某一分支,或者一个函数的内部。)
习惯上,类定义语句的内容通常是函数定义,不过其它语句也可以,有时会很有用——后面我们再回过 头来讨论。类中的函数定义通常包括了一个特殊形式的参数列表,用于方法调用约定——同样我们在后面讨论这些。
定义一个类的时候,会创 建一个新的命名空间,将其作为局部作用域使用——因此,所以对局部变量的赋值都引入新的命名空间。特别是函数定义将新函数的命名绑定 于此。
类定义完成时(正常退出),就创建了一个类对象。基本上它是对类定义创建的命名空间进行了一个包装;我们在下一节进一步学习类 对象的知识。原始的局部作用域(类定义引入之前生效的那个)得到恢复,类对象在这里绑定到类定义头部的类名(例子中是ClassName)。
9.3.2 类对象
类对象支持两种操作:属性引用和实例化。
属性引用使用和Python中所有的属性引用一样的标准语法: obj.name。类对象创建 后,类命名空间中所有的命名都是有效属性名。所以如果类定义是这样:
class MyClass:
"A simple example class"
i = 12345
def f (self):
return 'hello world'
那么 MyClass.i 和 MyClass.f 是有效的属性引用,分别返回一个整数和一个方法对象。也可以对类属性赋值 ,你可以通过给 MyClass.i 赋值来修改它。 __doc__ 也是一个有效的属性,返回类的文档字符串:“A simple example class”。
类的实例 化使用函数符号。只要将类对象看作是一个返回新的类实例的无参数函数即可。例如(假设沿用前面的类):
x = MyClass()
以上创建了一个 新的类实例并将该对象赋给局部变量x。
这个实例化操作(“调用”一个类对象)来创建一个空的对象。很多类都倾向于将对象创建为有初始 状态的。因此类可能会定义一个名为__init__() 的特殊方法,像下面这样:
def __init__(self):
self.data = []
类定义了 __init__() 方 法的话,类的实例化操作会自动为新创建的类实例调用 __init__() 方法。所以在下例中,可以这样创建一个新的实例:
x = MyClass()
当然 ,出于弹性的需要, __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.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等等方法。然而,在这里,除非特别说明,我们提到的方法特指类方法)
实例对象的有效名称依赖于它的类。按照定义,类中所有(用户 定义)的函数对象对应它的实例中的方法。所以在我们的例子中,x..f是一个有效的方法引用,因为MyClass.f是一个函数。但x.i不是,因为 MyClass.i是不是函数。不过x.f和MyClass.f不同--它是一个方法对象,不是一个函数对象。
9.3.4 方法对象
通常方法是直接调用的:
x.f()
在我们的例子中,这会返回字符串‘hello world’。然而,也不是一定要直接调用方法。x.f是一个方法对象,它可以存储起来以后调 用。例如:
xf = x.f
while True:
print xf()
会不断的打印“Hello world”。
调用方法时发生了什么?你可能注意到调用 x.f() 时没有 引用前面标出的变量,尽管在f的函数定义中指明了一个参数。这个参数怎么了?事实上如果函数调用中缺少参数,Python会抛出异常--甚至 这个参数实际上没什么用……
实际上,你可能已经猜到了答案:方法的特别之处在于实例对象作为函数的第一个参数传给了函数。在我们的例 子中,调用x.f() 相当于MyClass.f(x)。通常,以n个参数的列表去调用一个方法就相当于将方法的对象插入到参数列表的最前面后,以这个列 表去调用相应的函数。
如果你还是不理解方法的工作原理,了解一下它的实现也许有帮助。引用非数据属性的实例属性时,会搜索它的类。如 果这个命名确认为一个有效的函数对象类属性,就会将实例对象和函数对象封装进一个抽象对象:这就是方法对象。以一个参数列表调用方法 对象时,它被重新拆封,用实例对象和原始的参数列表构造一个新的参数列表,然后函数对象调用这个新的参数列表。
(本文已被浏览 次) | | |