2.5 Python脚本编程
UNIX/Linux 系统管理技术手册(第四版)
随着项目变得越来越大、越来越复杂,面向对象的设计和实现所带来的好处,也就变得越来越清楚。Perl错过了大概5年时间,没有提供OO特性,虽然它后来又拼命去追赶,但Perl版的面向对象编程仍然显得有点儿牵强。
本节介绍Python 2。Python 3尚在开发之中,可能在本书没过时之前就能发布。但是和Perl 6不一样的是,它看上去更像是一种增量更新。
有着很强OO背景的工程师通常会喜欢Python和Ruby,这两种脚本编程语言都有一种明显的OO特质。目前,Python似乎正处于采纳曲线的下降沿,所以对于系统管理来说,它是一种相当流行的工具。包括OpenSolaris在内的几种操作系统,主要利用了Python的脚本功能。与此相对照,Ruby仍然主要用于Web开发,很少用作一般性的脚本编程。
Guido van Rossum发明了Python。与Perl相比,Python的代码更好写,也更好读。Python提供了一种易于理解的语法,即使没开发过这种代码,也很容易掌握。如果觉得要记住用哪种比较操作符很累人,那么会喜欢上Python整齐划一的方式。Python还提供更多的数据类型,有些系统管理员会发现它们很有用。
如果系统上还没装好Python,可以看看操作系统提供商或者发布方给出的软件包清单。它是一种极其常见的软件包,应该到处都有。如果没找到,可以从python.org下载Python的源代码。这个地方也一个集中位置,可以找到其他人开发的附加模块。
对于Python而言,要想获得比我们在这儿给出的介绍更全面的内容,Mark Pilgrim的Dive Into Python是一个非常好的起步点。从diveintopython.org可以(免费)阅读或者下载这份文档,也可以购买Apress出版的印刷版图书。在2.7节里可以找到完整的参考文献。
2.5.1 Python快速入门
还像往常一样,我们先从一个简单“Hello,world!”脚本开始。果不其然,Python的“Hello, world!”几乎和Perl的一模一样。
#!/usr/bin/python print "Hello, world!"
要让这个脚本可以运行,只要设置它的可执行位,扩展直接调用python来解释这个脚本:
$ chmod +x helloworld
$ ./helloworld
Hello, world!
这样的一行程序不能体现出Python对传统的突破,这一突破恶名在外,也就是说,缩行在逻辑上很重要。Python不用花括号、方括号,或者begin和end来界定代码块。处于同一缩进级别的语句自动构成代码块。缩行的风格(空格或者制表符,缩进的深度)则无关紧要。Python的代码分块最好用例子来展示,所以下面就用一条if-then-else语句来说明:
#!/usr/bin/python import sys
a = sys.argv[1]
if a == "1":
print 'a is one'
print 'This is still the then clause of the if statement.'
else:
print 'a is', a
print 'This is still the else clause of the if statement.'
print 'This is after the if statement.'
第三行导入sys模块,它包含数组argv。then和else子句都有两行,每一部分都缩进到相同的层次。最后的print语句在if语句之外。和在Perl里的情况一样,Python的print语句也接受任意数量的参数。但和Perl不一样的是,Python自动在每对参数之间都插入一个空格,并提供一个换行符。可以在print行的末尾多加一个逗号,消除这个换行符;参数为空则告诉print不要输出换行符。
在一行末尾的冒号一般是该行引入的一个联系符,它和随后的一个缩进块关联到一起。
$ python blockexample 1
a is one
This is still the then clause of the if statement. This is after the if statement.
$ python blockexample 2
a is 2
This is still the else clause of the if statement. This is after the if statement.
Python的缩行惯例使得代码格式上的灵活度更低了,但它也有优点,即不同的人所编写的代码看上去都一样,而且还意味着不需要搞得代码里到处都有分号,而这些烦人的分号就只为了结束语句。
Python里的注释用一个井号(#)开头,一直持续到行尾,同bash和Perl里的用法一样。
在代码行用反斜线结尾,就可以把长长的代码行分成多行来写。这样做的时候,只有第一行代码的缩进才重要。不过,如果愿意,可以让后续的代码行都缩进。即使没有出现反斜线,但圆括号、方括号或者花括号不配对的代码行仍会自动触发续行,但如果出现这样的情况,可以在代码里包含反斜线,让代码的结构更清晰。
有些剪切和粘贴操作会把制表符(tab)转换为空格,除非知道自己要什么,否则这会让人抓狂。黄金法则是,绝对不要混用制表符和空格;缩进要么用制表符,要么用空格。许多软件采用传统的假设,让制表符应该等于8个空格的距离,对于代码的可读性来说,这确实缩进得太厉害了。大多数Python用户似乎都偏向于采用空格,而且缩进4个字符。
不管决定怎样处理缩行的问题,大多数编辑器都有若干选项,要么只用空白让制表符为非法,要么让空格和制表符显示得不一样,从而帮助用户搞得清楚。在万不得已的情况下,还可以用expand命令把制表符转为空格,或者通过perl -pe命令用一个更容易看到的字符串替换制表符。
2.5.2 对象、字符串、数、列表、字典、元组和文件
Python中所有的数据类型都是对象,比起Perl中的数据类型来说,这一点让它们更强大,也更灵活。
在Python里,列表用方括号而不是圆括号括起来。数组的索引(下标)从0开始,在本章介绍的3种脚本编程语言中,这是为数不多的几种没有变化的概念之一。
Python里新出现了一种叫做“元组(tuple)”的数据类型,它实质上是不能变的列表。元组比数组更快,对于实际上不应该被修改的数据来说,用元组表示更合适。除了用圆括号而不是方括号做定界符之外,元组的语法和列表的一样。因为(thing)看上去是一个简单的代数表达式,因此,对于只包含一个元素的元组,需要用一个额外的逗号来消除它们的含糊意思:(thing, )。
下面是Python中出现的几种基本变量和数据类型:
#!/usr/bin/python name = 'Gwen'
rating = 10
characters = [ 'SpongeBob', 'Patrick', 'Squidward' ]
elements = ( 'lithium', 'carbon', 'boron' )
print "name:\t%s\nrating:\t%d" % (name, rating)
print "characters:\t%s" % characters print "elements:\t%s" % (elements, )
这个例子产生的输出如下:
$ python objects name: Gwen rating: 10
characters: ['SpongeBob', 'Patrick', 'Squidward']
elements: ('lithium', 'carbon', 'boron')
Python中的变量不会从语法上体现出来,也不会做类型声明,但是它们所指的对象的确有一种支持类型。在大多数情况下,Python不会替用户自动做类型转换,但是单个函数或者操作符可以做转换。例如,不显式地把数值转换为它的字符串表示形式的话,就不能把一个数和一个字符串(用操作符+)连接起来。不过,格式化操作符和语句会强制把所有东西都转为字符串形式。每个对象都有一个字符串表示。
字符串格式化操作符%很像C或者Perl语言里的sprintf函数,但它可以用在字符串出现的任何地方。它是一个双目操作符,左边是字符串,右边是要插入的数值。如果要插入一个以上的数值,那么必须用元组来表示这些值。
Python字典和Perl的哈希一样;也就是说,它是“键/值”对的一个列表。字典常量用花括号括起来,每一对“键/值”都用一个冒号分隔。
#!/usr/bin/python
ordinal = { 1 : 'first', 2 : 'second', 3 : 'third' } print "The ordinal array contains", ordinal print "The ordinal of 1 is", ordinal[1]
在使用上,Python的字典又非常像数组,区别在于下标(键)可以是对象而不只是整数。
$ python dictionary
The ordinal array contains {1: 'first', 2: 'second', 3: 'third'} The ordinal of 1 is first
Python用对象所关联的方法把打开的文件按对象来处理。顾名思义,readline方法读取一行,所以下面的例子从/etc/passwd文件读取并打印两行内容。
#!/usr/bin/python
f = open('/etc/passwd', 'r')
print f.readline(), print f.readline(), f.close()
$ python fileio
at:x:25:25:Batch jobs daemon:/var/spool/atjobs:/bin/true bin:x:1:1:bin:/bin:/bin/true
print语句里最后的逗号消除了换行符,因为每行在从原来的文件读入的时候就已经带一个换行符了。
2.5.3 确认输入的例子
下面的脚本片段是Python版的确认输入的程序,我们现在已经很熟悉这个例子了。它展示了子例程和命令行参数的用法,还体现了其他两种Python化的特性。
#!/usr/bin/python import sys
import os
def show_usage(message, code = 1):
print message
print "%s: source_dir dest_dir" % sys.argv[0]
sys.exit(code)
if len(sys.argv) != 3:
show_usage("2 arguments required; you supplied %d" % (len(sys.argv) - 1))
elif not os.path.isdir(sys.argv[1]):
show_usage("Invalid source directory")
elif not os.path.isdir(sys.argv[2]):
show_usage("Invalid destination directory")
source, dest = sys.argv[1:3]
print "Source Directory is", source print "Destination Directory is", dest
除了导入sys模块之外,我们还导入了os模块,从而可以使用os.path.isdir这个例程。注意,对于模块定义的任何代号来说,import命令不会提供访问它们的捷径;必须使用从模块名开始的全名。
例程show_usage的定义里给退出码赋予了一个默认值,万一调用程序没有显式地指定这个参数,就用这个默认值退出。既然所有的数据类型都是对象,所以用引用来给函数传参。
数组sys.argv在第0个位置保存有该脚本的名字,所以它的长度比实际提供的命令行参数个数正好多1。sys.argv[1:3]这样的形式表示一个数组段。有意思的是,数组段不包括指定范围里最后的那个元素,所以这个数组段只有sys.argv[1]和sys.argv[2]两个元素。可以简单地用sys.argv[1:]把从第二个开始的所有元素都包括进来。
同bash和Perl一样,Python也有专门的一种“else if”条件;其关键字是elif。Python也没有case或者switch语句。
给soure和dest变量的平行赋值与Perl有点儿不一样,因为这两个变量本身不在一个列表里。两种形式的平行赋值在Python里都可以。
Python给数值和字符串的比较运算符都一样。“不相等”的比较运算符是!=,但却没有!这样的单目运算符;这样的单目运算符是not。也要搞清楚布尔运算符and和or。
2.5.4 循环
下面的代码片段用一个for…in结构从1循环到10。
for counter in range(1, 10):
print counter,
和前面例子里的数组段一样,这个范围的右端点实际上没有包括进来。输出值只有1到9。
1 2 3 4 5 6 7 8 9
这是Python里唯一的for循环类型,但它功能很强大。Python的for语句有几项特性,有别于其他语言。
数值范围没有什么特殊之处。任何对象都可以支持Python的循环模型,而且最常见的对象都支持。可以通过一个字符串(按逐个字符)、一个列表、一个文件(按逐个字符、逐行或者逐块),以及一个数组段等。
循环可以产生多个值,循环变量也可以有多个。在每次循环的开头进行的赋值,就和Python正常的多重赋值一样。
for和while循环都可以在末尾加上else子句。只有当循环正常终止之后,才执行这个else子句,这和通过一条break语句退出正好相反。这一功能乍看起来似乎与直觉相反,但它能很好地处理某些用例。
下面的脚本示例接受命令行上的一个正则表达式,把它同一个列表进行匹配,列表里是白雪公主中七个小矮人的名字及其衣服的颜色。该脚本打印第一个匹配的结果,而且匹配正则表达式的部分两边用下划线区分出来。
#!/usr/bin/python import sys
import re
suits = { 'Bashful':'red', 'Sneezy':'green', 'Doc':'blue', 'Dopey':'orange',
'Grumpy':'yellow', 'Happy':'taupe', 'Sleepy':'puce' }
pattern = re.compile("(%s)" % sys.argv[1])
for dwarf, color in suits.items():
if pattern.search(dwarf) or pattern.search(color):
print "%s's dwarf suit is %s." % \
(pattern.sub(r"_\1_", dwarf), pattern.sub(r"_\1_", color))
break
else:
print "No dwarves or dwarf suits matched the pattern."
下面是一些输出的例子:
$ python dwarfsearch '[aeiou]{2}'
Sn_ee_zy's dwarf suit is gr_ee_n.
$ python dwarfsearch go
No dwarves or dwarf suits matched the pattern.
给suits赋值的语句,展示了Python用于字典常量的编码方式。suits.items()方法是“键/值”对的迭代器——每次循环都提取一个小矮人的名字和一种衣服颜色。如果只想通过键做循环,只需要把代码写为for dwarf in suits。
Python通过它的re模块实现对正则表达式的处理。Python语言本身没有任何有关正则表达式的功能,所以用Python处理正则表达式比用Perl要稍微麻烦一点儿。在本例中,正则表达式的pattern一开始由compile方法,用圆括号括起来第一个命令行参数进行编译,形成了一个捕获组。接着,用正则表达式对象的search和sub方法测试和修改字符串。还可以像函数那样直接调用re.search等方法,把正则表达式当做第一个参数来用。替换字符串里的\1是反过去引用第一个捕获组的内容。