第3章 Python基础
本章主题
♦ 语句和语法
♦ 变量赋值
♦ 基本风格指南
♦ 内存管理
♦ 第一个Python程序
我们下一个目标是了解基本的Python语法,介绍一些基本的编程风格,之后简要介绍一下标识符、变量和关键字。我们也会讨论变量占用的内存是如何分配和回收的。最后,我们会给出一个较大的Python样例程序,让你实际体验一下这些特性。不必担心,在你畅游Python的过程中有很多救生员在保护着你。
3.1 语句和语法
Python语句中有一些基本规则和特殊字符:
井号(#)表示之后的字符为Python注释;
换行(\n)是标准的行分隔符(通常一个语句一行);
反斜线(\)继续上一行;
分号(;)将两个语句连接在一行中;
冒号(:)将代码块的头和体分开;
语句(代码块)用缩进块的方式体现;
不同的缩进深度分隔不同的代码块;
Python文件以模块的形式组织。
3.1.1 注释(#)
首要说明的事情是:尽管Python是可读性最好的语言之一,这并不意味着程序员在代码中就可以不写注释。和很多Unix脚本类似,Python注释语句从#字符开始,注释可以在一行的任何地方开始,解释器会忽略掉该行#之后的所有内容。要正确地使用注释。
3.1.2 继续(\)
Python语句,一般使用换行分隔,也就是说一行一个语句。一行过长的语句可以使用反斜杠(\)分解成几行,如下例。
有两种例外情况一个语句不使用反斜线也可以跨行。在使用闭合操作符时,单一语句可以跨多行,例如:在含有小括号、中括号、花括号时可以多行书写。另外就是三引号包括下的字符串也可以跨行书写,如下例。
显示一个三引号字符串
给一些变量赋值
如果要在使用反斜线换行和使用括号元素换行两者之间作一个选择,我们推荐使用括号,这样可读性会更好。
3.1.3 多个语句构成代码组(:)
缩进相同的一组语句构成一个代码块,我们称之为代码组。像if、while、def和class这样的复合语句,首行以关键字开始,以冒号(:)结束,该行之后的一行或多行代码构成代码组。我们将首行及后面的代码组称为一个子句(clause)。
3.1.4 代码组由不同的缩进分隔
我们在2.10一节中曾提到,Python使用缩进来分隔代码组。代码的层次关系是通过同样深度的空格或制表符缩进体现的。同一代码组的代码行必须严格左对齐(左边有同样多的空格或同样多的制表符),如果不严格遵守这个规则,同一组的代码就可能被当成另一个组,甚至会导致语法错误。
核心风格:缩进4个空格宽度,避免使用制表符
对一个初次使用空白字符作为代码块分界的人来说,遇到的第一个问题是,缩进多大宽度才合适?2个太少,6~8个又太多,因此我们推荐使用4个空格宽度。需要说明一点,不同的文本编辑器中制表符代表的空白宽度不一,如果你的代码要跨平台应用,或者会被不同的编辑器读写,建议你不要使用制表符。使用空格或制表符这两种风格都得到了 Python创始人的支持,并被收录到Python代码风格指南文档。在3.4节中你会看到同样的建议。
随着缩进深度的增加,代码块的层次也在加深,没有缩进的代码块是最高层次的,被称做脚本的“主体”(main)部分。
使用缩进对齐这种方式组织代码,不但代码风格优雅,而且也大大提高了代码的可读性。而且它有效避免了 “悬挂else”(dangling-else)问题,和未写大括号的单一子句问题。(如果C语言中if语句没写大括号,而后面却跟着两个缩进的语句,这会造成不论条件表达式是否成立,第二个语句总会执行。这种问题很难调试,不知道困惑了多少程序员。)
最后一点,由于Python只使用缩进方式表达代码块逻辑,因此“神圣的大括号战争”永远不会发生在Python身上。 C、C++和Java语言中,开始大括号可以在第1行的尾部,也可以在第2行的头部,也可以在第2行空几格后开始,这就造成不同的人选择不同的风格,于是你就会看到大括号战争的场景了。
3.1.5 同一行书写多个语句(;)
分号(;)允许你将多个语句写在同一行上,语句之间用分号隔开,而这些语句也不能在这行开始一个新的代码块。这里有一个例子:
必须指出一点,同一行上书写多个语句会大大降低代码的可读性,Python虽然允许但不提倡你这么做。
3.1.6 模块
每一个Python脚本文件都可以被当成是一个模块。模块以磁盘文件的形式存在。当一个模块变得过大,并且驱动了太多功能的话,就应该考虑拆一些代码出来另外建一个模块。模块里的代码可以是一段直接执行的脚本,也可以是一堆类似库函数的代码,从而可以被别的模块导入(import)调用。
3.2 变量赋值
本节主题是变量赋值。我们将在3.3小节中讨论什么样的标识符才是合法的变量名。
3.2.1 赋值操作符
Python语言中,等号(=)是主要的赋值操作符(其他的是增量赋值操作符)。
注意,赋值并不是直接将一个值赋给一个变量,尽管你可能根据其他语言编程经验认为应该如此。在Python语言中,对象是通过引用传递的。在赋值时,不管这个对象是新创建的,还是一个已经存在的,都是将该对象的引用(并不是值)赋值给变量。如果此刻你还不是100%理解清楚,也不用着急。在本章的后面部分,我们还会再讨论这个话题,现在你只需要有这么一个概念即可。
同样的,如果你比较熟悉C,你会知道赋值语句其实是被当成一个表达式(可以返回值)。不过这条并不适合于Python,Python的赋值语句不会返回值。类似下面的语句在Python中是非法的。
链式赋值没问题,比如(本章稍后部分会给出更多的例子):
3.2.2 增量赋值
从Python 2.0开始,等号可以和一个算术操作符组合在一起,将计算结果重新赋值给左边的变量。这被称为增量赋值,类似下面这样的语句:
现在可以被写成:
增量赋值通过使用赋值操作符,将数学运算隐藏在赋值过程当中。如果你用过C、C++或者Java,会觉得下面的操作符很熟悉。
增量赋值相对普通赋值不仅仅是写法上的改变,最有意义的变化是第一个对象(我们例子中的A)仅被处理一次。可变对象会被就地修改(无修拷贝引用),不可变对象则和A=A+B的结果一样(分配一个新对象),我们前面提到过,有一个例外就是A仅被求值一次。
Python不支持类似x++或--x这样的前置/后置自增/自减运算。
3.2.3 多重赋值
在上面的例子中,一个值为1的整型对象被创建,该对象的同一个引用被赋值给x、y和z。也就是将一个对象赋给了多个变量。当然,在Python当中,将多个对象赋给多个变量也是可以的。
3.2.4 “多元”赋值
另一种将多个变量同时赋值的方法我们称为多元赋值(multuple)。这不是Python官方术语,而是我们将“mul-tuple”连在一起自创的。因为采用这种方式赋值时,等号两边的对象都是元组(我们在2.8节讲过元组是一种Python基本数据类型)。
在上面的例子里,两个整型对象(值分别为1和2)及一个字符串对象,被分别赋值给x, y和z。通常元组需要用圆括号(小括号)括起来,尽管它们是可选的。我们建议总是加上圆括号以使你的代码有更高的可读性。
在其他类似C的语言中,如果你要交换两个值,你会想到使用一个临时变量如tmp来临时保存其中一个值。
在上面的C代码片段中,变量x和变量y的值被互相交换。临时变量tmp用于在将y赋值给x前先保存x的值。将y的值赋给x之后,才可以将保存在tmp变量中的x的值赋给y。Python的多元赋值方式可以实现无需中间变量交换两个变量的值。
显然,Python在赋值之前已经事先对x和y的新值做了计算。
3.3 标识符
标识符是计算机语言中允许作为名字的有效字符串集合。其中,有一部分是关键字,构成语言的标识符。这样的标识符是保留字,不能用于其他用途,否则会引起语法错误(SyntaxError异常)。
Python还有称为“内建”(built-in)的标识符集合,虽然它们不是保留字,但是不推荐使用这些特别的名字(见3.3.3)。
3.3.1 合法的Python标识符
Python标识符字符串规则和其他大部分用C编写的高级语言相似:
第一个字符必须是字母或下划线(_);
剩下的字符可以是字母和数字或下划线;
大小写敏感。
标识符不能以数字开头;除了下划线,其他的符号都不允许使用。处理下划线最简单的方法是把它们当成字母字符。大小写敏感意味着标识符foo不同于Foo,而这两者也不同于FOO。
3.3.2 关键字
表3.1列出了Python关键字。一般来说,任何语言的关键字都是相对稳定的,但事情总会改变(Python是一种发展和进化中的语言),Keyword模块中同时包含了一个关键字列表和一个iskeyword()函数。
3.3.3 内建
除了关键字之外,Python还有可以在任何一级代码使用的“内建”(built-in)的名字集合,这些名字可以由解释器设置或使用。虽然built-in不是关键字,但是应该把它当作“系统保留字”,不做他用。然而,有些情况要求覆盖(也就是重定义、替换)它们。Python不支持重载标识符,所以任何时刻都只有一个名字绑定。
我们还可以告诉高级读者built-in是__builtins__模块的成员,在你的程序开始或在交互解释器中给出 >>>提示之前,由解释器自动导入的。把它们看成适用在任何一级Python代码的全局变量。
3.3.4 专用下划线标识符
Python用下划线作为变量前缀和后缀指定特殊变量。稍后我们会发现,对于程序来说,其中的有些变量是非常有用的,而其他的则是未知或无用的。这里对Python中下划线的特殊用法做了总结。
_xxx 不用‘from module import*’导入
_xxx_ 系统定义名字
_xxx 类中的私有变量名
核心风格:避免用下划线作为变量名的开始
因为下划线对解释器有特殊的意义,而且是内建标识符所使用的符号,我们建议程序员避免用下划线作为变量名的开始。一般来讲,变量名_xxx被看作是“私有的”,在模块或类外不可以使用。当变量是私有的时候,用_xxx来表示变量是很好的习惯。因为变量名_xxx对Python来说有特殊含义,对于普通的变量应当避免这种命名风格。
3.4 基本风格指南
注释
注释对于自己和后来人来说都是非常重要的,特别是对那些很久没有被动过的代码而言,注释更显得有用了。既不能缺少注释,也不能过度使用注释。尽可能使注释简洁明了,并放在最合适的地方。这样注释便为每个人节省了时间和精力。记住,要确保注释的准确性。
文档
Python还提供了一个机制,可以通过__doc__特别变量,动态获得文档字串。在模块、类声明、或函数声明中第一个没有赋值的字符串可以用属性obj.__doc__来进行访问,其中obj是一个模块、类、或函数的名字。这在运行时也可以进行!
缩进
因为缩进对齐有非常重要的作用,你得考虑用什么样的缩进风格才让代码容易阅读。在选择要空的格数的时候,常识也起着非常大的作用。
1个或2个可能不够,很难确定代码语句属于哪个块。
8〜10个可能太多,如果代码内嵌的层次太多,就会使得代码很难阅读。4个空格非常的流行,更不用说Python的创造者也支持这种风格。5和6个也不坏,但是文本编辑器通常不支持这样的设置,所以也不经常使用。3个和7个是边界情况。
当使用制表符Tab的时候,请记住不同的文本编辑器对它的设置是不一样。如果你的代码会存在并运行在不同的平台上,或者会用不同的文本编辑器打开,建议你不要使用Tab。
选择标识符名称
好的判断也适用于选择标识符名称,请为变量选择短而意义丰富的标识符。虽然变量名的长度对于今天的编程语言不再是一个问题,但是使用简短的名字依然是个好习惯,这个原则同样使用于模块 (Python文件)的命名。
Python风格指南
Guido van Rossum在多年前写下Python代码风格指南。目前它已经被至少3个PEP代替:7(C代码风格指南)、8 (Python代码风格指南)和257(文档字符串规范)。这些PEP被归档、维护并定期更新。
渐渐地,你会听到“Pythonic”这个术语,它指的是以Python的方式去编写代码、组织逻辑和对象行为。更久以后,你才会真正理解它的含义。PEP 20写的是Python之禅,你可以从那里开始探索 “Pythonic”真正含义的旅程。如果你不能上网,但想看到它,那就从你的Python解释器输入import this然后回车。下面是一些网上资源。
www.Python.org/doc/essays/styleguide.html
www.Python.org/dev/peps/pep-0007/
www.Python.org/dev/peps/pep-0008/
www.Python.org/dev/peps/pep-0020/
www.Python.org/dev/peps/pep-0257
3.4.1 模块结构和布局
用模块来合理组织你的Python代码是简单又自然的方法。你应该建立一种统一且容易阅读的结构,并将它应用到每一个文件中去。下面就是一种非常合理的布局。
(1)起始行(Unix)
(2)模块文档
(3)模块导入
(4)变量定义
(5)类定义
(6)函数定义
(7)主程序
(1) 起始行
通常只有在类Unix环境下才使用起始行,有起始行就能够仅输入脚本名字来执行脚本,无需直接调用解释器。
(2) 模块文档
简要介绍模块的功能及重要全局变量的含义,模块外可通过module.__doc__访问这些内容。
图 3-1 典型Python文件结构
(3) 模块导入
导入当前模块的代码需要的所有模块;每个模块仅导入一次(当前模块被加载时);函数内部的模块导入代码不会被执行,除非该函数正在执行。
(4) 变量定义
这里定义的变量为全局变量,本模块中的所有函数都可直接使用。从好的编程风格角度说,除非必须,否则就要尽量使用局部变量代替全局变量,如果坚持这样做,你的代码就不但容易维护,而且还可以提高性能并节省内存。
(5) 类定义语句
所有的类都需要在这里定义。当模块被导入时class语句会被执行,类也就会被定义。类的文档变量是class.__doc__。
(6) 函数定义语句
此处定义的函数可以通过module.function()在外部被访问到,当模块被导入时def语句会被执行,函数也就都会定义好,函数的文档变量是function.__doc__。
(7) 主程序
无论这个模块是被别的模块导入还是作为脚本直接执行,都会执行这部分代码。通常这里不会有太多功能性代码,而是根据执行的模式调用不同的函数。
核心风格:主程序调用main()函数
主程序代码通常都和你前面看到的代码相似,检查__name__变量的值然后再执行相应的调用(参阅下一个核心笔记)。主程序中的代码通常包括变量赋值、类定义和函数定义,随后检查__name__来决定是否调用另一个函数(通常调用main()函数)来完成该模块的功能。主程序通常都是做这些事。(我们上面的例子中使用test()而不是main()是为了避免你在读到核心笔记前感到迷惑。)不管用什么名字,我们想强调的是:这儿是放置测试代码的好地方。我们在3.4.2小节中曾经说过,大部分的Python模块都是用于导入调用的,直接运行模块应该调用该模块的回归测试代码。
很多项目都是一个主程序,由它导入所有需要的模块。所以请记住,绝大部分的模块创建的目的是为了被别人调用而不是作为独立执行的脚本。我们也很可能创建一个Python库风格的模块,这种模块的创建目的就是为了被其他模块调用。总之,只有一个模块,也就是包含主程序的模块会被直接执行,或由用户通过命令行执行,或作为批处理执行,或由Unix cron任务定时执行,或通过Web服务器调用,或通过GUI执行。
时刻记住一个事实,那就是所有的模块都有能力来执行代码。最高级别的Python语句——也就是说,那些没有缩进的代码行——在模块被导入时就会执行,不管是不是真的需要执行。由于有这样一个“特性”,比较安全的写代码的方式就是除了那些真正需要执行的代码以外,几乎所有的功能代码都在函数当中。再说一遍,通常只有主程序模块中有大量的顶级可执行代码,所有其他被导入的模块只应该有很少的顶级执行代码,所有的功能代码都应该封装在函数或类当中。(参阅核心笔记了解更多信息)
核心笔记:__name__指示模块应如何被加载
由于主程序代码无论模块是被导入还是被直接执行都会运行,我们必须知道模块如何决定运行方向。一个应用程序可能需要导入另一个应用程序的一个模块,以便重用一些有用的代码 (否则就只能用拷贝粘贴那种非面向对象的笨拙手段)。这种情况下,你只想访问那些位于其他应用程序中的代码,而不是想运行那个应用程序。因此一个问题出现了,“Python是否有一种方法,能在运行时检测该模块是被导入还是被直接执行呢?”答案就是…(掌声雷动)…没错!__name__系统变量就是正确答案。
如果模块是被导入,__name__的值为模块名字;
如果模块是被直接执行,__name__的值为‘main’。
3.4.2 在主程序中书写测试代码
优秀的程序员和软件工程师,总是会为我们的应用程序提供一组测试代码或者简单教程。对那些仅仅为了让别的程序导入而创建的模块来说,Python有效地简化了这个任务。这些模块理论上永远不会被直接执行,那么,在这个模块被直接执行时进行系统测试岂不妙哉?设置起来难吗?一点儿也不难。
测试代码仅当该文件被直接执行时运行,也就是说,不是在被别的模块导入时。上文及核心笔记中提到如何判断一个模块是被直接运行还是被导入的。我们应该利用__name__变量这个有利条件。将测试代码放在一个叫做main()或test()(或者你随便取个名字)的函数中,如果该模块是被当成脚本运行,就调用这个函数。
这些测试代码应该随着测试条件及测试结果的变更及时修改,每次代码更新都应该运行这些测试代码,以确认修改没有引发新问题。只要坚持这样做,你的代码就会足够健壮,更不用提验证和测试新特性和更新了。
在主程序中放置测试代码是测试模块的简单快捷的手段。Python标准库中还提供了unittest模块,有时候它被称为PyUnit,是一个测试框架。如何使用unittest超出了本书的范围,不过当需要对一个大系统的组件进行正规系统的回归测试时,它就会派上用场。
3.5 内存管理
到现在为止,你已经看了不少Python代码的例子。我们本节的主题是变量和内存管理的细节,包括:
变量无须事先声明;
变量无须指定类型;
程序员不用关心内存管理;
变量名会被“回收”;
del语句能够直接释放资源。
3.5.1 变量定义
大多数编译型语言,变量在使用前必须先声明,其中的C语言更加苛刻:变量声明必须位于代码块最开始,且在任何其他语句之前。其他语言,像C++和Java,允许“随时随地”声明变量,比如,变量声明可以在代码块的中间,不过仍然必须在变量被使用前声明变量的名字和类型。在Python中,无需此类显式变量声明语句,变量在第一次被赋值时自动声明。和其他大多数语言一样,变量只有被创建和赋值后才能被使用。
变量一旦被赋值,你就可以通过变量名来访问它。
3.5.2 动态类型
还要注意一点,Python中不但变量名无需事先声明,而且也无需类型声明。在Python语言中,对象的类型和内存占用都是运行时确定的。尽管代码被编译成字节码,Python仍然是一种解释型语言。在创建——也就是赋值时,解释器会根据语法和右侧的操作数来决定新对象的类型。在对象创建后,一个该对象的应用会被赋值给左侧的变量。
3.5.3 内存分配
作为一个负责任的程序员,我们知道在为变量分配内存时,是在借用系统资源,在用完之后,应该释放借用的系统资源。Python解释器承担了内存管理的复杂任务,这大大简化了应用程序的编写。你只需要关心你要解决的问题,至于底层的事情放心交给Python解释器去做就行了。
3.5.4 引用计数
要保持追踪内存中的对象,Python使用了引用计数这一简单技术。也就是说Python内部记录着所有使用中的对象各有多少引用。你可以将它想像成扑克牌游戏“黑杰克”或“21点”。一个内部跟踪变量,称为一个引用计数器。每个对象各有多少个引用,简称引用计数。当对象被创建时,就创建了一个引用计数,当这个对象不再需要时,也就是说,这个对象的引用计数变为0时,它被垃圾回收。(严格来说这不是100%正确,不过现阶段你可以就这么理解)
1. 增加引用计数
当对象被创建并(将其引用)赋值给变量时,该对象的引用计数就被设置为1。
当同一个对象(的引用)又被赋值给其他变量时,或作为参数传递给函数、方法或类实例时,或者被赋值为一个窗口对象的成员时,该对象的一个新的引用,或者称作别名,就被创建(则该对象的引用计数自动加1)。
请看以下声明。
语句x=3.14创建了一个浮点型对象并将其引用赋值给x。x是第一个引用,因此,该对象的引用计数被设置为1。语句y=x创建了一个指向同一对象的别名y(参阅图3-2)。事实上并没有为Y创建一个新对象,而是该对象的引用计数增加了1次(变成了2)。这是对象引用计数增加的方式之一。还有一些其他的方式也能增加对象的引用计数,比如该对象作为参数被函数调用或这个对象被加入到某个容器对象当中时。
图3-2 有两个引用的同一对象
总之,对象的引用计数增加时:
对象被创建
或另外的别名被创建
或被作为参数传递给函数(新的本地引用)
或成为容器对象的一个元素
下面让我们来看一下引用计数是如何变少的。
2. 减少引用计数
当对象的引用被销毁时,引用计数会减小。最明显的例子就是当引用离开其作用范围时,这种情况最经常出现在函数运行结束时,所有局部变量都被自动销毁,对象的引用计数也就随之减少。
当变量被赋值给另外一个对象时,原对象的引用计数也会自动减1:
当字符串对象“xyz”被创建并赋值给foo时,它的引用计数是1。当增加了一个别名bar时,引用计数变成了2。不过当foo被重新赋值给整型对象123时,xyz对象的引用计数自动减1,又重新变成了1。
其他造成对象的引用计数减少的方式包括使用del语句删除一个变量(参阅稍后小节),或者当一个对象被移出一个窗口对象时(或该容器对象本身的引用计数变成了0时)。总结一下,一个对象的引用计数在以下情况下会减少。
一个本地引用离开了其作用范围。比如foobar()(参见刚才的例子)函数结束时。
对象的别名被显式销毁。
对象的一个别名被赋值给其他对象。
对象被从一个窗口对象中移除。
窗口对象本身被销毁。
参阅11.8节了解更多变量作用范围的信息。
3. del语句
Del语句会删除对象的一个引用,它的语法如下。
例如,在上例中执行del y会产生两个结果。
从现在的名称空间中删除y。
x的引用计数减1。
引申一步,执行del x会删除该对象的最后一个引用,也就是该对象的引用计数会减为0,这会导致该对象从此“无法访问”或“无法抵达”。从此刻起,该对象就成为垃圾回收机制的回收对象。注意任何追踪或调试程序会给一个对象增加一个额外的引用,这会推迟该对象被回收的时间。
3.5.5 垃圾收集
不再使用的内存会被一种称为垃圾收集的机制释放。像上面说的,虽然解释器跟踪对象的引用计数,但垃圾收集器负责释放内存。垃圾收集器是一块独立代码,它用来寻找引用计数为0的对象。它也负责检查那些虽然引用计数大于0但也应该被销毁的对象。特定情形会导致循环引用。
一个循环引用发生在当你有至少两个对象互相引用时,也就是说所有的引用都消失时,这些引用仍然存在,这说明只靠引用计数是不够的。Python的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。当一个对象的引用计数变为0,解释器会暂停,释放掉这个对象和仅有这个对象可访问 (可到达)的其他对象。作为引用计数的补充,垃圾收集器也会留心被分配的总量很大的(及未通过引用计数销毁的那些)对象。在这种情况下,解释器会暂停下来,试图清理所有未引用的循环。
3.6 第一个Python程序
我们已经熟悉了语法、代码风格、变量赋值及内存分配,现在来看一点略微复杂的代码。这个例子中还有你不熟悉(我们还未讲到的)的Python结构,不过我们相信因为Python非常的简单和优雅,你一定可以弄懂每一行代码的用途。
我们将要介绍两段处理文本文件的相关脚本。首先是makeTextFile.py,创建一个文本文件;它提示用户输入每一行文本,然后将结果写到文件中。另一个是readTextFile.py,读取并显示该文本文件的内容。研究一下这两段代码,看看他们是如何工作的。
例3.1 创建文件(makeTextFile.py)
这个脚本提醒用户输入一个(尚不存在的)文件名,然后由用户输入该文件的每一行。最后,将所有文本写入文本文件。
1 ~ 3行
UNIX启动行之后是模块的文档字符串。应该坚持写简洁并有用的文档字符串。这里我们写的有点短,不过对这段代码已经够用了。(建议读者看一下标准库中cgi模块的文档字符串,那是一个很好的示例)。
5 ~ 6行
之后我们导入os模块,在第6行我们为os.linesep属性取了一个新别名。这样做一方面可以缩短变量名,另一方面也能改善访问该变量的性能。
核心提示:使用局部变量替换模块变量
类似os.linesep这样的名字需要解释器做两次查询:(1)查找os以确认它是一个模块, (2)在这个模块中查找linesep变量。&为模块也是全局变量,我们多消耗了系统资源。如果你在一个函数中像这样频繁使用一个属性,我们建议你为该属性取一个本地变量别名。变量查找速度将会快很多——在查找全局变量之前,总是先查找本地变量。这也是一个让你的程序跑的更快的技巧:将经常用到的模块属性替换为一个本地引用。代码 “跑”得更快,而也不用老是敲那么长的变量名了。在我们的代码片段中,并没有定义函数,所以不能给你定义本地别名的示例。不过我们有一个全局别名,至少也减少了一次名字查询。
8 ~ 4行
显然这是一个无限循环,也就是说除非我们在while语句体中提供break语句,否则它会一直循环下去。
while语句根据后面的表达式决定是否进行下一次循环,而True则确保它一直循环下去。
10 ~ 14行
提示用户输入一个未使用的文件名。raw_input()内建函数接受一个“提示字符串”参数,作为对用户的提示信息。raw_input()返回用户输入的字符串,也就是为fname赋值。如果用户不小心输入了一个已经存在的文件的¿字,我们要提示这个用户重新输入另一个名字。os.path.exists()是os模块中一个有用的函数,帮助我们确认这一点。当有输入一个不存在的文件名时,os.path.exists()才会返回False,这时我们中断循环继续下面的代码。
16 ~ 26行
这部分代码提供用户指令,引导用户输入文件内容,一次一行。我们在第17行初始化了列表all,它用来保存每一行文本。第21行开始另一个无限循环,提示用户输入每一行文本,一行仅输入一个句点 (.)表示输入结束。23~26行的if-else语句判断是否满足结束条件以中止循环(24行),否则就再添加新的一行(26行)。
28 ~ 32行
现在所有内容都保存在内存当中,我们需要将它们保存到文件。第29行打开文件准备进行写操作,第30行将内存中的内容逐行写入文件。每个文件都需要一个行结束符(或文件结束字符)。第30行的结构称为列表解析,它进行以下工作:对我们文件的每一行,根据程序运行平台添加一个合适的行结束符。 ‘%s%s’为每一行添加行结束符,(x,ls)表示每一行及其行结束符,对Unix平台,是‘\n’,对DOS或win32平台,则是‘\r\n’。通过使用os.lineseq,我们不必关心程序运行在什么平台,也不必要根据不同的平台决定使用哪种行结束符。文件对象的writelines()方法接收包含行结束符的结果列表,并将它写入文件。
不错吧。现在来看一下如何查看刚刚创建的文件。出于这个目的,我们创建了第二个Python脚本, readTextFile.py。你会看到,它比makeTextFile.py短的多。创建一个文件的复杂度总是比读取它要大。你可能感兴趣的、有新意的一点在于异常处理的出现。
1 ~ 3行
和前面一样,是Unix启动行及模块文档字符串。
5 ~ 7行
不同于makeTextFil.py,我们在这个例子中不再关心用户是否输入合适的文件名。
例3.2 文件读取和显示(readTextFile.py)
换句话说,我们在其他地方进行验证工作(如果需要)。第7行打印一个空行,以便将提示信息和文件内容分隔开来。
9 ~ 18行
脚本的剩余部分展示了一种新的Python结构,try-except-else语句。try子句是一段我们希望监测错误的代码块。在第10〜11行代码,我们尝试打开用户输入的文件。except子句是我们处理错误的地方。在12〜13行,我们检查open()是否失败——通常是IOError类型的错误。
最后,14〜18行的else子句在try代码块运行无误时执行。我们在这儿将文件的每一行显示在屏幕上。注意由于我们没有移除代表每行结束的行结束符,我们不得不抵制print语句自动生成的行结束符——通过在print语句的最后加一个逗号可以达到这一目的。第18行关闭文件,从而结束这段脚本。
最后要讲的一点是关于使用os.path.exists()和异常处理:一般程序员倾向于使用前者,因为有一个现成的函数可以检查错误条件——并且很简单,这是个布尔函数,它会告你“是”还是“不是”(注意,这个函数内可能已经有异常处理代码)。那你为什么还要重新发明一个轮子来干同样一件事?异常处理最适用的场合,是在没有合适的函数处理异常状况的时候。这时程序员必须识别这些非正常的错误,并做出相应处理。对我们的例子来说,我们能够通过检查文件是否存在来避免异常发生,不过因为有可能因为其他原因造成文件打开失败,比如缺少权限,网络驱动器突然连接失败等等。从更安全的角度来说,就不应该使用类似os.path.exists()之类的函数,而是使用异常处理,尤其是在没有合适函数的情况下更应如此。
你会在第9章中找到更多文件系统函数的例子,在第10章则有更多关于异常处理的知识。
3.7 相关模块和开发工具
《Python风格指南》(Python Style Guide,PEP8)、《Python快速参考指南》(Python Quick Reference Guide)和《Python常见问答》(Python FAQ)都是开发者很重要的“工具”。另外,还有一些模块会帮助你成为一个优秀的Python程序员。
调试器:pdbo
记录器:logging。
性能测试器:profile、hotshot、cProfile。
logging模块是在Python2.3中新增的,它定义了一些函数和类帮助你的程序实现灵活的日志系统。共有五级日志级别:紧急、错误、警告、信息和调试。
历史上,不同的人们为了满足不同的需求重复实现了很多性能测试器,Python也有好几个性能测试模块。最早的Python profile模块是Python写成的,用来测试函数的执行时间及每次脚本执行的总时间,既没有特定函数的执行时间也没有被包含的子函数调用时间。在三个profile模块中,它是最老的也是最慢的,尽管如此,它仍然可以提供一些有价值的性能信息。hotshot模块是在Python2.2中新增的,它的目标是取代profile模块,它修复了profile模块的一些错误,因为它是用C语言写成,所以它有效地提高了性能。注意hotshot重点解决了性能测试过载的问题,但却需要更多的时间来生成结果。Python2.5版修复了hotshot模块的一个关于时间计量的严重bug。
cProfile模块是Python2.5新增的,它用来替换掉已经有历史的hotshot和profile模块。作者已确认的它的一个较明显的缺点是它需要花较长时间从日志文件中载入分析结果,不支持子函数状态细节及某些结果不准。它也是用C语言来实现的。
3.8 练习
3-1.标识符。为什么Python中不需要变量名和变量类型声明?
3-2.标识符。为什么Python中不需要声明函数类型?
3-3.标识符。为什么应当避免在变量名的开始和和结尾使用双下划线?
3-4.语句。在Python中一行可以书写多个语句吗?
3-5.语句。在Python中可以将一个语句分成多行书写吗?
3-6.变量赋值
(a)赋值语句x,y,z=1,2,3会在x、y、z中分别赋什么值?
(b)执行z,x,y=y,z,x后,x、y、z中分别含有什么值?
3-7.标识符。下面哪些是Python合法的标识符?如果不是,请说明理由。在合法的标识符中,哪些是关键字?
下面的问题涉及了makeTextFile.py和readTextFile.py脚本。
3-8.Python代码。将脚本拷贝到你的文件系统中,然后修改它。可以添加注释,修改提示符(‘>’太单调了)等,修改这些代码,使它看上去更舒服。
3-9.移植。如果你在不同类型的计算机系统中分别安装有Python,检查一下,os.linesep的值是否有不同。记下操作系统的类型及linesep的值。
3-10.异常。使用类似readTextFile.py中异常处理的方法取代readTextFile.py makeTextFile.py中对os.path.exists()的调用。反过来,用os.path.exists()取代readTextFile.py中的异常处理方法。
3-11.字符串格式化不再抑制readTextFile.py中print语句生成的NEWLINE字符,修改你的代码,在显示一行之前删除每行末尾的空白。这样,你就可以移除print语句末尾的逗号了。提示:使用字符串对象的strip()方法。
3-12.合并源文件。将两段程序合并成一个,给它起一个你喜欢的名字,比如readNwriteTextFiles.py。让用户自己选择是创建还是显示一个文本文件。
3-13.*添加新功能。将你上一个问题改造好的readNwriteTextFiles.py增加一个新功能:允许用户编辑一个已经存在的文本文件。你可以使用任何方式,无论是一次编辑一行,还是一次编辑所有文本。需要提醒一下的是,一次编辑全部文本有一定难度,你可能需要借助GUI工具包或一个基于屏幕文本编辑的模块比如curses模块。要允许用户保存他的修改(保存到文件)或取消他的修改(不改变原始文件),并且要确保原始文件的安全性(不论程序是否正常关闭)。