第七章 输入输出
有几种办法可以从程序输出;数据可以用可读的形式显示,或保存到文件中以备日后使用。本章讨论一些输入输出的办法。
7.1 输出格式控制
到现在为止我们已经看到了两种输出值的方法:表达式语句和print语句。(第三种方法是使用文件对象的write()方法,标准输出文件可以用sys.stdout引用。参见库参考手册)。
我们常常需要控制输出格式,而不仅仅是显示空格分开的值。有两种办法控制输出格式:一种办法是自己进行字符串处理,用字符串的片断和合并操作可以产生任何可以想象的格式。标准模块string包含了诸如把字符串填充到指定的列宽这样的有用操作,后面会有提及。
另一种办法是使用%运算符,此运算符以一个字符串为左运算元,它按C的sprintf()函数格式把右运算元转换为字符串,返回转换结果。
问题是:如何把值转换为字符串?
幸运的是,Python有一种办法可以把任何值转换为字符串:使用repr()函数,或把值写在两个反向引号(``)之间。例如:
>>> x = 10 * 3.14
>>> y = 200*200
>>> s = 'The value of x is ' + `x` + ', and y is ' + `y` + '...'
>>> print s
The value of x is 31.4, and y is 40000...
>>> # 反向引号也适用于非数值型
... p = [x, y]
>>> ps = repr(p)
>>> ps
'[31.4, 40000]'
>>> # 转换字符串对字符串加字符串引号和反斜杠
... hello = 'hello, world\n'
>>> hellos = `hello`
>>> print hellos
'hello, world\012'
>>> # 反向引号内可以是一个序表
... `x, y, ('spam', 'eggs')`
"(31.4, 40000, ('spam', 'eggs'))"
下面是两种写出平方、立方表的方法:
>>> import string
>>> for x in range(1, 11):
... print string.rjust(`x`, 2), string.rjust(`x*x`, 3),
... # 前一行的结尾逗号表示不换行
... print string.rjust(`x*x*x`, 4)
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
>>> for x in range(1,11):
... print'%2d %3d %4d' % (x, x*x, x*x*x)
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
注意print输出的各项之间额外加了一个空格,这是print的规定。
此例显示了函数string.rjust()的用法,此函数可以把一个字符串放进指定宽度右对齐,左边用空格填充。类似函数还有string.ljust()和string.center()。这些函数不向外输出,只是返回转换后的字符串。如果输入字符串太长也不会被截断而是被原样返回。这样的处理可能会使你的列对齐失效,但这可能比截断要好一些,截断的结果是我们看到一个错误的值。(如果你确实需要截断的话总可以再加一层片断,如string.ljust(x,n)[0:n])。
还有一个函数string.zfill(),可以在数值左边填零。此函数可以处理带有加减号的情况:
>>> string.zfill('12', 5)
'00012'
>>> string.zfill('-3.14', 7)
'-003.14'
>>> string.zfill('3.14159265359', 5)
'3.14159265359'
%操作符的用法如下例:
>>> import math
>>> print 'The value of PI is approximately %5.3f.' % math.pi
The value of PI is approximately 3.142.
如果有多个值可以用一个序表给出,这时格式字符串中要有多个格式,如:
>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> for name, phone in table.items():
... print'%-10s ==> %10d' % (name, phone)
...
Jack ==> 4098
Dcab ==> 8637678
Sjoerd ==> 4127
大多数格式与C用法相同,要求要输出的值的类型符合格式的需要。但是,如果你没有引发例外错误的话也不会产生内核堆列。Python的%s格式要宽得多:如果相应的输出项不是字符串对象,就先用str()内置函数把它变成字符串。在格式指定中宽度指定为*号表示后面的输出项中多出一个指定宽度的整数。C格式%n和%p未被支持。
如果你有一个长格式串不想把它分开,可以在指定格式的地方指定名字,这样就不需要按次序去把格式和名字对应起来,这种格式为“%(变量名)格式”,例如:
>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print 'Jack: %(Jack)d; Sjoerd: %(Sjoerd)d; Dcab: %(Dcab)d' % table
Jack: 4098; Sjoerd: 4127; Dcab: 8637678
这里输出项总是一个字典,字典的各项值是要输出的值,字典的键值是各值的名字。这种输出格式经常与内置函数var()配合使用,var()返回包含所有局部变量的字典。
7.2 读写文件
open()打开一个文件对象,经常使用两个参数:“open(文件名,模式)”。例如:
>>> f=open('/tmp/workfile', 'w')
>>> print f
<open file '/tmp/workfile', mode 'w' at 80a0960>
第一自变量是一个包含了文件名的字符串,第二自变量是文件打开方式的字符串。模式‘r '表示读取,‘w'表示只写(已有的同名文件被清除),‘a'表示打开文件在尾部添加, ‘r+'表示打开文件既可以读也可以写。打开方式参数可选,缺省为‘r'模式。
在Windows和Macintosh中在模式中加入‘b'表示以二进制格式打开文件,如‘rb'、‘wb '、‘r+b'。Windows对文本文件和二进制文件有不同的处理,文本文件中的换行字符在读写时有变化。这种对文件数据的幕后的修改不影响ASCII文本文件,但是会破坏二进制数据如JPEG 或“.EXE”文件的数据。读写这样的文件一定要使用二进制格式。(Macintosh中文本模式的精确描述依赖于使用的C库)。
7.2.1 文件对象的方法
本节后面的例子假设已经建立了一个名为f的文件对象。
为了读取文件内容,调用f.read(size),可以读入一定字节数的数据返回为一个字符串。size 是一个可选数值参数,省略size或size取负值时读入整个文件并返回为一个字符串;如果文件比你的机器内存大一倍,那是你的问题。指定了正的size的时候之多读入并返回size字节。如果读到了文件尾,f.read()返回一个空串("")。如:
>>> f.read()
'This is the entire file.\012'
>>> f.read()
''
f.readline()从文件中读入一行,返回的字符串中将包括结尾的一个换行符(\n),如果文件的最后一行没有换行符则由该行读入的字符串也没有结尾的换行符。这样,由readline() 返回的结果不会有歧义,读入结果为空串时一定是到了文件尾,读入一个'\n'时为空行。
>>> f.readline()
'This is the first line of the file.\012'
>>> f.readline()
'Second line of the file\012'
>>> f.readline()
''
f.readlines()反复调用f.readline(),返回一个包含文件所有行的列表。
>>> f.readlines()
['This is the first line of the file.\012', 'Second line of the file\012']
f.write(string)把string的内容写入到文件中,返回None。
>>> f.write('This is a test\n')
f.tell()返回文件对象的当前读写为止,按从文件开始的字节数算。为了改变读写位置,使用“f.seek(位移,从哪里)”。读写位置按一个参考点加上位移来计算,参考点用“从那里”参数指定,取0时从文件头开始算,取1时按当前位置算,取2时从文件尾算。缺省值是0 ,从文件开始算。
>>> f=open('/tmp/workfile', 'r+')
>>> f.write('0123456789abcdef')
>>> f.seek(5) # 从文件头前进5个字节,到达第6个字符
>>> f.read(1)
'5'
>>> f.seek(-3, 2) # 转到结尾前3个字符
>>> f.read(1)
'd'
用外一个文件后调用f.close()关闭文件,释放打开文件所占用的系统资源。文件关闭后再使用此文件对象就无效了。
>>> f.close()
>>> f.read()
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: I/O operation on closed file
文件对象还有其它一些不太常用的方法,例如isatty()和truncate(),参见库参考手册。
7.2.2 pickle模块
字符串可以很容易地从文件读入或向文件写出。读入数值要麻烦一些,因为read()方法总是返回字符串,要把读入的字符串传给象string.atoi()这样的函数,把象‘123'这样的字符串转换为对应的整数值123。但是,当你想保存更复杂的数据类型如列表、字典或类实例时,读写就要复杂得多。
Python的设计使程序员可以不必反复编写调试保存复杂数据类型的代码,它提供了一个叫做pickle的标准模块。这个令人惊异的模块可以把几乎任何Python对象转换为字符串表示,这个过程叫做腌制,从对象的字符串表示恢复对象叫做恢复。在腌制和反腌制之间,对象的字符串表示可以保存在文件或数据中,甚至于通过网络连接传送到远程计算机上。
如果你有一个对象x,有一个可写的文件对象f,最简单的腌制对象的办法是下面一行代码:
pickle.dump(x, f)
为了恢复对象,如果刚才的文件已打开用于读取,文件对象名仍为f,则:
x = pickle.load(f)
(腌制和恢复还有其它用法,可以腌制多个对象,可以不把数据写入文件,详见库参考手册)。
pickle是保存Python对象并被其它程序或同一程序以后再运行时调用的标准办法,这种做法的专用术语叫做“持久对象”。因为pickle使用广泛,许多Python扩展模块的作者都留意使新增加的数据类型如矩阵可以正确地腌制和恢复。
第八章 错误与例外
到现在为止我们只是提到了错误信息而没有详细讨论,如果你运行了前面的例子可能已经看到了一些错误信息。至少有两种不同错误:句法错和例外错(exceptions)。
8.1 句法错
句法错也称为语法分析错,是你在学习Python的时候最可能犯的错误。
>>> while 1 print 'Hello world'
File "<stdin>", line 1
while 1 print 'Hello world'
^
SyntaxError: invalid syntax
语法分析器重复出错行,并用一个小‘箭头'指向行内最早发现错误的位置。错误是由箭头前面的记号引起的(至少是在这里检测到的)。在本例中,错误在关键字print处检测到,因为它前面应该有一个冒号(“:”)。错误信息中显示了文件名和行号这样如果错误发生在一个脚本文件中你就知道到哪里去找。
8.2 例外
即使语句或表达式句法没有问题,在试图运行的时候也可能发生错误。运行时检测到的错误叫做例外,这种错误不一定必然是致命的:你很快就会学到如何在Python程序中处理例外。然而,多数例外不能被程序处理,这是会产生错误信息,如:
>>> 10 * (1/0)
Traceback (innermost last):
File "<stdin>", line 1
ZeroDivisionError: integer division or modulo
>>> 4 + spam*3
Traceback (innermost last):
File "<stdin>", line 1
NameError: spam
>>> '2' + 2
Traceback (innermost last):
File "<stdin>", line 1
TypeError: illegal argument type for built-in operation
错误信息的最后一行显示发生的情况。例外有不同的类型,类型作为错误信息的一部分显示:上例中错误的类型有ZeroDivisionError、NameError和TypeError。作为例外类型显示的字符串是发生的例外的内置名。这对于所有内置例外成立,但对用户自定义例外不一定成立(用户最好能遵守这样的约定)。标准例外名是内置的标识符(不是保留关键字)。
此行的其余部分是错误的细节,其解释依赖于例外类型。错误信息前面的部分以堆栈反跟踪的形式显示了发生错误的上下文环境。一般这包含了列出源代码行的一个列出源程序行的堆栈反跟踪;然而,它不会显示从标准输入读进的行。
库参考手册列出了内置例外和其含义。
8.3 例外处理
可以编程序来处理选定的例外。请看下面的例子,显示一些浮点数的倒数:
>>> numbers = [0.3333, 2.5, 0, 10]
>>> for x in numbers:
... print x,
... try:
... print 1.0 / x
... except ZeroDivisionError:
... print '*** has no inverse ***'
...
0.3333 3.00030003
2.5 0.4
0 *** has no inverse ***
10 0.1
try语句是这样工作的:
首先,运行try子句(在try和except之间的语句)。
如果没有发生例外,跳过except子句,try语句运行完毕。
如果在try子句中发生了例外错误而且例外错误匹配except后指定的例外名,则跳过try 子句剩下的部分,执行except子句,然后继续执行try语句后面的程序。
如果在try子句中发生了例外错误但是例外错误不匹配except后指定的例外名,则此例外被传给外层的try语句。如果没有找到匹配的处理程序则此例外称作是未处理例外,程序停止运行,显示错误信息。
try语句可以有多个except子句,为不同的例外指定不同处理。至多只执行一个错误处理程序。错误处理程序只处理相应的try子句中发生的例外,如果同try语句中其它的错误处理程序中发生例外错误处理程序不会反应。一个except子句可以列出多个例外,写在括号里用逗号分开,例如:
... except (RuntimeError, TypeError, NameError):
... pass
最后一个except子句可以省略例外名,作为一个通配项。这种方法要谨慎使用,因为这可能会导致程序实际已出错却发现不了。
try ... except语句有一个可选的else子句,如有的话要放在所有except子句之后。else 的意思是没有发生例外,我们可以把try子句中没有发生例外时要做的事情放在这个子句里。例如:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except IOError:
print '不能打开', arg
else:
print arg, '有', len(f.readlines()), '行'
f.close()
例外发生时可能伴有一个值,叫做例外的参数。参数是否存在及其类型依赖于例外的类型。对于有参数的例外,except在自居可以在例外名(或表)后指定一个变量用来接受例外的参数值,如:
>>> try:
... spam()
... except NameError, x:
... print 'name', x, 'undefined'
...
name spam undefined
有参数的例外未处理时会在错误信息的最后细节部分列出其参数值。
例外处理程序不仅处理直接产生于try子句中的例外,也可以处理try子句中调用的函数(甚至是间接调用的函数)中的例外。如:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError, detail:
... print 'Handling run-time error:', detail
...
Handling run-time error: integer division or modulo
8.4 产生例外
raise语句允许程序员强行产生指定的例外。例如:
>>> raise NameError, 'HiThere'
Traceback (innermost last):
File "<stdin>", line 1
NameError: HiThere
raise语句的第一个参数指定要产生的例外的名字。可选的第二参数指定例外的参数。
8.5 用户自定义例外
程序中可以定义自己的例外,只要把一个字符串赋给一个变量即可。例如:
>>> my_exc = 'my_exc'
>>> try:
... raise my_exc, 2*2
... except my_exc, val:
... print 'My exception occurred, value:', val
...
My exception occurred, value: 4
>>> raise my_exc, 1
Traceback (innermost last):
File "<stdin>", line 1
my_exc: 1
许多标准模块用这种方法报告自己定义的函数中发生的错误。
8.6 定义清理动作
try语句还有另一个finally可选子句,可以用来规定不论出错与否都要执行的动作。例如:
>>> try:
... raise KeyboardInterrupt
... finally:
... print 'Goodbye, world!'
...
Goodbye, world!
Traceback (innermost last):
File "<stdin>", line 2
KeyboardInterrupt
finally子句不论try子句中是否发生例外都会执行。例外发生时,先执行finally子句然后重新提出该例外。当try语句用break或return语句退出时也将执行finally子句。
要注意的是,try语句有了except子句就不能有finally子句,有了finally子句就不能有except 子句,不能同时使用except子句和finally子句。需要的话可以嵌套。
第九章 类
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()转换成字符串的结果。
第十章 进一步学习
希望通过这个入门课程的学习增强了你对使用Python的兴趣。下一步怎么办呢?
你可以进一步去读《库参考手册》,该手册对类型、函数、模块等给出了完整的(尽管比较简短)参考材料,可以节省你写Python程序时的大量时间。标准Python软件包括了大量的C 和Python的代码,有读取Unix信箱的,用HTTP接收文档的,生成随机数的,解析命令行选项的,写CGI程序的,压缩数据的,等等许多功能;粗略看一看库参考手册就可以知道有那些内容。
Python的主网站是http://www.python.org,这里有程序代码、文档及有Python相关的其它网页的信息。这个网站在世界上许多地方有镜像,例如欧洲、日本、澳大利亚,访问距离近的镜像网站可能比访问主网站快。还有一个非正式的网站是 http://starship.skyport.net ,包含了一系列有关Python的个人网页,许多人在这里放了可下载的软件。
对于关于Python的问题及错误报告,可以向comp.lang.python新闻组投稿,或向python-list@cwi.nl 邮件表发邮件。该新闻组和邮件表是互相转发的,所以向其中一个发信会自动转寄到另一个。每天有35-45份邮件,有问(答)问题的、建议新功能的、声明新模块的。在发信前应该先查阅 http://www.python.org/doc/FAQ.html 处的常见问题表(FAQ),或在Python软件的Misc/ 子目录中寻找该文件。该FAQ回答了许多反复出现的问题,可能已经有了你的问题的答案。
你可以通过加入Python软件活动来支持Python团体,该团体负责python.org网站、ftp和电子邮件服务器,并组织Python研讨班。关于如何加入参见 http://www.python.org/psa/