Python 3.X中文入门教程
Python是一门容易学习且功能强大的编程语言。它有高效的高级数据结构和简单有效的面向对象方法。语法优美,使用动态类型(dynamic typing),是解释型语言,对于在多个平台上在多个领域内写脚本和快速开发应用程序来说,它都是一门理想的语言。
Python解释器(interpreter)和丰富的标准库在所有主流平台上都可通过官网 https://www.python.org/ 自由获取源码和二进制代码,并且可以自由分发。官网还包含和链接了很多自由的第三方Python模块、程序、工具和文档。
可以轻松地往Python解释器里面加C/C++写的新功能和数据类型(其他可以被C调用的语言也OK)。Python也能作为可定制应用程序的扩展语言。
这份入门教程大致介绍了Python语言和系统的基本概念和功能。手把手教你用Python解释器,所有例子都是独立的,离线阅读这份教程没有问题。
想了解标准对象和模块请看 Python标准库 。 Python语言参考 有该语言的正式说明。想用C/C++写扩展请看 扩展和嵌入Python解释器 和 Python/C API参考手册 。也有深入介绍Python的书。
这份入门教程不是百科全书,对常用功能也无法面面俱到。主要介绍Python最值得关注的功能,并让你对这门语言的风格有个印象。读完以后你可以阅读和书写Python模块和程序,并能够进一步从 Python标准库 学习各种Python库。
Python术语表 也值得过一遍。
说说好处都有啥
- 1 对面的程序员看过来
- 2 上手Python解释器
- 3 谈谈Python
- 4 更多控制流,更多发达
- 5 数据结构(Data Structures)
- 6 模块(Module)
- 7 输入输出,俗称IO
- 8 错误(error)和异常(exception)
- 9 类(class)
- 10 标准库到此一游,上篇
- 11 标准库到此一游,下篇
- 12 何去何从?
- 13 交互式输入编辑和历史替换
- 14 浮点运算的问题和局限
- 15 附录
- 16 写两句吧
1 对面的程序员看过来
如果你用电脑干很多活,到头来有些任务肯定要自动化。比如对一个很大的文本文件进行查找替换,或者重命名和重排一些照片。也许你想写一个小型自定义数据库,或者有特定图形界面(GUI)的应用程序,或者干脆写一个简单的游戏。
你要是软件开发大牛,有可能用到一些C/C++/Java的库,但是通常的写程序/编译/测试/再编译步骤实在太慢。可能你给这样的库写一个测试集然后发现测试代码真是烦啊。或者你的程序可以使用某种扩展语言,但你不想为自己的应用程序去设计和实现一种全新的语言。
Python就是Mr. Right.
你可以给某些任务写个Unix shell脚本或Windows批处理(batch)文件,但是shell脚本最适合处理文件和更改文本数据,不适合图形界面(GUI)程序和游戏。你可以写C/C++/Java程序,但写完第一版就要花很长的开发时间。Python用起来更简洁,跨Windows, Mac OS X和Unix操作系统,能帮你更快地把事情搞定。
别看Python写起来简洁,它确实是一门真正的编程语言,对大程序来说它提供比shell脚本和批处理(batch)文件更多的结构和支持。另外Python的错误检查比C完善,作为一种 非常高级的语言 ,它有自带(built-in)的高级数据类型,比如灵活的数组(array)和字典(dictionary)。数据类型更通用,Python比Awk和Perl更适合解决大问题,也不会比其他语言繁琐。
Python把你的程序分成模块(module),其他程序可以反复使用。它的标准模块应有尽有,写个基本的程序或入门学习的话足够用了。一些模块提供文件输入输出(I/O),系统调用(system call),套接字(socket),图形用户界面工具(比如Tk)的接口(interface)。
Python是一门解释型(interpreted)语言,不需要编译(compilation)和链接(linking),能节省相当多的开发时间。解释器可以交互地使用,很容易玩一下这门语言的特性,写个用完即扔的程序,在自底向上(bottom-up)程序开发中测试某些功能。Python解释器还是一个方便的桌面计算器。
Python程序简洁,可读性好。Python程序一般比C/C++/Java程序短得多。因为:
- 高级数据类型让你一条语句就能表达复杂的操作
- 用缩进(indentation)实现语句分组,不用大括号(bracket)
- 不需声明变量和实参(argument)
Python是 可扩展的 :如果你会写C程序,那就很容易给解释器加个新的自带(built-in)功能或模块,或最快速地执行某些关键操作,或把Python程序链接到只能拿到二进制代码的库上(比如某些销售商的图形库)。你若觉得好玩,可以把Python解释器链接到一个C程序上作为扩展或命令行语言。
另外,这门语言的名字是从BBC节目“Monty Python’s Flying Circus”得来的,和爬行动物没啥关系。最好在文档里提一下Monty Python这个滑稽短剧。
觉得Python有意思就多学点吧。学语言最好的方式就是用,请一边玩Python解释器一边读这份入门教程。
下面我们来看看解释器怎么用。很无聊,很必要。
剩余部分举例介绍Python各种功能,从简单的表达式、语句、数据类型开始,到函数和模块,最后说说高大上的概念比如异常、用户定义的类。
2 上手Python解释器
2.1 召唤解释器
如果你的系统自带Python解释器,通常它安在 /usr/local/bin/python3.4,把 /usr/local/bin 放到你Unix shell的搜索路径里,就能通过这个启动啦:
[1] 解释器装到哪里也是可选的,问问你身边的Python大牛或系统管理员吧。(/usr/local/python 是个挺常见的安装位置)
Windows里Python一般安在 C:Python34,可以在安装程序中选择其他安装位置. 想把这个目录加入系统路径,可在DOS环境下用这个命令:
输入文件结束符(end-of-file,Unix下 Control-D ,Windows下 Control-Z )会使解释器以返回码0状态退出。不管用的话可以使用命令: quit()
解释器的行编辑功能包括交互式编辑、历史替换、在支持readline的系统上代码补全。最快验证是否支持命令行编辑的方法就是一遇到Python提示符就键入Control-P,如果有提示音,那么恭喜。请看附录 13 交互式输入编辑和历史替换 的介绍。如果啥事也没有,或显示 ^P 则不支持命令行编辑。你只能用退格键从当前行删除字符。
解释器有点像Unix shell:用连到tty设备的标准输入调用它时,它可以交互地读入和执行命令;用文件名参数或作为标准输入的文件调用它,它从此文件读入和执行 脚本(script) 。
第二种启动解释器的方法是 python -c command [arg] ... ,以 命令的形式 执行语句,就像shell的 -c 选项。Python语句常常有空格和其他对shell较为特殊的字符,所以建议用单引号把整个 命令 引起来。
有些Python模块(module)作为脚本也很有用,可以用 python -m module [arg] ... 启动,为 模块(module) 执行源文件,就像命令行下给出完整路径一样。
有时候需要运行完一个脚本文件再进入交互模式,这时候加一个 -i 选项即可。
2.1.1 传参数
对于解释器来说,脚本(script)名字和参数(argument)会被转成一个字符串列表(list),放到 sys 模块的 argv 变量里。想用这个列表 import sys 即可。这个列表的长度最少是1,没有脚本名字也没有参数时,sys.argv[0] 是一个空字符串。如果脚本名字是 '-' (就是标准输入 standard input),sys.argv[0] 会被设为 '-' 。使用 -c 命令时,sys.argv[0] 被设为 '-c' 。使用 -m 模块 时,sys.argv[0] 会被设为模块的完整路径。Python解释器对 -c 命令 和 -m 模块 之后的东西撒手不管,都交给 sys.argv 。
2.1.2 交互模式
交互模式是指解释器从终端(tty)读命令。这时候解释器从 主提示符(primary prompt) 提示你输入下一条命令,主提示符通常是 >>> 。解释器用 二级提示符(secondary prompt) 提示续行,默认的是 ... 。给出第一个提示符前,解释器会显示版本号和版权提示:
什么是续行?输入一个多行的东西时会用到。看一下这个 if 语句的例子:
意犹未尽?请看 15.1 交互模式.
2.2 解释器和环境
2.2.1 源代码的编码问题
默认情况下,Python源文件用UTF-8编码,这样一来世界上绝大多数语言都可以一齐出现在字符串常量、标识符和注释里。但注意标准库仅仅使用ASCII字符来表示标识符,任何可移植的代码都应遵循这个传统。想要正确显示所有字符,你的编辑器必须知道文件是UTF-8编码的,所用的字体也必须支持文件中的所有字符。
源文件当然也可以用其他的编码方式。在 #! 行的后面再加上这样的一行:
这样声明之后,源文件会以 encoding 这种方式编码,而不是UTF-8。Python库参考手册里有支持的编码方式,在这里 codecs 。
比如你的编辑器不支持UTF-8编码的文件,要用到Windows-1252,可以这样写:
然后在源文件中继续使用所有Windows-1252的字符集。这一行编码注释必须出现在文件的 第一行或第二行 。
3 谈谈Python
下面的例子里输入有提示符(>>> 和 ...),输出没有,以示区分。想自己试一下的话(当然要自己试一下!!),在提示符后依葫芦画瓢吧。如果一行只有一个二级提示符(secondary prompt),意思是你需要输入一个空行,这用于结束一个多行命令。
很多例子有注释。Python的注释以井号字符(hash character) # 开头,这种注释只管一行。注释可以出现在一行的开头,也可以出现在空格或代码的后面,但不能在一个字符串里。字符串里的井号字符就表示它自己。不想输入那么多的话,就忽略注释吧,Python不会执行这些注释的。
举个例子:
3.1 Python是个好计算器
试试几个简单的Python命令。召唤Python解释器,等待出现 >>> 。
3.1.1 数
解释器可以当一个简易计算器使用,你给出表达式,它给出结果。表达式语法很简单,操作符 +,-,*,/ 和其他语言一样(比如Pascal和C)。括号 () 用来分组。
整数(integer, 比如 2,4,20)类型是 int 。实数的类型是 float 。我们还会看到更多关于数的类型的介绍。
除法(/)总是返回float。想要整除 (floor division) 并得到整数结果舍弃小数部分,用 // 。求余数用 %:
用 ** 做乘方运算(power) [2]:
等号 = 用于赋值,就是把一个值给一个变量,赋值操作不显示结果。
使用没定义(赋值了就定义了)的变量会出错:
操作数有整数有小数时,整数要被转成小数:
交互模式下,最后打印(print)的表达式被赋给变量 _ 。这样就能用之前的计算结果:
这个变量应该当成只读的(read-only)。不要显式地(explicitly)给它赋值,否则你就创造了一个有相同名字的独立的局部变量,把自带的这个变量屏蔽掉了,它的神奇功能也没法用了。
除了 int 和 float,Python还有其他类型的数,比如 Decimal 和 Fraction。Python还有自带的(built-in)复数 complex numbers ,用 j 或 J 后缀来表示虚部。
3.1.2 字符串
Python字符串有多种表示方法。可以用单引号 '...' 也可以用双引号 "..." [3]。 表示转义(escape)字符:
在交互式解释器里,输出字符串有引号,特殊字符带有反斜线(backslash)。看起来和输入不同的字符串可能是相等的。如果字符串里面不含双引号但包含单引号,则这个字符串是用双引号引起来的,否则就是用单引号引起来的。print() 函数输出的形式会更加易读,省掉了两头的引号,实际的转义字符也显示出来了:
如果你不想用 表示特殊字符,可以使用 原始字符串(raw string) ,在第一个引号之前加 r 即可:
字符串常量可以跨多行,用三引号即可: """...""" 或 '''...'''。行结束符(End of lines)自动包含在字符串里,不想要的话在行尾加一个
输出是这样的(注意这里忽略了一开始那个换行符):
字符串可以用 + 拼接起来(concatenate),用 * 重复:
多个 字符串常量(string literals) 相邻的时候会自动拼接:
相邻拼接只对字符串常量管用,变量和表达式就别想了:
想拼接变量和字符串,用 +:
相邻拼接相当有用,比如你想表示一个长字符串:
字符串可以 索引(index) ,也就是有下标(subscript),第一个字符的索引是0。Python没有字符类型(character type),一个字符就是一个长度为1的字符串:
索引可以是负数,意思是从右边数:
因为-0就是0,所以负的索引是从-1开始的。
再来看看 切片(slicing) 吧。索引用于拿到单个字符,切片可以拿到子串(substring):
注意啦,开头字符总是包含的,结尾字符总是不包含的。这样一来 s[:i] + s[i:] 总是等于 s:
切片的索引有默认值,第一个索引省去的话默认值就是0,第二个索引省去的话默认值就是字符串的长度:
想要记住切片是咋回事,请设想索引指向字符 之间 ,第一个字符的左边缘是0,最后一个字符的右边缘是长度 n ,就像这样:
数字第一行给出索引0~6,第二行给出负数。从 i 到 j 的切片包含 i 和 j 之间的所有字符。
对于非负数的索引,若两个索引都没有超范围,则切片长度是两者的差。比如 word[1:3] 长度为2。
太大的索引会报错:
但是超范围的切片索引没问题:
不能修改Python字符串,也就是说它们是 不可变的(immutable) 。所以不能给字符串的某个索引位置赋值:
需要一个不同的字符串,就创建一个新的吧:
自带的函数 len() 返回字符串的长度:
参见
- 文本序列类型 — str
- 字符串是一种 序列类型sequence types ,支持此种类型的通用操作。
- 字符串的方法(method)
- 字符串有很多基本的变形和搜索方法。
- 字符串格式
- 详见 str.format() 。
- printf风格的字符串格式
- 这是老式的格式化操作,字符串和Unicode字符串在 % 操作符的左边。
3.1.3 列表(Lists)
Python有很多 聚合的(compound) 数据类型,用于把一些值放到一起。最通用的就是 列表(list) ,放在方括号内,值用逗号分隔。列表可以包含不同类型的项(item),但是通常这些项类型都相同:
就像字符串一样(还有所有自带的 sequence 类型),列表可以索引,分片:
分片操作返回一个新的列表,下面的分片返回一个列表的拷贝(浅拷贝shallow copy)。Python里只有聚合(compound)对象(也就是包含其他对象的对象,如列表、类的实例)有深拷贝(deep copy)和浅拷贝的区别。浅拷贝构建一个新的聚合对象,然后尽可能地把原来聚合对象所包含的对象的引用(reference)加入其中。深拷贝创建一个新的聚合对象,然后递归地(recursively)把原来聚合对象所包含的对象的拷贝加入其中:
列表也有拼接(concatenation)操作:
字符串是 不可变的(immutable) ,列表是 可变的(mutable) ,可以改变列表内容:
可以用 append() 方法 向列表的末尾加入新项:
可以向列表的切片赋值,这样可以改变列表大小甚至清空列表:
自带的函数 len 也可以用于列表:
可以列表套列表(nest):
3.2 编程热身
当然啦,我们写Python不可能总是处理2+2这种问题。比如我们可以求 斐波那契(Fibonacci) 数列的前几项:
这个例子有这样几个特点:
-
第一行是个 多重赋值(multiple assignment) :变量 a 和 b 同时得到新值0和1。最后一行也是这样的,这说明等号右边的表达式都是先求值(evaluate)再赋值(assignment)。等号右边的表达式从左向右求值。
-
只有条件为真(这里是 b < 10) while 循环就一直执行。Python像C一样,非零的整数值都是true,0是false。条件也可以是字符串或列表,实际上任意序列(sequence)都行;所有非零长度的东西为true,空的序列为false。上面例子用的测试条件是一个简单的比较。标准的比较操作符和C一样: < (小于),> (大于),= (等于),<= (小于等于),>= (大于等于),!= (不等于)。
-
循环的 函数体 是 缩进的(indented):缩进是Python把语句进行分组的方式。在交互式提示符下,可以用一个tab或一个或多个空格来进行每行的缩进。实践中你往文本编辑器输入的Python代码可能更复杂,合格的文本编辑器都有自动缩进。当交互式地输入一组语句时,因为语法分析程序(parser)没法猜测你在最后一行打了一些什么,所以必须用一个空行来指明输入结束。一个块内每一行缩进的数目要相同。
-
print 函数打印出参数的值。这和直接写一个表达式(之前计算器的例子我们这么干的)是不同的,这个函数处理多个参数,浮点数和字符串的时候略有不同。比如字符串打印出来是没有引号的,项(item)与项之间加入了一个空格,格式看上去比较漂亮:
关键字参数(keyword argument) end 可以避免换行,可以在结尾输出别的字符串:
4 更多控制流,更多发达
除了刚刚说的 while 语句,Python也有类似其他语言中的控制流(control flow)语句。
4.1 if 语句
它知名度应该最高了,比如:
elif 可以有0或多个,else 是可选的。elif 是’else if’的缩写,避免了过多的缩进。Python没有其他语言中的 switch 和 case,这种情况可以用一个 if ... elif ...elif ... 序列代替。
4.2 for 语句
Python的 for 语句和C、Pascal的不同,Pascal的for循环遍历一个算数级数,C的for循环定义了遍历的步长和停止条件,Python的 for 语句可以对任意序列(sequence, 比如列表或字符串)中的项按顺序进行遍历(iterate,即一个一个地访问)。比如:
如果在循环里面需要修改正在遍历的序列(比如复制某些项),建议先备份一下(make a copy)。遍历一个序列并不会隐式地备份。切片让这变得很方便:
4.3 range 函数
如果真的需要对数的序列进行遍历,Python自带的 range() 函数很好用。它产生算术级数(arithmetic progression):
结束的那个数(上面例子的5)是不包含在产生的序列内的。range(10) 产生10个数,0到9,即长度为10的序列的10个索引。也可以让这个范围从其他的数开始,增量也不一定是1,甚至可以是负数,这个增量叫步长( step ):
想对某序列的索引进行遍历(一个一个地访问),可以用 range() 函数和 len() 函数:
多数情况下还是用 enumerate() 函数更方便,请看 5.6 循环(looping)技术哪家强.
要是这么干,诡异的事情就出现了:
range 函数返回的对象看起来像是列表,实际上非也。为了节省空间,返回的这个对象不是列表,而是一个你遍历时它可以不断给你下一项的对象。
说一个对象是 可遍历的或可迭代的(iterable) 指的是它可以不断提供下一项下一项下一项,一直到用尽所有的项为止。我们已经看到 for 语句就是一个 迭代器(iterator)。list() 函数也是迭代器,它为可迭代的对象创建列表:
还有一些函数的参数是可迭代的,返回值也是可迭代的。
4.4 break 语句 continue 语句,循环的 else 子句
break 语句和C一样,中断最内层的(smallest enclosing) for 循环,while 循环。
循环语句可以有 else 子句,循环遍历完整个列表时(for)或条件变成false时(while),else 子句会执行。但是循环被 break 语句中断的时候不执行 else 子句。 else 子句配合 break 使用可能会省去一个状态变量,让程序更简洁。下面的找质数(prime number)的程序是个不错的例子:
上面的程序是正确的,看仔细哦,else 子句是 for 循环的,不是 if 语句的。
用于循环的 else 子句跟 try 语句的 else 子句更像,而不是 if 的 else 子句。try 的 else 子句在没有异常(exception)的时候执行,循环的 else 子句在没有发生 break 的时候执行。关于 try 语句和异常,请看 8.3 怎么处理异常呢。
continue 语句也是从C语言借鉴来的,意思是继续下一次循环:
4.5 pass 语句
pass 什么都不干,用于语法上要求这里有个语句但实际上这里真的啥也不需要的情况:: 用于
这样可以创建一个最小的类(class):
pass 的另一个用处是写新程序时在函数或条件中占位(place-holder),这样可以更抽象地思考,pass 也就被默默地忽略了:
4.6 怎么定义函数(function)呢
我们可以创建一个求任意多项斐波那契(Fibonacci)序列的函数:
关键字 def 给出函数的 定义(definition) 。后面必须跟函数名和括号括起来的行参(formal parameter)列表。下一行是函数体,必须缩进。
函数体第一条语句可以是一个字符串常量,作为函数的文档字符串,叫 docstring,请看 4.7.6 文档字符串(docstring)。有些工具可以用docstring自动产生在线或打印的文档,或者让用户交互地浏览代码。记得写docstring,养成好习惯有木有。
函数的 执行(execution) 会引入新的符号表(symbol table),用于记录函数的局部变量(local variable)。准确地说,函数内所有的变量赋值都会把值存在局部符号表(local symbol table)里,变量引用(reference)首先看局部符号表,然后看外层函数的局部符号表,然后看全局(global)符号表,最后看自带名字(build-in name)表。所以在函数内可以引用全局变量(global variable),但不能给它们赋值,除非用 global 语句。
函数被调用时,实参(actual parameter或argument)被引入局部符号表,实参传递时是 传值(call by value) 的,这里说的 值(value) 总是指对象的 引用(reference) 而不是对象的值。[4] 函数调用另一个函数时,就创建一个新的局部符号表。
定义一个函数就会在当前的符号表内引入函数名。在解释器里,函数名的类型是用户自定义(user-defined)函数。这个值是可以赋给另一个名字的,这样一来后者也变成了函数,这是个通用的重命名机制:
如果你用过其他语言,你可能会说 fib 不是函数啊,它是个过程(procedure)啊,因为它没有返回值啊。其实没有 return 语句的函数它也会返回一个值,不过是个相当无聊的值。这个值叫 None ,什么都没有的意思,是Python自带的名字。如果 None 是仅有的输出内容,解释器一般不显示。如果一定要显示的话,请用 print():
很容易可以写一个函数,返回斐波那契序列的列表,而不是把它们打印出来:
这个例子也告诉我们一些新的Python特性:
- 函数中的 return 语句返回一个值,单独的一个 return 不带表达式,则返回 None。函数执行到最后也会返回 None。
- result.append(a) 语句调用列表对象 result 的一个 方法(method) 。方法是属于对象的函数,命名方式为 obj.methodname,obj 是某个对象,也可以是表达式,methodname 是由对象类型定义的方法名。不同的类型定义了不同的方法。不同类型的方法名字可以相同,不会引起歧义。当然啦,可以用 类(class) 定义你自己的对象类型和方法,请看 9 类(class)。上面例子的 append() 方法是列表对象的方法,它往列表的末尾加一个新元素。本例中它和 result = result + [a] 是等效的,但是 append 更高效。
4.7 怎么定义函数呢,接着整
定义函数时,参数的数目是可变的。有三种形式,它们可以结合起来使用。
4.7.1 默认参数(argument)的值
最有用的形式是为一个或多个参数指定默认值。这样的话调用函数时就不用给出那么多的实参,比如:
可以用多种方式调用上述函数:
- 仅仅给出必须的参数: ask_ok('Do you really want to quit?')
- 给出一个可选参数: ask_ok('OK to overwrite the file?', 2)
- 给出全部参数: ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
上面例子同时介绍了关键词 in ,它用来测试一个序列是否含有某个值。
默认值在函数定义的时候在 定义的(defining) 范围内求值(evaluate),所以:
打印出来的是 5.
重要警告: 默认值只求值一次。默认值是可变(mutable)对象时要注意,比如列表、字典(dictionary)、大多数类的实例(instance)。比如下面的函数在后续调用时会累积传给它的参数:
打印出来的是:
如果不想让默认值被后续调用用到,可以这么写:
注意这时print(f(2))和print(f(3))进入f时L还是None。
4.7.2 关键字(keyword)参数
我们可以用 关键字参数keyword argument 来调用函数,形式为 kwargs=value。比如下面这个函数:
它的参数有一个是必需的 (voltage),三个是可选的(state ,action , type)。下面的调用方式随便玩:
但是下面这些调用方式就不行了:
关键字(keyword)参数必须跟在位置(positional)参数(比如 voltage )的后面。关键字参数的名字必须符合函数的关键字参数定义,比如对于 parrot 函数 actor 就是一个无效参数。关键字参数的顺序是无所谓的。必须给出的参数也可以用关键字参数表示,比如 parrot(voltage=1000) 也是有效的。所有参数顶多赋值一次。多次赋值的话就会像下面这样挂掉:
位置放最后的行参如果以 **name 的形式出现,它将会收到一个含有关键字参数的字典(dictionary)。还可以和下面介绍的 *name 一起使用,*name 收到的是位置参数的元组(tuple)。*name 必须放到 **name 前面。我们可以定义这样的函数:
我们这样调用它,limburger林堡干酪产自比利时林堡附近,但现时德国制的林堡干酪已经取代了比利时生产的干酪。runny意思是松软的,水分过多的:
打印出来的信息是:
关键字参数的名字经过了排序,打印之前我们对字典的 keys() 使用sorted()进行了排序。如果没排序的话,字典的 键(key) 输出的顺序是不确定的。
4.7.3 任意的参数列表
最后,这是个很少用到的东西,调用函数时可以用任意个数的参数。这些参数会打包(wrap up)到一个元组(tuple)里,请看 5.3 元组(tuple)和序列(sequence) 。可变数目的参数之前,可以有0或多个正常参数:
通常这些可变参数应该放到行参列表的末尾,因为它们把后面的参数都包括进来了。所有在 *args 参数后面的行参都只能是关键字参数,不能再把这些参数当作位置(positional)参数了:
4.7.4 对参数列表进行拆包(unpacking)
这是什么情况呢?参数都放到了列表或元组中,但函数需要单独的位置参数。比如Python自带的 range() 函数需要 start 和 stop 两个参数。如果这两个参数都在一个列表里或都在一个元组里,而不是分开的形式,那么就需要拆了,用 * 运算符把参数从列表或元组中拆包:
同理,字典的拆包用 ** 操作符:
如果给这只鹦鹉接上4百万伏,它是不会叫的,因为它早就挂了。话说你们怎么这么暴力呢...
4.7.5 Lambda表达式,别忘了不发音的b
用关键字 lambda 可以创建短小的无名字函数。比如这个函数返回两参数的和: lambda a, b: a+b 。能用函数对象的地方都可以用lambda函数,语法上它是一个表达式。语义上它是正式函数定义的语法糖(syntactic sugar)。和嵌套(nested)函数定义一样,lambda函数可以从它包含的范围内引用变量:
上面的例子用lambda表达式返回了一个函数,即f是lambda x: x + 42。另一个用法是把一个短小的函数当参数进行传递:
上面这个例子以每个元组的第二项作为key进行排序。
4.7.6 文档字符串(docstring)
来看看文档字符串(documentation string,简称docstring)的内容和格式。
第一行要短,简要概括对象的用处。为了简洁起见,只要名字不是描述函数操作的动词,就不要显式地说明对象的名字和类型,因为用其他方法可以查到。这一行大写字母开头,句点结尾。
如果还有什么话要说,第二行要空着,把第一行的概括和其余部分的描述分开(git的commit message也是这么干的)。从第三行开始的这些行应该写成一段或多段,描述对象的调用惯例,副作用之类的。
Python的语法分析程序(parser)没有去掉(strip)多行字符串常量头尾的缩进,所以处理docstring的工具就需要做这事了。一般遵循下列惯例,第一行 之后的 第一个非空行决定整个docstring的缩进,把这一行称为N吧。(为啥不直接用第一行?因为第一行一般紧挨着引号 """ 或 ''',不好确定它的缩进。) 若行N缩进了M个空白字符(whitespace),其余所有行的开头就应该去掉M个空白字符。缩进少于M的行不用去开头的空白字符,如果要去的话就把开头的空白字符全部去掉。tab展开后应该测一下M是多少,一般一个tab相当于8个空格。
这是一个多行docstring的例子:
可见Python的语法分析程序没有去掉缩进。
4.7.7 函数注释(annotation)
函数注释Function annotations 是纯粹可选的,任意的用户自定义函数的元数据(metadata)信息。元数据是描述数据的数据。Python本身和标准库都不用函数注释,本节就是说说这个语法。第三方project可以用函数注释来进行写文档,类型检查或其他。
注释以字典(dictionary)的形式存储在函数的 __annotations__ 属性里。参数的注释是跟在参数名后面的表达式,用冒号间隔。返回值的注释用 -> 定义,位于参数列表和def 语句的那个冒号之间。下面例子有一个位置参数,有一个关键字参数,返回值被注释了:
4.8 间奏曲: 代码风格
现在你可以写出更长更复杂的Python程序了,该说说 代码风格(coding style) 了。大多数语言都能被写成多种风格,有些风格更易读(readable)。要让别人能很容易地读懂你的代码,遵守良好的代码风格帮助极大。
对于Python来说 PEP 8 是大多数project遵循的风格指导。每个Python开发者都应该多少瞅一眼,下面是最重要的几点:
-
使用4个空格缩进,不用tab。
4个空格这个缩进不很长(可以多嵌套几层了)不很短(代码容易阅读)。tab容易引起混乱,弃之不用。
-
每行不超过79个字符。
小显示屏用户的福音啊,大显示屏上也能并排打开好几个代码文件。(几个? 这显示屏是要多大...)
-
用空行来间隔函数、类、函数内部较大的代码块。
-
尽可能让注释独占一行。
-
要写docstring啊。
-
操作符之间、逗号之后记得用空格,括号不用,比如 a = f(1, 2) + g(3, 4) 。
-
类和函数的命名要有一致性,惯例是用 CamelCase 来命名类,用 lower_case_with_underscores 来命名函数和方法。总是用 self 作为方法的第一个参数名,请看 9.3 看到类(class)的第一眼 关于类和方法的更多介绍。
-
如果你的代码要在国际化环境中使用,就不要用花哨的编码方式。任何情况下Python默认的UTF-8(甚至是纯文本ASCII)的效果最好。
-
如果不太可能由讲其他语言的人来阅读和维护代码的话,就不要在标识符(identifier)中使用非ASCII字符。
5 数据结构(Data Structures)
这个概念可能你已经深入学习过了,这里说点新东西。
5.1 列表(list),接着整
列表这个数据类型还有很多的方法,这些是列表对象的全部方法:
- list.append(x)
-
在列表末尾加一项,和 a[len(a):] = [x] 等价。
- list.extend(L)
-
把L这个列表加到列表末尾,和 a[len(a):] = L 等价。
- list.insert(i, x)
-
在指定位置i的前面插入一项,a.insert(0, x) 在列表前面插入一项x,a.insert(len(a), x) 和 a.append(x) 等效。
- list.remove(x)
-
删除第一个值为 x 的项,如果没有这一项就报错(error)。
- list.pop([i])
-
删除位置i的项并返回该项。没有制定参数时,a.pop() 删除并返回列表的最后一项。i周围的方括号(square bracket)是说该参数是可选的,不是说这里应该输入方括号。这种可选参数的表示方法在Python库参考手册里很常见。
- list.clear()
-
删除列表中所有的项,等价于 del a[:] 。
- list.index(x)
-
返回第一个值为 x 的项的索引,没有此项就报错(error)。
- list.count(x)
-
返回 x 在列表中出现的次数。
- list.sort()
-
原地( in place )对列表中的项排序。原地算法是使用小的,固定数量的额外空间来转换资料的算法。当算法执行时,输入的资料通常会被要输出的部份覆盖掉。非原地算法叫 not-in-place 或 out-of-place。
- list.reverse()
-
原地翻转列表中的元素。
- list.copy()
-
返回列表的浅拷贝(shallow copy),等价于 a[:] 。
下面例子使用了列表的这些方法:
你可能注意到像 insert,remove,sort 这些能修改列表的方法没有打印出返回值,他们返回的是默认的 None。[5] 这是Python对所有可变数据结构的一条设计原则。
5.1.1 列表者,栈(stack)也
很容易就能把列表当栈用,栈的特点是后加入的元素先取到,即后进先出 (“last-in, first-out”)。用 append() 把一项压入栈顶(top of stack),用 pop() 从栈顶取一项,不用说明具体的索引。比如:
5.1.2 列表者,队列(queue)也
同样很容易能把列表当队列用,队列的特点是先加入的元素先取到,即先进先出 (“first-in, first-out”)。但是把列表当队列用并不高效,从列表末尾执行 append() 和pop() 速度快。但问题是从列表头部执行 insert() 和 pop() 速度慢,因为所有元素都需要后移一个位置或前移一个位置。
想实现一个队列,请用 collections.deque,它可以快速地从头尾执行 append() 和 pop() 。比如:
5.1.3 列表推导(List Comprehension),用列表创建列表
列表推导提供了一种创建列表的简洁方式。通常的用法是对一个序列(sequence)或可迭代(iterable)对象的每个元素操作一下,得到一个新的列表,或者创建某个这些元素的子序列(subsequence)。
比如我们想整一个平方列表:
上面的方式创建或覆写(overwrite)了一个变量 x,循环结束这个 x 还存在。我们如果这样计算平方就可以避免这个副作用:
上面的表达式可以等价地写成:
更简洁,更易读吧。
列表推导指的是中括号里一个表达式后面跟着一个 for 语句,后面再跟上0或多个 for 或 if 语句。结果是一个新的列表,由 for 和 if 语句对表达式求值得来。下面这个列表推导把两个列表不相等的元素放到一块:
和下面的语句是等价的:
如果表达式是个元组(tuple,比如说上面的那个 (x,y)),别忘了圆括号,下面的loganberry是杨莓,passion fruit是百香果,俗称巴西果,原产于巴西,flatten意思是拍扁弄平:
列表推导可以包含复杂的表达式和嵌套(nested)函数:
5.1.4 嵌套的列表推导
列表推导的表达式可以是任意表达式,当然可以是一个列表推导。
下面是一个3x4的矩阵,有3个长度为4的列表:
下面的列表推导对矩阵进行转置(transpose),也就是颠倒行和列:
和前面一样,第二个 for 对内层的列表推导求值,和下面的例子等价:
进而和下面等价:
实际写代码时,对于比较复杂的语句应该用自带的(built-in)函数,zip() 函数正好适合这种情况:
请看 4.7.4 对参数列表进行拆包(unpacking) 对星号(asterisk)的详细解释。
5.2 del 语句
用 del 语句可以按索引从列表中删除元素,而不是按元素的值进行删除。这个 pop() 方法不同,pop() 方法会返回一个值。
del 还能删除变量:
之后不再进行赋值的话,引用名字 a 会出错。之后还会有一些 del 的用法介绍。
5.3 元组(tuple)和序列(sequence)
列表和字符串有很多共同属性,比如索引和分片操作。它们是 序列(sequence) 这个数据类型的两个例子,请看 序列类型-列表,元组,范围 。Python在不断进化,以后可能加入其他的序列数据类型。我们来瞅瞅一种标准序列数据类型:元组(tuple) ,u的发音有人读“优”有人读“阿”。
一个元组包含若干的值,用逗号分隔:
如上所示,元组输出时总是括在小括号里,有了括号嵌套的元组就表示得很清楚。输入的时候可以没有小括号,但如果元组是一个大表达式的一部分时,小括号还是需要的。不能给元组的项赋值,但是元组可以包含可变的对象,比如列表。
元组和列表看起来很像,但是用处不同。元组是 不可变的(immutable) ,通常包含不同类型的元素,通过拆包(unpacking)访问这些元素(请看本节的后面部分),或者通过索引或属性(请看 带名字的元组namedtuples )。列表是 可变的(mutable) ,元素类型一般都是相同的,通过遍历列表来访问每个元素。
一个特殊情形是构建含有0项或1项的元组,语法稍有点怪。一对小括号构建一个空元组,只有1项的元组后面要跟一个逗号(把单独一个值用小括号括起来是不够的哦)。这样很丑,但是很有效:
t = 12345, 54321, 'hello!' 就是一个 元组打包(packing) 的例子:值 12345 ,54321 , 'hello!' 被打包进了一个元组。相反操作也可以:
这被恰当地称为 序列的拆包(unpacking) ,对右手边(right-hand side,等号右边)的任意序列都适用。序列拆包要求等号左边要有足够多的变量与等号右边序列中的项相对应。一行语句实现多个变量赋值(multiple assignment)其实就是同时使用了元组的打包和序列的拆包。
5.4 集合(set)
Python也有 集合(set) 这个数据类型。集合中的元素是无序的,没有重复的元素。基本用法是测试某个变量是不是某个组织的“会员(membership)” 和 去除重复元素。集合对象也支持数学操作,比如集合的并(union),交(intersection),差(difference)和对称差(symmetric difference)。
用大括号或 set() 函数来创建集合,但是空集只能用 set() 来创建,不能用 {}。因为 {} 创建了一个空的字典(dictionary),我们下一节再讨论它。
这是一个简单的例子:
和 列表推导 原理一样,集合推导是这样的:
5.5 字典(dictionary)
Python另一个自带的很有用的数据类型是 字典(dictionary) (请看 映射类型-字典)。其他语言有类似字典的“关联内存(associative memory)”或“关联数组(associative array)”。序列用数作为索引,字典用 键(key) 作为索引,键可以是任何不可变的(immutable)类型。字符串、数都可以作为键,只包含字符串、数字、元组的元组也可以作为键。如果一个元组直接或间接地包含了可变的(mutable)对象,它就不能作为键了。列表不能作为键,因为列表可以原地(in place)修改自身的值,通过索引来给列表元素赋值,切片赋值,append() 和 extend() 都可以修改列表。
最好把字典想成无序的一些 键值对(key: value) ,一个字典内的键必须是唯一的(unique)。大括号创建一个空字典:{} 。字典里面的键值对用逗号分隔,字典的输出也是这样的。
字典的主要操作是存储键值对,用键来访问值。可以用 del 来删除一个键值对。如果给一个已存在的键赋一个新的值,原来的值就被忽略了。访问一个不存在的键会出错。
对字典执行 list(d.keys)) 将返回一个字典中所有键的列表,顺序是不定的,如果想排序就用 sorted(d.keys()) [6]。 用 in 可以检查一个键是不是在字典里。
来个例子:
函数 dict() 能把一序列的键值对构建成字典:
字典推导(dict comprehension)可以用任意的键值表达式来创建字典:
键如果是简单的字符串的话,用关键字参数(keyword argument)就行了:
5.6 循环(looping)技术哪家强
对字典进行循环访问时,键和值能同时取出,用 items() 方法:
对序列进行循环访问时,索引和值能同时取出,用 enumerate() 函数:
对两个或多个序列同时进行循环访问,可以用 zip() 函数:
反向循环访问一个序列,首先指明想要操作的那个序列,然后用 reversed() 函数:
想有序地循环访问一个序列,可以用 sorted() 函数,它返回一个新的序列,原来的序列没有被修改:
在循环内修改正在遍历的序列(比如复制某一项),推荐的做法是首先创建一个拷贝。循环访问一个序列不会隐式地创建拷贝,我们可以用切片(slice)来方便地创建拷贝:
5.7 关于条件(Condition)再多说两句
用于 while 和 if 语句的条件可以包含任何操作符,不单单限于比较表达式。
比较操作符 in 和 not in 用于检查一个值是不是在一个序列里面。操作符 is 和 is not 用于比较两个对象是不是同一个对象,这仅仅对可变对象(比如列表)才有意义。所有的比较操作符的优先级(priority)都相同,它们的优先级都比数值操作符的优先级低。
比较操作可以链接(can be chained),比如 a < b == c 测试是否 a 比 b 小,并测试是否 b 等于 c 。
比较操作可以用布尔操作符 and 和 or 结合起来,比较表达式或布尔表达式的结果可以用 not 进行否定。布尔操作符的优先级低于比较操作符。这三个布尔操作符中 not 优先级最高, or 优先级最低。A and not B or C 等价于 (A and (not B)) or C。老规矩,善用小括号来表达自己想要的组合。
布尔操作符 and 和 or 被称为 短路操作符(short-circuit operator) :它们的参数从左向右求值,一旦结果能确定了就不继续求值了。如果 A 和 C 是true,但 B 是false,那么 A and B and C 就不会对表达式 C 进行求值。当作一个普通值而不是布尔值时,短路操作符的返回值是最后求值的那个参数。
可以把比较表达式或布尔表达式的结果赋给一个变量:
和C不同,Python的表达式内不能赋值。C程序员可能要发牢骚了,但这样避免了C程序中的一个常见的问题:在表达式里想输入 == 结果输成了 = 。
5.8 序列和其他类型比大小
一个序列对象可以和同类型的另一个序列对象比大小。比较起来按照 字典编纂顺序(lexicographical) (这个字典是日常生活中的字典)。首先比较前两项,如果不同就能定出大小了,如果相同比较接下来的两项,一直这样进行下去到比完整个序列为止。如果正在比较的两项是类型相同的序列,就按照字典编纂顺序递归地(recursively)比下去。两个序列的所有项都相同,则两个序列相同。如果序列A是序列B的起始子序列,则序列A小。对于字符串来说,字典编纂顺序使用Unicode编码点(codepoint)数来决定单个字母的顺序。我们看一下同类型序列是怎么比较的吧:
只要对象提供了合适的比较方法,我们也可以用 < 或 > 来比较不同类型的对象。比如混合的数值类型按值的大小来比较,所以0和0.0是相等的。如果对象没有提供合适的比较方法,解释器并不会随随便便排个序,而是报一个 TypeError 的错误。
6 模块(Module)
退出Python解释器(interpreter)再打开的话,定义的函数和变量就都丢了。要写个长点的程序,最好用文本编辑器写一个文件,然后用解释器运行这个文件。这叫写一个 脚本(script) 。随着程序越写越长,为了便于维护(maintenance),可能需要把程序分散到几个文件中。也有可能你的好几个程序都需要使用一个事先写好的函数,总不能把函数定义拷到每个程序里吧。
Python当然有办法把定义(definition)放到一个文件里,然后在脚本或解释器里调用它。这样的文件叫 模块(module) ,模块中的定义可以被 导入(import) 到其他模块或主模块(main module) 中。主模块是你能在一个执行于顶层(top level)或计算器模式(还记得之前的计算器例子吗?)的脚本中访问的那些变量。
模块就是包含Python定义(definition)和语句(statement)的文件。文件名就是模块名加 .py 后缀。在一个模块内部,模块名是一个字符串,可用全局(global)变量 __name__访问。来个例子,在当前目录下用你最喜欢的文本编辑器创建一个名字是 fibo.py 的文件,输入以下内容:
现在召唤Python解释器并导入这个模块:
上面的语句没有往当前符号表中导入 fibo 里的函数名,仅仅导入了模块名 fibo 。可以用这个模块名来访问它的函数:
要是某个函数经常用,可以给它一个本地的名字:
6.1 模块,接着整
一个模块可以包含函数定义和可执行的语句。这些语句用于初始化(initialize)这个模块,仅仅在import语句中 第一次 出现模块名字的时候才执行。[7] (该文件被当成一个脚本来执行的时候,这些语句也会执行。)
每个模块都有自己的私有符号表(private symbol table),这个表被该模块定义的所有函数当成全局符号表(global symbol table)。所以模块的作者可以在模块内使用全局变量,不需要担心与用户的全局变量冲突。另外如果你知道自己在干啥的话,可用类似表示模块函数的办法来表示模块的全局变量,模块名.变量名 (modname.itemname) 。
模块可以导入其他模块。虽然没有强制要求,但按照惯例所有的 import 语句要放在模块或脚本的开头。(kitt温馨提示: 为了调试方便, import pdb 语句有时要放在模块中间,后面紧跟 pdb.set_trace() 就可以设置一个断点了。) 导入的模块名字放入当前模块的全局符号表里。
有个 import 语句的变体,可以直接把某模块中的某些名字导入当前模块的符号表。比如:
这样就没有导入模块名,import的这些名字被当成在局部(local)符号表中,比如上面例子的 fibo 就是未定义的名字(没有被导入的模块名)。
还有个变体可以导入某模块定义的所有名字:
这样就导入了除了 _ 开头的所有名字。多数情况下Python程序员不这么用,因为这样就引入了未知的一堆名称,可能把你已经定义的一些东西隐藏了。
一般情况下不要整什么从一个模块或包(package)导入 * 这种操作,这样会让代码的可读性变差。但在交互模式时为了少打几个字,这么做是OK的。
注解
为了高效,一个解释器会话(session)对每个模块只会导入一次。所以如果你的模块被改过了,需要重启解释器。或者如果你只想交互地测试某个模块,用imp.reload() ,比如这样 import imp; imp.reload(modulename) 。
6.1.1 把模块当成脚本来执行
这么运行Python模块的时候:
模块中的代码会执行,就像你导入了它们一样,但是 __name__ 被设置成了 "__main__" 。意思就是把下面的代码加入到你的模块末尾:
可以令一个文件即作为脚本又作为可导入的模块,因为分析命令行输入的代码只有在模块被当成“主(main)”文件的时候才会运行:
如果导入模块的话,代码是不会运行的:
这样做的用处是为模块提供一个方便的用户接口,或用于测试,把模块当脚本,执行一个测试集。
6.1.2 搜索模块的路径
导入一个名叫 spam 的模块的时候,解释器首先从Python自带的(built-in)模块中找这个名字。没找到的话,再从 sys.path 这个变量给出的目录列表里找 spam.py 这个文件。sys.path 从这些位置进行初始化:
- 包含输入脚本的目录,没有指明输入文件时就是当前目录。
- PYTHONPATH ,这是个目录名字的列表,和shell变量 PATH 一样的语法。
- 和安装有关的默认值。
注解
在支持符号链接(symlink)的文件系统中,符号链接先作用,然后再计算含有输入脚本的目录。也就是说含有符号链接的目录 没有 加到模块搜索路径里面。
初始化以后,Python程序可以修改 sys.path 。正在运行的脚本所在的目录(叫目录A吧)被放到了搜索路径的开头,后面跟的是标准库的路径。这样放置顺序的意思就是如果有重名的模块,目录A的脚本会被载入,标准库的脚本就被忽略了。如果不注意放置顺序的话,这样就可能出错喽。请看 6.2 标准模块 的介绍。
6.1.3 编译后的(compiled)Python文件
为了加速载入模块,Python缓存了(cache)每个模块编译后的版本,位置是 __pycache__ 目录下的 module.version.pyc ,版本号(version)含有编译后文件的格式,一般都会含有Python版本号。比如在CPython发行版本(release) 3.3下,编译后的spam.py会被缓存成 __pycache__/spam.cpython-33.pyc 。这样命名的好处是不同发行版本的Python编译后的模块可以共存。
Python会比较源文件(source)的修改时间和编译后版本的时间,来看看要不要重新编译,这是全自动的。编译后的模块和平台无关(platform-independent),不同架构的系统之间可以共享同一个库。
Python不会在不同环境下检查缓存。首先,命令行直接载入的模块总是会被重新编译的,不会存起来的。其次,没有源模块的时候不检查缓存。想发布一个不含源文件(只有编译后文件)的发布(distribution),编译后的模块必须在源目录里,而且不能有源模块。
高手请看:
- 命令行下可以用 -O 或 -OO 选项来减小编译后模块的大小。-O 开关去掉了assert语句,-OO 开关把assert语句和 __doc__ 字符串都去掉了。请谨慎使用,因为有些程序非常依赖这两样东西。优化后(optimized)的模块后缀名是 .pyo 而不是 .pyc ,占的空间更少了。Python以后的发行版可能会对优化效果做出修改。
- 读自 .pyc 或 .pyo 的程序并不会比读自 .py 文件的程序运行得更快。 .pyc 或 .pyo 文件唯一快的地方就是载入(load)速度快。
- 模块 compileall 可以为一个目录里所有的模块创建 .pyc文件,或.pyo文件(使用 -O 选项)。
- 想要了解这个过程的更多细节,比如决定过程的流程图(flow chart),请看PEP 3147。
6.2 标准模块
Python带了一个标准模块的库,在Python库参考手册(之后的”Library Reference”)这篇文档中有说明。有些模块是解释器自带的,这样就可以执行一些非Python语言核心(core)自带的操作,提高了效率,也可以执行系统调用(system call)。这些模块和底层(underlying)平台有关。比如 winreg 模块只有Windows系统里有。有个模块要特别注意一下: sys ,每个Python解释器都有。变量 sys.ps1 和 sys.ps2 定义了主提示符和二级提示符字符串:
仅在交互模式下使用解释器时会出现这两个变量。
变量 sys.path 是个列表,含有解释器对模块的搜索路径。它根据环境变量 PYTHONPATH 被初始化成一个默认路径。没有 PYTHONPATH 的话就根据一个自带的默认值来设置。可以用标准的列表操作方法来修改它:
6.3 dir() 函数
自带的 dir() 函数用于找出一个模块定义了哪些名字。返回的是一个排序过的字符串列表:
没有参数的话,dir() 列出当前定义的名字:
它列出的是所有类型的名字: 变量,模块,函数等等。(kitt温馨提示: vars() 能列出这些名字的值)
dir() 没有列出自带的函数名和变量名。如果想看一下的话,请用标准模块 builtins
6.4 包(package)
包使用“带点的模块名”来组织Python模块命名空间(namespace)。比如模块名 A.B 指定了一个包 A 中的一个子模块 B。就像使用模块能让不同模块的作者不必担心其他人的全局变量名一样,使用带点的模块名可以让不同包(比如NumPy,Python Imaging Library)的作者不必担心其他人的模块名。
假设我们想设计一些模块(就是“包”),来统一处理声音文件和声音数据。声音文件一般根据扩展名(比如 .wav, .aiff, .au)分为不同的格式,所以需要创建和维护一堆不断增多的模块,来进行各类声音文件格式的转换。也需要对声音数据进行一些不同的操作,比如mixing(混频),adding echo(加回声),applying an equalizer function(搞一个均衡器的功能),creating an artificial stereo effect(创建人工立体声效果)等等,所以我们需要一堆永无止境的模块。下面用一个分层次的(hierarchical)文件系统的形式来展示一下这个包的可能的结构:
导入这个包的时候,Python从 sys.path 里面找包的子目录。
__init__.py 文件告诉Python这个目录是含有包的,这样就避免了一些有大众名的目录(比如 string )无意中隐藏之后可能要用的一些模块。最简单的 __init__.py 可以是个空文件,它也可以做一些包的初始化工作或者设置 __all__ 变量,稍后介绍这个变量。
用户可以导入包中的个别模块,比如:
这就导入了子模块 sound.effects.echo ,必须用全名来引用:
另一种导入子模块的方式是:
这样也导入了子模块 echo 并且不再需要包前缀,直接用就行:
或者直接导入想用的函数或变量:
这样还是导入了子模块 echo ,但我们可以直接引用函数 echofilter()
用 from package import item 的时候,item可以是包的子包或子模块,也可以是包中定义的其他名字,比如函数、类、变量。 import 语句首先检测item是否在包中定义了,没有定义的话就假设它是个模块并尝试导入。如果还不行就引发一个 ImportError 异常。
不同的是,用 import item.subitem.subsubitem 的时候,除了最后一个,每个item都必须是包。最后一个item可以是包,可以是模块,但不能是之前的item定义的类、函数、变量。
6.4.1 从包中导入 * (Importing * From a Package)
用户写 from sound.effects import * 会发生什么呢?理想情况下我们希望到文件系统中,找到包中的所有子模块并导入。这样很费时,导入子模块后可能有副作用,这样的副作用仅应该在显式导入子模块时发生。
唯一的解决方法就是包的作者提供一个明确的包的索引(index)。import 语句使用这个惯例:如果包的 __init__.py 文件定义了一个叫 __all__ 的列表,列表中的模块就是 frompackage import * 要导入的模块。包有新版本时,是否更新这个列表要看包作者的心情。包作者如果觉得 from package import * 没用,也可以不用 __all__。比如文件sound/effects/__init__.py 可以有这样的代码:
意思是 from sound.effects import * 会导入 sound 包的这三个子模块。
__all__ 没定义的话, from sound.effects import * 并不会把 sound.effects 包的所有子模块都导入到当前的命名空间(namespace),它仅导入 sound.effects 这个包,运行其__init__.py 文件的初始化代码,再导入包中定义的所有名字,包括由文件 __init__.py 定义的名字和由此文件显式导入的子模块定义的名字。当然也包括由之前的 import 语句显式导入的子模块,看看这个:
在这个例子中,echo 和 surround 模块被导入到当前的命名空间,因为执行 from...import 语句的时候这两个模块已经在 sound.effects 包中定义了(如果 __all__ 列表里有这两个模块名,它们也会被导入)。
虽然用 import * 时某些模块被设计成仅仅导入某些名字,但在产品级代码中这样用 * 还是一个坏习惯。
但 from Package import specific_submodule 这种用法没问题哦,而且还推荐这样用,只要导入的模块不和其他包的模块重名即可。
6.4.2 包内部的引用
包有子包的时候,比如上面例子的 sound 包,可以用绝对导入来引用兄弟(sibling)包的子模块。比如 sound.filters.vocoder 模块想用 sound.effects 包的 echo 模块,它可以写 from sound.effects import echo 。
也可以用相对导入,用前导点(leading dots)来表示当前包和父包。对于 surround 模块来说,可以这样写:
相对导入依据的是当前模块的名字。因为主模块名字总是 "__main__" ,Python程序里要被当成主模块的那些模块只能使用绝对导入。
6.4.3 位于多个目录中的包
包有个特别的属性,__path__ ,它被初始化成一个包含 __init__.py 文件所在目录名字的列表,这发生在 __init__.py 的代码执行之前。这个属性是可以修改的,用来修改一个包所包含的子包和模块。
这个属性不常用,但可以扩展包中的模块集。
7 输入输出,俗称IO
程序的输出有多种方式,数据可以用可读的形式打印出来,或写到文件里去,以后用得着。本章讨论一下这些可能性。
7.1 更犀利的输出格式
目前我们见到两种写值的方式:表达式语句 和 print() 函数。(第三种方式是用file对象的 write() 方法,用 sys.stdout 表示标准输出文件。请看库参考手册的介绍)
可能打印的空格间隔的值不好看,你想控制一下输出格式,有两种方法:一种是自己处理所有字符串,用字符串切片和拼接操作,可以整出无数花样。字符串类型有些有用的方法可以以指定的列宽来填充字符串,稍后讨论。另一种是用 str.format() 方法。
string 模块包括一个 Template 类,提供了另一种替换字符串中的值的方式。
如何把值转成字符串呢?幸运的是Python有办法把任意的值转成一个字符串:把值传给 repr() 或 str() 函数就好了。
str() 函数返回的值是人类可读的,repr() 函数返回的值是给解释器读的(或者没有等价语法的时候给一个 SyntaxError )。如果一个对象不是人类可读的, str() 函数就返回和 repr() 相同的值。许多值(比如数、字典、列表)用这两个函数表示结果都相同。字符串却有两种不同的表示形式,来看个例子:
两种方式来表示平方表和立方表:
第一个例子里,每列之间由 print() 函数加入一个空格,该函数默认在它的参数之间加空格。
这个例子也展示了 str.rjust() 方法,用于字符串在一列中进行右对齐(right-justify),左边补空格。类似地有 str.ljust() 方法和 str.center() 方法。这些方法并不会写什么,而是返回一个新的字符串。输入字符串太长的话,这些方法并不会截断(truncate),而是原样返回输入的字符串,虽说列的格式会被搞乱,但这样保证了值是正确的,仍是一个更好的选择。如果真的需要截断,可以切片,比如 x.ljust(n)[:n] 。
还有一个方法叫 str.zfill() ,用处是给数值型字符串左边补0,它能识别正负号:
str.format() 的基本用法是:
花括号和其中的字符(叫格式字段,format field)被传入 str.format() 方法的对象取代。花括号内的数对应 str.format() 方法的对象的位置:
str.format() 方法也支持关键字参数:
位置参数和关键字参数可随意组合:
'!' (用于 ascii()),'!s' (用于 str()),'!r' (用于 repr()) 可以在格式化前把值转化好:
可选的 ':' 和格式说明符(specifier)可以跟在格式字段名的后面,实现更精确的格式控制。下面的例子把Pi四舍五入到小数点后三位:
':' 后面如果是整数,字段会是所有字符宽度的最小数,这样可以把表变得更漂亮:
格式字符串太长又不想分拆的话,可以用名字而不是用位置来表示要被格式化的变量。只需要传一个字典并用 '[]' 来访问键即可:
也可以把表作为关键字参数用 ‘**’ 来传递:
这个相当有用,可以与自带的 vars() 函数结合使用,该函数返回含有所有局部变量的字典。
想全面了解字符串的格式化和 str.format() 方法,请看 格式化字符串语法 。
7.2 读写文件
函数 open() 返回一个 文件对象(file object) ,最常见的参数有两个: open(filename, mode) ,文件名和模式。
第一个参数是文件名,第二个参数是处理文件的方式。模式(mode) 为 'r' 表示文件只读,'w' 表示文件只写,同名文件原来的内容会被删除。'a' 表示在文件末尾添加内容(appending)。'r+' 意思是对文件既读又写。mode 参数是可选的,不指定的话默认是 'r' 。
一般情况下文件都是以 文本模式(text mode) 打开的,意思是从文件读出的字符串和写入文件的字符串都有特定编码方式(默认为UTF-8)。如果模式(mode)加一个 'b' ,就表示以 二进制模式(binary mode) 打开文件,意思是数据的读写用字节对象(bytes objects)的形式。二进制模式应该用于所有不含文本(text)的文件。
文本模式下,读文件时Python默认把平台相关的行结束符(Unix为 n,Windows为 )转成 n ,写文件时默认把 n 转成平台相关的行结束符。这种幕后转换对文本文件没问题,但是会损坏二进制数据,比如 JPEG 或 EXE 文件。读写二进制文件时要小心翼翼。
7.2.1 文件对象有哪些方法呢
下面的例子中我们把一个已经创建的文件对象称为 f 。
用 f.read(size) 来读文件内容,返回的是一个字符串或字节对象(bytes object)。size 是一个可选的数值参数。该参数省略掉或者是负数的时候,文件的全部内容会被读取并返回。文件大小如果是你电脑内存的两倍大,那就是你的问题喽。size 非负的时候就读取这些个字节并返回。到达文件末尾的时候 f.read() 会返回一个空字符串(''):
f.readline() 读取文件的一行,返回字符串的末尾有换行符(n),仅当文件最后一行没有换行符的时候,返回字符串的末尾才没有 n ,这样返回字符串就很明确了。如果f.readline() 返回一个空字符串,说明已经到了文件末尾。空行返回的是 'n'
如果要按行读取文件,可以对文件对象使用循环。这样做内存高效,快速,代码也更简洁:
如果想把文件所有的行读到一个列表里面,可以用 list(f) 或 f.readlines() 。
f.write(string) 把 字符串 的内容写到文件里,返回值是写入的字符个数:
想写入的对象如果不是字符串,则先要转成字符串:
f.tell() 返回一个整数,表示文件对象在文件的当前位置。二进制模式下,这个整数表示此位置距离文件开头有多少字节。文本模式下,这个整数不确定。
想改变文件对象的位置,可以用 f.seek(offset, from_what) (offset指的是偏移量)。新位置就是相对于一个参考点的偏移量,参考点由 from_what 参数指定。from_what 值为0时表示从文件开头开始算起,值为1表示从当前位置开始算起,值为2表示从文件结尾开始算起。省略掉 from_what 参数的话就是默认从文件开头开始算起:
在文本文件中(就是那些没有用 b 模式打开的文件),只允许参考点是文件开头(有一个例外 seek(0, 2)),唯一有效的 偏移量(offset) 是 f.tell() 的返回值和0。其他的偏移量会产生一些未定义的行为。
处理完文件后,用 f.close() 关闭文件,释放被该文件占用的系统资源。使用 f.close() 之后,任何企图使用文件对象的做法都自动失败:
使用关键字 with 是个好习惯,优势是文件的一组操作完成后它会被正确关闭,即使中间引发了异常(exception)。而且比写 try -finally 语句块更简洁:
文件对象还有别的方法,比如 isatty() 和 truncate(),不常用。请看文件对象的库参考手册来获得完全的指导。
7.2.2 用 json 保存结构化的数据
文件读写字符串很方便,数值就费事一点,read() 方法只能返回字符串,然后用 int() 函数来转换,'123' 变成数值123。想要存更复杂的列表、字典等数据类型时,手动分析和序列化(serialize)就麻烦了。
那就别让用户自己写和调试代码来往文件里存复杂的数据类型了,Python可以使用流行的数据交换格式 JSON (JavaScript Object Notation) JavaScript对象表示法 。标准模块叫 json ,可以把Python数据的层次结构转成字符串,这叫 serializing(序列化)。从字符串中重建数据,这叫 deserializing(反序列化)。表示对象的字符串可以存到文件或数据里,或通过网络发送到远程的机器上。
注解
现在的应用程序广泛使用JSON格式来交换数据,很多程序员对JSON很熟悉,对于互用性(interoperability)来说它是个很棒的选择。
有一个对象 x,一行代码可得它的JSON字符串表示:
dumps() 函数的变体 dump() 可以把一个对象序列化到 文本文件(text file) 中。如果 f 是个 文本文件,以写的模式打开,我们可以这样:
解码这个对象也是一行代码,用 f 来表示一个读模式打开的 文本文件
这个简洁的序列化技巧可以处理列表和字典,想用JSON序列化任意的类的实例还要复杂点。请看 json 模块参考手册。
8 错误(error)和异常(exception)
我们还没有好好说说错误信息,如果上面的例子程序你试过了,可能也碰到一些错误信息了吧。有两种(至少两种)错误: syntax errors(语法错误) 和 exceptions(异常) 。
8.1 语法错误(syntax error)
语法错误,也叫解析错误(parsing error),可能是学Python时碰到的最常见错误了:
分析程序指出哪一行有问题,小箭头指向行中检测到的第一个错误。错误是由箭头 前面的 的单词(token)引起的或在那里检测到的。上面例子中,在函数 print() 处检测到了错误,少了一个冒号(colon, ':')。文件名和行号被显示出来以便于查找。
8.2 异常(exception)
语句和表达式即使语法对了,还是可能在执行的时候出错。执行时检测到的错误叫 异常(exception) ,它们并不是无条件地致命的(unconditionally fatal),也就是说碰到异常没什么大不了的,我们很快会看到如何在Python程序中处理异常。多数异常没有在程序中处理,错误信息就出现了:
错误信息的最后一行说明出错原因。异常有不同种类,异常的种类也会打印出来,上面例子的异常种类有 ZeroDivisionError除零错误,NameError名字错误 和 TypeError类型错误 。打印出的异常类型是自带的(built-in)异常类型名字,自带的异常总会打印出来,用户自定义的异常不一定会打印出来,但按照惯例最好还是打印出来。标准异常名字是自带的标识符(identifier),不是保留关键字。
错误信息最后一行还根据异常的类型和怎么引发的来提供细节。
错误信息的前半部分说明异常发生的环境(context,也叫上下文),用的是stack traceback(堆栈回溯)。一般就是有一个stack traceback列出了源程序的某些行,但是不会列出来自标准输入的行。(kitt温馨提示: Python有 traceback 模块)
Python自带的异常 列出了自带异常和含义。
8.3 怎么处理异常呢
可以写程序来处理某些异常,请看下面的例子,要求用户输入一个有效的整数,用户可以中断这个程序(用 Control-C 或操作系统支持的方式)。来自用户的中断(interruption)引发 KeyboardInterrupt键盘中断 异常:
try 语句这样工作:
- 首先,try子句 (就是关键字 try 和 except 之间的部分)执行。
- 没有发生异常的话,跳过 except子句,try 语句结束执行。
- try子句执行过程中如果发生异常,就跳过剩下的语句。如果异常的类型出现在了 except 后面,except子句就执行,然后 try 后面的语句继续执行(这不是刚才说的被跳过的那些语句)。
- 如果发生异常,并且except子句里没有这种异常类型,该异常会被传到 try 语句的外面,如果没有语句处理它,它就是一个 未处理的异常(unhandled exception),程序停止,给出一个错误信息。
try 语句可以有多个except子句,来处理不同种类的异常,至多一个except语句会被执行。except子句只处理相应try子句中发生的异常,不处理其他 try 语句中的异常。except子句可以把多个异常放到一个元组(tuple)里:
最后一个except子句可以省略掉异常的名字,用作一个通配符(wildcard),意思是剩下的异常我都包了。小心使用哦,因为这样很容易就隐藏了一个真正的程序错误!可以用它打印错误信息,再次引发(re-raise)这个异常,让调用程序(caller)来处理这个异常:
try ... except 语句有一个可选的 else子句,跟在所有except子句的后面,用处是当try子句中没有异常的时候执行一些代码。比如:
使用 else 子句比在 try 子句中加入额外的代码要好,因为这样避免了一不小心抓住(catch)一个不是由该 try ... except 语句保护的异常。
异常发生时可以有相关的值,这叫异常的 参数(argument) 。参数的类型取决于异常的类型。
except子句可以在异常名字的后面指定一个变量,这个变量绑定于一个异常的实例,存在 instance.args 里。为了方便起见,异常实例定义了 __str__() 方法,所以这个参数可以直接被打印出来,不需要引用 .args 。也可以先实例化(instantiate)一个异常再引发(raise)它,加入自己想要的属性:
异常如果有参数,会显示于未处理异常的错误信息的最后部分(‘detail’,异常的细节)。
异常处理者(handler)不仅处理try子句中的异常,而且还处理其调用函数(可以是间接调用)发生的异常:
8.4 引发异常(Raising Exceptions)
raise 语句允许程序员让某个特定的异常发生。比如:
raise 的唯一参数说明引发的是什么异常,必须是一个异常实例或异常类(该类源于 Exception)。
如果想看看是否有异常产生,又不想处理它,可以用一个简单形式的 raise 来再次引发(re-raise) 这个异常:
8.5 用户自定义的异常
可以创建新的异常类 (请看Python类(class)的介绍 9 类(class))。异常一般直接或间接地源于(derive) Exception 类,比如:
上面例子中,Exception 的默认方法 __init__() 被覆盖了,新的行为就是简单地创建 value 这个属性(attribute),替换了原来的默认行为,原来的默认行为是创建 args属性(回想一下前面的except子句可以在异常名字的后面指定一个变量,对应于一个异常的实例,存在 instance.args 里)。
异常这个类可以做其他类能做的任何事情,但一般异常类都很简洁,一般仅仅提供一些属性,能让错误信息被异常的处理者(handler)提取出来。如果你的模块能引发几个不同的异常,惯例是创建一个基类,为不同的错误创建不同的异常子类:
大多数异常名字都是以”Error”结尾,和标准异常的命名方式一样。
许多标准模块都定义了自己的异常,用于报告错误。关于类的更多介绍请看 9 类(class) 。
8.6 怎么定义清理(clean-up)操作呢
try 语句有个可选的子句,可以做一些清理操作,什么情况下都会执行的清理操作。比如:
finally子句 总是在离开 try 语句之前执行,不管有没有异常发生。如果 try 子句发生异常,而且异常没有被 except 子句处理(或 except 子句或 else 子句发生异常),该异常会在 finally 子句执行之后再次引发。finally 子句在“跳出” try 语句之前都会执行,“跳出”指的是执行了 break 或 continue 或 return 语句。看看下面这个例子:
可见 finally 子句啥时候都执行。由两个字符串相除引发的 TypeError 没有被 except 子句处理,因此在 finally 子句执行后这个异常被再次引发(re-raise)。
实际编程时,finally 子句很有用,可以释放外部资源(比如文件或网络连接),不管有没有成功使用这些资源。
8.7 预定义的(predefined)清理操作
一些对象定义了当这个对象不会再被使用时要执行的清理操作,不管有没有成功使用这个对象。下面的例子试图打开一个文件并把内容打印到屏幕上:
这份代码的问题是执行完后,保持文件处于打开状态一段时间,这段时间是不确定的。简单脚本中这没什么大不了,大程序中这就有问题了。with 语句可以让文件等对象总是被及时地正确地清理(clean up)。:
这样的语句执行后,文件 f 总会被关闭,即使处理文件行的时候出现问题。提供了预定义(predefined)清理操作的对象(比如文件)会在其文档中说明的。
9 类(class)
和其他编程语言比起来,Python类的语法(syntax)和语义(semantic)很少,算是C++和Modula-3类机制的混合。Python的类提供所有面向对象编程(Object Oriented Programming)的标准特性: 类继承(inheritance)机制允许有多个基类(base class),派生类(derived class)可以覆盖其基类(一个或多个)的任何方法,一个方法可以调用基类的同名方法。对象(object)可以含有任意数量和种类的数据。同模块一样,类也有Python的动态(dynamic)特性:在运行时创建,创建后可以修改。
在C++术语中,一般情况下类成员(包括数据成员(data member))是 public 的(例外情况请看 9.6 私有(private)变量),所有的成员函数是 virtual 的。在Modula-3中,从对象方法中引用对象成员没有什么简写办法:成员函数明确地用第一个参数表示这个对象,调用的时候这个参数被隐式地(implicitly)提供。在Smalltalk中,类本身就是对象。这样就提供了导入和重命名的语义。和C++、Modula-3不同,如果用户想做某些扩展,自带类型可以被当成基类。和C++一样,大多数有特定语法的自带操作符(算术操作符,下标(subscripting))可以在类的实例(instance)中重新定义。
(没有关于类的被一致认可的术语,我会偶尔用一下Smalltalk和C++的术语。我也会用Modula-3的术语,因为它的面向对象语义比起C++来更接近Python,但估计很少有读者听说过。)
9.1 名字和对象,简略一说
对象有自己的特性,多个名字(在多个作用域(scope)内)可以绑定到同一个对象上,其他语言中这叫别名(alias)。你在Python中第一眼见到时可能并不欣赏,处理不可变(immutable)基本类型(数、字符串、元组)时,别名这个东西也会被忽视。但是别名对于可变(mutable)对象(列表、字典等)却可能有惊人的作用。某种程度上,别名像指针(pointer),用好了程序会受益。比如传递一个对象的代价很小,因为实现时仅仅传递了一个指针。某个函数若修改了当作参数传入的对象,调用者(caller)会看到这个改变,这样就避免了Pascal中的两个不同参数的传递机制。
9.2 Python的作用域(scope)和命名空间(namespace)
介绍类之前,先来看看Python作用域的规则。类的定义对命名空间玩了巧妙的把戏,你需要知道作用域和命名空间才能明白这是咋回事。顺便说一句,这个话题对于任何高级Python程序员都挺有用。
先来看看定义。
命名空间(namespace) 是名字到对象的映射。大多数命名空间是用Python字典实现的,一般情况下这是注意不到的(除非是为了测试性能),以后可能有所改变。命名空间的例子有: 一系列的自带的名字(包括自带的函数,比如 abs() ,自带的异常名字),模块中的全局名字,函数调用中的局部名字。从某种意义上来说,对象的属性也形成一个命名空间。关于命名空间很重要的一点是,不同命名空间的名字绝对没有任何关系。比如两个不同的模块可能都定义了函数 maximize ,这样没有歧义,用户必须在函数前面加上模块名字。
顺便一提,对所有带点(dot)的名字,我都用 属性(attribute) 这个词。比如在表达式 z.real 中,real 是对象 z 的属性。严格来说,引用模块中的名字是引用属性: 在表达式 modname.funcname 中,modnmae 是模块对象,funcname 是它的一个属性。这样模块的属性和模块中定义的全局名字就有一个很直接的映射了: 它们共用同一个命名空间! [8]
属性有的是只读的,有的是可写的。可写的属性意思就是可以给它赋值。模块的属性是可写的: 比如 modname.the_anser = 42。可写的属性也能用 del 语句删除。比如 delmodname.the_answer 会删掉模块 modname 的 the_answer 属性。
命名空间创建的时间不同,生命时间也不同。含有自带名字的命名空间是在Python解释器启动的时候创建的,而且一直不会被删除。模块的全局命名空间是在读入模块的定义的时候被创建的,一般来说,模块的命名空间的生命时间也会持续到解释器退出。由解释器顶层调用而执行的语句,要么是从脚本文件读入,要么是用户在交互模式下输入,这些语句被认为是 __main__ 模块的一部分,所以它们有自己的全局命名空间。(自带的名字实际上在一个叫 buildints 的模块中。)
函数的本地命名空间在函数调用时创建,函数返回或引发了函数无法处理的异常时删除。(其实,遗忘(forgetting)是对实际发生事情的更好的描述。) 当然啦,递归调用(recursive invocations)时,每次函数都有自己的本地命名空间。
作用域(scope) 是Python程序的某个文本区域,在这里命名空间可以直接被访问。“直接被访问”意思是非法(unqualified)名字引用试图找到命名空间中的名字。
尽管作用域是静态决定的,但是它是动态使用的。程序执行的任意时刻,至少有3个嵌套的作用域,它们的命名空间可以直接被访问:
- 最内层的作用域,最先被搜索,包含本地名字(local names)
- 封闭函数(enclosing function)的作用域,从最内层的封闭(enclosing)作用域开始搜索,包含非本地也非全局的名字
- 次外层(next-to-last scope)作用域,包含当前模块的全局名字
- 最外层作用域(最后搜索),是包含自带(built-in)名字的命名空间
如果一个名字是全局的,那么所有的引用和赋值都会直接到包含模块全局名字的中间作用域去寻找。想要重新绑定最内层作用域外的变量,可以用 nonlocal 关键字。没有声明成为nonlocal的话,这些变量就是只读的(试图对这个变量写操作则会在最内层作用域创建一个 新的 局部变量,外部的同名变量没有改变)。
一般来说,局部作用域引用当前函数的局部名字(正文处的(textually))。在函数外面,局部作用域引用和全局作用域一样的命名空间: 模块的命名空间。类的定义则在局部作用域里面放了另一个命名空间。
很重要的一点,作用域是由文本(textually)决定的:模块中的函数的全局作用域是模块的命名空间,不管这个函数是在哪里调用的,也不管调用的是不是这个函数的别名。另一方面,实际查找名字的过程是在运行时自动完成的,但是Python语言定义正在朝着静态名字解析(name resolution)进化,也就是在“编译”时解析名字,所以不要依赖动态名字解析!(其实局部变量已经实现静态解析了。)
Python很奇葩的一点,没有 global 语句时,给名字赋值总是去最内层作用域查找。赋值并不会拷贝数据,只是把名字绑定到对象上。删除也是这样: 语句 del x 从局部作用域引用的命名空间中移除 x 这个绑定。实际上,所有引入新名字的操作都使用局部作用域: 特别是 import 语句和函数定义,它们在本地作用域中绑定模块名字和函数名字。
global 语句用于指出在全局作用域中的某个特定变量应该在那里被重新绑定。nonlocal 语句指出在一个封闭作用域中的某个特定变量应该在那里被重新绑定。
9.3 看到类(class)的第一眼
类引入了新的语法,3个新的对象类型还有一些新的语义。
9.3.1 类定义的语法
最简单的类定义的形式是:
类定义像函数定义一样(函数定义用 def 语句),必须先定义后使用。(可以把类的定义放到 if 语句的一个分支内,或放到一个函数里面。)
实际编程时,类定义中的语句通常是函数定义,其他语句也可以而且很有用,稍后再谈。类里面的函数定义通常有个比较奇特的参数列表,由方法(method)的调用惯例决定,也是稍后再谈。
进入类的定义之后,会创建一个新的命名空间,用作局部(本地)作用域(local scope),所有对局部变量的赋值都进入这个新的命名空间。尤其是函数定义会在这里绑定新的函数名字。
正常情况下离开类的定义的时候(也就是类定义结束了),一个 类对象(class object) 被创建。这就是个对类定义所创建的命名空间内容的封装(wrapper),下一节我们再谈类对象。原来的局部作用域(就是进入类的定义之前的那个)恢复,类对象在这里被绑定到类名字上,类名字由类的定义头部给出(例子中的 ClassName)。
9.3.2 类对象
类对象支持两种操作: 属性引用和实例化(instantiation)。
属性的引用 和Python所有标准的属性引用语法一样: obj.name 。有效的属性名字是类对象创建后所有在类的命名空间中的名字。如果类的定义是这样的:
那么 MyClass.i 和 MyClass.f 就是有效的属性引用,返回的分别是一个整数和一个函数对象。也可以对类的属性赋值,所以可以改变 MyClass.i 的值。 __doc__ 也是一个有效属性,返回属于类的文档字符串(docstring): "A simple example class" 。
类的实例化使用函数表示法,就假装类对象是一个无参函数,返回一个新的类实例就行啦。比如对于上面的类:
这样就创建了类的一个新的 实例(instance) ,并且把这个实例对象赋给了局部变量 x 。
实例化操作(调用类对象)创建一个空的对象。许多类可以创建自定义的某个初始状态的实例,因此类可以定义一个叫 __init__() 的方法:
一个类定义了 __init__() 方法后,实例化的时候自动为新创建的类实例调用该方法,在这个例子中,一个新的而且初始化好的实例可以这样获得:
当然啦,__init__() 方法可以有更灵活的参数,类实例化时给的参数就传给 __init__() 方法,比如:
9.3.3 实例对象
我们能对实例对象做些什么呢?仅有的操作就是属性引用了。两种有效的属性名,数据属性(data attribute)和方法(method)。
数据属性 对应于Smalltalk中的“实例变量(instance variable)”,C++中的“数据成员(data member)”。数据属性不需要声明,像局部变量一样,给它们赋值的时候它们就开始存在了。比如,x 是 MyClass 类的实例,下面的代码会打印出值16,不会有traceback:
另一种实例的属性引用就是 方法(method) 了。方法是“属于”对象的函数。(在Python里,方法这个术语不只是类的实例用到,其他对象类型也可以有方法。比如列表对象的方法有append,insert,remove,sort等等。但在下面的讨论中如果没有额外说明,我们就用方法这个术语特指类实例对象的方法。)
实例对象的有效方法名字取决于它所属的类。按照定义,类的所有函数对象(function object)属性定义了相应的实例的方法。所以在我们的例子里,x.f 是有效的方法引用,因为 MyClass.f 是个函数。x.i 不是有效的方法引用,因为 MyClass.i 不是函数。但是 x.f 和 MyClass.f 不是一回事,前者是 方法对象(method object) ,不是函数对象(function object)。
9.3.4 方法对象
一般来说,一个方法在绑定之后立即调用:
在 MyClass 例子中,上面的语句返回字符串 'hello world' 。但不是说必须立即调用一个方法: x.f 是个方法对象,可以存起来,之后再调用。比如:
上面的代码会一直打印 hello world,直到地老天荒。
调用方法时到底发生了什么?可能你注意到了,x.f() 并没有参数啊,但是 f() 的定义里有一个参数。这个参数怎么回事?当然啦,调用函数时要求有参数而没给参数,即使没用到这个参数,Python也会引发异常...
其实你可能已经猜到了,方法的特别之处在于对象作为第一个参数传给了函数。在上面的例子里,x.f() 等价于 MyClass.f(x) 。一般来说,调用一个有 n 个参数的方法,等价于调用相应的函数,它的参数列表是把该方法的对象插入到第一个参数之前。
如果还是不明白,可以看一下实现的细节,也许代码说得更清楚。当实例属性被引用时,如果这个属性不是数据属性,那么就搜寻它的类。如果这个名字表示一个有效的类属性,也就是函数对象,那就通过把实例对象(的指针)和刚找到的函数对象(的指针)打包成一个抽象对象(abstract object)的方式来创建一个方法对象,这个抽象对象就是方法对象。当调用该方法对象时,从实例对象和函数对象的参数列表中创建一个新的参数列表,用这个新的参数列表来调用函数对象。
9.3.5 类变量(class variable)和实例变量(instance variable)
一般来讲,实例变量是每个实例独有的,类变量是类的所有实例共享的:
如在 9.1 名字和对象,简略一说 中所说,共享的数据牵扯到 可变的(mutable) 对象时,比如列表和字典,会有惊人的效果。比如下面的 tricks 列表不应该是类变量,因为所有的 Dog 实例都会共享它:
正确的做法应该是用实例变量:
9.4 随意的一些备注
数据属性能覆盖同名的方法属性。为了避免一不小心的名字冲突(往往造成大程序中很难找的bug),最好遵循惯例,最小化冲突的可能性。可能的惯例包括方法的名字用大写的,给数据属性名字加一个短小的独特字符串前缀(比如仅仅是一个下划线),或方法属性的名字用动词,数据属性的名字用动词。
数据属性可以被方法引用,也可以被一个对象的普通用户(客户(client))引用。也就是说,要实现纯粹的抽象数据类型,类是没有用的。其实Python里面没有什么机制可以强制隐藏数据,都是根据惯例来的。(另一方面,用C写的Python的实现,可以完全地隐藏一个对象的细节并控制对这个对象的访问,这一点可以用于C写的Python扩展(extension))。
客户(client)应该谨慎地使用数据属性,因为有可能客户会通过标记(stamping on)数据属性来搞砸方法维护的不变量。客户可以给一个实例对象增加他们自己的数据属性,而且不影响方法的正确性,只要避免名字冲突。再次说明,命名惯例可以省去很多麻烦事。
在方法内部访问数据属性(或其他方法)没有什么捷径。我发现这样还增加了方法的可读性: 瞅一眼一个方法的时候,不可能把局部变量和实例变量弄混的。
通常方法的第一个参数叫 self 。这仅仅是个惯例而已,self 这个名字对Python来说没有什么特别含义。但是要不遵循这个惯例的话,其他Python程序员读你的代码的时候就很费事。而且可以想得到,类浏览器(class browser) 程序可能就是根据这个惯例写的。
任何是类属性的函数对象都定义了类的实例的一个方法。把函数定义的文本放到类定义的内部不是必须的: 把函数对象赋给类里面的局部变量是OK的,比如:
现在 f,g 和 h 都是类 C 的属性,它们都引用了函数对象,所以它们都是类 C 的实例的方法 — h 和 g 是完全等价的。但这种做法通常只会让程序的读者感到头晕。
方法可以通过 self 参数的方法属性来调用其他方法:
方法可以引用全局名字,同普通函数一样。方法的全局作用域是包含其定义的模块。(类从不用作全局作用域。) 很少会碰到一个正当理由要在方法中使用全局数据,有很多全局作用域的正当用法: 导入全局作用域的函数和模块可以被方法使用,其中定义的类和函数也能被方法使用。通常来说包含方法的类在当前全局作用域中有自身的定义,下一节我们会看到为啥一个方法想要引用自己所属的类。
每个值都是一个对象(Python里面一切皆对象),所以每个值都有一个 类(class) ,也叫 类型(type) ,存在 object.__class__ 这里。
9.5 继承
不支持继承的话,“类”这个名字就没什么意义了。派生类(derived class)定义是这样的:
BaseClassName (基类)这个名字必须定义在派生类所在的作用域中。基类名字的地方也可以是任意表达式,如果基类是在其他模块中定义的那就用得到了:
派生类定义的执行和基类是一样的。构建类对象的时候,基类会被记住。这用于解析属性引用: 如果想要的属性在当前类里没有找到,就去基类中寻找。这条规则递归地执行,如果基类还是由某个类派生出来的话,还会去这个类中再找。
派生类的实例化没有什么特殊之处: DerivedClassName() 创建类的一个新的实例。方法引用是这样解析的:先搜寻当前类的属性,然后沿着基类链搜寻,如果找到函数对象那么方法引用就是有效的。
派生类可以覆盖基类的方法,因为调用同一个对象的其他方法时,方法并没有什么特权,基类的方法调用同一个基类定义的其他方法的时候,可能调用的是派生类的方法,基类方法被覆盖了。(对C++程序员来说,Python中的所有方法实际上都是 虚(virtual) 的)。
派生类覆盖基类的方法可能是想扩展这个方法,而非仅仅是替换基类的同名方法。有个简单的技巧可以直接调用基类方法: BaseClassName.methodname(self, arguments) 。有时候客户(client)会用到。(BaseClassName 要在全局作用域才行。)
关于继承,Python有两个自带的函数:
- isinstance() 可以检查实例的类型: isinstance(obj, int) 只有在 obj.__class__ 是 int 或由 int 派生出来的类时才为 True。
- issubclass() 可以检查类的继承: issubclass(bool, int) 是 True,因为 bool 是 int 的子类(subclass)。但是 issubclass(float, int) 是 False,因为 float 不是 int 的子类。
9.5.1 多重继承
Python支持多重继承(multiple inheritance)。带有多重继承的类定义是这样的:
多数情况下,可以就简单地认为搜寻从父类继承的属性的顺序是深度优先,从左往右,同一个类中层次有重叠时不搜两次。因此,如果某个属性在 DerivedClassName 里找不到,就从 Base1 里面找,然后递归地从 Base1 的基类里面找,如果还没有找到,就从 Base2 里面找,依此类推。
实际情况是稍微复杂一些的,方法的解析顺序是动态变化的,以支持对 super() 的调用。这个函数在一些其他的有多重继承的语言中被称为call-next方法(call-next-method),比在单一继承语言的super调用更强大。
动态顺序是有必要的,因为多重继承的例子都显示出一个或多个菱形关系(diamond relationship),意思是至少有一个父类可以通过多重路径被最底部的类来访问。比如说,所有的类都继承 object 类,所以任何多重继承的例子都提供了一条以上的路径来到达 object 。为了避免基类被访问多于一次,某种动态算法把搜寻顺序线性化,用一种避免按每个类中指定的从左向右顺序的方法,该方法只调用每个父类一次,是单调的(monotonic,意思是一个类可以成为子类,并且不影响它的基类的优先级顺序)。总之,这些特性有利于设计可靠和可扩展的能多重继承的类。想要了解更多?请看 https://www.python.org/download/releases/2.3/mro/ 。
9.6 私有(private)变量
Python可没有那种只能从对象内部访问的”私有(private)” 实例变量。但大多数Python代码都遵循一个惯例: 带有下划线前缀的名字(比如 _spam)应该被当成是API(函数、方法或数据成员)的非公有(non-public)部分。这应该被当成Python实现上的细节,如有更改,恕不另行通知。
类私有成员确实是用得到的(即避免和子类定义的名字有冲突),对这样的机制Python支持有限,叫 name mangling(名字重整) 。任何这种形式的标识符 __spam (至少两个下划线开头,至多一个下划线结尾)都会在文本上被替换成 _classname__spam ,classname 是当前类的名字,带一个前缀下划线。名字重整不考虑标识符的语法位置,只要这样的标识符在类内定义了,就会有名字重整。
名字重整让子类可以覆盖某些方法,而不会影响类内的方法调用,比如:
名字重整规则主要是为了防错,我们仍然可以访问、修改私有变量,可能某些特殊场合比如调试程序会用得到。
传给 exec() 或 eval() 的代码不会认为调用类的类名是当前类,有点像 global 语句的作用,作用限于字节编译的代码。同样的限制适用于 getattr(),setattr(),delattr(),还有直接引用 __dict__ 的时候。
9.7 杂七杂八
有时候可能需要用到类似Pascal的“记录(record)”或C的“结构体(struct)”这种数据类型,把几个有名字的数据项绑定到一起。空的类定义就可以很漂亮地完成工作:
可以传给想实现某种抽象数据类型的Python代码一个类,模拟那种数据类型的方法。比如有个函数可以从某个文件对象读入一些数据并把数据格式化输出,可以定义一个有方法 read() 和 readline() 的类,从字符串缓冲区读数据,把数据当参数传。
实例方法对象也有属性: m.__self__ 是指有方法 m() 的实例对象,m.__func__ 是指该方法的函数对象。
9.8 异常也是类
用户定义的异常也是用类来标识的,这样就能创建可扩展的异常层次结构了。
raise 语句有两种新的有效语法:
第一种,Class 必须是 type 的实例或者是其派生类。其实是下面形式的缩写:
在 except 语句中的类和同类或基类的异常是兼容的(但反过来就不成立了,异常类不和它的子类兼容)。下面的例子会依次打印B,C,D:
如果except语句顺序反过来(也就是 except B 在前面),那么会打印B,B,B,也就是匹配第一个符合的except语句。
当未处理的异常要打印错误信息时,先打印异常的类名,然后跟着一个冒号和一个空格,最后用自带的 str() 方法把异常实例转化成字符串再打印出来。
9.9 迭代器(iterator)
你可能注意到了多数容器对象都可以用 for 语句来进行循环:
这种访问方式很清晰、很简洁、很方便,迭代器的使用在Python中随处可见并把Python统一起来。其实 for 语句对容器对象调用了 iter() 函数。该函数返回一个迭代器对象,这个对象定义了 __next__() 方法,每次从容器中访问一个元素。没有元素的时候 __next__() 就引发一个 StopIteration 异常,告诉 for 循环可以终止了。可以用自带的 next() 函数来调用 __next__() 方法,来看下面的例子:
看到了迭代器协议背后的机制,那就可以很容易地给你自己的类加上迭代器行为啦。定义一个 __iter__() 方法,该方法返回一个有 __next__() 方法的对象。如果类已经定义了 __next__() 方法,那 __iter__() 返回 self 就行了:
9.10 生成器(generator)
生成器(generator) 是创建迭代器的简单而强大的工具。写起来像普通函数,但是返回数据的时候用 yield 语句。每次对生成器调用 next() 的时候,生成器从上一次停下的地方开始(它能记住上一次执行的所有语句和数据的值)。看看下面的例子,生成器很容易创建的:
生成器能做的事,上面一节基于类的迭代器也能做。生成器为什么这么简洁呢?因为 __iter__() 和 generator.__next__() 方法是自动创建的。
另一个关键特性是每次调用时的局部变量和执行状态都会被保存的,这样函数就很容易写了,比用实例变量(像是 self.index 和 self.data)的方式也清晰得多。
除了自动创建方法和保存程序状态以外,生成器结束时会自动引发 StopIteration 异常。这些特性结合起来,一口气写一个函数就能创建迭代器啦,不费劲~
9.11 生成器表达式
一些简单的生成器可以简洁地写成表达式,语法和列表推导类似,但用的是圆括号而不是方括号。这些表达式用于生成器马上会被其所在的函数用到的情况。生成器表达式更简洁,但是比完整的生成器定义的灵活性差。生成器表达式比等价的列表推导更节省内存。
上例子:
10 标准库到此一游,上篇
10.1 操作系统接口
os 模块提供了很多和操作系统交互的函数:
要用 import os ,不要用 from os import * ,这样可以避免 os.open() 覆盖Python自带的 open() ,这两个open函数可是相当不一样的。
自带的 dir() 和 help() 函数在交互模式时对于大的模块,比如 os 很有用:
对于日常管理文件和目录的任务,模块 shutil 提供了更高一级的接口,更易于使用:
10.3 命令行参数
常见的实用程序(utility)脚本往往需要处理命令行参数。这些参数以列表的形式存在 sys 模块的 argv 属性里。在命令行输入 python demo.py one two three 会得到下面的输出结果:
getopt 模块按照Unix函数 getopt() 的惯例来处理 sys.argv 。更强大和灵活的命令行处理工具是 argparse 模块。
10.4 错误输出的重定向(redirection)和程序终止
sys 模块有 stdin,stdout,stderr 这些属性。当 stdout 被重定向时,stderr 可以发出警告和错误信息:
终止一段脚本的最直接方式是用 sys.exit(),可以带参数,比如 sys.exit("some error message)。
10.5 字符串模式匹配
re 模块为提供正则表达式工具,用于字符串处理的高级玩法。对于复杂的匹配和操作,正则表达式提供简洁和优化的解法:
如果只是简单的字符串处理,还是用字符串自己的方法吧,更易读,更易调试:
10.7 访问互联网
很多模块可以访问互联网并处理互联网协议。两个最简单的是用于从URL中提取数据的 urllib.request 模块和发邮件的 smtplib 模块:
第二个例子需要本地主机(localhost)运行一个邮件服务器。
10.10 性能测试
有些Python用户对解决同一问题的不同方法的相对性能有浓厚兴趣,Python提供了一个测量工具。
比如使用元组的打包拆包特性,而不用传统方法,一样可以交换参数的值。timeit 模块可以快速展示这一性能优势:
10.11 质量控制
开发高质量软件的一种办法就是在开发过程中给每个函数写测试,并且频繁地运行这些测试。
doctest 模块可以扫描一个模块,验证嵌入在程序docstring中的测试。构建一个测试很简单,只需要把一个典型的函数调用和结果复制粘贴到docstring里面就行了。这样就改进了文档,提供给用户一个例子,并且让doctest模块能够确定代码和文档是一致的:
unittest 模块就不像 doctest 模块那样容易了,但是它可以把更复杂的测试集存到一个单独的文件中:
10.12 自带电池
Python的哲学是“自带电池”,因为它有大量先进和强大的包。比如:
- xmlrpc.client 模块和 xmlrpc.server 模块让远程过程调用(remote procedure call)变得小菜一碟。尽管模块名里面有XML,但用户不需知道直接的处理XML的知识。
- email 包是管理邮件的库,包括MIME(Multi-Purpose Internet Mail Extensions,多用途互联网邮件扩展)和其他基于RFC 2822的消息文档。和负责发信息的 smtplib模块与负责收信息的 poplib 模块不同,email包还有全套的工具集,用于创建和解码复杂的信息结构(包括附件),还有互联网编码和包头协议(header protocol)。
- xml.dom 包和 xml.sax 包提供了分析XML这一流行数据交换格式的强大工具。csv 模块支持直接读写常见的数据库格式。总之,这些模块和包极大地简化了Python程序和其他工具的数据交互。
- 很多包支持国际化(多语言),比如 gettext ,locale 和 codecs 。
11 标准库到此一游,下篇
下篇说一下支持专业程序需求的高级模块,这些模块在短小的脚本里很少用到。
11.1 输出格式化
reprlib 模块提供了一种自定义的 repr() 版本,用于缩略显示大的或深度嵌套的容器:
pprint 模块提供了更精密的解释器可读的打印自带对象和用户自定义对象的方法。结果多于一行时,这个“漂亮的打印机”会加上换行符和缩进,可以更清楚地反映数据结构:
textwrap 模块可以格式化文本段落,以适应给定的屏幕宽度:
(上面说的是textwrap.wrap()返回list of strings,而textwrap.fill()返回a big string。)
locale 模块访问一个专门存特定文化的数据格式的数据库。locale的格式化函数的分组属性提供了一种直接的方式,可以用组分隔符来格式化数字:
11.2 模板(templating)
string 模块有个多用途的 Template 类,用户可以用简单的语法来编辑它。这样用户就能不改变自己的程序而又能自定义自己的程序了。
模版的格式使用占位符(placeholder),即 $ 加上有效的Python标识符(字母数字下划线)。占位符如果用大括号,那么后面可以跟更多的字母数字,没有空格。$$ 得到一个 $(转义字符):
字典或关键字参数里没有提供占位符时,substitute() 方法引发一个 KeyError 异常。对于邮件合并风格的程序,用户给的数据可能是不完整的,用 safe_substitute() 方法可能更合适—如果数据缺失,它就原样保留占位符:
模版子类可以自定义分隔符(delimiter),比如照片浏览器的批处理重命名工具,可能要对当前日期、图像序列号或文件格式之类的数据使用百分号作为占位符,而不用默认的 $:
模版的另一个用处是把程序逻辑和多种输出格式的细节分开来。这样就可能替换XML文件、纯文本报告、HTML web报告的自定义模版了。
11.3 碰到了二进制数据记录格式
struct 模块提供了 pack() 和 unpack() 函数,用于处理变长度的二进制记录格式。下面的例子展示了如何用循环访问ZIP文件的头部信息,没有用到 zipfile 模块。代码"H" 和 "I" 分别表示两字节和四字节的无符号数。"<" 表示它们是标准大小并且使用小端序(little-endian, 低字节存在低内存地址处,比如Register:0A0B0C0D Memory: a存0D a+1存0C a+2存0B a+3存0A)的字节顺序:
11.4 多线程(multi-threading)
线程是一种解除没有顺序依赖关系的任务的耦合的技术,可用于提升程序响应速度,程序可以一边接受用户输入,一边在后台跑其他任务。相关的例子是一个线程负责I/O(输入输出),一个线程负责计算。
下面的例子展示了在主程序运行的时候,高级的 threading 模块如果在后台运行任务:
多线程程序的主要挑战就是如何协调那些共享数据和其他资源的线程。为此,threading模块提供了很多同步原语(synchronization primitive),有锁(lock),事件(event),条件变量(condition variable),信号量(semaphore)。
尽管这些工具很强大,轻微的设计失误仍然可以导致难以复现(reproduce)的问题。所以对任务协调比较好的办法是把对资源的所有访问都集中到单一线程里,然后通过queue 模块用其他线程的资源来供应这个线程。用 Queue对象 的程序对内部线程交互和协调来说更容易设计、代码更易读,并且更可靠。
11.5 日志(logging)
logging 模块提供了全功能和灵活的日志系统。最简单的形式是把日志信息写入文件或给 sys.stderr
会产生下面的输出:
默认情况下,提供信息(informational)和调试(debugging)的消息被忽略,默认的输出是标准错误输出(standard error)。其他的输出选项包括通过邮件、数据报(datagram)、套接字(socket)来发消息,或发给一个HTTP服务器。新的过滤器可以根据消息的优先级来选择不同的路由,优先级有 DEBUG, INFO, WARNING, ERROR, CRITICAL。
日志系统可以直接在Python里面配置,或不修改程序而载入一个用户可编辑的配置文件来自定义日志系统。
11.6 弱引用(weak reference)
Python的内存管理是自动的(对大多数对象都用引用计数,垃圾回收(garbage collection)来消除循环)。对某一内存的最后一次引用被消除后不久,该内存就被释放。
这个办法对大多数程序都没问题,偶尔需要跟踪某些对象,仅当这些对象还在被使用的时候。但只是跟踪这些对象的话,却会创建一个引用,使得这些对象永久存在。weakref 模块提供了工具,可以追踪对象,但不创建引用。不再需要此对象的时候,会自动从弱引用表中删除该对象,对弱引用对象会触发一个回调函数(callback)。典型用途是缓存那些创建起来很费资源的对象:
11.7 处理列表的一些大杀器
很多数据结构用Python自带的列表类型就可以搞定了,但有时候想要更好的性能,可能需要另一种实现列表的方式。
array 模块提供了 array() 对象,类似一个只能存同类型数据的列表,占的存储空间也更小。下面的例子展示了一个2字节无符号整数数组(类型代码是 "H"),存Python int对象的列表每一项通常需要16字节:
collections 模块提供了 deque() 对象,也是类似列表的一种类型,但从左边append和pop更快,从中间查找更慢。这种对象很适合来实现队列(queue)和广度优先的树的搜索:
除了列表的其他实现,Python库还提供了其他工具,比如 bisect 模块的函数可以处理已排序的列表:
模块 heapq 提供的函数可以基于普通列表来实现堆(heap)。最小值总在位置0,这种类型可用于需要重复访问最小元素但不想对整个列表排序的程序:
11.8 十进制(decimal)浮点运算
decimal模块 提供了 Decimal 类,用于十进制的浮点运算。比起Python自带的 float 这种二进制浮点实现,Decimal类对以下情形尤其有用:
- 财务程序和其他要求准确十进制表示的程序
- 高精度控制
- 需控制四舍五入以满足法律和法规要求
- 跟踪有效十进制位数
- 用户希望计算结果和手算结果一致的程序
比如说,计算70美分电话费的5%的税,十进制浮点和二进制浮点结果就不同,如果四舍五入到美分这个精确度,这种不同就很明显了:
Decimal 的结果会保留尾部的0,被乘数有2位有效数字的话,会自动推断结果有4位有效数字。Decimal重现了手算的数学结果,避免了二进制浮点无法准确表示十进制数而可能引发的问题。
准确的表示让 Decimal 类能够进行取模(modulo, 即求余数)运算的测试和判断相等的测试,这些测试用二进制浮点数都是搞不定的:
decimal模块 提供了尽可能精确的算术结果:
12 何去何从?
读了这份教程以后,估计你对Python也很感兴趣了吧,尽量用Python去解决现实生活中的问题吧。下一步学些什么呢?
本教程是Python文档的一部分,其他的文档有:
-
浏览一下这份手册,它简洁而全面地介绍了标准库中的类型、函数和模块。标准Python发行版包括 很多 附加代码。有读取Unix信箱的模块、通过HTTP获取文档的模块、产生随机数的模块、分析命令行参数的模块、写CGI(Common Gateway Interface/通用网关接口)程序的模块、压缩数据的模块和许多执行其他任务的模块。略读一下库参考手册可以对Python能做什么有个大致印象。
-
安装Python模块 解释了怎样安装其他Python用户写的模块。
-
Python语言参考手册 详细说明了Python的语法和语义,读起来可不轻松,但这是一份很全面的语言指导。
更多Python资源:
- https://www.python.org: 官网,有Python相关的代码、文档页面,本站在世界各地有镜像,比如欧洲、日本、澳大利亚。根据你所处地理位置的不同,镜像网站可能比主网站要快。
- https://docs.python.org: Python文档速查。
- https://pypi.python.org/pypi: Python包索引(Python Package Index),之前也叫奶酪店,是可下载的用户创建的Python模块的索引。一旦你开始发布代码,就可以在这里注册,其他人就能找到你的代码啦。
- http://code.activestate.com/recipes/langs/python/: The Python Cookbook是一个相当大的代码范例、大模块和有用脚本的集合。特别引人注目的贡献被收录到一本也叫Python Cookbook的书里了(O’Reilly & Associates, ISBN 0-596-00797-3。)
- http://scipy.org: 科学Python项目,包括用于数组快速计算和操作的模块,还有很多包,比如线性代数、傅立叶变换、非线性求解程序、随机数分布、统计分析等等。
如果需要报告有关Python的问题,请发到新闻组 comp.lang.python 或发到邮件列表 python-list@python.org。新闻组和邮件列表是互联的,发到其中一个上的消息会被自动转发到另一个上。每天大约有120个帖子,高峰期能达到几百个,这些帖子提问题并回答问题、建议加入新的特性、宣布新的模块。发帖之前,先看一下 常见问题/Frequently Asked Questions (又叫FAQ)。邮件列表存档在 https://mail.python.org/pipermail/ 。FAQ里回答了很多一而再、再而三提出的问题,可能就已经有对你的问题的解答啦。
13 交互式输入编辑和历史替换
某些版本的Python解释器支持编辑当前输入行和历史替换,跟Korn shell和GNU Bash shell中的工具类似。这是由 GNU Readline 库来实现的,这个库支持各种类型的编辑风格,请自行查阅它的文档吧。
13.1 Tab补全和历史编辑
补全变量名和模块名是解释器启动时 自动打开 的,Tab 键调用补全功能,对Python语句名、当前局部变量名和可用的模块名都能补全。遇到带点的表达式比如 string.a ,则对最后一个点之前的表达式求值,再对结果对象的属性给出补全的建议。注意如果带 __getattr()__ 方法的物体是表达式一部分的话, 这可能要执行应用程序定义的代码。默认的配置把历史保存到了用户目录(比如Mac和Linux下的 cd ~)的 .python_history 文件中,下一次打开交互式解释器会话时可以访问这些历史。
13.2 除了交互式解释器,我还能用啥?
这一版本的解释器比起之前的版本来是巨大的进步,但如果续行也给出合适的缩进(语法分析程序知道下一步是否需要缩进记号)就好了。补全机制可以使用解释器的符号表。检查或建议括号匹配、引号匹配等等也是很有用的。
一个增强版的Python解释器已经出来有一段时间了,即 IPython ,有tab补全、对象探测和高级历史管理等功能。它也可以被完全地自定义,嵌入其他程序中。另一个类似的增强版交互环境是 bpython 。
14 浮点运算的问题和局限
浮点数(floating-point number)在计算机硬件中用二进制分数(fraction)表示,比如下面的这个十进制数:
它的值是 1/10 + 2/100 + 5/1000,同样地,下面这个二进制分数:
它的值是0/2 + 0/4 + 1/8。这两个分数的值完全相同,唯一不同的是第一个是十进制的分数表示,第二个是二进制的分数表示。
悲剧的是大多数十进制分数不能准确地表示成二进制分数,后果是通常你输入的浮点数都是在机器中用近似的二进制浮点数存储的。
十进制的时候这个问题很好理解,考虑1/3这个分数,可以用下面这个十进制分数来对它进行近似:
再精确点
再精确点
子子孙孙无穷匮也。不论写多少位,结果总不是准确的1/3,但是越来越近似1/3。
同样的道理,不管写多少个二进制小树位,十进制的0.1总是无法被准确地表示成二进制分数。二进制的时候,1/10就是无限循环的:
可以在任何一位停下,得到一个近似值。在今天大多数机器上,用二进制分数近似表示浮点数,分子使用从最高位(most significant bit)开始的53位,分母使用二的幂。以1/10为例,二进制分数是 ``3602879701896397 / 2 ** 55`` ,接近但不等于1/10。
许多用户并没有意识到这种近似,因为显示看起来是正常的。Python仅打印真实十进制值的十进制近似,真实十进制值在机器中是以二进制近似的形式存储的。在大多数机器上,Python如果要打印0.1的存储的二进制近似,会这样显示:
后面的那些位多数用户用不到,所以Python就管理了显示位数,显示了一个四舍五入的值:
需要记住的是,尽管打印出来的结果看起来像是准确值,但其实机器存储的值只不过是最接近的可表示的二进制分数。
有意思的是,很多不同的十进制数共用相同的最接近的二进制分数近似。比如 0.1 、 0.10000000000000001 、 0.1000000000000000055511151231257827021181583404541015625 这三个数都用3602879701896397 / 2 ** 55 来表示。因为所有这些十进制值都共用相同的近似,每个都能显示,但仍保持不变的 eval(repr(x)) == x 。
由于历史原因,Python自带的 repr() 函数会选择带17位有效数字的那个, 0.10000000000000001 。从Python 3.1开始,大多数系统上的Python可以从上述三者中选择最短的一个,简单地显示 0.1。
二进制浮点就是这样的,它不是Python的bug,也不是你代码的bug。所有支持你硬件浮点算数的语言都是这样的,尽管有些语言在(所有)输出模式中都默认不会 显示 这种区别。
想要更漂亮的输出,可以用字符串格式化,限定有效数字的位数:
请注意这真的是幻觉(illusion): 你只是在对真实机器值的 显示结果(display) 进行了四舍五入。
一个幻觉可能引发另一个,比如因为0.1不是准确的1/10,把三个0.1加起来也不是准确的0.3:
因为0.1不是准确的1/10而只是近似,0.3也不是准确的3/10,提前用 round() 函数四舍五入也没用:
尽管这个数无法跟准确值一样,但 round() 函数可用于后期的四舍五入,这样不准确的结果也可以和另一个数进行比较了:
关于二进制浮点运算这种令人惊讶的结果还有很多,下面“表示误差”一节解释了“0.1”问题的细节。请看 浮点的危险 以了解更全面的令人惊讶的东东。
如这篇文章最后所说的,“事情没有这么简单”,但也别过度担心浮点的问题!Python的浮点操作误差来自浮点硬件,在多数机器上误差的数量级不会超过每2**53次操作产生一次。对大多数任务来说这远远足够了,但是要想着这不是十进制运算,每次浮点操作都可能造成新的四舍五入误差。
误差情况确实存在,但对大多数随意使用浮点运算来说,如果按自己需要对最终结果的显示结果进行四舍五入的话,你会看到期望的结果的。函数 str() 通常就足够了,想要更精细的控制请看 str.format() 方法的格式说明符(specifier),在 格式字符串语法 里。
要求准确表示十进制小数的时候,用 decimal模块 吧,它实现了适合财务程序和高精度程序的十进制运算。
fractions 模块提供了另一种准确的运算形式,它实现了基于有理数的运算(像1/3这样的数就可以被准确地表示了)。
如果经常要用到浮点运算,应该看一下SciPy project的Numerical Python包和其他用于数学和统计操作的包,请看 <http://scipy.org>。
如果你 真的真的 想知道一个浮点数(float)的准确值,Python也提供了工具。float.as_integer_ratio() 方法把一个float表示成分数:
这个比率是准确的,可用于无损地重新创建原来的值:
float.hex() 方法用十六进制(hexadecimal)表示浮点数,也给的是电脑里存的准确值:
这个精确的十六进制表示也可以准确地重新创建出浮点值:
因为这样的表示是准确的,所以它可以用于在不同版本的Python之间(平台无关)移植数据,也能和其他支持相同格式的语言(比如Java和C99)交换数据。
另一个有用的工具是 math.fsum() 函数,可以减轻求和过程中的精度损失。数值加到一个运行中的总和的时候,它会跟踪”丢失的位数”。这样就改善了整体的精确度,所以误差就不会积累到影响最终结果的程度:
14.1 表示误差
这一节解释了“0.1”例子的细节,并展示了如何准确地分析这种情形,假设你已经知道了二进制浮点怎么表示了。
表示误差(representation error) 是指多数十进制分数没法准确表示成二进制分数。这就是为啥Python(Perl, C, C++, Java, Fortran和许多语言)无法显示预期的十进制数的主要原因。
怎么回事呢?1/10无法准确地表示成一个二进制分数。几乎今天所有的机器(2000年11月)使用IEEE-754浮点运算,而且几乎所有的平台都把Python的float映射到IEEE-754的“双精度型(double precision)”。754 double包含53个比特位的精度,所以输入的时候,电脑会力争把0.1转成最接近的、能用 J/2**N 形式表示的小数,这里 J 是包含53个比特位的整数。重写过程是这样的:
转换一下,即
J 有53个比特位(>= 2**52 但 < 2**53),N 的最佳值是56:
就是说56是唯一的能让 J 保持53个比特位的 N 值。最好的 J 的可能的值就是对商进行四舍五入了:
因为余数6比10的一半要大,最好的近似就是入上去(round up):
因此754双精度对1/10最好的可能的近似就是:
约分,分子分母同除以2,简化得到:
因为我们刚才“五入”了,这个值其实比1/10略大。如果我们刚才是“四舍”的话,这个值就会比1/10略小。但就是没法 准确 等于1/10!
所以电脑绝不会“明白”1/10: 它见到的是上面给出的分数形式,即它所能得到的最好的754双精度:
如果我们把这个分数乘以10**55,就可以看到这个值是有55位十进制数字的:
这就意味着电脑里存的准确的值是0.1000000000000000055511151231257827021181583404541015625。许多语言(包括旧版本的Python)并不会显示全部的十进制值,它们会把这个值四舍五入成17位有效数字:
fractions 模块和 decimal模块 会让上述计算容易一些:
15 附录
15.1 交互模式
15.1.1 错误处理
发生错误(error)时,解释器会打印错误信息和堆栈跟踪。在交互模式下,会回到主提示符。输入来自文件时,打印堆栈追踪后以非0状态退出(这种上下文时,被 try 语句中的 except 从句处理的异常就不算是错误了)。某些错误是无条件地致命的,引起非0状态的退出,这适用于内部不一致(inconsistency)和某些内存耗尽的情形。所有的错误信息写到标准错误流(standard error stream)。执行命令的正常输入写到标准输出(standard output)。
向主提示符或二级提示符输出中断字符(一般是Control-C 或 DEL)会取消输入,并返回主提示符。[9] 命令执行的时候输入中断字符会引发 KeyboardInterrupt 异常,这个异常可以被 try 语句处理。
15.1.2 可执行的Python脚本
在类BSD Unix系统中,Python脚本可以被设置成可执行的(executable),就像shell脚本一样。把这一行:
(假设解释器在用户的 PATH 里)放到脚本的开头,并且给予该文件可执行权限。#! 必须是文件的头两个字符。在某些平台上第一行必须用Unix风格的换行符('n'),而不能是Windows风格的换行符('')。井号字符(叫hash,也叫pound) '#' 用于在Python中开始一行注释。
用 chmod 命令给予该脚本以可执行模式或权限。
在Windows系统中,没有“可执行模式”的概念。Python安装器自动关联 .py 文件和 python.exe,所以双击一个Python文件就可以把它当脚本运行了。扩展名也可以是 .pyw,这样的话通常出现的控制台窗口(console window)就不会出现了。
15.1.3 交互模式的启动文件
交互使用Python时,常常需要在解释器启动时执行某些标准命令,可以设置一个环境变量 PYTHONSTARTUP ,给出一个文件名,该文件包含你需要的启动命令。这类似于Unix shell的 .profile 特性。
该文件仅在交互模式会话的时候被读入,不用于Python从脚本中读命令的情形,也不用于给出 /dev/tty 作为命令的显式来源的情形(否则的话它就像交互模式会话)。该文件和交互式命令在同一个命名空间下执行,所以该文件定义或导入的对象可以无条件地用于交互会话。还可以在该文件中更改提示符 sys.ps1 和 sys.ps2 。
想要从当前目录读取额外的启动文件,可以在全局启动文件中编程,比如 if os.path.isfile('.pythonrc.py'): exec(open('.pythonrc.py').read()) (如果.pythonrc.py文件存在就打开读取,并执行其内容)。如果想在脚本中使用启动文件,必须在脚本中显式地这样做:
15.1.4 自定义模块
Python提供了两种自定义模块的办法: sitecustomize 和 usercustomize 模块。想了解它们,先要找到你的用户site-package目录,打开Python并运行:
然后在这个目录里创建一个名为 usercustomize.py 的文件,文件内容随自己的想法写。它会影响Python的每一次调用,除非使用了 -s 选项来禁止自动导入。
sitecustomize 也是一样的,不过它一般由计算机管理员在全局site-packages目录创建,而且它在 usercustomize 之前导入。想要了解更多?请看 site 模块的介绍。
16 写两句吧
终于翻完啦...Python版本进化得很快,过不久说不定这份文档就要落后官网文档许多了,还是尽量跟随官网文档、Google、StackOverflow、GitHub、知乎等各种资源吧。
如果发现错误或想提意见、建议,请毫不犹豫地 留言。
最后附上Python之禅(The Zen of Python)这首小诗,可以用 import this 打开它:
补充一下
[1] | Unix上Python 3.x解释器可执行文件名不是 python,它和同时存在的Python 2.x可执行文件不冲突。 |
[2] | ** 优先级(precedence)高于 -,所以 -3**2 会被解释为 -(3**2) ,结果为-9。(-3)**2 结果是9。 |
[3] | 不同于其他语言,Python里像 n 这样的特殊字符在单引号('...')和双引号("...")里面含义一样。唯一的区别是单引号里不需要对 " 进行转义(escape),可以直接用",但是必须对单引号进行转义 '。对于双引号依此类推,总之能消除歧义即可。 |
[4] | 其实更好的说法是 传对象的引用(call by object reference) ,如果传了一个可变的(mutable)对象,调用者(caller)可以看到被调用者(callee)对此对象的更改(比如往列表里插入一项)。 |
[5] | 其他语言可能返回可变的(mutable)物体, 这就允许了方法链接(chaining),比如 d->insert("a")->remove("b")->sort(); 。 |
[6] | d.keys() 返回一个 字典视图(dictionary view) 对象。它支持遍历(iteration)和测试一个键在不在其中的操作,它的内容依赖于字典,因为它只是一个视图(view)。 |
[7] | 其实函数定义也是被“执行”的“语句”,执行一个模块级别的函数,函数名会被记入模块的全局符号表(global symbol table)。 |
[8] | 有一个例外,模块对象有一个秘密的只读属性叫 __dict__ ,它返回一个字典,实现模块的命名空间。__dict__ 这个名字是个属性,但不是全局名字。很明显这样用违反了命名空间实现的抽象化,它的使用应该受限制,仅用于像程序崩溃后用的调试器(post-mortem debugger)这种东西。 |
[9] | GNU Readline包的一个问题可能破坏这种行为。 |

- 上一篇:没有了
- 下一篇:没有了