审校 (5.4b):2007 年 6 月—9 月
译文版 (5.4):2005 年 12 月—2006 年 4 月 (update-060425)
英文原版 (5.4):2004 年 5 月 20 日
Copyright © 2000, 2001, 2002, 2003, 2004 Mark Pilgrim
Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007 CPyUG (邮件列表)
本书存放在 http://diveintopython.org/ (英文原版) 和 http://www.woodpecker.org.cn/diveintopython(中文版)。如果你是从别的地方看到它的,可能看到的不是最新版本。
Permission is granted to copy, distribute, and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in Appendix G, GNU Free Documentation License.
允许在 GNU 自由文档协议 (1.1 版,或自由软件基金会出版的任何更新版本) 的许可下复制、发行且/或修改本文档;本文档没有不变部分,没有前封面文本,没有封底文本。该协议的一份中文版参考译文包含在 附录 H, GNU 自由文档协议 中。
在这本书中的例程是自由软件。你可以在遵守 Python 协议 (Python 软件基金会发布) 条款的规定下,重新发布,且/或修改它们。在 附录 I, Python license 中包含了此协议的一份拷贝。
本译本由 Zoom.Quiet 负责项目管理。感谢啄木鸟社区提供 SVN 项目空间和 Wiki 协作空间。
本译本由 啄木鸟/CPUG 的 obp 团队完成。可以在附录 E, 修订历史中找到一个翻译和修订人员的清单。如果您对当前版本的 Dive Into Python 中文版有任何意见和建议,可以到本书的 Wiki 协作空间中留下你的评论。
本译文遵守 GFDL 的规定。你可以复制、发行、修改此文档,但请保留此版权信息。
目录
欢迎来到 Python 世界,让我们开始吧。在本章中,将学习适合您的 Python 安装。
学习 Python 的第一件事就是安装,不是吗?
如果您在公网的服务器上有个用户账号,那么您的 ISP 或许已经安装了 Python。 大多数 Linux 发行版在默认安装的情况下就已经提供了 Python。 虽然您可能希望在苹果机上安装一个拥有类 Mac 的图形操作界面,但在 Mac OS X 10.2 或更高的版本上已经包含了一个 Python 的命令行版本。
Windows 环境默认不提供任何版本的 Python,但是不要担心!本章将提供几种 Windows 环境下安装 Python 的方法。
正像您所看到的,Python 可以运行于很多操作系统平台。包括 Windows、Mac OS、Mac OS X、所有免费的类 UNIX 变种 (如 Linux)。也有运行于 Sun Solaris、AS/400、Amiga、OS/2、BeOS 的版本,甚至是您从来没听说过的其他操作系统平台。
有太多的平台可以运行 Python 了。在一种平台下编写的 Python 程序稍作修改,就可以运行于任何 其他支持的平台。例如,我通常在 Windows 平台上开发 Python 程序,然后适当配置后使之能在 Linux 平台上运行。
回到开始的问题,“哪一种 Python 适合您?” 回答是:哪一个已经安装在您计算机上的均可。
在 Windows 上,安装 Python 有两种选择。
ActiveState 制作的 ActivePython 是专门针对 Windows 的 Python 套件,它包含了一个完整的 Python 发布、一个适用于 Python 编程的 IDE 以及一些 Python 的 Windows 扩展,提供了全部的访问 Windows APIs 的服务,以及 Windows 注册表的注册信息。
虽然 ActivePython 不是开源软件,但它可以自由下载。ActivePython 是我学习 Python 时使用过的 IDE。除非有别的原因,我建议您使用它。可能的一个原因是:ActiveState 通常要在新的 Python 版本发布几个月以后才更新它的安装程序。如果您就需要 Python 的最新版本,并且 ActivePython 仍然落后于最新版本的话,您应该直接跳到在 Windows 上安装 Python 的第二种选项。
第二种选择是使用由 Python 发布的 “官方” Python 安装程序。她是可自由下载的开源软件,并且您总是可以获得当前 Python 的最新版本。
下面描述 ActivePython 的安装过程:
从 http://www.activestate.com/Products/ActivePython/ 下载 ActivePython 。
如果您正在使用 Windows 95、Windows 98 或 Windows ME,还需要在安装 ActivePython 之前下载并安装Windows Installer 2.0 。
双击安装程序 ActivePython-2.2.2-224-win32-ix86.msi。
按照安装程序的提示信息一步步地执行。
如果磁盘空间不足,您可以执行定制安装,不选文档,但是笔者不建议您这样做,除非您实在是挤不出14M空间来。
在安装完后之后,关闭安装程序,打开
-> -> -> 。您将看到类似如下的信息:
PythonWin 2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)] on win32.
Portions Copyright 1994-2001 Mark Hammond (mhammond@skippinet.com.au) -
see 'Help/About PythonWin' for further copyright information.
>>>
从 http://www.python.org/ftp/python/ 选择最新的 Python Windows 安装程序,下载 .exe 安装文件。
双击安装程序 Python-2.xxx.yyy.exe。文件名依赖于您所下载的 Python 安装程序文件。
按照安装程序的提示信息一步步地执行。
如果磁盘空间不足,可以取消 HTMLHelp 文件、实用脚本 (Tools/)、和/或测试套件 (Lib/test/)。
如果您没有机器的管理员权限,您可以选择 Non-Admin Install。这只会对登记注册表和开始菜单中创建的快捷方式有影响。
,然后选择在安装完成之后,关闭安装程序,打开
-> -> -> 。您将看到类似如下的信息:
Python 2.3.2 (#49, Oct 2 2003, 20:02:00) [MSC v.1200 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
****************************************************************
Personal firewall software may warn about the connection IDLE
makes to its subprocess using this computer's internal loopback
interface. This connection is not visible on any external
interface and no data is sent to or received from the Internet.
****************************************************************
IDLE 1.0
>>>
在 Mac OS X 上,对于安装 Python 有两种选择:安装或不安装。您可能想要安装它。
Mac OS X 10.2 及其后续版本已经预装了一个 Python 的命令行版本。如果您习惯使用命令行,那么您可以使用它学完本书的三分之一。然而,预安装的版本不带 XML 解析器,所以当您学到 XML 的章节时,您会需要安装完整版。
您还可以安装优于预装版本的最新的包含图形界面 Shell 的完整版本。
使用预装的 Python 版本的步骤:
打开 /Applications 文件夹。
打开 Utilities 文件夹。
双击 Terminal 打开一个终端进入命令行窗口。
在提示符下键入 python。
试验:
Welcome to Darwin! [localhost:~] you% python Python 2.2 (#1, 07/14/02, 23:25:09) [GCC Apple cpp-precomp 6.14] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you%
下面介绍下载并安装 Python 最新版本的过程:
从 http://homepages.cwi.nl/~jack/macpython/download.html 下载 MacPython-OSX 磁盘镜像 。
下载完毕,双击 MacPython-OSX-2.3-1.dmg 将磁盘镜像挂载到桌面。
双击安装程序 MacPython-OSX.pkg.
安装程序将提示要求您的管理员用户名和口令。
按照安装程序的提示一步步执行。
安装完毕后,关闭安装程序,打开 /Applications 文件夹。
打开 MacPython-2.3 文件夹。
双击 PythonIDE 来运行 Python 。
MacPython IDE 将显示启动画面将您带进交互 shell。如果交互 shell 没有出现,选择 -> (Cmd-0)。您将看到类似如下的信息:
Python 2.3 (#2, Jul 30 2003, 11:45:28)
[GCC 3.1 20020420 (prerelease)]
Type "copyright", "credits" or "license" for more information.
MacPython IDE 1.0.1
>>>
请注意,安装完最新版本后,预装版本仍然存在。如果您从命令行运行脚本,那您需要知道正在使用的是哪一个版本的 Python 。
[localhost:~] you% python Python 2.2 (#1, 07/14/02, 23:25:09) [GCC Apple cpp-precomp 6.14] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you% /usr/local/bin/python Python 2.3 (#2, Jul 30 2003, 11:45:28) [GCC 3.1 20020420 (prerelease)] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you%
Mac OS 9 上没有预装任何版本的 Python,安装相对简单,只有一种选择。
下面介绍在 Mac OS 9 上安装 Python 的过程:
从 http://homepages.cwi.nl/~jack/macpython/download.html 下载 MacPython23full.bin。
如果浏览器不能自动解压文件,那么双击 MacPython23full.bin 用 Stuffit Expander 解压。
双击安装程序 MacPython23full。
按照安装程序的提示一步步执行。
安装完毕后,关闭安装程序,打开 /Applications 文件夹。
打开 MacPython-OS9 2.3 文件夹。
双击 PythonIDE 来运行 Python 。
MacPython IDE 将显示启动画面将您带进交互 shell。如果交互 shell 没有出现,选择 -> (Cmd-0)。您将看到类似如下的信息:
Python 2.3 (#2, Jul 30 2003, 11:45:28)
[GCC 3.1 20020420 (prerelease)]
Type "copyright", "credits" or "license" for more information.
MacPython IDE 1.0.1
>>>
在类 UNIX 的操作系统 (如 Linux) 上安装二进制包很容易。预编译好的二进制包对大多数 Linux 发行版是可用的。或者您可以通过源码进行编译。
在 http://www.python.org/ftp/python/ 选择列出的最新的版本号, 然后选择 其中的rpms/ 目录下载最新的 Python RPM 包。 使用 rpm 命令进行安装,操作如下所示:
localhost:~$ su - Password: [enter your root password] [root@localhost root]# wget http://python.org/ftp/python/2.3/rpms/redhat-9/python2.3-2.3-5pydotorg.i386.rpm Resolving python.org... done. Connecting to python.org[194.109.137.226]:80... connected. HTTP request sent, awaiting response... 200 OK Length: 7,495,111 [application/octet-stream] ... [root@localhost root]# rpm -Uvh python2.3-2.3-5pydotorg.i386.rpm Preparing... ########################################### [100%] 1:python2.3 ########################################### [100%] [root@localhost root]# python Python 2.2.2 (#1, Feb 24 2003, 19:13:11) [GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-4)] on linux2 Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to exit] [root@localhost root]# python2.3 Python 2.3 (#1, Sep 12 2003, 10:53:56) [GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2 Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to exit] [root@localhost root]# which python2.3 /usr/bin/python2.3
如果您运行在 Debian GNU/Linux 上,安装 Python 需要使用 apt 命令。
localhost:~$ su - Password: [enter your root password] localhost:~# apt-get install python Reading Package Lists... Done Building Dependency Tree... Done The following extra packages will be installed: python2.3 Suggested packages: python-tk python2.3-doc The following NEW packages will be installed: python python2.3 0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded. Need to get 0B/2880kB of archives. After unpacking 9351kB of additional disk space will be used. Do you want to continue? [Y/n] Y Selecting previously deselected package python2.3. (Reading database ... 22848 files and directories currently installed.) Unpacking python2.3 (from .../python2.3_2.3.1-1_i386.deb) ... Selecting previously deselected package python. Unpacking python (from .../python_2.3.1-1_all.deb) ... Setting up python (2.3.1-1) ... Setting up python2.3 (2.3.1-1) ... Compiling python modules in /usr/lib/python2.3 ... Compiling optimized python modules in /usr/lib/python2.3 ... localhost:~# exit logout localhost:~$ python Python 2.3.1 (#2, Sep 24 2003, 11:39:14) [GCC 3.3.2 20030908 (Debian prerelease)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> [press Ctrl+D to exit]
如果您宁愿从源码创建,可以从 http://www.python.org/ftp/python/下载 Python 的源代码。选择最新的版本,下载.tgz 文件,执行通常的 configure, make, make install 步骤。
localhost:~$ su - Password: [enter your root password] localhost:~# wget http://www.python.org/ftp/python/2.3/Python-2.3.tgz Resolving www.python.org... done. Connecting to www.python.org[194.109.137.226]:80... connected. HTTP request sent, awaiting response... 200 OK Length: 8,436,880 [application/x-tar] ... localhost:~# tar xfz Python-2.3.tgz localhost:~# cd Python-2.3 localhost:~/Python-2.3# ./configure checking MACHDEP... linux2 checking EXTRAPLATDIR... checking for --without-gcc... no ... localhost:~/Python-2.3# make gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Modules/python.o Modules/python.c gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Parser/acceler.o Parser/acceler.c gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Parser/grammar1.o Parser/grammar1.c ... localhost:~/Python-2.3# make install /usr/bin/install -c python /usr/local/bin/python2.3 ... localhost:~/Python-2.3# exit logout localhost:~$ which python /usr/local/bin/python localhost:~$ python Python 2.3.1 (#2, Sep 24 2003, 11:39:14) [GCC 3.3.2 20030908 (Debian prerelease)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] localhost:~$
既然我们已经安装了 Python,那么我们运行的这个交互 shell 是什么东西呢?
Python 扮演着两种角色。首先它是一个脚本解释器,可以从命令行运行脚本,也可以在脚本上双击,像运行其他应用程序一样。它还是一个交互 shell,可以执行任意的语句和表达式。这一点对调试、快速组建和测试相当有用。我甚至知道一些人把 Python 的交互 shell 当作计算器来使用!
在您的计算机平台上启动 Python 的交互 shell,接下来让我们尝试着做些操作:
大家都很清楚,其他书籍是如何一步步从编程基础讲述到构建完整的可运行程序的,但还是让我们跳过这个部分吧!
这是一个完整的、可执行的 Python 程序。
它可能对您来说根本无法理解。别着急,我们将逐行地进行剖析。不过首先把代码通读一遍,看一看是否有些可以理解的内容。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ } print buildConnectionString(myParams)
现在运行一下这个程序,看一看结果是什么。
在 Windows 的 ActivePython IDE 中,可以选择 -> (Ctrl-R) 来运行 Python 程序。输出结果将显示在交互窗口中。 |
在 Mac OS 的 Python IDE 中,可以选择 -> (Cmd-R) 来运行 Python 程序,但首先要设置一个重要的选项。在 IDE 中打开 .py 模块,点击窗口右上角的黑色三角,弹出这个模块的选项菜单,然后将 选中。 这个设置是同模块一同保存的,所以对于每个模块您都需要这样做。 |
在 UNIX 兼容的操作系统中 (包括 Mac OS X),可以通过命令行:python odbchelper.py 运行模块。 |
与其它大多数语言一样 Python 有函数,但是它没有像 C++ 一样的独立的头文件;或者像 Pascal 一样的分离的 interface/implementation 段。在需要函数时,像下面这样声明即可:
def buildConnectionString(params):
首先,函数声明以关键字 def 开始,接着为函数名,再往后为参数,参数放在小括号里。多个参数之间 (这里没有演示)用逗号分隔。
其次,函数没有定义返回的数据类型。Python 不需要指定返回值的数据类型;甚至不需要指定是否有返回值。实际上,每个 Python 函数都返回一个值;如果函数执行过 return 语句,它将返回指定的值,否则将返回 None (Python 的空值)。
在 Visual Basic 中,函数 (有返回值) 以 function 开始,而子程序 (无返回值) 以 sub 开始。在 Python 中没有子程序。只有函数,所有的函数都有返回值 (尽管可能为 None),并且所有的函数都以 def 开始。 |
最后需要指出的是,在 Python 中参数,params 不需要指定数据类型。Python 会判定一个变量是什么类型,并在内部将其记录下来。
在 Java、C++ 和其他静态类型语言中,必须要指定函数返回值和每个函数参数的数据类型。在 Python 中,永远也不需要明确指定任何东西的数据类型。Python 会根据赋给它的值在内部将其数据类型记录下来。 |
一位博学的读者发给我 Python 如何与其它编程语言的比较的解释:
所以说 Python 既是动态类型语言 (因为它不使用显示数据类型声明),又是强类型语言 (因为只要一个变量获得了一个数据类型,它实际上就一直是这个类型了)。
可以通过给出一个 doc string (文档字符串) 来文档化一个 Python 函数。
def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string."""
三重引号表示一个多行字符串。在开始与结束引号间的所有东西都被视为单个字符串的一部分,包括硬回车和其它的引号字符。您可以在任何地方使用它们,但是您可能会发现,它们经常被用于定义 doc string。
三重引号也是一种定义既包含单引号又包含双引号的字符串的简单方法,就像 Perl 中的 qq/.../ 。 |
在三重引号中的任何东西都是这个函数的 doc string,它们用来说明函数可以做什么。如果存在 doc string,它必须是一个函数要定义的第一个内容 (也就是说,在冒号后面的第一个内容)。在技术上不要求给出函数的 doc string,但是您应该这样做。我相信在您上过的每一种编程课上都听到过这一点,但是 Python 带给您一些额外的动机:doc string 在运行时可作为函数的属性。
许多 Python IDE 使用 doc string 来提供上下文敏感的文档信息,所以当键入一个函数名时,它的 doc string 显示为一个工具提示。这一点可以说非常有用,但是它的好坏取决于您书写的 doc string 的好坏。 |
也许您没在意,我刚才的意思是 Python 函数有属性,并且这些属性在运行时是可用的。
在 Python 中,函数同其它东西一样也是对象。
打开您习惯使用的 Python IDE 执行如下的操作:
>>> import odbchelper >>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> print odbchelper.buildConnectionString(params) server=mpilgrim;uid=sa;database=master;pwd=secret >>> print odbchelper.buildConnectionString.__doc__ Build a connection string from a dictionary Returns string.
第一行将 odbchelper 程序作为模块导入。模块是指一个可以交互使用,或者从另一 Python 程序访问的代码段。(您在 第 4 章 将会看到多模块 Python 程序的许多例子。) 只要导入了一个模块,就可以引用它的任何公共的函数、类或属性。模块可以通过这种方法来使用其它模块的功能,您也可以在 IDE 中这样做。这是一个很重要的概念,在后面我们将谈得更多。 | |
当使用在被导入模块中定义的函数时,必须包含模块的名字。所以不能只使用 buildConnectionString,而应该使用 odbchelper.buildConnectionString。如果您用过 Java 的类,对此应该不感到陌生。 | |
访问函数的 __doc__ 属性不像您想象的那样是通过函数调用。 |
在 Python 中的 import 就像 Perl 中的 require。import 一个 Python 模块后,您就可以使用 module.function 来访问它的函数;require 一个 Perl 模块后,您就可以使用 module::function 来访问它的函数。 |
在我们继续之前,我想简要地提一下库的搜索路径。当导入一个模块时,Python 在几个地方进行搜索。明确地,它会对定义在 sys.path 中的目录逐个进行搜索。它只是一个list (列表),您可以容易地查看它或通过标准的list方法来修改它。(在本章的后面我们将学习更多关于list的知识。)
>>> import sys >>> sys.path ['', '/usr/local/lib/python2.2', '/usr/local/lib/python2.2/plat-linux2', '/usr/local/lib/python2.2/lib-dynload', '/usr/local/lib/python2.2/site-packages', '/usr/local/lib/python2.2/site-packages/PIL', '/usr/local/lib/python2.2/site-packages/piddle'] >>> sys <module 'sys' (built-in)> >>> sys.path.append('/my/new/path')
导入 sys 模块,使得它的所有函数和属性都有效。 | |
sys.path 是一个指定当前搜索路径的目录列表。(您的输出结果可能有所不同,这取决于您的操作系统、正在运行的 Python 版本和初始安装的位置。)Python 将搜索这些目录 (按顺序) 来查找一个与您正试着导入的模块名相匹配的 .py 文件。 | |
实际上,我没说实话。真实情况要比这更复杂,因为不是所有的模块都保存为 .py 文件。有一些模块 (像 sys),是“内置模块”,它们实际上是置于 Python 内部的。内置模块的行为如同一般的模块,但是它们的 Python 源代码是不可用的,因为它们不是用 Python 写的!(sys 模块是用 C 写的。) | |
在运行时,通过向 sys.path 追加目录名,就可以在 Python 的搜索路径中增加新的目录,然后当您导入模块时,Python 也会在那个目录中进行搜索。这个作用在 Python 运行时一直生效。(在 第 3 章 我们将讨论更多的关于 append 和其它的 list 方法。) |
在 Python 中一切都是对象,并且几乎一切都有属性和方法。所有的函数都有一个内置的 __doc__ 属性,它会返回在函数源代码中定义的 doc string;sys 模块是一个对象,它有一个叫作 path 的属性;等等。
我们仍然在回避问题的实质,究竟何谓对象?不同的编程语言以不同的方式定义 “对象” 。 某些语言中,它意味着所有 对象必须 有属性和方法;另一些语言中,它意味着所有的对象都可以子类化。在 Python 中,定义是松散的;某些对象既没有属性也没有方法 (关于这一点的说明在 第 3 章),而且不是所有的对象都可以子类化 (关于这一点的说明在第 5 章)。但是万物皆对象从感性上可以解释为:一切都可以赋值给变量或作为参数传递给函数 (关于这一点的说明在第 4 章)。
这一点太重要了,所以我会在刚开始就不止一次地反复强调它,以免您没注意到:在 Python 中万物皆对象。字符串是对象。列表是对象。函数是对象。甚至模块也是对象,这一点我们很快会看到。
Python 函数没有明显的 begin 和 end,没有标明函数的开始和结束的花括号。唯一的分隔符是一个冒号 (:),接着代码本身是缩进的。
def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
代码块是通过它们的缩进来定义的。我所说的“代码块”是指:函数、if 语句、for 循环、while 循环,等等。开始缩进表示块的开始,取消缩进表示块的结束。不存在明显的括号,大括号或关键字。这就意味着空白是重要的,并且要一致。在这个例子中,函数代码 (包括 doc string) 缩进了 4 个空格。不一定非要是 4 个,只要一致就可以了。没有缩进的第一行则被视为在函数体之外。
例 2.6 “if 语句” 展示了一个 if 语句缩进的例子。
def fib(n): print 'n =', n if n > 1: return n * fib(n - 1) else: print 'end of the line' return 1
在经过一些最初的抗议和几个与 Fortran 的嘲讽的类比之后,您会心平气和地对待代码缩进,并且开始看到它的好处。一个主要的好处就是所有的 Python 程序看上去都差不多,因为缩进是一种语言的要求而不是一种风格。这样就使得阅读和理解他人的 Python 代码容易得多。
Python 使用硬回车来分割语句,冒号和缩进来分割代码块。C++ 和 Java 使用分号来分割语句,花括号来分割代码块。 |
所有的 Python 模块都是对象,并且有几个有用的属性。您可以使用这些属性方便地测试您所编写的模块。下面是一个使用 if __name__ 的技巧。
在继续学习新东西之前,有几个重要的观察结果。首先,if 表达式无需使用圆括号括起来。其次,if 语句以冒号结束,紧跟其后的是缩进代码。
与 C 一样,Python 使用 == 做比较,使用 = 做赋值。与 C 不一样,Python 不支持行内赋值,所以不会出现想要进行比较却意外地出现赋值的情况。 |
那么为什么说这个特殊的 if 语句是一个技巧呢?模块是对象,并且所有的模块都有一个内置属性 __name__。一个模块的 __name__ 的值取决于您如何应用模块。如果 import 模块,那么 __name__ 的值通常为模块的文件名,不带路径或者文件扩展名。但是您也可以像一个标准的程序一样直接运行模块,在这种情况下 __name__ 的值将是一个特别的缺省值,__main__。
>>> import odbchelper >>> odbchelper.__name__ 'odbchelper'
只要了解到这一点,您就可以在模块内部为您的模块设计一个测试套件,在其中加入这个 if 语句。当您直接运行模块,__name__ 的值是 __main__,所以测试套件执行。当您导入模块,__name__ 的值就是别的东西了,所以测试套件被忽略。这样使得在将新的模块集成到一个大程序之前开发和调试容易多了。
在 MacPython 上,需要一个额外的步聚来使得 if __name__ 技巧有效。点击窗口右上角的黑色三角,弹出模块的属性菜单,确认 被选中。 |
让我们用点儿时间来回顾一下您的第一个 Python 程序。但首先,先说些其他的内容,因为您需要了解一下 dictionary (字典)、tuple (元组) 和 list (列表)(哦,我的老天!)。如果您是一个 Perl hacker,当然可以撇开 dictionary 和 list,但是仍然需要注意 tuple。
Dictionary 是 Python 的内置数据类型之一,它定义了键和值之间一对一的关系。
Python 中的 dictionary 就像 Perl 中的 hash (哈希数组)。在 Perl 中,存储哈希值的变量总是以 % 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。 |
Python 中的 dictionary 像 Java 中的 Hashtable 类的实例。 |
Python 中的 dictionary 像 Visual Basic 中的 Scripting.Dictionary 对象的实例。 |
>>> d = {"server":"mpilgrim", "database":"master"} >>> d {'server': 'mpilgrim', 'database': 'master'} >>> d["server"] 'mpilgrim' >>> d["database"] 'master' >>> d["mpilgrim"] Traceback (innermost last): File "<interactive input>", line 1, in ? KeyError: mpilgrim
>>> d {'server': 'mpilgrim', 'database': 'master'} >>> d["database"] = "pubs" >>> d {'server': 'mpilgrim', 'database': 'pubs'} >>> d["uid"] = "sa" >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'}
在一个 dictionary 中不能有重复的 key。给一个存在的 key 赋值会覆盖原有的值。 | |
在任何时候都可以加入新的 key-value 对。这种语法同修改存在的值是一样的。(是的,它可能某天会给您带来麻烦。假设你一次次地修改一个 dictionary,但其间您使用的 key 并未按照您的想法进行改变。您可能以为加入了新值,但实际上只是一次又一次地修改了同一个值。) |
请注意新的元素 (key 为 'uid',value 为 'sa') 出现在中间。实际上,在第一个例子中的元素看上去是的有序不过是一种巧合。现在它们看上去的无序同样是一种巧合。
Dictionary 没有元素顺序的概念。说元素 “顺序乱了” 是不正确的,它们只是序偶的简单排列。这是一个重要的特性,它会在您想要以一种特定的,可重现的顺序 (像以 key 的字母表顺序) 存取 dictionary 元素的时候骚扰您。有一些实现这些要求的方法,它们只是没有加到 dictionary 中去。 |
当使用 dictionary 时,您需要知道:dictionary 的 key 是大小写敏感的。
>>> d = {} >>> d["key"] = "value" >>> d["key"] = "other value" >>> d {'key': 'other value'} >>> d["Key"] = "third value" >>> d {'Key': 'third value', 'key': 'other value'}
>>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'} >>> d["retrycount"] = 3 >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 'retrycount': 3} >>> d[42] = "douglas" >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 42: 'douglas', 'retrycount': 3}
List 是 Python 中使用最频繁的数据类型。如果您对 list 仅有的经验就是在 Visual Basic 中的数组或 Powerbuilder 中的数据存储,那么就打起精神学习 Python 的 list 吧。
Python 的 list 如同 Perl 中的数组。在 Perl 中,用来保存数组的变量总是以 @ 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。 |
Python 中的 list 更像 Java 中的数组 (您可以简单地这样理解,但 Python 中的 list 远比 Java 中的数组强大)。一个更好的类比是 ArrayList 类,它可以保存任意对象,并且可以在增加新元素时动态扩展。 |
>>> li = ["a", "b", "mpilgrim", "z", "example"] >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[0] 'a' >>> li[4] 'example'
>>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[-1] 'example' >>> li[-3] 'mpilgrim'
>>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[1:3] ['b', 'mpilgrim'] >>> li[1:-1] ['b', 'mpilgrim', 'z'] >>> li[0:3] ['a', 'b', 'mpilgrim']
>>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[:3] ['a', 'b', 'mpilgrim'] >>> li[3:] ['z', 'example'] >>> li[:] ['a', 'b', 'mpilgrim', 'z', 'example']
如果左侧分片索引为 0,您可以将其省略,默认为 0。所以 li[:3] 同 例 3.8 “list 的分片 (slice)” 的 li[0:3] 是一样的。 | |
同样的,如果右侧分片索引是 list 的长度,可以将其省略。所以 li[3:] 同 li[3:5] 是一样的,因为这个 list 有 5 个元素。 | |
请注意这里的对称性。在这个包含 5 个元素的 list 中,li[:3] 返回前 3 个元素,而 li[3:] 返回后 2 个元素。实际上,li[:n] 总是返回前 n 个元素,而 li[n:] 将返回剩下的元素,不管 list 有多长。 | |
如果将两个分片索引全部省略,这将包括 list 的所有元素。但是与原始的名为 li 的 list 不同,它是一个新 list,恰好拥有与 li 一样的全部元素。li[:] 是生成一个 list 完全拷贝的一个简写。 |
>>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li.append("new") >>> li ['a', 'b', 'mpilgrim', 'z', 'example', 'new'] >>> li.insert(2, "new") >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new'] >>> li.extend(["two", "elements"]) >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements']
>>> li = ['a', 'b', 'c'] >>> li.extend(['d', 'e', 'f']) >>> li ['a', 'b', 'c', 'd', 'e', 'f'] >>> len(li) 6 >>> li[-1] 'f' >>> li = ['a', 'b', 'c'] >>> li.append(['d', 'e', 'f']) >>> li ['a', 'b', 'c', ['d', 'e', 'f']] >>> len(li) 4 >>> li[-1] ['d', 'e', 'f']
>>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements'] >>> li.index("example") 5 >>> li.index("new") 2 >>> li.index("c") Traceback (innermost last): File "<interactive input>", line 1, in ? ValueError: list.index(x): x not in list >>> "c" in li False
在 2.2.1 版本之前,Python 没有单独的布尔数据类型。为了弥补这个缺陷,Python 在布尔环境 (如 if 语句) 中几乎接受所有东西,遵循下面的规则:
|
>>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements'] >>> li.remove("z") >>> li ['a', 'b', 'new', 'mpilgrim', 'example', 'new', 'two', 'elements'] >>> li.remove("new") >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two', 'elements'] >>> li.remove("c") Traceback (innermost last): File "<interactive input>", line 1, in ? ValueError: list.remove(x): x not in list >>> li.pop() 'elements' >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two']
>>> li = ['a', 'b', 'mpilgrim'] >>> li = li + ['example', 'new'] >>> li ['a', 'b', 'mpilgrim', 'example', 'new'] >>> li += ['two'] >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two'] >>> li = [1, 2] * 3 >>> li [1, 2, 1, 2, 1, 2]
Lists 也可以用 + 运算符连接起来。list = list + otherlist 相当于 list.extend(otherlist)。但 + 运算符把一个新 (连接后) 的 list 作为值返回,而 extend 只修改存在的 list。也就是说,对于大型 list 来说,extend 的执行速度要快一些。 | |
Python 支持 += 运算符。li += ['two'] 等同于 li.extend(['two'])。+= 运算符可用于 list、字符串和整数,并且它也可以被重载用于用户自定义的类中 (更多关于类的内容参见 第 5 章)。 | |
* 运算符可以作为一个重复器作用于 list。li = [1, 2] * 3 等同于 li = [1, 2] + [1, 2] + [1, 2],即将三个 list 连接成一个。 |
Tuple 是不可变的 list。一旦创建了一个 tuple,就不能以任何方式改变它。
>>> t = ("a", "b", "mpilgrim", "z", "example") >>> t ('a', 'b', 'mpilgrim', 'z', 'example') >>> t[0] 'a' >>> t[-1] 'example' >>> t[1:3] ('b', 'mpilgrim')
>>> t ('a', 'b', 'mpilgrim', 'z', 'example') >>> t.append("new") Traceback (innermost last): File "<interactive input>", line 1, in ? AttributeError: 'tuple' object has no attribute 'append' >>> t.remove("z") Traceback (innermost last): File "<interactive input>", line 1, in ? AttributeError: 'tuple' object has no attribute 'remove' >>> t.index("example") Traceback (innermost last): File "<interactive input>", line 1, in ? AttributeError: 'tuple' object has no attribute 'index' >>> "z" in t True
那么使用 tuple 有什么好处呢?
Tuple 可以转换成 list,反之亦然。内置的 tuple 函数接收一个 list,并返回一个有着相同元素的 tuple。而 list 函数接收一个 tuple 返回一个 list。从效果上看,tuple 冻结一个 list,而 list 解冻一个 tuple。 |
现在您已经了解了有关 dictionary、tuple、和 list 的相关知识 (哦,我的老天!),让我们回到 第 2 章 的例子程序 odbchelper.py。
Python 与大多数其它语言一样有局部变量和全局变量之分,但是它没有明显的变量声明。变量通过首次赋值产生,当超出作用范围时自动消亡。
if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ }
首先注意缩进。if 语句是代码块,需要像函数一样缩进。
其次,变量的赋值是一条被分成了多行的命令,用反斜线 (“\”) 作为续行符。
当一条命令用续行符 (“\”) 分割成多行时,后续的行可以以任何方式缩进,此时 Python 通常的严格的缩进规则无需遵守。如果您的 Python IDE 自由对后续行进行了缩进,您应该把它当成是缺省处理,除非您有特别的原因不这么做。 |
严格地讲,在小括号,方括号或大括号中的表达式 (如定义一个 dictionary) 可以用或者不用续行符 (“\”) 分割成多行。甚至在不是必需的时候,我也喜欢使用续行符,因为我认为这样会让代码读起来更容易,但那只是风格问题。
第三,您从未声明过变量 myParams,您只是给它赋了一个值。这点就像是 VBScript 没有设置 option explicit 选项一样。幸运的是,与 VBScript 不同,Python 不允许您引用一个未被赋值的变量,试图这样做会引发一个异常。
>>> x Traceback (innermost last): File "<interactive input>", line 1, in ? NameError: There is no variable named 'x' >>> x = 1 >>> x 1
迟早有一天您会为此而感谢 Python 。
Python 中比较 “酷” 的一种编程简写是使用序列来一次给多个变量赋值。
这种用法有许多种用途。我经常想要将一定范围的值赋给多个变量。在 C 语言中,可以使用 enum 类型,手工列出每个常量和其所对应的值,当值是连续的时候这一过程让人感到特别繁琐。而在 Python 中,您可以使用内置的 range 函数和多变量赋值的方法来快速进行赋值。
>>> range(7) [0, 1, 2, 3, 4, 5, 6] >>> (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) >>> MONDAY 0 >>> TUESDAY 1 >>> SUNDAY 6
您也可以使用多变量赋值来创建返回多个值的函数,只要返回一个包含所有值的 tuple 即可。调用者可以将其视为一个 tuple,或将值赋给独立的变量。许多标准的 Python 库都是这样做的,包括 os 模块,我们将在 第 6 章 中讨论。
Python 支持格式化字符串的输出 。尽管这样可能会用到非常复杂的表达式,但最基本的用法是将一个值插入到一个有字符串格式符 %s 的字符串中。
在 Python 中,字符串格式化使用与 C 中 sprintf 函数一样的语法。 |
注意 (k, v) 是一个 tuple。我说过它们对某些东西有用。
您可能一直在想,做了这么多工作只不过是为了做简单的字符串连接。您想的不错,只不过字符串格式化不只是连接。它甚至不仅仅是格式化。它也是强制类型转换。
>>> uid = "sa" >>> pwd = "secret" >>> print pwd + " is not a good password for " + uid secret is not a good password for sa >>> print "%s is not a good password for %s" % (pwd, uid) secret is not a good password for sa >>> userCount = 6 >>> print "Users connected: %d" % (userCount, ) Users connected: 6 >>> print "Users connected: " + userCount Traceback (innermost last): File "<interactive input>", line 1, in ? TypeError: cannot concatenate 'str' and 'int' objects
如同 printf 在 C 中的作用,Python 中的字符串格式化是一把瑞士军刀。它有丰富的选项,不同的格式化格式符和可选的修正符可用于不同的数据类型。
>>> print "Today's stock price: %f" % 50.4625 50.462500 >>> print "Today's stock price: %.2f" % 50.4625 50.46 >>> print "Change since yesterday: %+.2f" % 1.5 +1.50
Python 的强大特性之一是其对 list 的解析,它提供一种紧凑的方法,可以通过对 list 中的每个元素应用一个函数,从而将一个 list 映射为另一个 list。
>>> li = [1, 9, 8, 4] >>> [elem*2 for elem in li] [2, 18, 16, 8] >>> li [1, 9, 8, 4] >>> li = [elem*2 for elem in li] >>> li [2, 18, 16, 8]
让我们回过头来看看位于 第 2 章 的函数 buildConnectionString 对 list 的解析:
["%s=%s" % (k, v) for k, v in params.items()]
首先,注意到你调用了dictionary params 的 items 函数。这个函数返回一个 dictionary 中所有数据的 tuple 的 list。
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> params.keys() ['server', 'uid', 'database', 'pwd'] >>> params.values() ['mpilgrim', 'sa', 'master', 'secret'] >>> params.items() [('server', 'mpilgrim'), ('uid', 'sa'), ('database', 'master'), ('pwd', 'secret')]
现在让我们看一看 buildConnectionString 做了些什么。它接收一个 list,params.items(),通过对每个元素应用字符串格式化将其映射为一个新 list。这个新 list 将与 params.items() 一一对应:新 list 中的每个元素都是 dictionary params 中的一个键-值对构成的的字符串。
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> params.items() [('server', 'mpilgrim'), ('uid', 'sa'), ('database', 'master'), ('pwd', 'secret')] >>> [k for k, v in params.items()] ['server', 'uid', 'database', 'pwd'] >>> [v for k, v in params.items()] ['mpilgrim', 'sa', 'master', 'secret'] >>> ["%s=%s" % (k, v) for k, v in params.items()] ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret']
请注意我们正在使用两个变量对 list params.items() 进行遍历。这是多变量赋值的另一种用法。params.items() 的第一个元素是 ('server', 'mpilgrim'),所以在 list 解析的第一次遍历中,k 将为 'server',v 将为 'mpilgrim'。在本例中,我们忽略了返回 list 中 v 的值,而只包含了 k 的值,所以这个 list 解析最后等于 params.keys()。 | |
这里我们做着相同的事情,但是忽略了 k 的值,所以这个 list 解析最后等于 params.values()。 | |
用一些简单的 字符串格式化将前面两个例子合并起来 ,我们就得到一个包括了 dictionary 中每个元素的 key-value 对的 list。这个看上去有点像程序的输出结果,剩下的就只是将这个 list 中的元素接起来形成一个字符串了。 |
您有了一个形如 key=value 的 key-value 对 list,并且想将它们合成为单个字符串。为了将任意包含字符串的 list 连接成单个字符串,可以使用字符串对象的 join 方法。
下面是一个在 buildConnectionString 函数中连接 list 的例子:
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
在我们继续之前有一个有趣的地方。我一直在重复函数是对象,字符串是对象,每个东西都是对象的概念。您也许认为我的意思是说字符串值 是对象。但是不对,仔细地看一下这个例子,您将会看到字符串 ";" 本身就是一个对象,您在调用它的 join 方法。
总之,这里的 join 方法将 list 中的元素连接成单个字符串,每个元素用一个分号隔开。分隔符不必是一个分号;它甚至不必是单个字符。它可以是任何字符串。
join 只能用于元素是字符串的 list;它不进行任何的强制类型转换。连接一个存在一个或多个非字符串元素的 list 将引发一个异常。 |
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> ["%s=%s" % (k, v) for k, v in params.items()] ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> ";".join(["%s=%s" % (k, v) for k, v in params.items()]) 'server=mpilgrim;uid=sa;database=master;pwd=secret'
上面的字符串是从 odbchelper 函数返回的,被调用块打印出来,这样就给出了您开始阅读本章时令人感到吃惊的输出结果。
您可能在想是否存在一个适当的方法来将字符串分割成一个 list。当然有,它叫做 split。
>>> li = ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> s = ";".join(li) >>> s 'server=mpilgrim;uid=sa;database=master;pwd=secret' >>> s.split(";") ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> s.split(";", 1) ['server=mpilgrim', 'uid=sa;database=master;pwd=secret']
anystring.split(delimiter, 1) 是一个有用的技术,在您想要搜索一个子串,然后分别处理字符前半部分 (即 list 中第一个元素) 和后半部分 (即 list 中第二个元素) 时,使用这个技术。 |
当我开始学 Python 时,我以为 join 是 list 的方法,它会使用分隔符作为一个参数。很多人都有同样的感觉:在 join 方法的背后有一段故事。在 Python 1.6 之前,字符串完全没有这些有用的方法。有一个独立的 string 模块包含所有的字符串函数,每个函数使用一个字符串作为它的第一个参数。这些函数被认为足够重要,所以它们移到字符串中去了,这就使得诸如 lower、upper 和 split 之类的函数是有意义的。但许多核心的 Python 程序员反对新的 join 方法,争论说应该换成是 list 的一个方法,或不应该移动而仅仅保留为旧的 string 模块 (现仍然还有许多有用的东西在里面) 的一部分。我只使用新的 join 方法,但是您还是会看到其它写法。如果它真的使您感到麻烦,您可以使用旧的 string.join 函数来替代。
现在 odbchelper.py 程序和它的输出结果都应该非常清楚了。
def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ } print buildConnectionString(myParams)
下面是 odbchelper.py 的输出结果:
server=mpilgrim;uid=sa;database=master;pwd=secret
在深入下一章学习之前,确保您可以无阻碍地完成下面的事情:
本章论述了 Python 众多强大功能之一:自省。正如你所知道的,Python 中万物皆对象,自省是指代码可以查看内存中以对象形式存在的其它模块和函数,获取它们的信息,并对它们进行操作。用这种方法,你可以定义没有名称的函数,不按函数声明的参数顺序调用函数,甚至引用事先并不知道名称的函数。
下面是一个完整可运行的 Python 程序。大概看一下这段程序,你应该可以理解不少了。用数字标出的行阐述了 第 2 章 第一个 Python 程序 中涉及的一些概念。如果剩下来的代码看起来有点奇怪,不用担心,通过阅读本章你将会理解所有这些。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
def info(object, spacing=10, collapse=1): """Print methods and doc strings. Takes module, class, list, dictionary, or string.""" methodList = [method for method in dir(object) if callable(getattr(object, method))] processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) if __name__ == "__main__": print info.__doc__
该模块有一个声明为 info 的函数。根据它的函数声明可知,它有三个参数: object、spacing 和 collapse。实际上后面两个参数都是可选参数,关于这点你很快就会看到。 | |
info 函数有一个多行的 doc string,简要地描述了函数的功能。注意这里并没有提到返回值;单独使用这个函数只是为了这个函数产生的效果,并不是为了它的返回值。 | |
函数内的代码是缩进形式的。 | |
if __name__ 技巧允许这个程序在自己独立运行时做些有用的事情,同时又不妨碍作为其它程序的模块使用。在这个例子中,程序只是简单地打印出 info 函数的 doc string。 | |
if 语句使用 == 进行比较,而且不需要括号。 |
info 函数的设计意图是提供给工作在 Python IDE 中的开发人员使用,它可以接受任何含有函数或者方法的对象 (比如模块,含有函数,又比如list,含有方法) 作为参数,并打印出对象的所有函数和它们的 doc string。
>>> from apihelper import info >>> li = [] >>> info(li) append L.append(object) -- append object to end count L.count(value) -> integer -- return number of occurrences of value extend L.extend(list) -- extend list by appending list elements index L.index(value) -> integer -- return index of first occurrence of value insert L.insert(index, object) -- insert object before index pop L.pop([index]) -> item -- remove and return item at index (default last) remove L.remove(value) -- remove first occurrence of value reverse L.reverse() -- reverse *IN PLACE* sort L.sort([cmpfunc]) -- sort *IN PLACE*; if given, cmpfunc(x, y) -> -1, 0, 1
缺省地,程序输出进行了格式化处理,以使其易于阅读。多行 doc string 被合并到单行中,要改变这个选项需要指定 collapse 参数的值为 0。如果函数名称长于10个字符,你可以将 spacing 参数的值指定为更大的值以使输出更容易阅读。
>>> import odbchelper >>> info(odbchelper) buildConnectionString Build a connection string from a dictionary Returns string. >>> info(odbchelper, 30) buildConnectionString Build a connection string from a dictionary Returns string. >>> info(odbchelper, 30, 0) buildConnectionString Build a connection string from a dictionary Returns string.
Python 允许函数参数有缺省值;如果调用函数时不使用参数,参数将获得它的缺省值。此外,通过使用命名参数还可以以任意顺序指定参数。SQL Server Transact/SQL 中的存储过程也可以做到这些;如果你是脚本高手,你可以略过这部分。
info 函数就是这样一个例子,它有两个可选参数。
def info(object, spacing=10, collapse=1):
spacing 和 collapse 是可选参数,因为它们已经定义了缺省值。object 是必备参数,因为它没有指定缺省值。如果调用 info 时只指定一个参数,那么 spacing 缺省为 10 ,collapse 缺省为 1。如果调用 info 时指定两个参数,collapse 依然默认为 1。
假如你要指定 collapse 的值,但是又想要接受 spacing 的缺省值。在绝大部分语言中,你可能运气就不太好了,因为你需要使用三个参数来调用函数,这势必要重新指定 spacing 的值。但是在 Python 中,参数可以通过名称以任意顺序指定。
info(odbchelper) info(odbchelper, 12) info(odbchelper, collapse=0) info(spacing=15, object=odbchelper)
这些看上去非常累,除非你意识到参数不过是一个字典。“通常” 不使用参数名称的函数调用只是一个简写的形式,Python 按照函数声明中定义的的参数顺序将参数值和参数名称匹配起来。大部分时间,你会使用“通常”方式调用函数,但是如果你需要,总是可以提供附加的灵活性。
调用函数时唯一必须做的事情就是为每一个必备参数指定值 (以某种方式);以何种具体的方式和顺序都取决于你。 |
Python 有小部分相当有用的内置函数。除这些函数之外,其它所有的函数都被分到了各个模块中。其实这是一个非常明智的设计策略,避免了核心语言变得像其它脚本语言一样臃肿 (咳 咳,Visual Basic)。
type 函数返回任意对象的数据类型。在 types 模块中列出了可能的数据类型。这对于处理多种数据类型的帮助者函数 [1] 非常有用。
str 将数据强制转换为字符串。每种数据类型都可以强制转换为字符串。
>>> str(1) '1' >>> horsemen = ['war', 'pestilence', 'famine'] >>> horsemen ['war', 'pestilence', 'famine'] >>> horsemen.append('Powerbuilder') >>> str(horsemen) "['war', 'pestilence', 'famine', 'Powerbuilder']" >>> str(odbchelper) "<module 'odbchelper' from 'c:\\docbook\\dip\\py\\odbchelper.py'>" >>> str(None) 'None'
info 函数的核心是强大的 dir 函数。dir 函数返回任意对象的属性和方法列表,包括模块对象、函数对象、字符串对象、列表对象、字典对象 …… 相当多的东西。
>>> li = [] >>> dir(li) ['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] >>> d = {} >>> dir(d) ['clear', 'copy', 'get', 'has_key', 'items', 'keys', 'setdefault', 'update', 'values'] >>> import odbchelper >>> dir(odbchelper) ['__builtins__', '__doc__', '__file__', '__name__', 'buildConnectionString']
li 是一个列表,所以 dir(li) 返回一个包含所有列表方法的列表。注意返回的列表只包含了字符串形式的方法名称,而不是方法对象本身。 | |
d 是一个字典,所以 dir(d) 返回字典方法的名称列表。其中至少有一个方法,keys,看起来还是挺熟悉的。 | |
这里就是真正变得有趣的地方。odbchelper 是一个模块,所以 dir(odbchelper) 返回模块中定义的所有部件的列表,包括内置的属性,例如 __name__、__doc__,以及其它你所定义的属性和方法。在这个例子中,odbchelper 只有一个用户定义的方法,就是在第 2 章中论述的 buildConnectionString 函数。 |
最后是 callable 函数,它接收任何对象作为参数,如果参数对象是可调用的,返回 True;否则返回 False。可调用对象包括函数、类方法,甚至类自身 (下一章将更多的关注类)。
>>> import string >>> string.punctuation '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' >>> string.join <function join at 00C55A7C> >>> callable(string.punctuation) False >>> callable(string.join) True >>> print string.join.__doc__ join(list [,sep]) -> string Return a string composed of the words in list, with intervening occurrences of sep. The default separator is a single space. (joinfields and join are synonymous)
string 模块中的函数现在已经不赞成使用了 (尽管很多人现在仍然还在使用 join 函数),但是在这个模块中包含了许多有用的变量,例如 string.punctuation,这个字符串包含了所有标准的标点符号字符。 | |
string.join 是一个用于连接字符串列表的函数。 | |
string.punctuation 是不可调用的对象;它是一个字符串。(字符串确有可调用的方法,但是字符串本身不是可调用的。) | |
string.join 是可调用的;这个函数可以接受两个参数。 | |
任何可调用的对象都有 doc string。通过将 callable 函数作用于一个对象的每个属性,可以确定哪些属性 (方法、函数、类) 是你要关注的,哪些属性 (常量等等) 是你可以忽略、之前不需要知道的。 |
type、str、dir 和其它的 Python 内置函数都归组到了 __builtin__ (前后分别是双下划线) 这个特殊的模块中。如果有帮助的话,你可以认为 Python 在启动时自动执行了 from __builtin__ import *,此语句将所有的 “内置” 函数导入该命名空间,所以在这个命名空间中可以直接使用这些内置函数。
像这样考虑的好处是,你是可以获取 __builtin__ 模块信息的,并以组的形式访问所有的内置函数和属性。猜到什么了吗,现在我们的 Python 有一个称为 info 的函数。自己尝试一下,略看一下结果列表。后面我们将深入到一些更重要的函数。(一些内置的错误类,比如 AttributeError,应该看上去已经很熟悉了。)
>>> from apihelper import info >>> import __builtin__ >>> info(__builtin__, 20) ArithmeticError Base class for arithmetic errors. AssertionError Assertion failed. AttributeError Attribute not found. EOFError Read beyond end of file. EnvironmentError Base class for I/O related errors. Exception Common base class for all exceptions. FloatingPointError Floating point operation failed. IOError I/O operation failed. [...snip...]
Python 提供了很多出色的参考手册,你应该好好地精读一下所有 Python 提供的必备模块。对于其它大部分语言,你会发现自己要常常回头参考手册或者 man 页来提醒自己如何使用这些模块,但是 Python 不同于此,它很大程度上是自文档化的。 |
你已经知道 Python 函数是对象。你不知道的是,使用 getattr 函数,可以得到一个直到运行时才知道名称的函数的引用。
>>> li = ["Larry", "Curly"] >>> li.pop <built-in method pop of list object at 010DF884> >>> getattr(li, "pop") <built-in method pop of list object at 010DF884> >>> getattr(li, "append")("Moe") >>> li ["Larry", "Curly", "Moe"] >>> getattr({}, "clear") <built-in method clear of dictionary object at 00F113D4> >>> getattr((), "pop") Traceback (innermost last): File "<interactive input>", line 1, in ? AttributeError: 'tuple' object has no attribute 'pop'
该语句获取列表的 pop 方法的引用。注意该语句并不是调用 pop 方法;调用 pop 方法的应该是 li.pop()。这里指的是方法对象本身。 | |
该语句也是返回 pop 方法的引用,但是此时,方法名称是作为一个字符串参数传递给 getattr 函数的。getattr 是一个有用到令人无法致信的内置函数,可以返回任何对象的任何属性。在这个例子中,对象是一个 list,属性是 pop 方法。 | |
如果不确信它是多么的有用,试试这个:getattr 的返回值是 方法,然后你就可以调用它,就像直接使用 li.append("Moe") 一样。但是实际上你没有直接调用函数;只是以字符串形式指定了函数名称。 | |
getattr 也可以作用于字典。 | |
理论上,getattr 可以作用于元组,但是由于元组没有方法,所以不管你指定什么属性名称 getattr 都会引发一个异常。 |
getattr 不仅仅适用于内置数据类型,也可作用于模块。
>>> import odbchelper >>> odbchelper.buildConnectionString <function buildConnectionString at 00D18DD4> >>> getattr(odbchelper, "buildConnectionString") <function buildConnectionString at 00D18DD4> >>> object = odbchelper >>> method = "buildConnectionString" >>> getattr(object, method) <function buildConnectionString at 00D18DD4> >>> type(getattr(object, method)) <type 'function'> >>> import types >>> type(getattr(object, method)) == types.FunctionType True >>> callable(getattr(object, method)) True
该语句返回 odbchelper 模块中 buildConnectionString 函数的引用,第 2 章 第一个 Python 程序 你已经研习过这个方法了。(你看到的这个十六进制地址是我机器上的;你的输出结果会有所不同。) | |
使用 getattr,你能够获得同一函数的同一引用。通常,getattr(object, "attribute") 等价于 object.attribute。如果 object 是一个模块的话,那么 attribute 可能是定义在模块中的任何东西:函数、类或者全局变量。 | |
接下来的是你真正用在 info 函数中的东西。object 作为一个参数传递给函数; method 是方法或者函数的名称字符串。 | |
在这个例子中,method 是函数的名称,通过获取 type 可以进行验证。 | |
由于 method 是一个函数,所以它是可调用的。 |
getattr 常见的使用模式是作为一个分发者。举个例子,如果你有一个程序可以以不同的格式输出数据,你可以为每种输出格式定义各自的格式输出函数,然后使用唯一的分发函数调用所需的格式输出函数。
例如,让我们假设有一个以 HTML、XML 和普通文本格式打印站点统计的程序。输出格式在命令行中指定,或者保存在配置文件中。statsout 模块定义了三个函数:output_html、output_xml 和 output_text。然后主程序定义了唯一的输出函数,如下:
import statsout def output(data, format="text"): output_function = getattr(statsout, "output_%s" % format) return output_function(data)
你是否发现前面示例的一个 Bug?即字符串和函数之间的松耦合,而且没有错误检查。如果用户传入一个格式参数,但是在 statsout 中没有定义相应的格式输出函数,会发生什么呢?还好,getattr 会返回 None,它会取代一个有效函数并被赋值给 output_function,然后下一行调用函数的语句将会失败并抛出一个异常。这种方式不好。
值得庆幸的是,getattr 能够使用可选的第三个参数,一个缺省返回值。
import statsout def output(data, format="text"): output_function = getattr(statsout, "output_%s" % format, statsout.output_text) return output_function(data)
正如你所看到,getattr 是相当强大的。它是自省的核心,在后面的章节中你将看到它更强大的示例。
如你所知,Python 具有通过列表解析 (第 3.6 节 “映射 list”) 将列表映射到其它列表的强大能力。这种能力同过滤机制结合使用,使列表中的有些元素被映射的同时跳过另外一些元素。
过滤列表语法:
[mapping-expression for element in source-list if filter-expression]
这是你所知所爱的列表解析的扩展。前三部分都是相同的;最后一部分,以 if 开头的是过滤器表达式。过滤器表达式可以是返回值为真或者假的任何表达式 (在 Python 中是几乎任何东西)。任何经过滤器表达式演算值为真的元素都可以包含在映射中。其它的元素都将忽略,它们不会进入映射表达式,更不会包含在输出列表中。
>>> li = ["a", "mpilgrim", "foo", "b", "c", "b", "d", "d"] >>> [elem for elem in li if len(elem) > 1] ['mpilgrim', 'foo'] >>> [elem for elem in li if elem != "b"] ['a', 'mpilgrim', 'foo', 'c', 'd', 'd'] >>> [elem for elem in li if li.count(elem) == 1] ['a', 'mpilgrim', 'foo', 'c']
回到 apihelper.py 中的这一行:
methodList = [method for method in dir(object) if callable(getattr(object, method))]
这行看上去挺复杂――确实也很复杂――但是基本结构都还是一样的。整个过滤表达式返回一个列表,并赋值给 methodList 变量。表达式的前半部分是列表映射部分。映射表达式是一个和遍历元素相同的表达式,因此它返回每个元素的值。dir(object) 返回 object 对象的属性和方法列表――你正在映射的列表。所以唯一新出现的部分就是在 if 后面的过滤表达式。
过滤表达式看上去很恐怖,其实不是。你已经知道了 callable、getattr 和 in。正如你在前面的部分中看到的,如果 object 是一个模块,并且 method 是上述模块中某个函数的名称,那么表达式 getattr(object, method) 将返回一个函数对象。
所以这个表达式接收一个名为 object 的对象,然后得到它的属性、方法、函数和其他成员的名称列表,接着过滤掉我们不关心的成员。执行过滤行为是通过对每个属性/方法/函数的名称调用 getattr 函数取得实际成员的引用,然后检查这些成员对象是否是可调用的,当然这些可调用的成员对象可能是方法或者函数,同时也可能是内置的 (比如列表的 pop 方法) 或者用户自定义的 (比如 odbchelper 模块的 buildConnectionString 函数)。这里你不用关心其它的属性,如内置在每一个模块中的 __name__ 属性。
在Python 中,and 和 or 执行布尔逻辑演算,如你所期待的一样。但是它们并不返回布尔值,而是返回它们实际进行比较的值之一。
>>> 'a' and 'b' 'b' >>> '' and 'b' '' >>> 'a' and 'b' and 'c' 'c'
使用 and 时,在布尔环境中从左到右演算表达式的值。0、''、[]、()、{}、None 在布尔环境中为假;其它任何东西都为真。还好,几乎是所有东西。默认情况下,布尔环境中的类实例为真,但是你可以在类中定义特定的方法使得类实例的演算值为假。你将会在第 5 章中了解到类和这些特殊方法。如果布尔环境中的所有值都为真,那么 and 返回最后一个值。在这个例子中,and 演算 'a' 的值为真,然后是 'b' 的演算值为真,最终返回 'b'。 | |
如果布尔环境中的某个值为假,则 and 返回第一个假值。在这个例子中,'' 是第一个假值。 | |
所有值都为真,所以 and 返回最后一个真值,'c'。 |
>>> 'a' or 'b' 'a' >>> '' or 'b' 'b' >>> '' or [] or {} {} >>> def sidefx(): ... print "in sidefx()" ... return 1 >>> 'a' or sidefx() 'a'
如果你是一名 C 语言黑客,肯定很熟悉 bool ? a : b 表达式,如果 bool 为真,表达式演算值为 a,否则为 b。基于 Python 中 and 和 or 的工作方式,你可以完成相同的事情。
>>> a = "first" >>> b = "second" >>> 1 and a or b 'first' >>> 0 and a or b 'second'
然而,由于这种 Python 表达式单单只是进行布尔逻辑运算,并不是语言的特定构成,这是 and-or 技巧和 C 语言中的 bool ? a : b 语法非常重要的不同。如果 a 为假,表达式就不会按你期望的那样工作了。(你能知道我被这个问题折腾过吗?不止一次?)
and-or 技巧,也就是 bool and a or b 表达式,当 a 在布尔环境中的值为假时,不会像 C 语言表达式 bool ? a : b 那样工作。
在 and-or 技巧后面真正的技巧是,确保 a 的值决不会为假。最常用的方式是使 a 成为 [a] 、 b 成为 [b],然后使用返回值列表的第一个元素,应该是 a 或 b中的某一个。
到现在为止,这个技巧可能看上去问题超过了它的价值。毕竟,使用 if 语句可以完成相同的事情,那为什么要经历这些麻烦事呢?哦,在很多情况下,你要在两个常量值中进行选择,由于你知道 a 的值总是为真,所以你可以使用这种较为简单的语法而且不用担心。对于使用更为复杂的安全形式,依然有很好的理由要求这样做。例如,在 Python 语言的某些情况下 if 语句是不允许使用的,比如在 lambda 函数中。
Python 支持一种有趣的语法,它允许你快速定义单行的最小函数。这些叫做 lambda 的函数,是从 Lisp 借用来的,可以用在任何需要函数的地方。
>>> def f(x): ... return x*2 ... >>> f(3) 6 >>> g = lambda x: x*2 >>> g(3) 6 >>> (lambda x: x*2)(3) 6
总的来说,lambda 函数可以接收任意多个参数 (包括可选参数) 并且返回单个表达式的值。lambda 函数不能包含命令,包含的表达式不能超过一个。不要试图向 lambda 函数中塞入太多的东西;如果你需要更复杂的东西,应该定义一个普通函数,然后想让它多长就多长。
lambda 函数是一种风格问题。不一定非要使用它们;任何能够使用它们的地方,都可以定义一个单独的普通函数来进行替换。我将它们用在需要封装特殊的、非重用代码上,避免令我的代码充斥着大量单行函数。 |
apihelper.py 中的 lambda 函数:
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
注意这里使用了 and-or 技巧的简单形式,它是没问题的,因为 lambda 函数在布尔环境中总是为真。(这并不意味这 lambda 函数不能返回假值。这个函数对象的布尔值为真;它的返回值可以是任何东西。)
还要注意的是使用了没有参数的 split 函数。你已经看到过它带一个或者两个参数的使用,但是不带参数它按空白进行分割。
>>> s = "this is\na\ttest" >>> print s this is a test >>> print s.split() ['this', 'is', 'a', 'test'] >>> print " ".join(s.split()) 'this is a test'
这是一个多行字符串,通过使用转义字符的定义代替了三重引号。\n 是一个回车,\t 是一个制表符。 | |
不带参数的 split 按照空白进行分割。所以三个空格、一个回车和一个制表符都是一样的。 | |
通过 split 分割字符串你可以将空格统一化;然后再以单个空格作为分隔符用 join 将其重新连接起来。这也就是 info 函数将多行 doc string 合并成单行所做的事情。 |
那么 info 函数到底用这些 lambda 函数、split 函数和 and-or 技巧做了些什么呢?
processFunc 现在是一个函数,但是它到底是哪一个函数还要取决于 collapse 变量。如果 collapse 为真,processFunc(string) 将压缩空白;否则 processFunc(string) 将返回未改变的参数。
在一个不很健壮的语言中实现它,像 Visual Basic,你很有可能要创建一个函数,接受一个字符串参数和一个 collapse 参数,并使用 if 语句确定是否压缩空白,然后再返回相应的值。这种方式是低效的,因为函数可能需要处理每一种可能的情况。每次你调用它,它将不得不在给出你所想要的东西之前,判断是否要压缩空白。在 Python 中,你可以将决策逻辑拿到函数外面,而定义一个裁减过的 lambda 函数提供确切的 (唯一的) 你想要的。这种方式更为高效、更为优雅,而且很少引起那些令人讨厌 (哦,想到那些参数就头昏) 的错误。
最后一行代码是唯一还没有解释过的,它完成全部的工作。但是现在工作已经简单了,因为所需要的每件事都已经按照需求建立好了。所有的多米诺骨牌已经就位,到了将它们推倒的时候了。
下面是 apihelper.py 的关键
print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList])
注意这是一条命令,被分隔成了多行,但是并没有使用续行符 (\)。还记得我说过一些表达式可以分割成多行而不需要使用反斜线吗?列表解析就是这些表达式之一,因为整个表达式包括在方括号里。
现在,让我们从后向前分析。
for method in methodList
告诉我们这是一个列表解析。如你所知 methodList 是 object 中所有你关心的方法的一个列表。所以你正在使用 method 遍历列表。
>>> import odbchelper >>> object = odbchelper >>> method = 'buildConnectionString' >>> getattr(object, method) <function buildConnectionString at 010D6D74> >>> print getattr(object, method).__doc__ Build a connection string from a dictionary of parameters. Returns string.
在 info 函数中,object 是要得到帮助的对象,作为一个参数传入。 | |
在你遍历 methodList 时,method 是当前方法的名称。 | |
通过 getattr 函数,你可以得到 object 模块中 method 函数的引用。 | |
现在,很容易就可以打印出方法的 doc string 。 |
接下来令人困惑的是 doc string 周围 str 的使用。你可能记得,str 是一个内置函数,它可以强制将数据转化为字符串。但是一个 doc string 应该总是一个字符串,为什么还要费事地使用 str 函数呢?答案就是:不是每个函数都有 doc string ,如果没有,这个 __doc__ 属性为 None。
>>> >>> def foo(): print 2 >>> >>> foo() 2 >>> >>> foo.__doc__ >>> foo.__doc__ == None True >>> str(foo.__doc__) 'None'
在 SQL 中,你必须使用 IS NULL 代替 = NULL 进行 null 值比较。在 Python,你可以使用 == None 或者 is None 进行比较,但是 is None 更快。 |
现在你确保有了一个字符串,可以把这个字符串传给 processFunc,这个函数已经定义是一个既可以压缩空白也可以不压缩空白的函数。现在你看出来为什么使用 str 将 None 转化为一个字符串很重要了。processFunc 假设接收到一个字符串参数然后调用 split 方法,如果你传入 None ,将导致程序崩溃,因为 None 没有 split 方法。
再往回走一步,你再一次使用了字符串格式化来连接 processFunc 的返回值 和 method 的 ljust 方法的返回值。ljust 是一个你之前没有见过的新字符串方法。
>>> s = 'buildConnectionString' >>> s.ljust(30) 'buildConnectionString ' >>> s.ljust(20) 'buildConnectionString'
几乎已经完成了。有了 ljust 方法填充过的方法名称和来自调用 processFunc 方法得到的 doc string (可能压缩过),你就可以将两者连接起来并得到单个字符串。因为对 methodList 进行了映射,最终你将获得一个字符串列表。利用 "\n" 的 join 函数,将这个列表连接为单个字符串,列表中每个元素独占一行,接着打印出结果。
上述就是最后一个令人困惑的地方了。但是现在你应该已经理解这段代码了。
print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList])
apihelper.py 程序和它的输出现在应该非常清晰了。
def info(object, spacing=10, collapse=1): """Print methods and doc strings. Takes module, class, list, dictionary, or string.""" methodList = [method for method in dir(object) if callable(getattr(object, method))] processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) if __name__ == "__main__": print info.__doc__
apihelper.py 的输出:
>>> from apihelper import info >>> li = [] >>> info(li) append L.append(object) -- append object to end count L.count(value) -> integer -- return number of occurrences of value extend L.extend(list) -- extend list by appending list elements index L.index(value) -> integer -- return index of first occurrence of value insert L.insert(index, object) -- insert object before index pop L.pop([index]) -> item -- remove and return item at index (default last) remove L.remove(value) -- remove first occurrence of value reverse L.reverse() -- reverse *IN PLACE* sort L.sort([cmpfunc]) -- sort *IN PLACE*; if given, cmpfunc(x, y) -> -1, 0, 1
这一章,和此后的许多章,均讨论了面向对象的 Python 程序设计。
下面是一个完整的,可运行的 Python 程序。请阅读模块、类和函数的 doc strings,可以大概了解这个程序所做的事情和工作情况。像平时一样,不用担心你不理解的东西,这就是本章其它部分将告诉你的内容。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Framework for getting filetype-specific metadata. Instantiate appropriate class with filename. Returned object acts like a dictionary, with key-value pairs for each piece of metadata. import fileinfo info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3") print "\\n".join(["%s=%s" % (k, v) for k, v in info.items()]) Or use listDirectory function to get info on all files in a directory. for info in fileinfo.listDirectory("/music/ap/", [".mp3"]): ... Framework can be extended by adding classes for particular file types, e.g. HTMLFileInfo, MPGFileInfo, DOCFileInfo. Each class is completely responsible for parsing its files appropriately; see MP3FileInfo for example. """ import os import sys from UserDict import UserDict def stripnulls(data): "strip whitespace and nulls" return data.replace("\00", "").strip() class FileInfo(UserDict): "store file metadata" def __init__(self, filename=None): UserDict.__init__(self) self["name"] = filename class MP3FileInfo(FileInfo): "store ID3v1.0 MP3 tags" tagDataMap = {"title" : ( 3, 33, stripnulls), "artist" : ( 33, 63, stripnulls), "album" : ( 63, 93, stripnulls), "year" : ( 93, 97, stripnulls), "comment" : ( 97, 126, stripnulls), "genre" : (127, 128, ord)} def __parse(self, filename): "parse ID3v1.0 tags from MP3 file" self.clear() try: fsock = open(filename, "rb", 0) try: fsock.seek(-128, 2) tagdata = fsock.read(128) finally: fsock.close() if tagdata[:3] == "TAG": for tag, (start, end, parseFunc) in self.tagDataMap.items(): self[tag] = parseFunc(tagdata[start:end]) except IOError: pass def __setitem__(self, key, item): if key == "name" and item: self.__parse(item) FileInfo.__setitem__(self, key, item) def listDirectory(directory, fileExtList): "get list of file info objects for files of particular extensions" fileList = [os.path.normcase(f) for f in os.listdir(directory)] fileList = [os.path.join(directory, f) for f in fileList if os.path.splitext(f)[1] in fileExtList] def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): "get file info class from filename extension" subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] return hasattr(module, subclass) and getattr(module, subclass) or FileInfo return [getFileInfoClass(f)(f) for f in fileList] if __name__ == "__main__": for info in listDirectory("/music/_singles/", [".mp3"]): print "\n".join(["%s=%s" % (k, v) for k, v in info.items()]) print
下面就是从我的机器上得到的输出。你的输出将不一样,除非,由于某些令人吃惊的巧合,你与我有着共同的音乐品味。
album=
artist=Ghost in the Machine
title=A Time Long Forgotten (Concept
genre=31
name=/music/_singles/a_time_long_forgotten_con.mp3
year=1999
comment=http://mp3.com/ghostmachine
album=Rave Mix
artist=***DJ MARY-JANE***
title=HELLRAISER****Trance from Hell
genre=31
name=/music/_singles/hellraiser.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Rave Mix
artist=***DJ MARY-JANE***
title=KAIRO****THE BEST GOA
genre=31
name=/music/_singles/kairo.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Journeys
artist=Masters of Balance
title=Long Way Home
genre=31
name=/music/_singles/long_way_home1.mp3
year=2000
comment=http://mp3.com/MastersofBalan
album=
artist=The Cynic Project
title=Sidewinder
genre=18
name=/music/_singles/sidewinder.mp3
year=2000
comment=http://mp3.com/cynicproject
album=Digitosis@128k
artist=VXpanded
title=Spinning
genre=255
name=/music/_singles/spinning.mp3
year=2000
comment=http://mp3.com/artists/95/vxp
Python 有两种导入模块的方法。两种都有用,你应该知道什么时候使用哪一种方法。一种方法,import module,你已经在第 2.4 节 “万物皆对象”看过了。另一种方法完成同样的事情,但是它与第一种有着细微但重要的区别。
下面是 from module import 的基本语法:
from UserDict import UserDict
它与你所熟知的 import module 语法很相似,但是有一个重要的区别:UserDict 被直接导入到局部名字空间去了,所以它可以直接使用,而不需要加上模块名的限定。你可以导入独立的项或使用 from module import * 来导入所有东西。
Python 中的 from module import * 像 Perl 中的 use module ;Python 中的 import module 像 Perl 中的 require module 。 |
Python 中的 from module import * 像 Java 中的 import module.* ;Python 中的 import module 像 Java 中的 import module 。 |
>>> import types >>> types.FunctionType <type 'function'> >>> FunctionType Traceback (innermost last): File "<interactive input>", line 1, in ? NameError: There is no variable named 'FunctionType' >>> from types import FunctionType >>> FunctionType <type 'function'>
什么时候你应该使用 from module import?
除了这些情况,剩下的只是风格问题了,你会看到用两种方式编写的 Python 代码。
尽量少用 from module import * ,因为判定一个特殊的函数或属性是从哪来的有些困难,并且会造成调试和重构都更困难。 |
Python 是完全面向对象的:你可以定义自已的类,从自已的或内置的类继承,然后从你定义的类创建实例。
在 Python 中定义类很简单。就像定义函数,没有单独的接口定义。只要定义类,然后就可以开始编码。Python 类以保留字 class 开始,后面跟着类名。从技术上讲,有这些就够了,因为一个类并非必须从其它类继承。
在 Python 中的 pass 语句就像 Java 或 C 中的大括号空集 ({})。 |
当然,实际上大多数的类都是从其它的类继承来的,并且它们会定义自已的类方法和属性。但是就像你刚才看到的,除了名字以外,类没有什么必须要具有的。特别是,C++ 程序员可能会感到奇怪,Python 的类没有显示的构造函数和析构函数。Python 类的确存在与构造函数相似的东西:__init__ 方法。
from UserDict import UserDict class FileInfo(UserDict):
在 Python 中,类的基类只是简单地列在类名后面的小括号里。所以 FileInfo 类是从 UserDict 类 (它是从 UserDict 模块导进来的) 继承来的。UserDict 是一个像字典一样工作的类,它允许你完全子类化字典数据类型,同时增加你自已的行为。{也存在相似的类 UserList 和 UserString ,它们允许你子类化列表和字符串。)[2] 在这个类的背后有一些“巫术”,我们将在本章的后面,随着更进一步地研究 UserDict 类,揭开这些秘密。 |
在 Python 中,类的基类只是简单地列在类名后面的小括号里。不像在 Java 中有一个特殊的 extends 关键字。 |
Python 支持多重继承。在类名后面的小括号中,你可以列出许多你想要的类名,以逗号分隔。
本例演示了使用 __init__ 方法来进行 FileInfo 类的初始化。
class FileInfo(UserDict): "store file metadata" def __init__(self, filename=None):
类也可以 (并且应该) 有 doc strings ,就像方法和函数一样。 | |
__init__ 在类的实例创建后被立即调用。它可能会引诱你称之为类的构造函数,但这种说法并不正确。说它引诱,是因为它看上去像 (按照习惯,__init__ 是类中第一个定义的方法),行为也像 (在一个新创建的类实例中,它是首先被执行的代码),并且叫起来也像 (“init”当然意味着构造的本性)。说它不正确,是因为对象在调用 __init__ 时已经被构造出来了,你已经有了一个对类的新实例的有效引用。但 __init__ 是在 Python 中你可以得到的最接近构造函数的东西,并且它也扮演着非常相似的角色。 | |
每个类方法的第一个参数,包括 __init__,都是指向类的当前实例的引用。按照习惯这个参数总是被称为 self。在 __init__ 方法中,self 指向新创建的对象;在其它的类方法中,它指向方法被调用的类实例。尽管当定义方法时你需要明确指定 self,但在调用方法时,你不 用指定它,Python 会替你自动加上的。 | |
__init__ 方法可以接受任意数目的参数,就像函数一样,参数可以用缺省值定义,即可以设置成对于调用者可选。在本例中,filename 有一个缺省值 None,即 Python 的空值。 |
习惯上,任何 Python 类方法的第一个参数 (对当前实例的引用) 都叫做 self。这个参数扮演着 C++ 或 Java 中的保留字 this 的角色,但 self 在 Python 中并不是一个保留字,它只是一个命名习惯。虽然如此,也请除了 self 之外不要使用其它的名字,这是一个非常坚固的习惯。 |
当定义你自已的类方法时,你必须 明确将 self 作为每个方法的第一个参数列出,包括 __init__。当从你的类中调用一个父类的一个方法时,你必须包括 self 参数。但当你从类的外部调用你的类方法时,你不必对 self 参数指定任何值;你完全将其忽略,而 Python 会自动地替你增加实例的引用。我知道刚开始这有些混乱,它并不是自相矛盾的,因为它依靠于一个你还不了解的区别 (在绑定与非绑定方法之间),故看上去是矛盾的。
噢。我知道有很多知识需要吸收,但是你要掌握它。所有的 Python 类以相同的方式工作,所以一旦你学会了一个,就是学会了全部。如果你忘了别的任何事,也要记住这件事,因为我认定它会让你出错:
__init__ 方法是可选的,但是一旦你定义了,就必须记得显示调用父类的 __init__ 方法 (如果它定义了的话)。这样更是正确的:无论何时子类想扩展父类的行为,后代方法必须在适当的时机,使用适当的参数,显式调用父类方法。 |
在 Python 中对类进行实例化很直接。要对类进行实例化,只要调用类 (就好像它是一个函数),传入定义在 __init__ 方法中的参数。返回值将是新创建的对象。
>>> import fileinfo >>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") >>> f.__class__ <class fileinfo.FileInfo at 010EC204> >>> f.__doc__ 'store file metadata' >>> f {'name': '/music/_singles/kairo.mp3'}
你正在创建 FileInfo 类 (定义在 fileinfo 模块中) 的实例,并且将新创建的实例赋值给变量 f。你传入了一个参数,/music/_singles/kairo.mp3,它将最后作为在 FileInfo 中 __init__ 方法中的 filename 参数。 | |
每一个类的实例有一个内置属性,__class__,它是对象的类。(注意这个表示包括了在我机器上的实例的物理地址,你的表示不会一样。)Java 程序员可能对 Class 类熟悉,这个类包含了像 getName 和 getSuperclass 之类用来得到一个对象元数据信息的方法。在 Python 中,这类元数据可以直接通过对象本身的属性,像 __class__、__name__ 和 __bases__ 来得到。 | |
你可以像对函数或模块一样来访问实例的 doc string。一个类的所有实例共享相同的 doc string。 | |
还记得什么时候 __init__ 方法将它的 filename 参数赋给 self["name"] 吗?哦,答案在这。在创建类实例时你传入的参数被正确发送到 __init__ 方法中 (当我们创建类实例时,我们所传递的参数被正确地发送给 __init__ 方法 (随同一起传递的还有对象的引用,self,它是由 Python 自动添加的)。 |
在 Python 中,创建类的实例只要调用一个类,仿佛它是一个函数就行了。不像 C++ 或 Java 有一个明确的 new 操作符。 |
如果说创建一个新的实例是容易的,那么销毁它们甚至更容易。通常,不需要明确地释放实例,因为当指派给它们的变量超出作用域时,它们会被自动地释放。内存泄漏在 Python 中很少见。
>>> def leakmem(): ... f = fileinfo.FileInfo('/music/_singles/kairo.mp3') ... >>> for i in range(100): ... leakmem()
对于这种垃圾收集的方式,技术上的术语叫做“引用计数”。Python 维护着对每个实例的引用列表。在上面的例子中,只有一个 FileInfo 的实例引用:局部变量 f。当函数结束时,变量 f 超出作用域,所以引用计数降为 0,则 Python 自动销毁掉实例。
在 Python 的以前版本中,存在引用计数失败的情况,这样 Python 不能在后面进行清除。如果你创建两个实例,它们相互引用 (例如,双重链表,每一个结点有都一个指向列表中前一个和后一个结点的指针),任一个实例都不会被自动销毁,因为 Python (正确) 认为对于每个实例都存在一个引用。Python 2.0 有一种额外的垃圾回收方式,叫做“标记后清除”,它足够聪明,可以正确地清除循环引用。
作为曾经读过哲学专业的一员,让我感到困惑的是,当没有人对事物进行观察时,它们就消失了,但是这确实是在 Python 中所发生的。通常,你可以完全忘记内存管理,让 Python 在后面进行清理。
如你所见,FileInfo 是一个有着像字典一样的行为方式的类。为了进一步揭示这一点,让我们看一看在 UserDict 模块中的 UserDict 类,它是我们的 FileInfo 类的父类。它没有什么特别的,也是用 Python 写的,并且保存在一个 .py 文件里,就像我们其他的代码。特别之处在于,它保存在你的 Python 安装目录的 lib 目录下。
在 Windows 下的 ActivePython IDE 中,你可以快速打开在你的库路径中的任何模块,使用 -> (Ctrl-L)。 |
class UserDict: def __init__(self, dict=None): self.data = {} if dict is not None: self.update(dict)
注意 UserDict 是一个基类,不是从任何其他类继承而来。 | |
这就是我们在 FileInfo 类中进行了覆盖的 __init__ 方法。注意这个父类的参数列表与子类不同。很好,每个子类可以拥有自已的参数集,只要使用正确的参数调用父类就可以了。这里父类有一个定义初始值的方法 (通过在 dict 参数中传入一个字典),这一方法我们的 FileInfo 没有用上。 | |
Python 支持数据属性 (在 Java 和 Powerbuilder 中叫做 “实例变量”,在 C++ 中叫 “数据成员”),它是由某个特定的类实例所拥有的数据。在本例中,每个 UserDict 实例将拥有一个 data 数据属性。要从类外的代码引用这个属性,需要用实例的名字限定它,instance.data,限定的方法与你用模块的名字来限定函数一样。要在类的内部引用一个数据属性,我们使用 self 作为限定符。习惯上,所有的数据属性都在 __init__ 方法中初始化为有意义的值。然而,这并不是必须的,因为数据属性,像局部变量一样,当你首次赋给它值的时候突然产生。 | |
update 方法是一个字典复制器:它把一个字典中的键和值全部拷贝到另一个字典。这个操作并不 事先清空目标字典,如果一些键在目标字典中已经存在,则它们将被覆盖,那些键名在目标字典中不存在的则不改变。应该把 update 看作是合并函数,而不是复制函数。 | |
这个语法你可能以前没看过 (我还没有在这本书中的例子中用过它)。这是一条 if 语句,但是没有在下一行有一个缩近块,而只是在冒号后面,在同一行上有单条语句。这完全是合法的,它只是当你在一个块中仅有一条语句时的一个简写。(它就像在 C++ 中没有用大括号包括的单行语句。) 你可以用这种语法,或者可以在后面的行写下缩近代码,但是不能对同一个块同时用两种方式。 |
Java 和 Powerbuilder 支持通过参数列表的重载,也就是 一个类可以有同名的多个方法,但这些方法或者是参数个数不同,或者是参数的类型不同。其它语言 (最明显如 PL/SQL) 甚至支持通过参数名的重载,也就是 一个类可以有同名的多个方法,这些方法有相同类型,相同个数的参数,但参数名不同。Python 两种都不支持,总之是没有任何形式的函数重载。一个 __init__ 方法就是一个 __init__ 方法,不管它有什么样的参数。每个类只能有一个 __init__ 方法,并且如果一个子类拥有一个 __init__ 方法,它总是 覆盖父类的 __init__ 方法,甚至子类可以用不同的参数列表来定义它。 |
Python 的原作者 Guido 是这样解释方法覆盖的:“子类可以覆盖父类中的方法。因为方法没有特殊的优先级设置,父类中的一个方法在调用同类中的另一方法时,可能其实调用到的却是一个子类中覆盖父类同名方法的方法。 (C++ 程序员可能会这样想:所有的 Python 方法都是虚函数。)”如果你不明白 (它令我颇感困惑),不必在意。我想我要跳过它。[3] |
应该总是在 __init__ 方法中给一个实例的所有数据属性赋予一个初始值。这样做将会节省你在后面调试的时间,不必为捕捉因使用未初始化 (也就是不存在) 的属性而导致的 AttributeError 异常费时费力。 |
def clear(self): self.data.clear() def copy(self): if self.__class__ is UserDict: return UserDict(self.data) import copy return copy.copy(self) def keys(self): return self.data.keys() def items(self): return self.data.items() def values(self): return self.data.values()
clear 是一个普通的类方法,可以在任何时候被任何人公开调用。注意,clear 像所有的类方法一样 (常规的或专用的),使用 self 作为它的第一个参数。(记住,当你调用方法时,不用包括 self;这件事是 Python 替你做的。) 还应注意这个封装类的基本技术:将一个真正的字典 (data) 作为数据属性保存起来,定义所有真正字典所拥有的方法,并且将每个类方法重定向到真正字典上的相应方法。(你可能已经忘了,字典的 clear 方法删除它的所有关键字和关键字相应的值。) | |
真正字典的 copy 方法会返回一个新的字典,它是原始字典的原样的复制 (所有的键-值对都相同)。但是 UserDict 不能简单地重定向到 self.data.copy,因为那个方法返回一个真正的字典,而我们想要的是返回同一个类的一个新的实例,就像是 self。 | |
我们使用 __class__ 属性来查看 self 是否是一个 UserDict,如果是,太好了,因为我们知道如何拷贝一个 UserDict:只要创建一个新的 UserDict ,并传给它真正的字典,这个字典已经存放在 self.data 中了。然后你立即返回这个新的 UserDict,你甚至于不需要在下面一行中使用 import copy。 | |
如果 self.__class__ 不是 UserDict,那么 self 一定是 UserDict 的某个子类 (如可能为 FileInfo),生活总是存在意外。UserDict 不知道如何生成它的子类的一个原样的拷贝,例如,有可能在子类中定义了其它的数据属性,所以我们只能完全复制它们,确定拷贝了它们的全部内容。幸运的是,Python 带了一个模块可以正确地完成这件事,它叫做 copy。在这里我不想深入细节 (然而它是一个绝对酷的模块,你是否已经想到要自已研究它了呢?)。说 copy 能够拷贝任何 Python 对象就够了,这就是我们在这里用它的原因。 | |
其余的方法是直截了当的重定向到 self.data 的内置函数上。 |
在 Python 2.2 之前的版本中,你不可以直接子类化字符串、列表以及字典之类的内建数据类型。作为补偿,Python 提供封装类来模拟内建数据类型的行为,比如:UserString、UserList 和 UserDict。通过混合使用普通和特殊方法,UserDict 类能十分出色地模仿字典。在 Python 2.2 和其后的版本中,你可以直接从 dict 内建数据类型继承。本书 fileinfo_fromdict.py 中有这方面的一个例子。 |
如例子中所示,在 Python 中,你可以直接继承自内建数据类型 dict,这样做有三点与 UserDict 不同。
class FileInfo(dict): "store file metadata" def __init__(self, filename=None): self["name"] = filename
除了普通的类方法,Python 类还可以定义专用方法。专用方法是在特殊情况下或当使用特别语法时由 Python 替你调用的,而不是在代码中直接调用 (像普通的方法那样)。
就像你在上一节所看到的,普通的方法对在类中封装字典很有帮助。但是只有普通方法是不够的,因为除了对字典调用方法之外,还有很多事情可以做的。例如,你可以通过一种没有包括明确方法调用的语法来获得和设置数据项。这就是专用方法产生的原因:它们提供了一种方法,可以将非方法调用语法映射到方法调用上。
def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") >>> f {'name':'/music/_singles/kairo.mp3'} >>> f.__getitem__("name") '/music/_singles/kairo.mp3' >>> f["name"] '/music/_singles/kairo.mp3'
__getitem__ 专用方法很简单。像普通的方法 clear,keys 和 values 一样,它只是重定向到字典,返回字典的值。但是怎么调用它呢?哦,你可以直接调用 __getitem__,但是在实际中你其实不会那样做:我在这里执行它只是要告诉你它是如何工作的。正确地使用 __getitem__ 的方法是让 Python 来替你调用。 | |
这个看上去就像你用来得到一个字典值的语法,事实上它返回你期望的值。下面是隐藏起来的一个环节:暗地里,Python 已经将这个语法转化为 f.__getitem__("name") 的方法调用。这就是为什么 __getitem__ 是一个专用类方法的原因,不仅仅是你可以自已调用它,还可以通过使用正确的语法让 Python 来替你调用。 |
当然,Python 有一个与 __getitem__ 类似的 __setitem__ 专用方法,参见下面的例子。
def __setitem__(self, key, item): self.data[key] = item
>>> f {'name':'/music/_singles/kairo.mp3'} >>> f.__setitem__("genre", 31) >>> f {'name':'/music/_singles/kairo.mp3', 'genre':31} >>> f["genre"] = 32 >>> f {'name':'/music/_singles/kairo.mp3', 'genre':32}
__setitem__ 是一个专用类方法,因为它可以让 Python 来替你调用,但是它仍然是一个类方法。就像在 UserDict 中定义 __setitem__ 方法一样容易,我们可以在子类中重新定义它,对父类的方法进行覆盖。这就允许我们定义出在某些方面像字典一样动作的类,但是可以定义它自已的行为,超过和超出内置的字典。
这个概念是本章中我们正在学习的整个框架的基础。每个文件类型可以拥有一个处理器类,这些类知道如何从一个特殊的文类型得到元数据。只要知道了某些属性 (像文件名和位置),处理器类就知道如何自动地得到其它的属性。它的实现是通过覆盖 __setitem__ 方法,检查特别的关键字,然后当找到后加入额外的处理。
例如,MP3FileInfo 是 FileInfo 的子类。在设置了一个 MP3FileInfo 类的 name 时,并不只是设置 name 关键字 (像父类 FileInfo 所做的),它还要在文件自身内进行搜索 MP3 的标记然后填充一整套关键字。下面的例子将展示其工作方式。
def __setitem__(self, key, item): if key == "name" and item: self.__parse(item) FileInfo.__setitem__(self, key, item)
注意我们的 __setitem__ 方法严格按照与父类方法相同的形式进行定义。这一点很重要,因为 Python 将替你执行方法,而它希望这个函数用确定个数的参数进行定义。(从技术上说,参数的名字没有关系,只是个数。) | |
这里就是整个 MP3FileInfo 类的难点:如果给 name 关键字赋一个值,我们还想做些额外的事情。 | |
我们对 name 所做的额外处理封装在了 __parse 方法中。这是定义在 MP3FileInfo 中的另一个类方法,则当我们调用它时,我们用 self 对其限定。仅是调用 __parse 将只会看成定义在类外的普通方法,调用 self.__parse 将会看成定义在类中的一个类方法。这不是什么新东西,你用同样的方法来引用数据属性。 | |
在做完我们额外的处理之后,我们需要调用父类的方法。记住,在 Python 中不会自动为你完成,需手工执行。注意,我们在调用直接父类,FileInfo,尽管它没有 __setitem__ 方法。没问题,因为 Python 将会沿着父类树走,直到它找到一个拥有我们正在调用方法的类,所以这行代码最终会找到并且调用定义在 UserDict 中的 __setitem__。 |
当在一个类中存取数据属性时,你需要限定属性名:self.attribute。当调用类中的其它方法时,你属要限定方法名:self.method。 |
>>> import fileinfo >>> mp3file = fileinfo.MP3FileInfo() >>> mp3file {'name':None} >>> mp3file["name"] = "/music/_singles/kairo.mp3" >>> mp3file {'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31, 'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3', 'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'} >>> mp3file["name"] = "/music/_singles/sidewinder.mp3" >>> mp3file {'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder', 'name': '/music/_singles/sidewinder.mp3', 'year': '2000', 'comment': 'http://mp3.com/cynicproject'}
首先,我们创建了一个 MP3FileInfo 的实例,没有传递给它文件名。(我们可以不用它,因为 __init__ 方法的 filename 参数是可选的。) 因为 MP3FileInfo 没有它自已的 __init__ 方法,Python 沿着父类树走,发现了 FileInfo 的 __init__ 方法。这个 __init__ 方法手工调用了 UserDict 的 __init__ 方法,然后设置 name 关键字为 filename,它为 None,因为我们还没有传入一个文件名。所以,mp3file 最初看上去像是有一个关键字的字典,name 的值为 None。 | |
现在真正有趣的开始了。设置 mp3file 的 name 关键字触发了 MP3FileInfo 上的 __setitem__ 方法 (而不是 UserDict 的),这个方法注意到我们正在用一个真实的值来设置 name 关键字,接着调用 self.__parse。尽管我们完全还没有研究过 __parse 方法,从它的输出你可以看出,它设置了其它几个关键字:album、artist、genre、title、year 和 comment。 | |
修改 name 关键字将再次经受同样的处理过程:Python 调用 __setitem__,__setitem__调用 self.__parse,self.__parse 设置其它所有的关键字。 |
除了 __getitem__ 和 __setitem__ 之外 Python 还有更多的专用函数。某些可以让你模拟出你甚至可能不知道的功能。
下面的例子将展示 UserDict 一些其他专用方法。
def __repr__(self): return repr(self.data) def __cmp__(self, dict): if isinstance(dict, UserDict): return cmp(self.data, dict.data) else: return cmp(self.data, dict) def __len__(self): return len(self.data) def __delitem__(self, key): del self.data[key]
__repr__ 是一个专用的方法,在当调用 repr(instance) 时被调用。repr 函数是一个内置函数,它返回一个对象的字符串表示。它可以用在任何对象上,不仅仅是类的实例。你已经对 repr 相当熟悉了,尽管你不知道它。在交互式窗口中,当你只敲入一个变量名,接着按ENTER,Python 使用 repr 来显示变量的值。自已用一些数据来创建一个字典 d ,然后用 print repr(d) 来看一看吧。 | |
__cmp__ 在比较类实例时被调用。通常,你可以通过使用 == 比较任意两个 Python 对象,不只是类实例。有一些规则,定义了何时内置数据类型被认为是相等的,例如,字典在有着全部相同的关键字和值时是相等的。对于类实例,你可以定义 __cmp__ 方法,自已编写比较逻辑,然后你可以使用 == 来比较你的类,Python 将会替你调用你的 __cmp__ 专用方法。 | |
__len__ 在调用 len(instance) 时被调用。len 是一个内置函数,可以返回一个对象的长度。它可以用于任何被认为理应有长度的对象。字符串的 len 是它的字符个数;字典的 len 是它的关键字的个数;列表或序列的 len 是元素的个数。对于类实例,定义 __len__ 方法,接着自已编写长度的计算,然后调用 len(instance),Python 将替你调用你的 __len__ 专用方法。 | |
__delitem__ 在调用 del instance[key] 时调用 ,你可能记得它作为从字典中删除单个元素的方法。当你在类实例中使用 del 时,Python 替你调用 __delitem__ 专用方法。 |
在 Java 中,通过使用 str1 == str2 可以确定两个字符串变量是否指向同一块物理内存位置。这叫做对象同一性,在 Python 中写为 str1 is str2。在 Java 中要比较两个字符串值,你要使用 str1.equals(str2);在 Python 中,你要使用 str1 == str2。某些 Java 程序员,他们已经被教授得认为,正是因为在 Java 中 == 是通过同一性而不是值进行比较,所以世界才会更美好。这些人要接受 Python 的这个“严重缺失”可能要花些时间。 |
在这个地方,你可能会想,“所有这些工作只是为了在类中做一些我可以对一个内置数据类型所做的操作”。不错,如果你能够从像字典一样的内置数据类型进行继承的话,事情就容易多了 (那样整个 UserDict 类将完全不需要了)。尽管你可以这样做,专用方法仍然是有用的,因为它们可以用于任何的类,而不只是像 UserDict 这样的封装类。
专用方法意味着任何类 可以像字典一样保存键-值对,只要定义 __setitem__ 方法。任何类可以表现得像一个序列,只要定义 __getitem__ 方法。任何定义了 __cmp__ 方法的类可以用 == 进行比较。并且如果你的类表现为拥有类似长度的东西,不要定义 GetLength 方法,而定义 __len__ 方法,并使用 len(instance)。
其它的面向对象语言仅让你定义一个对象的物理模型 (“这个对象有 GetLength 方法”),而 Python 的专用类方法像 __len__ 允许你定义一个对象的逻辑模型 (“这个对象有一个长度”)。 |
Python 存在许多其它的专用方法。有一整套的专用方法,可以让类表现得象数值一样,允许你在类实例上进行加、减,以及执行其它算数操作。(关于这一点典型的例子就是表示复数的类,数值带有实数和虚数部分。) __call__ 方法让一个类表现得像一个函数,允许你直接调用一个类实例。并且存在其它的专用函数,允许类拥有只读或只写数据属性,在后面的章节中我们会更多地谈到这些。
你已经知道了数据属性,它们是被一个特定的类实例所拥有的变量。Python 也支持类属性,它们是由类本身所拥有的。
class MP3FileInfo(FileInfo): "store ID3v1.0 MP3 tags" tagDataMap = {"title" : ( 3, 33, stripnulls), "artist" : ( 33, 63, stripnulls), "album" : ( 63, 93, stripnulls), "year" : ( 93, 97, stripnulls), "comment" : ( 97, 126, stripnulls), "genre" : (127, 128, ord)}
>>> import fileinfo >>> fileinfo.MP3FileInfo <class fileinfo.MP3FileInfo at 01257FDC> >>> fileinfo.MP3FileInfo.tagDataMap {'title': (3, 33, <function stripnulls at 0260C8D4>), 'genre': (127, 128, <built-in function ord>), 'artist': (33, 63, <function stripnulls at 0260C8D4>), 'year': (93, 97, <function stripnulls at 0260C8D4>), 'comment': (97, 126, <function stripnulls at 0260C8D4>), 'album': (63, 93, <function stripnulls at 0260C8D4>)} >>> m = fileinfo.MP3FileInfo() >>> m.tagDataMap {'title': (3, 33, <function stripnulls at 0260C8D4>), 'genre': (127, 128, <built-in function ord>), 'artist': (33, 63, <function stripnulls at 0260C8D4>), 'year': (93, 97, <function stripnulls at 0260C8D4>), 'comment': (97, 126, <function stripnulls at 0260C8D4>), 'album': (63, 93, <function stripnulls at 0260C8D4>)}
在 Java 中,静态变量 (在 Python 中叫类属性) 和实例变量 (在 Python 中叫数据属性) 两者都是紧跟在类定义之后定义的 (一个有 static 关键字,一个没有)。在 Python 中,只有类属性可以定义在这里,数据属性定义在 __init__ 方法中。 |
类属性可以作为类级别的常量来使用 (这就是为什么我们在 MP3FileInfo 中使用它们),但是它们不是真正的常量。你也可以修改它们。
在 Python 中没有常量。如果你试图努力的话什么都可以改变。这一点满足 Python 的核心原则之一:坏的行为应该被克服而不是被取缔。如果你真正想改变 None 的值,也可以做到,但当无法调试的时候别来找我。 |
与大多数语言一样,Python 也有私有的概念:
与大多数的语言不同,一个 Python 函数,方法,或属性是私有还是公有,完全取决于它的名字。
如果一个 Python 函数,类方法,或属性的名字以两个下划线开始 (但不是结束),它是私有的;其它所有的都是公有的。 Python 没有类方法保护 的概念 (只能用于它们自已的类和子类中)。类方法或者是私有 (只能在它们自已的类中使用) 或者是公有 (任何地方都可使用)。
在 MP3FileInfo 中,有两个方法:__parse 和 __setitem__。正如我们已经讨论过的,__setitem__ 是一个专有方法;通常,你不直接调用它,而是通过在一个类上使用字典语法来调用,但它是公有的,并且如果有一个真正好的理由,你可以直接调用它 (甚至从 fileinfo 模块的外面)。然而,__parse 是私有的,因为在它的名字前面有两个下划线。
在 Python 中,所有的专用方法 (像 __setitem__) 和内置属性 (像 __doc__) 遵守一个标准的命名习惯:开始和结束都有两个下划线。不要对你自已的方法和属性用这种方法命名;到最后,它只会把你 (或其它人) 搞乱。 |
>>> import fileinfo >>> m = fileinfo.MP3FileInfo() >>> m.__parse("/music/_singles/kairo.mp3") Traceback (innermost last): File "<interactive input>", line 1, in ? AttributeError: 'MP3FileInfo' instance has no attribute '__parse'
实打实的对象把戏到此为止。你将在 第 12 章 中看到一个真实世界应用程序的专有类方法,它使用 getattr 创建一个到远程 Web 服务的代理。
下一章将继续使用本章的例程探索其他 Python 的概念,例如:异常、文件对象 和 for 循环。
在研究下一章之前,确保你可以无困难地完成下面的事情:
在本章中,将研究异常、文件对象、for 循环、os 和 sys 模块等内容。如果你已经在其它编程语言中使用过异常,你可以简单看看第一部分来了解 Python 的语法。但是本章其它的内容仍需仔细研读。
与许多面向对象语言一样,Python 具有异常处理,通过使用 try...except 块来实现。
Python 使用 try...except 来处理异常,使用 raise 来引发异常。Java 和 C++ 使用 try...catch 来处理异常,使用 throw 来引发异常。 |
异常在 Python 中无处不在;实际上在标准 Python 库中的每个模块都使用了它们,并且 Python 自已会在许多不同的情况下引发它们。在整本书中你已经再三看到它们了。
在这些情况下,我们都在简单地使用 Python IDE:一个错误发生了,异常被打印出来 (取决于你的 IDE,可能会有意地以一种刺眼的红色形式表示),这便是。这叫做未处理 异常;当异常被引发时,没有代码来明确地关注和处理它,所以异常被传给置在 Python 中的缺省的处理,它会输出一些调试信息并且终止运行。在 IDE 中,这不是什么大事,但是如果发生在你真正的 Python 程序运行的时候,整个程序将会终止。
然而,一个异常不一定会引起程序的完全崩溃。当异常引发时,可以被处理 掉。有时候一个异常实际是因为代码中的 bug (比如使用一个不存在的变量),但是许多时候,一个异常是可以预见的。如果你打开一个文件,它可能不存在。如果你连接一个数据库,它可能不可连接或没有访问所需的正确的安全证书。如果知道一行代码可能会引发异常,你应该使用一个 try...except 块来处理异常。
>>> fsock = open("/notthere", "r") Traceback (innermost last): File "<interactive input>", line 1, in ? IOError: [Errno 2] No such file or directory: '/notthere' >>> try: ... fsock = open("/notthere") ... except IOError: ... print "The file does not exist, exiting gracefully" ... print "This line will always print" The file does not exist, exiting gracefully This line will always print
异常可能看上去不友好 (毕竟,如果你不捕捉异常,整个程序将崩溃),但是考虑一下别的方法。你该不会希望获得一个指向不存在的文件的对象吧?不管怎么样你都得检查它的有效性,而且如果你忘记了,你的程序将会在下面某个地方给出奇怪的错误,这样你将不得不追溯到源程序。我确信你做过这种事;这可并不有趣。使用异常,一发生错误,你就可以在问题的源头通过标准的方法来处理它们。
除了处理实际的错误条件之外,对于异常还有许多其它的用处。在标准 Python 库中一个普通的用法就是试着导入一个模块,然后检查是否它能使用。导入一个并不存在的模块将引发一个 ImportError 异常。你可以使用这种方法来定义多级别的功能――依靠在运行时哪个模块是有效的,或支持多种平台 (即平台特定代码被分离到不同的模块中)。
你也能通过创建一个从内置的 Exception 类继承的类定义你自己的异常,然后使用 raise 命令引发你的异常。如果你对此感兴趣,请看进一步阅读的部分。
下面的例子演示了如何使用异常支持特定平台功能。代码来自 getpass 模块,一个从用户获得口令的封装模块。获得口令在 UNIX、Windows 和 Mac OS 平台上的实现是不同的,但是这个代码封装了所有的不同之处。
# Bind the name getpass to the appropriate function try: import termios, TERMIOS except ImportError: try: import msvcrt except ImportError: try: from EasyDialogs import AskPassword except ImportError: getpass = default_getpass else: getpass = AskPassword else: getpass = win_getpass else: getpass = unix_getpass
Python 有一个内置函数,open,用来打开在磁盘上的文件。open 返回一个文件对象,它拥有一些方法和属性,可以得到被打开文件的信息,以及对被打开文件进行操作。
>>> f = open("/music/_singles/kairo.mp3", "rb") >>> f <open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988> >>> f.mode 'rb' >>> f.name '/music/_singles/kairo.mp3'
open 方法可以接收三个参数:文件名、模式和缓冲区参数。只有第一个参数 (文件名) 是必须的;其它两个是可选的。如果没有指定,文件以文本方式打开。这里我们以二进制方式打开文件进行读取。(print open.__doc__ 会给出所有可能模式的很好的解释。) | |
open 函数返回一个对象 (到现在为止,这一点应该不会使你感到吃惊)。一个文件对象有几个有用的属性。 | |
文件对象的 mode 属性告诉你文件以何种模式被打开。 | |
文件对象的 name 属性告诉你文件对象所打开的文件名。 |
你打开文件之后,你要做的第一件事是从中读取,正如下一个例子所展示的。
>>> f <open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988> >>> f.tell() 0 >>> f.seek(-128, 2) >>> f.tell() 7542909 >>> tagData = f.read(128) >>> tagData 'TAGKAIRO****THE BEST GOA ***DJ MARY-JANE*** Rave Mix 2000http://mp3.com/DJMARYJANE \037' >>> f.tell() 7543037
打开文件消耗系统资源,并且其间其它程序可能无法访问它们 (取决于文件模式)。这就是一旦操作完毕就该关闭文件的重要所在。
>>> f <open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988> >>> f.closed False >>> f.close() >>> f <closed file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988> >>> f.closed True >>> f.seek(0) Traceback (innermost last): File "<interactive input>", line 1, in ? ValueError: I/O operation on closed file >>> f.tell() Traceback (innermost last): File "<interactive input>", line 1, in ? ValueError: I/O operation on closed file >>> f.read() Traceback (innermost last): File "<interactive input>", line 1, in ? ValueError: I/O operation on closed file >>> f.close()
文件对象的 closed 属性表示对象是打开还是关闭了文件。在本例中,文件仍然打开着 (closed 是 False)。 | |
为了关闭文件,调用文件对象的 close 方法。这样就释放掉你加在文件上的锁 (如果有的话),刷新被缓冲的系统还未写入的输出 (如果有的话),并且释放系统资源。 | |
closed 属性证实了文件被关闭了。 | |
文件被关闭了,但这并不意味着文件对象不再存在。变量 f 将继续存在,直到它超出作用域或被手工删除。然而,一旦文件被关闭,操作它的方法就没有一个能使用;它们都会引发异常。 | |
对一个文件已经关闭的文件对象调用 close 不会 引发异常,它静静地失败。 |
现在你已经足能理解前一章的例子程序 fileinfo.py 的文件处理代码了。下面这个例子展示了如何安全地打开文件和读取文件,以及优美地处理错误。
try: fsock = open(filename, "rb", 0) try: fsock.seek(-128, 2) tagdata = fsock.read(128) finally: fsock.close() . . . except IOError: pass
因为打开和读取文件有风险,并且可能引发异常,所有这些代码都用一个 try...except 块封装。(嘿,标准化的缩近不好吗?这就是你开始欣赏它的地方。) | |
open 函数可能引发 IOError 异常。(可能是文件不存在。) | |
seek 方法可能引发 IOError 异常。(可能是文件长度小于 128 字节。) | |
read 方法可能引发 IOError 异常。(可能磁盘有坏扇区,或它在一个网络驱动器上,而网络刚好断了。) | |
这是新的:一个 try...finally 块。一旦文件通过 open 函数被成功地打开,我们应该绝对保证把它关闭,即使是在 seek 或 read 方法引发了一个异常时。try...finally 块可以用来:在 finally 块中的代码将总是 被执行,甚至某些东西在 try 块中引发一个异常也会执行。可以这样考虑,不管在路上发生什么,代码都会被 “即将灭亡” 地执行。 | |
最后,处理我们的 IOError 异常。它可能是由调用 open、seek 或 read 引发的 IOError 异常。这里,我们其实不用关心,因为将要做的事就是静静地忽略它然后继续。(记住,pass 是一条不做任何事的 Python 语句。) 这样完全合法,“处理” 一个异常可以明确表示不做任何事。它仍然被认为处理过了,并且处理将正常继续,从 try...except 块的下一行代码开始。 |
正如你所期待的,你也能用与读取文件同样的方式写入文件。有两种基本的文件模式:
如果文件还不存在,任意一种模式都将自动创建文件,因此从来不需要任何复杂的逻辑:“如果 log 文件还不存在,将创建一个新的空文件,正因为如此,你可以第一次就打开它”。打开文件并开始写就可以了。
>>> logfile = open('test.log', 'w') >>> logfile.write('test succeeded') >>> logfile.close() >>> print file('test.log').read() test succeeded >>> logfile = open('test.log', 'a') >>> logfile.write('line 2') >>> logfile.close() >>> print file('test.log').read() test succeededline 2
与其它大多数语言一样,Python 也拥有 for 循环。你到现在还未曾看到它们的唯一原因就是,Python 在其它太多的方面表现出色,通常你不需要它们。
其它大多数语言没有像 Python 一样的强大的 list 数据类型,所以你需要亲自做很多事情,指定开始,结束和步长,来定义一定范围的整数或字符或其它可重复的实体。但是在 Python 中,for 循环简单地在一个列表上循环,与 list 解析的工作方式相同。
>>> li = ['a', 'b', 'e'] >>> for s in li: ... print s a b e >>> print "\n".join(li) a b e
要做一个 “通常的” (Visual Basic 标准的) 计数 for 循环也非常简单。
>>> for i in range(5): ... print i 0 1 2 3 4 >>> li = ['a', 'b', 'c', 'd', 'e'] >>> for i in range(len(li)): ... print li[i] a b c d e
正如你在 例 3.20 “连续值赋值” 所看到的,range 生成一个整数的 list,通过它来控制循环。我知道它看上去有些奇怪,但是它对计数循环偶尔 (我只是说偶尔) 会有用 。 | |
我们从来没这么用过。这是 Visual Basic 的思维风格。摆脱它吧。正确遍历 list 的方法是前面的例子所展示的。 |
for 循环不仅仅用于简单计数。它们可以遍历任何类型的东西。下面的例子是一个用 for 循环遍历 dictionary 的例子。
>>> import os >>> for k, v in os.environ.items(): ... print "%s=%s" % (k, v) USERPROFILE=C:\Documents and Settings\mpilgrim OS=Windows_NT COMPUTERNAME=MPILGRIM USERNAME=mpilgrim [...略...] >>> print "\n".join(["%s=%s" % (k, v) ... for k, v in os.environ.items()]) USERPROFILE=C:\Documents and Settings\mpilgrim OS=Windows_NT COMPUTERNAME=MPILGRIM USERNAME=mpilgrim [...略...]
os.environ 是在你的系统上所定义的环境变量的 dictionary。在 Windows 下,这些变量是可以从 MS-DOS 访问的用户和系统变量。在 UNIX 下,它们是在你的 shell 启动脚本中所 export (输出) 的变量。在 Mac OS 中,没有环境变量的概念,所以这个 dictionary 为空。 | |
os.environ.items() 返回一个 tuple 的 list:[(key1, value1), (key2, value2), ...]。for 循环对这个 list 进行遍历。第一轮,它将 key1 赋给 k ,value1 赋给 v,所以 k = USERPROFILE,v = C:\Documents and Settings\mpilgrim。第二轮,k 得到第二个键字 OS,v 得到相应的值 Windows_NT。 | |
使用多变量赋值和 list 解析,你可以使用单行语句来替换整个 for 循环。在实际的编码中是否这样做只是个人风格问题;我喜欢它是因为,将一个 dictionary 映射到一个 list,然后将 list 合并成一个字符串,这一过程显得很清晰。其它的程序员宁愿将其写成一个 for 循环。请注意在两种情况下输出是一样的,然而这一版本稍微快一些,因为它只有一条 print 语句而不是许多。 |
现在我们来看看在 第 5 章 介绍的样例程序 fileinfo.py 中 MP3FileInfo 的 for 循环 。
tagDataMap = {"title" : ( 3, 33, stripnulls), "artist" : ( 33, 63, stripnulls), "album" : ( 63, 93, stripnulls), "year" : ( 93, 97, stripnulls), "comment" : ( 97, 126, stripnulls), "genre" : (127, 128, ord)} . . . if tagdata[:3] == "TAG": for tag, (start, end, parseFunc) in self.tagDataMap.items(): self[tag] = parseFunc(tagdata[start:end])
tagDataMap 是一个类属性,它定义了我们正在一个 MP3 文件中搜索的标记。标记存储为定长字段,只要我们读出文件最后 128 个字节,那么第 3 到 32 字节总是歌曲的名字,33-62 总是歌手的名字,63-92 为专辑的名字,等等。请注意 tagDataMap 是一个 tuple 的 dictionary,每个 tuple 包含两个整数和一个函数引用。 | |
这个看上去复杂一些,但其实并非如此。这里的 for 变量结构与 items 所返回的 list 的元素的结构相匹配。记住,items 返回一个形如 (key, value) 的 tuple 的 list。list 第一个元素是 ("title", (3, 33, <function stripnulls>)),所以循环的第一轮,tag 为 "title",start 为 3,end 为 33,parseFunc 为函数 stripnulls。 | |
现在我们已经从一个单个的 MP3 标记中提取出了所有的参数,将标记数据保存起来挺容易。我们从 start 到 end 对 tagdata 进行分片,从而得到这个标记的实际数据,调用 parseFunc 对数据进行后续的处理,接着将 parseFunc 的返回值作为值赋值给伪字典 self 中的键字 tag。在遍历完 tagDataMap 中所有元素之后,self 拥有了所有标记的值,你知道看上去是什么样。 |
与其它任何 Python 的东西一样,模块也是对象。只要导入了,总可以用全局 dictionary sys.modules 来得到一个模块的引用。
>>> import sys >>> print '\n'.join(sys.modules.keys()) win32api os.path os exceptions __main__ ntpath nt sys __builtin__ site signal UserDict stat
下面的例子展示了如何使用 sys.modules。
>>> import fileinfo >>> print '\n'.join(sys.modules.keys()) win32api os.path os fileinfo exceptions __main__ ntpath nt sys __builtin__ site signal UserDict stat >>> fileinfo <module 'fileinfo' from 'fileinfo.pyc'> >>> sys.modules["fileinfo"] <module 'fileinfo' from 'fileinfo.pyc'>
下面的例子将展示通过结合使用 __module__ 类属性和 sys.modules dictionary 来获取已知类所在的模块。
>>> from fileinfo import MP3FileInfo >>> MP3FileInfo.__module__ 'fileinfo' >>> sys.modules[MP3FileInfo.__module__] <module 'fileinfo' from 'fileinfo.pyc'>
每个 Python 类都拥有一个内置的类属性 __module__,它定义了这个类的模块的名字。 | |
将它与 sys.modules 字典复合使用,你可以得到定义了某个类的模块的引用。 |
现在准备好了,看看在样例程序 第 5 章 sys.modules 介绍的 fileinfo.py 中是如何使用的。这个例子显示它的一部分代码。
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): "get file info class from filename extension" subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
这是一个有两个参数的函数;filename 是必须的,但 module 是可选的并且 module 的缺省值包含了 FileInfo 类。这样看上去效率低,因为你可能认为 Python 会在每次函数调用时计算这个 sys.modules 表达式。实际上,Python 仅会对缺省表达式计算一次,是在模块导入的第一次。正如后面我们会看到的,我们永远不会用一个 module 参数来调用这个函数,所以 module 的功能是作为一个函数级别的常量。 | |
我们会在后面再仔细研究这一行,在我们了解了 os 模块之后。那么现在,只要相信 subclass 最终为一个类的名字就行了,像 MP3FileInfo。 | |
你已经了解了 getattr,它可以通过名字得到一个对象的引用。hasattr 是一个补充性的函数,用来检查一个对象是否具有一个特定的属性;在本例中,用来检查一个模块是否有一个特别的类 (然而它可以用于任何类和任何属性,就像 getattr)。用英语来说,这行代码是说,“If this module has the class named by subclass then return it, otherwise return the base class FileInfo (如果这个模块有一个名为 subclass 的类,那么返回它,否则返回基类 FileInfo)”。 |
os.path 模块有几个操作文件和目录的函数。这里,我们看看如何操作路径名和列出一个目录的内容。
>>> import os >>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3") 'c:\\music\\ap\\mahadeva.mp3' >>> os.path.join("c:\\music\\ap", "mahadeva.mp3") 'c:\\music\\ap\\mahadeva.mp3' >>> os.path.expanduser("~") 'c:\\Documents and Settings\\mpilgrim\\My Documents' >>> os.path.join(os.path.expanduser("~"), "Python") 'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'
os.path 是一个模块的引用;使用哪一个模块要看你正运行在哪种平台上。就像 getpass 通过将 getpass 设置为一个与平台相关的函数从而封装了平台之间的不同。os 通过设置 path 封装不同的相关平台模块。 | |
os.path 的 join 函数把一个或多个部分路径名连接成一个路径名。在这个简单的例子中,它只是将字符串进行连接。(请注意在 Windows 下处理路径名是一个麻烦的事,因为反斜线字符必须被转义。) | |
在这个几乎没有价值的例子中,在将路径名加到文件名上之前,join 将在路径名后添加额外的反斜线。当发现这一点时我高兴极了,因为当用一种新的语言创建我自已的工具包时,addSlashIfNecessary 总是我必须要写的那些愚蠢的小函数之一。在 Python 中不要 写这样的愚蠢的小函数,聪明的人已经为你考虑到了。 | |
expanduser 将对使用 ~ 来表示当前用户根目录的路径名进行扩展。在任何平台上,只要用户拥有一个根目录,它就会有效,像 Windows、UNIX 和 Mac OS X,但在 Mac OS 上无效。 | |
将这些技术组合在一起,你可以容易地为在用户根目录下的目录和文件构造出路径名。 |
>>> os.path.split("c:\\music\\ap\\mahadeva.mp3") ('c:\\music\\ap', 'mahadeva.mp3') >>> (filepath, filename) = os.path.split("c:\\music\\ap\\mahadeva.mp3") >>> filepath 'c:\\music\\ap' >>> filename 'mahadeva.mp3' >>> (shortname, extension) = os.path.splitext(filename) >>> shortname 'mahadeva' >>> extension '.mp3'
split 函数对一个全路径名进行分割,返回一个包含路径和文件名的 tuple。还记得我说过你可以使用多变量赋值从一个函数返回多个值吗?对,split 就是这样一个函数。 | |
我们将 split 函数的返回值赋值给一个两个变量的 tuple。每个变量接收到返回 tuple 相对应的元素值。 | |
第一个变量,filepath,接收到从 split 返回 tuple 的第一个元素的值,文件路径。 | |
第二个变量,filename,接收到从 split 返回 tuple 的第二个元素的值,文件名。 | |
os.path 也包含了一个 splitext 函数,可以用来对文件名进行分割,并且返回一个包含了文件名和文件扩展名的 tuple。我们使用相同的技术来将它们赋值给独立的变量。 |
>>> os.listdir("c:\\music\\_singles\\") ['a_time_long_forgotten_con.mp3', 'hellraiser.mp3', 'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3', 'spinning.mp3'] >>> dirname = "c:\\" >>> os.listdir(dirname) ['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'cygwin', 'docbook', 'Documents and Settings', 'Incoming', 'Inetpub', 'IO.SYS', 'MSDOS.SYS', 'Music', 'NTDETECT.COM', 'ntldr', 'pagefile.sys', 'Program Files', 'Python20', 'RECYCLER', 'System Volume Information', 'TEMP', 'WINNT'] >>> [f for f in os.listdir(dirname) ... if os.path.isfile(os.path.join(dirname, f))] ['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'IO.SYS', 'MSDOS.SYS', 'NTDETECT.COM', 'ntldr', 'pagefile.sys'] >>> [f for f in os.listdir(dirname) ... if os.path.isdir(os.path.join(dirname, f))] ['cygwin', 'docbook', 'Documents and Settings', 'Incoming', 'Inetpub', 'Music', 'Program Files', 'Python20', 'RECYCLER', 'System Volume Information', 'TEMP', 'WINNT']
listdir 函数接收一个路径名,并返回那个目录的内容的 list。 | |
listdir 同时返回文件和文件夹,并不指出哪个是文件,哪个是文件夹。 | |
你可以使用过滤列表和 os.path 模块的 isfile 函数,从文件夹中将文件分离出来。isfile 接收一个路径名,如果路径表示一个文件,则返回 1,否则为 0。在这里,我们使用 os.path.join 来确保得到一个全路径名,但 isfile 对部分路径 (相对于当前目录) 也是有效的。你可以使用 os.getcwd() 来得到当前目录。 | |
os.path 还有一个 isdir 函数,当路径表示一个目录,则返回 1,否则为 0。你可以使用它来得到一个目录下的子目录列表。 |
def listDirectory(directory, fileExtList): "get list of file info objects for files of particular extensions" fileList = [os.path.normcase(f) for f in os.listdir(directory)] fileList = [os.path.join(directory, f) for f in fileList if os.path.splitext(f)[1] in fileExtList]
只要有可能,你就应该使用在 os 和 os.path 中的函数进行文件、目录和路径的操作。这些模块是对平台相关模块的封装模块,所以像 os.path.split 这样的函数可以工作在 UNIX、Windows、Mac OS 和 Python 所支持的任一种平台上。 |
还有一种获得目录内容的方法。它非常强大,并使用了一些你在命令行上工作时可能已经熟悉的通配符。
>>> os.listdir("c:\\music\\_singles\\") ['a_time_long_forgotten_con.mp3', 'hellraiser.mp3', 'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3', 'spinning.mp3'] >>> import glob >>> glob.glob('c:\\music\\_singles\\*.mp3') ['c:\\music\\_singles\\a_time_long_forgotten_con.mp3', 'c:\\music\\_singles\\hellraiser.mp3', 'c:\\music\\_singles\\kairo.mp3', 'c:\\music\\_singles\\long_way_home1.mp3', 'c:\\music\\_singles\\sidewinder.mp3', 'c:\\music\\_singles\\spinning.mp3'] >>> glob.glob('c:\\music\\_singles\\s*.mp3') ['c:\\music\\_singles\\sidewinder.mp3', 'c:\\music\\_singles\\spinning.mp3'] >>> glob.glob('c:\\music\\*\\*.mp3')
再一次,所有的多米诺骨牌都放好了。我们已经看过每行代码是如何工作的了。现在往回走一步,看一下放在一起是怎么样的。
def listDirectory(directory, fileExtList): "get list of file info objects for files of particular extensions" fileList = [os.path.normcase(f) for f in os.listdir(directory)] fileList = [os.path.join(directory, f) for f in fileList if os.path.splitext(f)[1] in fileExtList] def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): "get file info class from filename extension" subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] return hasattr(module, subclass) and getattr(module, subclass) or FileInfo return [getFileInfoClass(f)(f) for f in fileList]
listDirectory 是整个模块主要的有趣之处。它接收一个 dictionary (在我的例子中如 c:\music\_singles\) 和一个感兴趣的文件扩展名列表 (如 ['.mp3']),接着它返回一个类实例的 list ,这些类实例的行为像 dictionary,包含了在目录中每个感兴趣文件的元数据。并且实现起来只用了几行直观的代码。 | |
正如在前一节我们所看到的,这行代码得到一个全路径名的列表,它的元素是在 directory 中有着我们感兴趣的文件后缀 (由 fileExtList 所指定的) 的所有文件的路径名。 | |
老学校出身的 Pascal 程序员可能对嵌套函数感到熟悉,但大部分人,当我告诉他们 Python 支持嵌套函数时,都茫然地看着我。嵌套函数,从字面理解,是定义在函数内的函数。嵌套函数 getFileInfoClass 只能在定义它的函数 listDirectory 内进行调用。正如任何其它的函数一样,不需要一个接口声明或奇怪的什么东西,只要定义函数,开始编码就行了。 | |
既然你已经看过 os 模块了,这一行应该能理解了。它得到文件的扩展名 (os.path.splitext(filename)[1]),将其转换为大写字母 (.upper()),从圆点处进行分片 ([1:]),使用字符串格式化从其中生成一个类名。所以 c:\music\ap\mahadeva.mp3 变成 .mp3 再变成 MP3 再变成 MP3FileInfo。 | |
在生成完处理这个文件的处理类的名字之后,我们查阅在这个模块中是否存在这个处理类。如果存在,我们返回这个类,否则我们返回基类 FileInfo。这一点很重要:这个函数返回一个类。不是类的实例,而是类本身。 | |
对每个属于我们 “感兴趣文件” 列表 (fileList)中的文件,我们用文件名 (f) 来调用 getFileInfoClass。调用 getFileInfoClass(f) 返回一个类;我们并不知道确切是哪一个类,但是我们并不关心。接着我们创建这个类 (不管它是什么) 的一个实例,传入文件名 (又是 f) 给 __init__ 方法。正如我们在本章的前面所看到的,FileInfo 的 __init__ 方法设置了 self["name"],它将引发 __setitem__ 的调用,而 __setitem__ 在子类 (MP3FileInfo) 中被覆盖掉了,用来适当地对文件进行分析,取出文件的元数据。我们对所有感兴趣的文件进行处理,返回结果实例的一个 list。 |
请注意 listDirectory 完全是通用的。它事先不知道将得到哪种类型的文件,也不知道哪些定义好的类能够处理这些文件。它检查目录中要进行处理的文件,然后反观本身模块,了解定义了什么特别的处理类 (像 MP3FileInfo)。你可以对这个程序进行扩充,对其它类型的文件进行处理,只要用适合的名字定义类:HTMLFileInfo 用于 HTML 文件,DOCFileInfo 用于 Word .doc 文件,等等。不需要改动函数本身, listDirectory 将会对它们都进行处理,将工作交给适当的类,接着收集结果。
在 第 5 章 介绍的 fileinfo.py 程序现在应该完全理解了。
"""Framework for getting filetype-specific metadata. Instantiate appropriate class with filename. Returned object acts like a dictionary, with key-value pairs for each piece of metadata. import fileinfo info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3") print "\\n".join(["%s=%s" % (k, v) for k, v in info.items()]) Or use listDirectory function to get info on all files in a directory. for info in fileinfo.listDirectory("/music/ap/", [".mp3"]): ... Framework can be extended by adding classes for particular file types, e.g. HTMLFileInfo, MPGFileInfo, DOCFileInfo. Each class is completely responsible for parsing its files appropriately; see MP3FileInfo for example. """ import os import sys from UserDict import UserDict def stripnulls(data): "strip whitespace and nulls" return data.replace("\00", "").strip() class FileInfo(UserDict): "store file metadata" def __init__(self, filename=None): UserDict.__init__(self) self["name"] = filename class MP3FileInfo(FileInfo): "store ID3v1.0 MP3 tags" tagDataMap = {"title" : ( 3, 33, stripnulls), "artist" : ( 33, 63, stripnulls), "album" : ( 63, 93, stripnulls), "year" : ( 93, 97, stripnulls), "comment" : ( 97, 126, stripnulls), "genre" : (127, 128, ord)} def __parse(self, filename): "parse ID3v1.0 tags from MP3 file" self.clear() try: fsock = open(filename, "rb", 0) try: fsock.seek(-128, 2) tagdata = fsock.read(128) finally: fsock.close() if tagdata[:3] == "TAG": for tag, (start, end, parseFunc) in self.tagDataMap.items(): self[tag] = parseFunc(tagdata[start:end]) except IOError: pass def __setitem__(self, key, item): if key == "name" and item: self.__parse(item) FileInfo.__setitem__(self, key, item) def listDirectory(directory, fileExtList): "get list of file info objects for files of particular extensions" fileList = [os.path.normcase(f) for f in os.listdir(directory)] fileList = [os.path.join(directory, f) for f in fileList if os.path.splitext(f)[1] in fileExtList] def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): "get file info class from filename extension" subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] return hasattr(module, subclass) and getattr(module, subclass) or FileInfo return [getFileInfoClass(f)(f) for f in fileList] if __name__ == "__main__": for info in listDirectory("/music/_singles/", [".mp3"]): print "\n".join(["%s=%s" % (k, v) for k, v in info.items()]) print
在研究下一章之前,确保你可以无困难地完成下面的事情:
正则表达式是搜索、替换和解析复杂字符模式的一种强大而标准的方法。如果你曾经在其他语言 (如 Perl) 中使用过它,由于它们的语法非常相似,你仅仅阅读一下 re 模块的摘要,大致了解其中可用的函数和参数就可以了。
字符串也有很多方法,可以进行搜索 (index、find 和 count)、替换 (replace) 和解析 (split),但它们仅限于处理最简单的情况。搜索方法查找单个和固定编码的子串,并且它们总是大小写敏感的。对一个字符串s,如果要进行大小写不敏感的搜索,则你必须调用 s.lower() 或 s.upper() 将 s 转换成全小写或者全大写,然后确保搜索串有着相匹配的大小写。replace 和 split方法有着类似的限制。
如果你要解决的问题利用字符串函数能够完成,你应该使用它们。它们快速、简单且容易阅读,而快速、简单、可读性强的代码可以说出很多好处。但是,如果你发现你使用了许多不同的字符串函数和 if 语句来处理一个特殊情况,或者你组合使用了 split、join 等函数而导致用一种奇怪的甚至读不下去的方式理解列表,此时,你也许需要转到正则表达式了。
尽管正则表达式语法较之普通代码相对麻烦一些,但是却可以得到更可读的结果,与用一长串字符串函数的解决方案相比要好很多。在正则表达式内部有多种方法嵌入注释,从而使之具有自文档化 (self-documenting) 的能力。
这一系列的例子是由我几年前日常工作中的现实问题启发而来的,当时我需要从一个老化系统中导出街道地址,在将它们导入新的系统之前,进行清理和标准化。(看,我不是只将这些东西堆到一起,它有实际的用处。)这个例子展示我如何处理这个问题。
>>> s = '100 NORTH MAIN ROAD' >>> s.replace('ROAD', 'RD.') '100 NORTH MAIN RD.' >>> s = '100 NORTH BROAD ROAD' >>> s.replace('ROAD', 'RD.') '100 NORTH BRD. RD.' >>> s[:-4] + s[-4:].replace('ROAD', 'RD.') '100 NORTH BROAD RD.' >>> import re >>> re.sub('ROAD$', 'RD.', s) '100 NORTH BROAD RD.'
继续我的清理地址的故事。很快我发现,在上面的例子中,仅仅匹配地址末尾的 'ROAD' 不是很好,因为不是所有的地址都包括表示街道的单词 ('ROAD');有一些直接以街道名结尾。大部分情况下,不会遇到这种情况,但是,如果街道名称为 'BROAD',那么正则表达式将会匹配 'BROAD' 的一部分为 'ROAD',而这并不是我想要的。
>>> s = '100 BROAD' >>> re.sub('ROAD$', 'RD.', s) '100 BRD.' >>> re.sub('\\bROAD$', 'RD.', s) '100 BROAD' >>> re.sub(r'\bROAD$', 'RD.', s) '100 BROAD' >>> s = '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD$', 'RD.', s) '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD\b', 'RD.', s) '100 BROAD RD. APT 3'
你可能经常看到罗马数字,即使你没有意识到它们。你可能曾经在老电影或者电视中看到它们 (“版权所有 MCMXLVI” 而不是 “版权所有1946”),或者在某图书馆或某大学的贡献墙上看到它们 (“成立于 MDCCCLXXXVIII”而不是“成立于1888”)。你也可能在某些文献的大纲或者目录上看到它们。这是一个表示数字的系统,它实际上能够追溯到远古的罗马帝国 (因此而得名)。
在罗马数字中,利用7个不同字母进行重复或者组合来表达各式各样的数字。
下面是关于构造罗马数字的一些通用的规则的介绍:
怎样校验任意一个字符串是否为一个有效的罗马数字呢?我们每次只看一位数字,由于罗马数字一般是从高位到低位书写。我们从高位开始:千位。对于大于或等于 1000 的数字,千位由一系列的字符 M 表示。
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') <SRE_Match object at 0106FB58> >>> re.search(pattern, 'MM') <SRE_Match object at 0106C290> >>> re.search(pattern, 'MMM') <SRE_Match object at 0106AA38> >>> re.search(pattern, 'MMMM') >>> re.search(pattern, '') <SRE_Match object at 0106F4A8>
与千位数相比,百位数识别起来要困难得多,这是因为有多种相互独立的表达方式都可以表达百位数,而具体用那种方式表达和具体的数值有关。
因此有四种可能的模式:
后面两个模式可以结合到一起:
这个例子显示如何有效地识别罗马数字的百位数。
>>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' >>> re.search(pattern, 'MCM') <SRE_Match object at 01070390> >>> re.search(pattern, 'MD') <SRE_Match object at 01073A50> >>> re.search(pattern, 'MMMCCC') <SRE_Match object at 010748A8> >>> re.search(pattern, 'MCMC') >>> re.search(pattern, '') <SRE_Match object at 01071D98>
哎呀!看看正则表达式能够多快变得难以理解?你仅仅表示了罗马数字的千位和百位上的数字。如果你根据类似的方法,十位数和各位数就非常简单了,因为是完全相同的模式。让我们来看表达这个模式的另一种方式吧。
在前面的章节,你处理了相同字符可以重复三次的情况。在正则表达式中,有另外一个方式来表达这种情况,并且能提高代码的可读性。首先看看我们在前面的例子中使用的方法。
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') <_sre.SRE_Match object at 0x008EE090> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MM') <_sre.SRE_Match object at 0x008EEB48> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MMM') <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMMM') >>>
>>> pattern = '^M{0,3}$' >>> re.search(pattern, 'M') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MM') <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMM') <_sre.SRE_Match object at 0x008EEDA8> >>> re.search(pattern, 'MMMM') >>>
没有一个轻松的方法来确定两个正则表达式是否等价。你能采用的最好的办法就是列出很多的测试样例,确定这两个正则表达式对所有的相关输入都有相同的输出。在本书后面的章节,将更多地讨论如何编写测试样例。 |
现在我们来扩展一下关于罗马数字的正则表达式,以匹配十位数和个位数,下面的例子展示十位数的校验方法。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$' >>> re.search(pattern, 'MCMXL') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCML') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLX') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXX') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXX') >>>
对于个位数的正则表达式有类似的表达方式,我将省略细节,直接展示结果。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
用另一种 {n,m} 语法表达这个正则表达式会如何呢?这个例子展示新的语法。
>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$' >>> re.search(pattern, 'MDLV') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMDCLXVI') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMDCCCLXXXVIII') <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'I') <_sre.SRE_Match object at 0x008EEB48>
如果你在第一遍就跟上并理解了所讲的这些,那么你做的比我还要好。现在,你可以尝试着理解别人大规模程序里关键函数中的正则表达式了。或者想象着几个月后回头理解你自己的正则表达式。我曾经做过这样的事情,但是它并不是那么有趣。
在下一节里,你将会研究另外一种正则表达式语法,它可以使你的表达式具有更好的可维持性。
迄今为止,你只是处理过被我称之为“紧凑”类型的正则表达式。正如你曾看到的,它们难以阅读,即使你清楚正则表达式的含义,你也不能保证六个月以后你还能理解它。你真正所需的就是利用内联文档 (inline documentation)。
Python 允许用户利用所谓的松散正则表达式 来完成这个任务。一个松散正则表达式和一个紧凑正则表达式主要区别表现在两个方面:
用一个例子可以解释得更清楚。让我们重新来看前面的紧凑正则表达式,利用松散正则表达式重新表达。下面的例子显示实现方法。
>>> pattern = """ ^ # beginning of string M{0,3} # thousands - 0 to 3 M's (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), # or 500-800 (D, followed by 0 to 3 C's) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), # or 50-80 (L, followed by 0 to 3 X's) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), # or 5-8 (V, followed by 0 to 3 I's) $ # end of string """ >>> re.search(pattern, 'M', re.VERBOSE) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'M')
当使用松散正则表达式时,最重要的一件事情就是:必须传递一个额外的参数 re.VERBOSE,该参数是定义在 re 模块中的一个常量,标志着待匹配的正则表达式是一个松散正则表达式。正如你看到的,这个模式中,有很多空格 (所有的空格都被忽略),和几个注释 (所有的注释也被忽略)。如果忽略所有的空格和注释,它就和前面章节里的正则表达式完全相同,但是具有更好的可读性。 | |
这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的一个,接着匹配 CM,接着是字符 L 和三个可选 X 字符的所有字符,接着是 IX,然后是字符串的结尾。 | |
这个模式匹配字符串的开始,接着是三个可选的 M 字符的所有字符,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中所有字符,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中所有字符,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中所有字符,接着匹配字符串的结尾。 | |
这个没有匹配。为什么呢?因为没有 re.VERBOSE 标记,所以 re.search 函数把模式作为一个紧凑正则表达式进行匹配。Python 不能自动检测一个正则表达式是为松散类型还是紧凑类型。Python 默认每一个正则表达式都是紧凑类型的,除非你显式地标明一个正则表达式为松散类型。 |
迄今为止,你主要是匹配整个模式,不论是匹配上,还是没有匹配上。但是正则表达式还有比这更为强大的功能。当一个模式确实 匹配上时,你可以获取模式中特定的片断,你可以发现具体匹配的位置。
这个例子来源于我遇到的另一个现实世界的问题,也是在以前的工作中遇到的。问题是:解析一个美国电话号码。客户要能 (在一个单一的区域中) 输入任何数字,然后存储区号、干线号、电话号和一个可选的独立的分机号到公司数据库里。为此,我通过网络找了很多正则表达式的例子,但是没有一个能够完全满足我的要求。
这里列举了我必须能够接受的电话号码:
格式可真够多的!我需要知道区号是 800,干线号是 555,电话号的其他数字为 1212。对于那些有分机号的,我需要知道分机号为 1234。
让我们完成电话号码解析这个工作,这个例子展示第一步。
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') >>> phonePattern.search('800-555-1212').groups() ('800', '555', '1212') >>> phonePattern.search('800-555-1212-1234') >>>
我们通常从左到右阅读正则表达式。这个正则表达式匹配字符串的开始,接着匹配 (\d{3})。\d{3} 是什么呢?好吧,{3} 的含义是“精确匹配三个数字”;这是曾在前面见到过的 {n,m} 语法的一种变形。\d 的含义是 “任何一个数字” (0 到 9)。把它们放大括号中意味着要“精确匹配三个数字位,接着把它们作为一个组保存下来,以便后面的调用”。接着匹配一个连字符,接着是另外一个精确匹配三个数字位的组,接着另外一个连字符,接着另外一个精确匹配四个数字为的组,接着匹配字符串的结尾。 | |
为了访问正则表达式解析过程中记忆下来的多个组,我们使用 search 函数返回对象的 groups() 函数。这个函数将返回一个元组,元组中的元素就是正则表达式中定义的组。在这个例子中,定义了三个组,第一个组有三个数字位,第二个组有三个数字位,第三个组有四个数字位。 | |
这个正则表达式不是最终的答案,因为它不能处理在电话号码结尾有分机号的情况,为此,我们需要扩展这个正则表达式。 |
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') >>> phonePattern.search('800-555-1212-1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800 555 1212 1234') >>> >>> phonePattern.search('800-555-1212') >>>
下一个例子展示正则表达式处理一个电话号码内部,采用不同分隔符的情况。
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') >>> phonePattern.search('800 555 1212 1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212-1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('80055512121234') >>> >>> phonePattern.search('800-555-1212') >>>
下一个例子展示正则表达式处理没有 分隔符的电话号码的情况。
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') >>> phonePattern.search('80055512121234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800.555.1212 x1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() ('800', '555', '1212', '') >>> phonePattern.search('(800)5551212 x1234') >>>
下一个例子展示如何解决电话号码前面有其他字符的情况。
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') >>> phonePattern.search('(800)5551212 ext. 1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() ('800', '555', '1212', '') >>> phonePattern.search('work 1-(800) 555.1212 #1234') >>>
让我们往回看一下。迄今为止,正则表达式总是从一个字符串的开始匹配。但是现在你看到了,有很多不确定的情况需要你忽略。与其尽力全部匹配它们,还不如全部跳过它们,让我们采用一个不同的方法:根本不显式地匹配字符串的开始。下面的这个例子展示这个方法。
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') ('800', '555', '1212', '') >>> phonePattern.search('80055512121234') ('800', '555', '1212', '1234')
看看一个正则表达式能够失控得多快?回头看看前面的例子,你还能区别它们么?
当你还能够理解这个最终答案的时候 (这个正则表达式就是最终答案,即使你发现一种它不能处理的情况,我也真的不想知道它了),在你忘记为什么你这么选择之前,让我们把它写成松散正则表达式的形式。
>>> phonePattern = re.compile(r''' # don't match beginning of string, number can start anywhere (\d{3}) # area code is 3 digits (e.g. '800') \D* # optional separator is any number of non-digits (\d{3}) # trunk is 3 digits (e.g. '555') \D* # optional separator (\d{4}) # rest of number is 4 digits (e.g. '1212') \D* # optional separator (\d*) # extension is optional and can be any number of digits $ # end of string ''', re.VERBOSE) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') ('800', '555', '1212', '')
这只是正则表达式能够完成工作的很少一部分。换句话说,即使你现在备受打击,相信我,你也不是什么也没见过了。
现在,你应该熟悉下列技巧:
正则表达式非常强大,但是它并不能为每一个问题提供正确的解决方案。你应该学习足够多的知识,以辨别什么时候它们是合适的,什么时候它们会解决你的问题,什么时候它们产生的问题比要解决的问题还要多。
一些人,遇到一个问题时就想:“我知道,我将使用正则表达式。”现在他有两个问题了。 |
||
--Jamie Zawinski, in comp.emacs.xemacs |
我经常在 comp.lang.python 上看到关于如下的问题: “ 怎么才能从我的 HTML 文档中列出所有的 [头|图像|链接] 呢?” “怎么才能 [分析|解释|munge] 我的 HTML 文档的文本,但是又要保留标记呢?” “怎么才能一次给我所有的 HTML 标记 [增加|删除|加引号] 属性呢?” 本章将回答所有这些问题。
下面给出一个完整的,可工作的 Python 程序,它分为两部分。第一部分,BaseHTMLProcessor.py 是一个通用工具,它可以通过扫描标记和文本块来帮助您处理 HTML 文件。第二部分,dialect.py 是一个例子,演示了如何使用 BaseHTMLProcessor.py 来转化 HTML 文档,保留文本但是去掉了标记。阅读文档字符串 (doc string) 和注释来了解将要发生事情的概况。大部分内容看上去像巫术,因为任一个这些类的方法是如何调用的不是很清楚。不要紧,所有内容都会按进度被逐步地展示出来。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
from sgmllib import SGMLParser import htmlentitydefs class BaseHTMLProcessor(SGMLParser): def reset(self): # extend (called by SGMLParser.__init__) self.pieces = [] SGMLParser.reset(self) def unknown_starttag(self, tag, attrs): # called for each start tag # attrs is a list of (attr, value) tuples # e.g. for <pre class="screen">, tag="pre", attrs=[("class", "screen")] # Ideally we would like to reconstruct original tag and attributes, but # we may end up quoting attribute values that weren't quoted in the source # document, or we may change the type of quotes around the attribute value # (single to double quotes). # Note that improperly embedded non-HTML code (like client-side Javascript) # may be parsed incorrectly by the ancestor, causing runtime script errors. # All non-HTML code must be enclosed in HTML comment tags (<!-- code -->) # to ensure that it will pass through this parser unaltered (in handle_comment). strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) def unknown_endtag(self, tag): # called for each end tag, e.g. for </pre>, tag will be "pre" # Reconstruct the original end tag. self.pieces.append("</%(tag)s>" % locals()) def handle_charref(self, ref): # called for each character reference, e.g. for " ", ref will be "160" # Reconstruct the original character reference. self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): # called for each entity reference, e.g. for "©", ref will be "copy" # Reconstruct the original entity reference. self.pieces.append("&%(ref)s" % locals()) # standard HTML entities are closed with a semicolon; other entities are not if htmlentitydefs.entitydefs.has_key(ref): self.pieces.append(";") def handle_data(self, text): # called for each block of plain text, i.e. outside of any tag and # not containing any character or entity references # Store the original text verbatim. self.pieces.append(text) def handle_comment(self, text): # called for each HTML comment, e.g. <!-- insert Javascript code here --> # Reconstruct the original comment. # It is especially important that the source document enclose client-side # code (like Javascript) within comments so it can pass through this # processor undisturbed; see comments in unknown_starttag for details. self.pieces.append("<!--%(text)s-->" % locals()) def handle_pi(self, text): # called for each processing instruction, e.g. <?instruction> # Reconstruct original processing instruction. self.pieces.append("<?%(text)s>" % locals()) def handle_decl(self, text): # called for the DOCTYPE, if present, e.g. # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" # "http://www.w3.org/TR/html4/loose.dtd"> # Reconstruct original DOCTYPE self.pieces.append("<!%(text)s>" % locals()) def output(self): """Return processed HTML as a single string""" return "".join(self.pieces)
import re from BaseHTMLProcessor import BaseHTMLProcessor class Dialectizer(BaseHTMLProcessor): subs = () def reset(self): # extend (called from __init__ in ancestor) # Reset all data attributes self.verbatim = 0 BaseHTMLProcessor.reset(self) def start_pre(self, attrs): # called for every <pre> tag in HTML source # Increment verbatim mode count, then handle tag like normal self.verbatim += 1 self.unknown_starttag("pre", attrs) def end_pre(self): # called for every </pre> tag in HTML source # Decrement verbatim mode count self.unknown_endtag("pre") self.verbatim -= 1 def handle_data(self, text): # override # called for every block of text in HTML source # If in verbatim mode, save text unaltered; # otherwise process the text with a series of substitutions self.pieces.append(self.verbatim and text or self.process(text)) def process(self, text): # called from handle_data # Process text block by performing series of regular expression # substitutions (actual substitions are defined in descendant) for fromPattern, toPattern in self.subs: text = re.sub(fromPattern, toPattern, text) return text class ChefDialectizer(Dialectizer): """convert HTML to Swedish Chef-speak based on the classic chef.x, copyright (c) 1992, 1993 John Hagerman """ subs = ((r'a([nu])', r'u\1'), (r'A([nu])', r'U\1'), (r'a\B', r'e'), (r'A\B', r'E'), (r'en\b', r'ee'), (r'\Bew', r'oo'), (r'\Be\b', r'e-a'), (r'\be', r'i'), (r'\bE', r'I'), (r'\Bf', r'ff'), (r'\Bir', r'ur'), (r'(\w*?)i(\w*?)$', r'\1ee\2'), (r'\bow', r'oo'), (r'\bo', r'oo'), (r'\bO', r'Oo'), (r'the', r'zee'), (r'The', r'Zee'), (r'th\b', r't'), (r'\Btion', r'shun'), (r'\Bu', r'oo'), (r'\BU', r'Oo'), (r'v', r'f'), (r'V', r'F'), (r'w', r'w'), (r'W', r'W'), (r'([a-z])[.]', r'\1. Bork Bork Bork!')) class FuddDialectizer(Dialectizer): """convert HTML to Elmer Fudd-speak""" subs = ((r'[rl]', r'w'), (r'qu', r'qw'), (r'th\b', r'f'), (r'th', r'd'), (r'n[.]', r'n, uh-hah-hah-hah.')) class OldeDialectizer(Dialectizer): """convert HTML to mock Middle English""" subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'), (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'), (r'ick\b', r'yk'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'), (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'), (r'([aeiou])re\b', r'\1r'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'), (r'tion\b', r'cioun'), (r'ion\b', r'ioun'), (r'aid', r'ayde'), (r'ai', r'ey'), (r'ay\b', r'y'), (r'ay', r'ey'), (r'ant', r'aunt'), (r'ea', r'ee'), (r'oa', r'oo'), (r'ue', r'e'), (r'oe', r'o'), (r'ou', r'ow'), (r'ow', r'ou'), (r'\bhe', r'hi'), (r've\b', r'veth'), (r'se\b', r'e'), (r"'s\b", r'es'), (r'ic\b', r'ick'), (r'ics\b', r'icc'), (r'ical\b', r'ick'), (r'tle\b', r'til'), (r'll\b', r'l'), (r'ould\b', r'olde'), (r'own\b', r'oune'), (r'un\b', r'onne'), (r'rry\b', r'rye'), (r'est\b', r'este'), (r'pt\b', r'pte'), (r'th\b', r'the'), (r'ch\b', r'che'), (r'ss\b', r'sse'), (r'([wybdp])\b', r'\1e'), (r'([rnt])\b', r'\1\1e'), (r'from', r'fro'), (r'when', r'whan')) def translate(url, dialectName="chef"): """fetch URL and translate using dialect dialect in ("chef", "fudd", "olde")""" import urllib sock = urllib.urlopen(url) htmlSource = sock.read() sock.close() parserName = "%sDialectizer" % dialectName.capitalize() parserClass = globals()[parserName] parser = parserClass() parser.feed(htmlSource) parser.close() return parser.output() def test(url): """test all dialects against URL""" for dialect in ("chef", "fudd", "olde"): outfile = "%s.html" % dialect fsock = open(outfile, "wb") fsock.write(translate(url, dialect)) fsock.close() import webbrowser webbrowser.open_new(outfile) if __name__ == "__main__": test("http://diveintopython.org/odbchelper_list.html")
运行这个脚本会将 第 3.2 节 “List 介绍” 转换成模仿瑞典厨师用语 (mock Swedish Chef-speak) (来自 The Muppets)、模仿埃尔默唠叨者用语 (mock Elmer Fudd-speak) (来自 Bugs Bunny 卡通画) 和模仿中世纪英语 (mock Middle English) (零散地来源于乔叟的《坎特伯雷故事集》)。如果您查看输出页面的 HTML 源代码,您会发现所有的 HTML 标记和属性没有改动,但是在标记之间的文本被转换成模仿语言了。如果您观查得更仔细些,您会发现,实际上,仅有标题和段落被转换了;代码列表和屏幕例子没有改动。
<div class="abstract"> <p>Lists awe <span class="application">Pydon</span>'s wowkhowse datatype. If youw onwy expewience wif wists is awways in <span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow <span class="application">Pydon</span> wists.</p> </div>
HTML 处理分成三步:将 HTML 分解成它的组成片段,对片段进行加工,接着将片段再重新合成 HTML。第一步是通过 sgmllib.py 来完成的,它是标准 Python 库的一部分。
理解本章的关键是要知道 HTML 不只是文本,更是结构化文本。这种结构来源于开始与结束标记的或多或少分级序列。通常您并不以这种方式处理 HTML ,而是以文本方式 在一个文本编辑中对其进行处理,或以可视的方式 在一个浏览器中进行浏览或页面编辑工具中进行编辑。sgmllib.py 表现出了 HTML 的结构。
sgmllib.py 包含一个重要的类:SGMLParser。SGMLParser 将 HTML 分解成有用的片段,比如开始标记和结束标记。在它成功地分解出某个数据为一个有用的片段后,它会根据所发现的数据,调用一个自身内部的方法。为了使用这个分析器,您需要子类化 SGMLParser 类,并且覆盖这些方法。这就是当我说它表示了 HTML 结构 的意思:HTML 的结构决定了方法调用的次序和传给每个方法的参数。
SGMLParser 将 HTML 分析成 8 类数据,然后对每一类调用单独的方法:
Python 2.0 存在一个 bug,即 SGMLParser 完全不能识别声明 (handle_decl 永远不会调用),这就意味着 DOCTYPE 被静静地忽略掉了。这个错误在 Python 2.1 中改正了。 |
sgmllib.py 所附带的一个测试套件举例说明了这一点。您可以运行 sgmllib.py,在命令行下传入一个 HTML 文件的名字,然后它会在分析标记和其它元素的同时将它们打印出来。它的实现是通过子类化 SGMLParser 类,然后定义 unknown_starttag,unknown_endtag,handle_data 和其它方法来实现的。这些方法简单地打印出它们的参数。
在 Windows 下的 ActivePython IDE 中,您可以在 “Run script” 对话框中指定命令行参数。用空格将多个参数分开。 |
下面是一个片段,来自本书的 HTML 版本的目录,toc.html。当然,您的存储路径可能与我的有所不同。 (如果您还没有下载本书的 HTML 版本,可以从 http://diveintopython.org/ 下载。
c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html"
<!DOCTYPE html
PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Dive Into Python</title>
<link rel="stylesheet" href="diveintopython.css" type="text/css">
... 略 ...
通过 sgmllib.py 的测试套件来运行它,会得到如下的输出结果:
c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html" data: '\n\n' start tag: <html lang="en" > data: '\n ' start tag: <head> data: '\n ' start tag: <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" > data: '\n \n ' start tag: <title> data: 'Dive Into Python' end tag: </title> data: '\n ' start tag: <link rel="stylesheet" href="diveintopython.css" type="text/css" > data: '\n ' ... 略 ...
下面是本章其它部分的路标:
继续阅读本章,您还可以学习到有关 locals、globals 和基于 dictionary 的字符串格式化的内容。
为了从 HTML 文档中提取数据,将 SGMLParser 类进行子类化,然后对想要捕捉的标记或实体定义方法。
从 HTML 文档中提取数据的第一步是得到某个 HTML 文件。如果在您的硬盘里存放着 HTML 文件,您可以使用处理文件的函数将它读出来,但是真正有意思的是从实际的网页得到 HTML。
>>> import urllib >>> sock = urllib.urlopen("http://diveintopython.org/") >>> htmlSource = sock.read() >>> sock.close() >>> print htmlSource <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head> <meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'> <title>Dive Into Python</title> <link rel='stylesheet' href='diveintopython.css' type='text/css'> <link rev='made' href='mailto:mark@diveintopython.org'> <meta name='keywords' content='Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free'> <meta name='description' content='a free Python tutorial for experienced programmers'> </head> <body bgcolor='white' text='black' link='#0000FF' vlink='#840084' alink='#0000FF'> <table cellpadding='0' cellspacing='0' border='0' width='100%'> <tr><td class='header' width='1%' valign='top'>diveintopython.org</td> <td width='99%' align='right'><hr size='1' noshade></td></tr> <tr><td class='tagline' colspan='2'>Python for experienced programmers</td></tr> [...略...]
urllib 模块是标准 Python 库的一部分。它包含了一些函数,可以从基于互联网的 URL (主要指网页) 来获取信息并且真正取回数据。 | |
urllib 模块最简单的使用是提取用 urlopen 函数取回的网页的整个文本。打开一个 URL 同打开一个文件相似。urlopen 的返回值是像文件一样的对象,它具有一个文件对象一样的方法。 | |
使用由 urlopen 所返回的类文件对象所能做的最简单的事情就是 read,它可以将网页的整个 HTML 读到一个字符串中。这个对象也支持 readlines 方法,这个方法可以将文本按行放入一个列表中。 | |
当用完这个对象,要确保将它 close,就如同一个普通的文件对象。 | |
现在我们将 http://diveintopython.org/ 主页的完整的 HTML 保存在一个字符串中了,接着我们将分析它。 |
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
from sgmllib import SGMLParser class URLLister(SGMLParser): def reset(self): SGMLParser.reset(self) self.urls = [] def start_a(self, attrs): href = [v for k, v in attrs if k=='href'] if href: self.urls.extend(href)
reset 由 SGMLParser 的 __init__ 方法来调用,也可以在创建一个分析器实例时手工来调用。所以如果您需要做初始化,在 reset 中去做,而不要在 __init__ 中做。这样当某人重用一个分析器实例时,可以正确地重新初始化。 | |
只要找到一个 <a> 标记,start_a 就会由 SGMLParser 进行调用。这个标记可以包含一个 href 属性,或者包含其它的属性,如 name 或 title。attrs 参数是一个 tuple 的 list,[(attribute, value), (attribute, value), ...]。或者它可以只是一个有效的 HTML 标记 <a> (尽管无用),这时 attrs 将是个空 list。 | |
我们可以通过一个简单的多变量 list 映射来查找这个 <a> 标记是否拥有一个 href 属性。 | |
像 k=='href' 的字符串比较是区分大小写的,但是这里是安全的。因为 SGMLParser 会在创建 attrs 时将属性名转化为小写。 |
>>> import urllib, urllister >>> usock = urllib.urlopen("http://diveintopython.org/") >>> parser = urllister.URLLister() >>> parser.feed(usock.read()) >>> usock.close() >>> parser.close() >>> for url in parser.urls: print url toc/index.html #download #languages toc/index.html appendix/history.html download/diveintopython-html-5.0.zip download/diveintopython-pdf-5.0.zip download/diveintopython-word-5.0.zip download/diveintopython-text-5.0.zip download/diveintopython-html-flat-5.0.zip download/diveintopython-xml-5.0.zip download/diveintopython-common-5.0.zip ...略...
调用定义在 SGMLParser 中的 feed 方法,将 HTML 内容放入分析器中。 [4] 这个方法接收一个字符串,这个字符串就是 usock.read() 所返回的。 | |
像处理文件一样,一旦处理完毕,您应该 close 您的 URL 对象。 | |
您也应该 close 您的分析器对象,但出于不同的原因。feed 方法不保证对传给它的全部 HTML 进行处理,它可能会对其进行缓冲处理,等待接收更多的内容。只要没有更多的内容,就应调用 close 来刷新缓冲区,并且强制所有内容被完全处理。 | |
一旦分析器被 close,分析过程也就结束了。parser.urls 中包含了在 HTML 文档中所有的链接 URL。(如果当您读到此处发现输出结果不一样,那是因为下载了本书的更新版本。) |
SGMLParser 自身不会产生任何结果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用相应的一个方法,但是这些方法什么都不做。SGMLParser 是一个 HTML 消费者 (consumer):它接收 HTML,将其分解成小的、结构化的小块。正如您所看到的,在前一节中,您可以定义 SGMLParser 的子类,它可以捕捉特别标记和生成有用的东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser 所丢出来的所有东西的一个类,接着重建整个 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者 (producer)。
BaseHTMLProcessor 子类化 SGMLParser,并且提供了全部的 8 个处理方法:unknown_starttag、unknown_endtag、handle_charref、handle_entityref、handle_comment、handle_pi、handle_decl 和 handle_data。
class BaseHTMLProcessor(SGMLParser): def reset(self): self.pieces = [] SGMLParser.reset(self) def unknown_starttag(self, tag, attrs): strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) def unknown_endtag(self, tag): self.pieces.append("</%(tag)s>" % locals()) def handle_charref(self, ref): self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): self.pieces.append("&%(ref)s" % locals()) if htmlentitydefs.entitydefs.has_key(ref): self.pieces.append(";") def handle_data(self, text): self.pieces.append(text) def handle_comment(self, text): self.pieces.append("<!--%(text)s-->" % locals()) def handle_pi(self, text): self.pieces.append("<?%(text)s>" % locals()) def handle_decl(self, text): self.pieces.append("<!%(text)s>" % locals())
reset 由 SGMLParser.__init__ 来调用。在调用父类方法之前将 self.pieces 初始化为空列表。self.pieces 是一个数据属性,将用来保存将要构造的 HTML 文档的片段。每个处理器方法都将重构 SGMLParser 所分析出来的 HTML,并且每个方法将生成的字符串追加到 self.pieces 之后。注意,self.pieces 是一个 list。也许您想将它定义为一个字符串,然后不停地将每个片段追加到它的后面。这样做是可以的,但是 Python 在处理 list 方面效率更高一些。 [5] | |
因为 BaseHTMLProcessor 没有为特别标记定义方法 (如在 URLLister 中的start_a 方法), SGMLParser 将对每一个开始标记调用 unknown_starttag 方法。这个方法接收标记 (tag) 和属性的名字/值对的 list(attrs) 两参数,重新构造初始的 HTML,接着将结果追加到 self.pieces 后。 这里的字符串格式化有些陌生,我们将留到下一节再说明。 | |
重构结束标记要简单得多,只是使用标记名字,把它包在 </...> 括号中。 | |
当 SGMLParser 找到一个字符引用时,会用原始的引用来调用 handle_charref。如果 HTML 文档包含   这个引用,ref 将为 160。重构原始的完整的字符引用只要将 ref 包装在 &#...; 字符中间。 | |
实体引用同字符引用相似,但是没有#号。重建原始的实体引用只要将 ref 包装在 &...; 字符串中间。(实际上,一位博学的读者曾经向我指出,除些之外还稍微有些复杂。仅有某种标准的 HTML 实体以一个分号结束;其它看上去差不多的实体并不如此。幸运的是,标准 HTML 实体集已经定义在 Python 的一个叫做 htmlentitydefs 的模块中了。从而引出额外的 if 语句。) | |
文本块则简单地不经修改地追加到 self.pieces 后。 | |
HTML 注释包装在 <!--...--> 字符中。 | |
处理指令包装在 <?...> 字符中。 |
HTML 规范要求所有非 HTML (像客户端的 JavaScript) 必须包括在 HTML 注释中,但不是所有的页面都是这么做的 (而且所有的最新的浏览器也都容许不这样做) 。BaseHTMLProcessor 不允许这样,如果脚本嵌入得不正确,它将被当作 HTML 一样进行分析。例如,如果脚本包含了小于和等于号,SGMLParser 可能会错误地认为找到了标记和属性。SGMLParser 总是把标记名和属性名转换成小写,这样可能破坏了脚本,并且 BaseHTMLProcessor 总是用双引号来将属性封闭起来 (尽管原始的 HTML 文档可能使用单引号或没有引号) ,这样必然会破坏脚本。应该总是将您的客户端脚本放在 HTML 注释中进行保护。 |
def output(self): """Return processed HTML as a single string""" return "".join(self.pieces)
我们先偏离一下 HTML 处理的主题,讨论一下 Python 如何处理变量。Python 有两个内置的函数,locals 和 globals,它们提供了基于 dictionary 的访问局部和全局变量的方式。
还记得 locals 吗?您第一次是在这里看到的:
def unknown_starttag(self, tag, attrs): strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
不,等等,此时您还不能理解 locals 。首先,您需要学习关于命名空间的知识。这很枯燥,但是很重要,因此要要耐心些。
Python 使用叫做名字空间的东西来记录变量的轨迹。名字空间只是一个 dictionary ,它的键字就是变量名,它的值就是那些变量的值。实际上,名字空间可以像 Python 的 dictionary 一样进行访问,一会儿我们就会看到。
在一个 Python 程序中的任何一个地方,都存在几个可用的名字空间。每个函数都有着自已的名字空间,叫做局部名字空间,它记录了函数的变量,包括函数的参数和局部定义的变量。每个模块拥有它自已的名字空间,叫做全局名字空间,它记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。还有就是内置名字空间,任何模块均可访问它,它存放着内置的函数和异常。
当一行代码要使用变量 x 的值时,Python 会到所有可用的名字空间去查找变量,按照如下顺序:
如果 Python 在这些名字空间找不到 x,它将放弃查找并引发一个 NameError 异常,同时传递 There is no variable named 'x' 这样一条信息,回到 例 3.18 “引用未赋值的变量”,您会看到一路上都有这样的信息。但是您并没有体会到 Python 在给出这样的错误之前做了多少的努力。
Python 2.2 引入了一种略有不同但重要的改变,它会影响名字空间的搜索顺序:嵌套的作用域。
在 Python 2.2 版本之前,当您在一个嵌套函数或 lambda 函数中引用一个变量时,Python 会在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后在模块的名字空间。Python 2.2 将只在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后是在父函数的名字空间 中搜索,接着是模块的名字空间中搜索。Python 2.1 可 以两种方式工作,缺省地,按 Python 2.0 的方式工作。但是您可以把下面一行代码增加到您的模块头部,使您的模块工作起来像 Python 2.2 的方式:from __future__ import nested_scopes |
您是否为此而感到困惑?不要灰心!我敢说这一点非常酷。像 Python 中的许多事情一样,名字空间在运行时直接可以访问。怎么样?不错吧,局部名字空间可以通过内置的 locals 函数来访问。全局 (模块级别) 名字空间可以通过内置的 globals 函数来访问。
>>> def foo(arg): ... x = 1 ... print locals() ... >>> foo(7) {'arg': 7, 'x': 1} >>> foo('bar') {'arg': 'bar', 'x': 1}
locals 对局部 (函数) 名字空间做了些什么,globals 就对全局 (模块) 名字空间做了什么。然而 globals 更令人兴奋,因为一个模块的名字空间是更令人兴奋的。[6] 模块的名字空间不仅仅包含了模块级的变量和常量,还包括了所有在模块中定义的函数和类。除此以外,它还包括了任何被导入到模块中的东西。
回想一下 from module import 和 import module 之间的不同。使用 import module,模块自身被导入,但是它保持着自已的名字空间,这就是为什么您需要使用模块名来访问它的函数或属性:module.function 的原因。但是使用 from module import,实际上是从另一个模块中将指定的函数和属性导入到您自己的名字空间,这就是为什么您可以直接访问它们却不需要引用它们所来源的模块。使用 globals 函数,您会真切地看到这一切的发生。
看看下面列出的在文件 BaseHTMLProcessor.py 尾部的代码块:
if __name__ == "__main__": for k, v in globals().items(): print k, "=", v
不要被吓坏了,想想以前您已经全部都看到过了。globals 函数返回一个 dictionary,我们使用 items 方法和多变量赋值来遍历 dictionary。在这里唯一的新东西就是 globals 函数。 |
现在从命令行运行这个脚本,会得到下面的输出 (注意您的输出可能有略微的不同,这依赖于您的系统平台和所安装的 Python 版本):
c:\docbook\dip\py> python BaseHTMLProcessor.py
SGMLParser = sgmllib.SGMLParser htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> BaseHTMLProcessor = __main__.BaseHTMLProcessor __name__ = __main__ ... rest of output omitted for brevity...
我们使用了 from module import 把 SGMLParser 从 sgmllib 中导入。也就是说它被直接导入到我们的模块名字空间了,就是这样。 | |
把上面的例子和 htmlentitydefs 对比一下,它是用 import 被导入的。也就是说 htmlentitydefs 模块本身被导入了名字空间,但是定义在 htmlentitydefs 之中的 entitydefs 变量却没有。 | |
这个模块只定义一个类,BaseHTMLProcessor,不错。注意这儿的值就是类本身,不是一个特别的类实例。 | |
记得 if __name__ 技巧吗?当运行一个模块时 (相对于从另外一个模块中导入而言),内置的 __name__ 是一个特殊值 __main__。因为我们是把这个模块当作脚本从命令来运行的,故 __name__ 值为 __main__,这就是为什么我们这段简单地打印 globals 的代码可以执行的原因。 |
使用 locals 和 globals 函数,通过提供变量的字符串名字您可以动态地得到任何变量的值。这种方法提供了这样的功能:getattr 函数允许您通过提供函数的字符串名来动态地访问任意的函数。 |
在 locals 与 globals 之间有另外一个重要的区别,您应该在它困扰您之前就了解它。它无论如何都会困扰您的,但至少您还会记得曾经学习过它。
def foo(arg): x = 1 print locals() locals()["x"] = 2 print "x=",x z = 7 print "z=",z foo(3) globals()["z"] = 8 print "z=",z
为什么学习 locals 和 globals?因为接下来就可以学习关于基于 dictionary 的字符串格式化。或许您还能记起,字符串格式化提供了一种将值插入字符串中的一种便捷的方法。值被列在一个 tuple 中,按照顺序插入到字符串中每个格式化标记所在的位置上。尽管这种做法效率高,但还不是最容易阅读的代码,特别是当插入多个值的时候。仅用眼看一遍字符串,您不能马上就明白结果是什么;您需要经常地在字符串和值的 tuple 之间进行反复查看。
有另外一种字符串格式化的形式,它使用 dictionary 而不是值的 tuple。
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> "%(pwd)s" % params 'secret' >>> "%(pwd)s is not a good password for %(uid)s" % params 'secret is not a good password for sa' >>> "%(database)s of mind, %(database)s of body" % params 'master of mind, master of body'
那么为什么您偏要使用基于 dictionary 的字符串格式化呢?的确,仅为了进行字符串格式化,就事先创建一个有键字和值的 dictionary 看上去的确有些小题大作。它的真正最大用处是当您碰巧已经有了像 locals 一样的有意义的键字和值的 dictionary 的时候。
def handle_comment(self, text): self.pieces.append("<!--%(text)s-->" % locals())
def unknown_starttag(self, tag, attrs): strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
当这个模块被调用时,attrs 是一个键/值 tuple 的 list,就像一个 dictionary 的 items。这就意味着我们可以使用多变量赋值来遍历它。到现在这将是一种熟悉的模式,但是这里有很多东西,让我们分开来看:
|
|
现在,使用基于 dictionary 的字符串格式化,我们将 tag 和 strattrs 的值插入到一个字符串中。所以,如果 tag 是 'a',最终的结果会是 '<a href="index.html" title="Go to home page">',并且这就是追加到 self.pieces 后面的东西。 |
使用 locals 来应用基于 dictionary 的字符串格式化是一种方便的作法,它可以使复杂的字符串格式化表达式更易读。但它需要花费一定的代价。在调用 locals 方面有一点性能上的问题,这是由于 locals 创建了局部名字空间的一个拷贝引起的。 |
在 comp.lang.python 上的一个常见问题是 “我有一些 HTML 文档,属性值没有用引号括起来,并且我想将它们全部括起来,我怎么才能实现它呢?” [7] (一般这种事情的出现是由于一个项目经理加入到一个大的项目中来,而他又抱着 HTML 是一种标记语言的教条,要求所有的页面必须能够通过 HTML 校验器的验证。而属性值没有被引号括起来是一种常见的对 HTML 规范的违反。) 不管什么原因,未括起来的属性值通过将 HTML 送进 BaseHTMLProcessor 可以容易地修复。
BaseHTMLProcessor 消费 (consume) HTML (因为它是从 SGMLParser 派生来的) 并生成等价的 HTML。但是这个 HTML 输出与输入的并不一样。标记和属性名最终会转化为小写字母,即使它们可能以大写字母开始或是大小写的混合形式。属性值将被双引号引起来,即使它们原来可能是用单引号括起来的或根本没有括起来。这就是最后我们可以受益的边际效应。
>>> htmlSource = """ ... <html> ... <head> ... <title>Test page</title> ... </head> ... <body> ... <ul> ... <li><a href=index.html>Home</a></li> ... <li><a href=toc.html>Table of contents</a></li> ... <li><a href=history.html>Revision history</a></li> ... </body> ... </html> ... """ >>> from BaseHTMLProcessor import BaseHTMLProcessor >>> parser = BaseHTMLProcessor() >>> parser.feed(htmlSource) >>> print parser.output() <html> <head> <title>Test page</title> </head> <body> <ul> <li><a href="index.html">Home</a></li> <li><a href="toc.html">Table of contents</a></li> <li><a href="history.html">Revision history</a></li> </body> </html>
请注意,在 <a> 标记中的 href 属性值没有被适当地括起来 (还要注意,除了文档字符串之外,我们还将三重引号用到了 doc string 之外的其它地方,并且是不会少于直接在 IDE 中的使用。它们非常有用。) | |
装填分析器。 | |
使用定义在 BaseHTMLProcessor 中的 output 函数,我们得到单个字符串的输出,并且属性值被完全括起来了。让我们想一下这里实际上发生了多少事:SGMLParser 分析整个 HTML 文档,将其分解为一片片的标记、引用、数据等等。BaseHTMLProcessor 使用这些元素来重新构造 HTML 的片段 (如果您想查看的话它们仍然保存在 parser.pieces 中) 。最后,我们调用 parser.output,它将所有的 HTML 片段连接成一个字符串。 |
Dialectizer 是 BaseHTMLProcessor 的简单 (和拙劣) 的派生类。它通过一系列的替换对文本块进行了处理,但是它确保在 <pre>...</pre> 块之间的任何东西不被修改地通过。
为了处理 <pre> 块,我们在 Dialectizer 中定义了两个方法:start_pre 和 end_pre。
def start_pre(self, attrs): self.verbatim += 1 self.unknown_starttag("pre", attrs) def end_pre(self): self.unknown_endtag("pre") self.verbatim -= 1
每当 SGMLParser 在 HTML 源代码中发现一个 <pre> 时,都会调用 start_pre。(马上我们就会确切地看到它是如何发生的。) 这个方法使用单个参数:attrs,这个参数会包含标记的属性 (如果存在的话) 。attrs 是一个键/值 tuple 的 list,就像 unknown_starttag 中所使用的。 | |
在 reset 方法中,我们初始化了一个数据属性,它作为 <pre> 标记的一个计数器。每当我们找到一个 <pre> 标记,我们增加计数器的值;每当我们找到一个 </pre> 标记,我们将减少计数器的值。(我们本可以把它实现为一个标志,即或把它设为 1,或重置为 0,但这样做只是为了方便,并且这样做可以处理古怪 (但有可能) 的 <pre> 标记嵌套的情况。) 马上我们将会看到这个计数器是多么的好用。 | |
不错,这就是我们对 <pre> 标记所做的唯一的特殊处理。现在我们将属性列表传给 unknown_starttag,由它来进行缺省的处理。 | |
每当 SGMLParser 找到一个 </pre> 标记时,会调用 end_pre。因为结束标记不能包含属性,因此这个方法没有参数。 | |
首先我们要进行缺省处理,就像其它结束标记做的一样。 | |
其次我们将计数器减少,标记这个 <pre> 块已经被关闭了。 |
到了这个地方,有必要对 SGMLParser 更深入一层。我已经多次声明 (到目前为止您应已经把它做为信条了) ,就是 SGMLParser 查找每一个标记并且如果存在特定的方法就调用它们。例如:我们刚刚看到处理 <pre> 和 </pre> 的 start_pre 和 end_pre 的定义。但这是如何发生的呢?嗯,也没什么神奇的,只不过是出色的 Python 编码。
def finish_starttag(self, tag, attrs): try: method = getattr(self, 'start_' + tag) except AttributeError: try: method = getattr(self, 'do_' + tag) except AttributeError: self.unknown_starttag(tag, attrs) return -1 else: self.handle_starttag(tag, method, attrs) return 0 else: self.stack.append(tag) self.handle_starttag(tag, method, attrs) return 1 def handle_starttag(self, tag, method, attrs): method(attrs)
此处,SGMLParser 已经找到了一个开始标记,并且分析出属性列表。唯一要做的事情就是检查对于这个标记是否存在一个特别的处理方法,否则我们就应该求助于缺省方法 (unknown_starttag) 。 | |
SGMLParser 的 “神奇” 之处除了我们的老朋友 getattr 之外就没有什么了。您以前可能没注意到,getattr 将查找定义在一个对象的继承者中或对象自身的方法。这里对象是 self,即当前实例。所以,如果 tag 是 'pre',这里对 getattr 的调用将会在当前实例 (它是 Dialectizer 类的一个实例) 中查找一个名为 start_pre 的方法。 | |
如果 getattr 所查找的方法在对象或它的任何继承者中不存在的话,它会引发一个 AttributeError 的异常。但没有关系,因为我们把对 getattr 的调用包装到一个 try...except 块中了,并且显式地捕捉 AttributeError 异常。 | |
因为我们没有找到一个 start_xxx 方法,在放弃之前,我们将还要查找一个 do_xxx 方法。这个可替换的命名模式一般用于单独的标记,如 <br>,这些标记没有相应的结束标记。但是您可以使用任何一种模式,正如您看到的,SGMLParser 对每个标记尝试两次。(您不应该对相同的标记同时定义 start_xxx 和 do_xxx 处理方法,因为这样的话只有 start_xxx 方法会被调用。) | |
另一个 AttributeError 异常,它是说用 do_xxx 来调用 getattr 失败了。因为对同一个标记我们既没有找到 start_xxx 也没有找到 do_xxx 处理方法,这样我们捕捉到了异常并且求助于缺省方法:unknown_starttag。 | |
记得吗?try...except 块可以有一个 else 子句,当在 try...except 块中没有异常被引发时,它将被调用。逻辑上,意味着我们确实 找到了这个标记的 do_xxx 方法,所以我们将要调用它。 | |
顺便说,不要为这些不同的返回值而担心;理论上他们有意义,但实际上它们没有任何用处。也不要担心 self.stack.append(tag) ; SGMLParser 内部会知晓您的开始标记是否有合适的结束标记与之匹配,但是它不会对这些信息做任何操作。理论上,您能使用这个模块校验您的标记是否完全匹配,但是这或许没有多大价值,并且这样的内容已经超出了本章所要讨论的范畴。现在有您更需要担心的问题。 | |
start_xxx 和 do_xxx 方法并不被直接调用;标记名、方法和属性被传给 handle_starttag 这个方法,以便继承者可以覆盖它,并改变全部 开始标记分发的方式。我们不需要控制这个层面,所以我们只让这个方法做它自已的事,就是用属性 list 来调用方法 (start_xxx 或 do_xxx) 。记住 method 是一个从 getattr 返回的函数,而函数是对象。(我知道您已经听腻了,我发誓,一旦我们停止寻找新的使用方法来为我们服务时,我就决不再提它了。) 这时,函数对象作为一个参数传入这个分发方法,这个方法反过来再调用这个函数。在这里,我们不需要知道函数是什么,叫什么名字,或是在哪时定义的;我们只需要知道用一个参数 attrs 调用它。 |
现在回到我们已经计划好的程序:Dialectizer。当我们跑题时,我们定义了特别的处理方法来处理 <pre> 和 </pre> 标记。还有一件事没有做,那就是用我们预定义的替换处理来处理文本块。为了实现它,我们需要覆盖 handle_data 方法。
def handle_data(self, text): self.pieces.append(self.verbatim and text or self.process(text))
handle_data 在调用时只使用一个参数:要处理的文本。 | |
在祖先类 BaseHTMLProcessor 中,handle_data 方法只是将文本追加到输出缓冲区 self.pieces 之后。这里的逻辑稍微有点复杂。如果我们处于 <pre>...</pre> 块的中间,self.verbatim 将是大于 0 的某个值,接着我们想要将文本不作改动地传入输出缓冲区。否则,我们将调用另一个单独的方法来进行替换处理,然后将处理结果放入输出缓冲区中。在 Python 中,这是一个一行代码,它使用了and-or 技巧。 |
我们已经接近了对 Dialectizer 的全面理解。唯一缺少的一个环节是文本替换的特性。如果您知道点 Perl,您就会知道当需要复杂的文本替换时,唯一有效的解决方法就是正则表达式。在 dialect.py 文件后面的几个类中定义了一连串的正则表达式来操作 HTML 标记中的文本。我们已经学习过了正则表达式中的所有字符。我们不必重复学习正则表达式的艰难历程了,不是吗?上帝知道我反正不需要。我想现在这章您已经学得差不多了。
到了将迄今为止我们已经学过并用得不错的东西放在一起的时候了。我希望您专心些。
def translate(url, dialectName="chef"): import urllib sock = urllib.urlopen(url) htmlSource = sock.read() sock.close()
这个 translate 函数有一个可选参数 dialectName,它是一个字符串,指出我们将使用的方言。一会我们就会看到它是如何使用的。 | |
嘿,等一下,在这个函数中有一个 import 语句!它在 Python 中完全合法。您已经习惯了在一个程序的前面看到 import 语句,它意味着导入的模块在程序的任何地方都是可用的。但您也可以在一个函数中导入模块,这意味着导入的模块只能在函数中使用。如果您有一个只能用在一个函数中的模块,这是一个简便的方法,使您的代码更模块化。(当发现您周末的加班已经变成了一个 800 行的艺术作品,并且决定将其分割成一打可重用的模块时,您会感谢它的。) | |
现在我们得到了给定的 URL 源文件。 |
parserName = "%sDialectizer" % dialectName.capitalize() parserClass = globals()[parserName] parser = parserClass()
capitalize 是一个我们以前未曾见过的字符串方法;它只是将一个字符串的第一个字母变成大写,将其它的字母强制变成小写。再使用字符串格式化,我们就得到了一种方言的名字,并将它转化为了相应的方言变换器类的名字。如果 dialectName 是字符串 'chef',parserName 将是字符串 'ChefDialectizer'。 | |
我们有了一个字符串形式 (parserName) 的类名称,还有一个 dictionary (globals()) 形式的全局名字空间。合起来后,我们可以得到以前者命名的类的引用。(回想一下,类是对象,并且它们可以像其它对象一样赋值给一个变量。) 如果 parserName 是字符串 'ChefDialectizer',parserClass 将是类 ChefDialectizer。 | |
最后,我们拥有了一个类对象 (parserClass),接着我们想要生成这个类的一个实例。好,我们已经知道如何去做了:像函数一样调用类。这个类保存在一个局部变量中,但这个事实完全不会有什么影响;我们只是像函数一样调用这个局部变量,取出这个类的一个实例。如果 parserClass 是类 ChefDialectizer,parser 将是类 ChefDialectizer 的一个实例。 |
何必这么麻烦?毕竟只有三个 Dialectizer 类;为什么不只使用一个 case 语句? (噢,在 Python 中不存在 case 语句,但为什么不只使用一组 if 语句呢?) 理由之一是:可扩展性。这个 translate 函数完全不用关心我们定义了多少个方言变换器类。设想一下,如果我们明天定义了一个新的 FooDialectizer 类,把 'foo' 作为 dialectName 传给 translate ,translate 也能工作。
甚至会更好。设想将 FooDialectizer 放进一个独立的模块中,使用 from module import 将其导入。我们已经知道了,这样会将它包含在 globals() 中 ,所以不用修改 translate ,它仍然可以正确运行,尽管 FooDialectizer 位于一个独立的文件中。
现在设想一下方言的名字是从程序外面的某个地方来的,也许是从一个数据库中,或从一个表格中的用户输入的值中。您可以使用任意多的服务端 Python 脚本架构来动态地生成网页;这个函数将接收在页面请求的查询字符串中的一个 URL 和一个方言名字 (两个都是字符串) ,接着输出 “翻译” 后的网页。
最后,设想一下,使用了一种插件架构的 Dialectizer 框架。您可以将每个 Dialectizer 类放在分别放在独立的文件中,在 dialect.py 中只留下 translate 函数。假定一种统一的命名模式,这个 translate 函数能够动态地从合适的文件中导入合适的类,除了方言名字外什么都不用给出。(虽然您还没有看过动态导入,但我保证在后面的一章中会涉及到它。) 如果要加入一种新的方言,您只要在插件目录下加入一个以合适的名字命名的文件 (像 foodialect.py,它包含了 FooDialectizer 类) 。使用方言名 'foo' 来调用这个 translate 函数,将会查找 foodialect.py 模块,导入 FooDialectizer 类,这样就行了。
parser.feed(htmlSource) parser.close() return parser.output()
剩下的工作似乎会非常无聊,但实际上,feed 函数执行了全部的转换工作。我们拥有存在于单个字符串中的全部 HTML 源代码,所以我们只需要调用 feed 一次。然而,您可以按您的需要经常调用 feed,分析器将不停地进行分析。所以如果我们担心内存的使用 (或者我们已经知道了将要处理非常巨大的 HTML 页面) ,我们可以在一个循环中调用它,即我们读出一点 HTML 字节,就将其送进分析器。结果会是一样的。 | |
因为 feed 维护着一个内部缓冲区,当您完成时,应该总是调用分析器的 close 方法 (那怕您像我们做的一样,一次就全部送出) 。否则您可能会发现,输出丢掉了最后几个字节。 | |
回想一下,output 是我们在 BaseHTMLProcessor 上定义的函数,用来将所有缓冲的输出片段连接起来并且以单个字符串返回。 |
像这样,我们已经 “翻译” 了一个网页,除了给出一个 URL 和一种方言的名字外,什么都没有给出。
Python 向您提供了一个强大工具,sgmllib.py,可以通过将 HTML 结构转变为一种对象模型来进行处理。可以以许多不同的方式来使用这个工具。
学过了这些例子之后,您应该无障碍地完成下面的事情:
[4] 像 SGMLParser 这样的分析器,技术术语叫做消费者 (consumer)。它消费 HTML,并且拆分它。也许因此就选择了 feed 这个名字,以便同消费者 这个主题相适应。就个人来说,它让我想象在动物园看展览。里面有一个黑漆漆的兽穴,没有树,没有植物,没有任何生命的迹象。但只要您非常安静地站着,尽可能靠近着瞧,您会看到在远处的角落里有两只明眸在盯着您。但是您会安慰自已那不过是心理作用。唯一知道兽穴里并不是空无一物的方法,就是在栅栏上有一个不明显的标记,上面写着 “禁止给分析器喂食”。但也许只有我这么想,不管怎么样,这种心理想象很有意思。
[5] Python 处理 list 比字符串快的原因是:list 是可变的,但字符串是不可变的。这就是说向 list 进行追加只是增加元素和修改索引。因为字符串在创建之后不能被修改,像 s = s + newpiece 这样的代码将会从原值和新片段的连接结果中创建一个全新的字符串,然后丢弃原来的字符串。这样就需要大量昂贵的内存管理,并且随着字符串变长,所需要的开销也在增长。所以在一个循环中执行 s = s + newpiece 非常不好。用技术术语来说,向一个 list 追加 n 个项的代价为 O(n),而向一个字符串追加 n 个项的代价是 O(n2)。
[6] 我没有说得太多吧。
[7] 好吧,其实并不是那么普通的一个问题。在那不都是问 “我应该用何种编辑器来写 Python 代码?” (回答:Emacs) 或 “Python 比 Perl 是好还是坏?” (回答:“Perl 比 Python 差,因为人们想让它差的。” ――Larry Wall,1998 年 10 月 14 日) 但是关于 HTML 处理的问题,或者这种提法或者另一种提法,大约一个月就要出现一次,在这些问题之中,这个问题是最常见的一个。
下面两章是关于 Python 中 XML 处理的。如果你已经对 XML 文档有了一个大概的了解,比如它是由结构化标记构成的,这些标记形成了层次模型的元素,等等这些知识都是有帮助的。如果你不明白这些,这里有很多 XML 教程能够解释这些基础知识。
如果你对 XML 不是很感兴趣,你还是应该读一下这些章节,它们涵盖了不少重要的主题,比如 Python 包、Unicode、命令行参数以及如何使用 getattr 进行方法分发。
如果你在大学里主修哲学 (而不是像计算机科学这样的实用专业),并且曾不幸地被伊曼努尔·康德的著作折磨地够呛,那么你会非常欣赏本章的样例程序。(这当然不意味着你必须修过哲学。)
处理 XML 有两种基本的方式。一种叫做 SAX (“Simple API for XML”),它的工作方式是,一次读出一点 XML 内容,然后对发现的每一个元素调用一个方法。(如果你读了 第 8 章 HTML 处理,这应该听起来很熟悉,因为这是 sgmllib 工作的方式。) 另一种方式叫做 DOM (“Document Object Model”),它的工作方式是,一次性读入整个 XML 文档,然后使用 Python 类创建一个内部表示形式 (以树结构进行连接)。Python 拥有这两种解析方式的标准模块,但是本章只涉及 DOM。
下面是一个完整的 Python 程序,它根据 XML 格式定义的上下文无关语法生成伪随机输出。如果你不明白是什么意思,不用担心,下面两章中将会深入检视这个程序的输入和输出。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Kant Generator for Python Generates mock philosophy based on a context-free grammar Usage: python kgp.py [options] [source] Options: -g ..., --grammar=... use specified grammar file or URL -h, --help show this help -d show debugging information while parsing Examples: kgp.py generates several paragraphs of Kantian philosophy kgp.py -g husserl.xml generates several paragraphs of Husserl kpg.py "<xref id='paragraph'/>" generates a paragraph of Kant kgp.py template.xml reads from template.xml to decide what to generate """ from xml.dom import minidom import random import toolbox import sys import getopt _debug = 0 class NoSourceError(Exception): pass class KantGenerator: """generates mock philosophy based on a context-free grammar""" def __init__(self, grammar, source=None): self.loadGrammar(grammar) self.loadSource(source and source or self.getDefaultSource()) self.refresh() def _load(self, source): """load XML input source, return parsed XML document - a URL of a remote XML file ("http://diveintopython.org/kant.xml") - a filename of a local XML file ("~/diveintopython/common/py/kant.xml") - standard input ("-") - the actual XML document, as a string """ sock = toolbox.openAnything(source) xmldoc = minidom.parse(sock).documentElement sock.close() return xmldoc def loadGrammar(self, grammar): """load context-free grammar""" self.grammar = self._load(grammar) self.refs = {} for ref in self.grammar.getElementsByTagName("ref"): self.refs[ref.attributes["id"].value] = ref def loadSource(self, source): """load source""" self.source = self._load(source) def getDefaultSource(self): """guess default source of the current grammar The default source will be one of the <ref>s that is not cross-referenced. This sounds complicated but it's not. Example: The default source for kant.xml is "<xref id='section'/>", because 'section' is the one <ref> that is not <xref>'d anywhere in the grammar. In most grammars, the default source will produce the longest (and most interesting) output. """ xrefs = {} for xref in self.grammar.getElementsByTagName("xref"): xrefs[xref.attributes["id"].value] = 1 xrefs = xrefs.keys() standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs] if not standaloneXrefs: raise NoSourceError, "can't guess source, and no source specified" return '<xref id="%s"/>' % random.choice(standaloneXrefs) def reset(self): """reset parser""" self.pieces = [] self.capitalizeNextWord = 0 def refresh(self): """reset output buffer, re-parse entire source file, and return output Since parsing involves a good deal of randomness, this is an easy way to get new output without having to reload a grammar file each time. """ self.reset() self.parse(self.source) return self.output() def output(self): """output generated text""" return "".join(self.pieces) def randomChildElement(self, node): """choose a random child element of a node This is a utility method used by do_xref and do_choice. """ choices = [e for e in node.childNodes if e.nodeType == e.ELEMENT_NODE] chosen = random.choice(choices) if _debug: sys.stderr.write('%s available choices: %s\n' % \ (len(choices), [e.toxml() for e in choices])) sys.stderr.write('Chosen: %s\n' % chosen.toxml()) return chosen def parse(self, node): """parse a single XML node A parsed XML document (from minidom.parse) is a tree of nodes of various types. Each node is represented by an instance of the corresponding Python class (Element for a tag, Text for text data, Document for the top-level document). The following statement constructs the name of a class method based on the type of node we're parsing ("parse_Element" for an Element node, "parse_Text" for a Text node, etc.) and then calls the method. """ parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) parseMethod(node) def parse_Document(self, node): """parse the document node The document node by itself isn't interesting (to us), but its only child, node.documentElement, is: it's the root node of the grammar. """ self.parse(node.documentElement) def parse_Text(self, node): """parse a text node The text of a text node is usually added to the output buffer verbatim. The one exception is that <p class='sentence'> sets a flag to capitalize the first letter of the next word. If that flag is set, we capitalize the text and reset the flag. """ text = node.data if self.capitalizeNextWord: self.pieces.append(text[0].upper()) self.pieces.append(text[1:]) self.capitalizeNextWord = 0 else: self.pieces.append(text) def parse_Element(self, node): """parse an element An XML element corresponds to an actual tag in the source: <xref id='...'>, <p chance='...'>, <choice>, etc. Each element type is handled in its own method. Like we did in parse(), we construct a method name based on the name of the element ("do_xref" for an <xref> tag, etc.) and call the method. """ handlerMethod = getattr(self, "do_%s" % node.tagName) handlerMethod(node) def parse_Comment(self, node): """parse a comment The grammar can contain XML comments, but we ignore them """ pass def do_xref(self, node): """handle <xref id='...'> tag An <xref id='...'> tag is a cross-reference to a <ref id='...'> tag. <xref id='sentence'/> evaluates to a randomly chosen child of <ref id='sentence'>. """ id = node.attributes["id"].value self.parse(self.randomChildElement(self.refs[id])) def do_p(self, node): """handle <p> tag The <p> tag is the core of the grammar. It can contain almost anything: freeform text, <choice> tags, <xref> tags, even other <p> tags. If a "class='sentence'" attribute is found, a flag is set and the next word will be capitalized. If a "chance='X'" attribute is found, there is an X% chance that the tag will be evaluated (and therefore a (100-X)% chance that it will be completely ignored) """ keys = node.attributes.keys() if "class" in keys: if node.attributes["class"].value == "sentence": self.capitalizeNextWord = 1 if "chance" in keys: chance = int(node.attributes["chance"].value) doit = (chance > random.randrange(100)) else: doit = 1 if doit: for child in node.childNodes: self.parse(child) def do_choice(self, node): """handle <choice> tag A <choice> tag contains one or more <p> tags. One <p> tag is chosen at random and evaluated; the rest are ignored. """ self.parse(self.randomChildElement(node)) def usage(): print __doc__ def main(argv): grammar = "kant.xml" try: opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) except getopt.GetoptError: usage() sys.exit(2) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit() elif opt == '-d': global _debug _debug = 1 elif opt in ("-g", "--grammar"): grammar = arg source = "".join(args) k = KantGenerator(grammar, source) print k.output() if __name__ == "__main__": main(sys.argv[1:])
"""Miscellaneous utility functions""" def openAnything(source): """URI, filename, or string --> stream This function lets you define parsers that take any input source (URL, pathname to local or network file, or actual data as a string) and deal with it in a uniform manner. Returned object is guaranteed to have all the basic stdio read methods (read, readline, readlines). Just .close() the object when you're done with it. Examples: >>> from xml.dom import minidom >>> sock = openAnything("http://localhost/kant.xml") >>> doc = minidom.parse(sock) >>> sock.close() >>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml") >>> doc = minidom.parse(sock) >>> sock.close() >>> sock = openAnything("<ref id='conjunction'><text>and</text><text>or</text></ref>") >>> doc = minidom.parse(sock) >>> sock.close() """ if hasattr(source, "read"): return source if source == '-': import sys return sys.stdin # try to open with urllib (if source is http, ftp, or file URL) import urllib try: return urllib.urlopen(source) except (IOError, OSError): pass # try to open with native open function (if source is pathname) try: return open(source) except (IOError, OSError): pass # treat source as string import StringIO return StringIO.StringIO(str(source))
独立运行程序 kgp.py,它会解析 kant.xml 中默认的基于 XML 的语法,并以康德的风格打印出几段有哲学价值的段落来。
[you@localhost kgp]$ python kgp.py
As is shown in the writings of Hume, our a priori concepts, in
reference to ends, abstract from all content of knowledge; in the study
of space, the discipline of human reason, in accordance with the
principles of philosophy, is the clue to the discovery of the
Transcendental Deduction. The transcendental aesthetic, in all
theoretical sciences, occupies part of the sphere of human reason
concerning the existence of our ideas in general; still, the
never-ending regress in the series of empirical conditions constitutes
the whole content for the transcendental unity of apperception. What
we have alone been able to show is that, even as this relates to the
architectonic of human reason, the Ideal may not contradict itself, but
it is still possible that it may be in contradictions with the
employment of the pure employment of our hypothetical judgements, but
natural causes (and I assert that this is the case) prove the validity
of the discipline of pure reason. As we have already seen, time (and
it is obvious that this is true) proves the validity of time, and the
architectonic of human reason, in the full sense of these terms,
abstracts from all content of knowledge. I assert, in the case of the
discipline of practical reason, that the Antinomies are just as
necessary as natural causes, since knowledge of the phenomena is a
posteriori.
The discipline of human reason, as I have elsewhere shown, is by
its very nature contradictory, but our ideas exclude the possibility of
the Antinomies. We can deduce that, on the contrary, the pure
employment of philosophy, on the contrary, is by its very nature
contradictory, but our sense perceptions are a representation of, in
the case of space, metaphysics. The thing in itself is a
representation of philosophy. Applied logic is the clue to the
discovery of natural causes. However, what we have alone been able to
show is that our ideas, in other words, should only be used as a canon
for the Ideal, because of our necessary ignorance of the conditions.
[...snip...]
这当然是胡言乱语。噢,不完全是胡言乱语。它在句法和语法上都是正确的 (尽管非常罗嗦――康德可不是你们所说的踩得到点上的那种人)。其中一些实际上是正确的 (或者至少康德可能会认同的事情),其中一些则明显是错误的,大部分只是语无伦次。但所有内容都符合康德的风格。
让我重复一遍,如果你现在或曾经主修哲学专业,这会非常、非常有趣。
有趣之处在于,这个程序中没有一点内容是属于康德的。所有的内容都来自于上下文无关语法文件 kant.xml。如果你要程序使用不同的语法文件 (可以在命令行中指定),输出信息将完全不同。
[you@localhost kgp]$ python kgp.py -g binary.xml 00101001 [you@localhost kgp]$ python kgp.py -g binary.xml 10110100
在本章后面的内容中,你将近距离地观察语法文件的结构。现在,你只要知道语法文件定义了输出信息的结构,而 kgp.py 程序读取语法规则并随机确定哪些单词插入哪里。
实际上解析一个 XML 文档是很简单的:只要一行代码。但是,在你接触那行代码前,需要暂时岔开一下,讨论一下包。
>>> from xml.dom import minidom >>> xmldoc = minidom.parse('~/diveintopython/common/py/kgp/binary.xml')
听起来挺复杂的,其实不是。看一下确切的实现可能会有帮助。包不过是模块的目录;嵌套包是子目录。一个包 (或一个嵌套包) 中的模块也只是 .py 文件罢了,永远都是,只是它们是在一个子目录中,而不是在你的 Python 安装环境的主 lib/ 目录下。
Python21/ Python 安装根目录 (可执行文件的所在地)
|
+--lib/ 库目录 (标准库模块的所在地)
|
+-- xml/ xml包 (实际上目录中还有其它东西)
|
+--sax/ xml.sax包 (也只是一个目录)
|
+--dom/ xml.dom包 (包含 minidom.py)
|
+--parsers/ xml.parsers包 (内部使用)
所以你说 from xml.dom import minidom,Python 认为它的意思是“在 xml 目录中查找 dom 目录,然后在这个目录 中查找 minidom 模块,接着导入它并以 minidom 命名 ”。但是 Python 更聪明;你不仅可以导入包含在一个包中的所有模块,还可以从包的模块中有选择地导入指定的类或者函数。语法都是一样的; Python 会根据包的布局理解你的意思,然后自动进行正确的导入。
>>> from xml.dom import minidom >>> minidom <module 'xml.dom.minidom' from 'C:\Python21\lib\xml\dom\minidom.pyc'> >>> minidom.Element <class xml.dom.minidom.Element at 01095744> >>> from xml.dom.minidom import Element >>> Element <class xml.dom.minidom.Element at 01095744> >>> minidom.Element <class xml.dom.minidom.Element at 01095744> >>> from xml import dom >>> dom <module 'xml.dom' from 'C:\Python21\lib\xml\dom\__init__.pyc'> >>> import xml >>> xml <module 'xml' from 'C:\Python21\lib\xml\__init__.pyc'>
这里你正从一个嵌套包 (xml.dom)中导入一个模块 (minidom)。结果就是 minidom 被导入到了你 (程序) 的命名空间中了。要引用 minidom 模块中的类 (比如 Element),你必须在它们的类名前面加上模块名。 | |
这里你正从一个来自嵌套包 (xml.dom) 的模块 (minidom) 中导入一个类 (Element)。结果就是 Element 直接导入到了你 (程序) 的命名空间中。注意,这样做并不会干扰以前的导入;现在 Element 类可以用两种方式引用了 (但其实是同一个类)。 | |
这里你正在导入 dom 包 (xml 的一个嵌套包),并将其作为一个模块。一个包的任何层次都可以视为一个模块,一会儿就会看到。它甚至可以拥有自己的属性和方法,就像你在前面看到过的模块。 | |
这里你正在将根层次的 xml 包作为一个模块导入。 |
那么如何才能导入一个包 (它不过是磁盘上的一个目录) 并使其成为一个模块 (它总是在磁盘上的一个文件) 呢?答案就是神奇的 __init__.py 文件。你明白了吧,包不只是目录,它们是包含一个特殊文件 __init__.py 的目录。这个文件定义了包的属性和方法。例如,xml.dom 包含了 Node 类,它在xml/dom/__init__.py中有所定义。当你将一个包作为模块导入 (比如从 xml 导入 dom) 的时候,实际上导入了它的 __init__.py 文件。
一个包是一个其中带有特殊文件 __init__.py 的目录。__init__.py 文件定义了包的属性和方法。其实它可以什么也不定义;可以只是一个空文件,但是必须要存在。如果 __init__.py 不存在,这个目录就仅仅是一个目录,而不是一个包,它就不能被导入或者包含其它的模块和嵌套包。 |
那为什么非得用包呢?嗯,它们提供了在逻辑上将相关模块归为一组的方法。不使用其中带有 sax 和 dom 的 xml 包,作者也可以选择将所有的 sax 功能放入 xmlsax.py中,并将所有的 dom 功能放入 xmldom.py中,或者干脆将所有东西放入单个模块中。但是这样可能不实用 (写到这儿时,XML 包已经超过了 3000 行代码) 并且很难管理 (独立的源文件意味着多个人可以同时在不同的地方进行开发)。
如果你发现自己正在用 Python 编写一个大型的子系统 (或者,很有可能,当你意识到你的小型子系统已经成长为一个大型子系统时),你应该花费些时间设计一个好的包架构。它是 Python 所擅长的事情之一,所以应该好好利用它。
正如我说的,实际解析一个 XML 文档是非常简单的:只要一行代码。从这里出发到哪儿去就是你自己的事了。
>>> from xml.dom import minidom >>> xmldoc = minidom.parse('~/diveintopython/common/py/kgp/binary.xml') >>> xmldoc <xml.dom.minidom.Document instance at 010BE87C> >>> print xmldoc.toxml() <?xml version="1.0" ?> <grammar> <ref id="bit"> <p>0</p> <p>1</p> </ref> <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref> </grammar>
正如在上一节看到的,该语句从 xml.dom 包中导入 minidom 模块。 | |
这就是进行所有工作的一行代码:minidom.parse 接收一个参数并返回 XML 文档解析后的表示形式。这个参数可以是很多东西;在本例中,它只是我本地磁盘上一个 XML 文档的文件名。(你需要将路径改为指向下载的例子所在的目录。) 但是你也可以传入一个文件对象,或甚至是一个类文件对象。这样你就可以在本章后面好好利用这一灵活性了。 | |
从 minidom.parse 返回的对象是一个 Document 对象,它是 Node 类的一个子对象。这个 Document 对象是联锁的 Python 对象的一个复杂树状结构的根层次,这些 Python 对象完整表示了传给 minidom.parse 的 XML 文档。 | |
toxml 是 Node 类的一个方法 (因此可以在从 minidom.parse 中得到的 Document 对象上使用)。toxml 打印出了 Node 表示的 XML。对于 Document 节点,这样就会打印出整个 XML 文档。 |
现在内存中已经有了一个 XML 文档了,你可以开始遍历它了。
>>> xmldoc.childNodes [<DOM Element: grammar at 17538908>] >>> xmldoc.childNodes[0] <DOM Element: grammar at 17538908> >>> xmldoc.firstChild <DOM Element: grammar at 17538908>
>>> grammarNode = xmldoc.firstChild >>> print grammarNode.toxml() <grammar> <ref id="bit"> <p>0</p> <p>1</p> </ref> <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref> </grammar>
>>> grammarNode.childNodes [<DOM Text node "\n">, <DOM Element: ref at 17533332>, \ <DOM Text node "\n">, <DOM Element: ref at 17549660>, <DOM Text node "\n">] >>> print grammarNode.firstChild.toxml() >>> print grammarNode.childNodes[1].toxml() <ref id="bit"> <p>0</p> <p>1</p> </ref> >>> print grammarNode.childNodes[3].toxml() <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref> >>> print grammarNode.lastChild.toxml()
>>> grammarNode <DOM Element: grammar at 19167148> >>> refNode = grammarNode.childNodes[1] >>> refNode <DOM Element: ref at 17987740> >>> refNode.childNodes [<DOM Text node "\n">, <DOM Text node " ">, <DOM Element: p at 19315844>, \ <DOM Text node "\n">, <DOM Text node " ">, \ <DOM Element: p at 19462036>, <DOM Text node "\n">] >>> pNode = refNode.childNodes[2] >>> pNode <DOM Element: p at 19315844> >>> print pNode.toxml() <p>0</p> >>> pNode.firstChild <DOM Text node "0"> >>> pNode.firstChild.data u'0'
Unicode 是一个系统,用来表示世界上所有不同语言的字符。当 Python 解析一个 XML 文档时,所有的数据都是以unicode的形式保存在内存中的。
一会儿你就会了解,但首先,先看一些背景知识。
历史注解. 在 unicode 之前,对于每一种语言都存在独立的字符编码系统,每个系统都使用相同的数字(0-255)来表示这种语言的字符。一些语言 (像俄语) 对于如何表示相同的字符还有几种有冲突的标准;另一些语言 (像日语) 拥有太多的字符,需要多个字符集。在系统之间进行文档交流是困难的,因为对于一台计算机来说,没有方法可以识别出文档的作者使用了哪种编码模式;计算机看到的只是数字,并且这些数字可以表示不同的东西。接着考虑到试图将这些 (采用不同编码的) 文档存放到同一个地方 (比如在同一个数据库表中);你需要在每段文本的旁边保存字符的编码,并且确保在传递文本的同时将编码也进行传递。接着考虑多语言文档,即在同一文档中使用了不同语言的字符。(比较有代表性的是使用转义符来进行模式切换;噗,我们处于俄语 koi8-r 模式,所以字符 241 表示这个;噗,现在我们处于 Mac 希腊语模式,所以字符 241 表示其它什么。等等。) 这些就是 unicode 被设计出来要解决的问题。
为了解决这些问题,unicode 用一个 2 字节数字表示每个字符,从 0 到 65535。[8] 每个 2 字节数字表示至少在一种世界语言中使用的一个唯一字符。(在多种语言中都使用的字符具有相同的数字码。) 这样就确保每个字符一个数字,并且每个数字一个字符。Unicode 数据永远不会模棱两可。
当然,仍然还存在着所有那些遗留的编码系统的情况。例如,7 位 ASCII,它可以将英文字符存诸为从 0 到 127 的数值。(65 是大写字母 “A”,97 是小写字母 “a”,等等。) 英语有着非常简单的字母表,所以它可以完全用 7 位 ASCII 来表示。像法语、西班牙语和德语之类的西欧语言都使用叫做 ISO-8859-1 的编码系统 (也叫做“latin-1”),它使用 7 位 ASCII 字符表示从 0 到 127 的数字,但接着扩展到了 128-255 的范围来表示像 n 上带有一个波浪线(241),和 u 上带有两个点(252)的字符。Unicode 在 0 到 127 上使用了同 7 位 ASCII 码一样的字符表,在 128 到 255上同 ISO-8859-1 一样,接着使用剩余的数字,256 到 65535,扩展到表示其它语言的字符。
在处理 unicode 数据时,在某些地方你可能需要将数据转换回这些遗留编码系统之一。例如,为了同其它一些计算机系统集成,这些系统期望它的数据使用一种特定的单字节编码模式,或将数据打印输出到一个不识别 unicode 的终端或打印机。或将数据保存到一个明确指定编码模式的 XML 文档中。
在了解这个注解之后,让我们回到 Python上来。
从 2.0 版开始,Python 整个语言都已经支持 unicode。XML 包使用 unicode 来保存所有解析了的 XML 数据,而且你可以在任何地方使用 unicode。
>>> s = u'Dive in' >>> s u'Dive in' >>> print s Dive in
>>> s = u'La Pe\xf1a' >>> print s Traceback (innermost last): File "<interactive input>", line 1, in ? UnicodeError: ASCII encoding error: ordinal not in range(128) >>> print s.encode('latin-1') La Peña
还记得我说过:需要从一个 unicode 得到一个正常字符串时,Python 通常默认将 unicode 转换成 ASCII 吗?嗯,这个默认编码模式是一个可以定制的选项。
# sitecustomize.py # this file can be anywhere in your Python path, # but it usually goes in ${pythondir}/lib/site-packages/ import sys sys.setdefaultencoding('iso-8859-1')
>>> import sys >>> sys.getdefaultencoding() 'iso-8859-1' >>> s = u'La Pe\xf1a' >>> print s La Peña
如果你打算在你的 Python 代码中保存非 ASCII 字符串,你需要在每个文件的顶端加入编码声明来指定每个 .py 文件的编码。这个声明定义了 .py 文件的编码为 UTF-8:
#!/usr/bin/env python # -*- coding: UTF-8 -*-
现在,想想 XML 中的编码应该是怎样的呢?不错,每一个 XML 文档都有指定的编码。重复一下,ISO-8859-1 是西欧语言存放数据的流行编码方式。KOI8-R 是俄语流行的编码方式。编码――如果指定了的话――都在 XML 文档的首部。
<?xml version="1.0" encoding="koi8-r"?> <preface> <title>Предисловие</title> </preface>
>>> from xml.dom import minidom >>> xmldoc = minidom.parse('russiansample.xml') >>> title = xmldoc.getElementsByTagName('title')[0].firstChild.data >>> title u'\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435' >>> print title Traceback (innermost last): File "<interactive input>", line 1, in ? UnicodeError: ASCII encoding error: ordinal not in range(128) >>> convertedtitle = title.encode('koi8-r') >>> convertedtitle '\xf0\xd2\xc5\xc4\xc9\xd3\xcc\xcf\xd7\xc9\xc5' >>> print convertedtitle Предисловие
总结一下,如果你以前从没有看到过 unicode,倒是有些唬人,但是在 Python 处理 unicode 数据真是非常容易。如果你的 XML 文档都是 7 位的 ASCII (像本章中的例子),你差不多永远都不用考虑 unicode。Python 在进行解析时会将 XML 文档中的 ASCII 数据转换为 unicode,在任何需要的时候强制转换回为 ASCII,你甚至永远都不用注意。但是如果你要处理其它语言的数据,Python 已经准备好了。
通过一步步访问每一个节点的方式遍历 XML 文档可能很乏味。如果你正在寻找些特别的东西,又恰恰它们深深埋入了你的 XML 文档,有个捷径让你可以快速找到它:getElementsByTagName 。
在这部分,将使用 binary.xml 语法文件,它的内容如下:
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
它有两个 ref,'bit' (位) 和 'byte' (字节)。一个 bit 是 '0' 或者 '1',而一个 byte 是 8 个 bit。
>>> from xml.dom import minidom >>> xmldoc = minidom.parse('binary.xml') >>> reflist = xmldoc.getElementsByTagName('ref') >>> reflist [<DOM Element: ref at 136138108>, <DOM Element: ref at 136144292>] >>> print reflist[0].toxml() <ref id="bit"> <p>0</p> <p>1</p> </ref> >>> print reflist[1].toxml() <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref>
>>> firstref = reflist[0] >>> print firstref.toxml() <ref id="bit"> <p>0</p> <p>1</p> </ref> >>> plist = firstref.getElementsByTagName("p") >>> plist [<DOM Element: p at 136140116>, <DOM Element: p at 136142172>] >>> print plist[0].toxml() <p>0</p> >>> print plist[1].toxml() <p>1</p>
>>> plist = xmldoc.getElementsByTagName("p") >>> plist [<DOM Element: p at 136140116>, <DOM Element: p at 136142172>, <DOM Element: p at 136146124>] >>> plist[0].toxml() '<p>0</p>' >>> plist[1].toxml() '<p>1</p>' >>> plist[2].toxml() '<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>'
XML 元素可以有一个或者多个属性,只要已经解析了一个 XML 文档,访问它们就太简单了。
在这部分中,将使用 binary.xml 语法文件,你在上一节中已经看到过了。
这部分由于某个含义重叠的术语可能让人有点糊涂。在一个 XML 文档中,元素可以有属性,而 Python 对象也有属性。当你解析一个 XML 文档时,你得到了一组 Python 对象,它们代表 XML 文档中的所有片段,同时有些 Python 对象代表 XML 元素的属性。但是表示 (XML) 属性的 (Python) 对象也有 (Python) 属性,它们用于访问对象表示的 (XML) 属性。我告诉过你它让人糊涂。我会公开提出关于如何更明显地区分这些不同的建议。 |
>>> xmldoc = minidom.parse('binary.xml') >>> reflist = xmldoc.getElementsByTagName('ref') >>> bitref = reflist[0] >>> print bitref.toxml() <ref id="bit"> <p>0</p> <p>1</p> </ref> >>> bitref.attributes <xml.dom.minidom.NamedNodeMap instance at 0x81e0c9c> >>> bitref.attributes.keys() [u'id'] >>> bitref.attributes.values() [<xml.dom.minidom.Attr instance at 0x81d5044>] >>> bitref.attributes["id"] <xml.dom.minidom.Attr instance at 0x81d5044>
每个 Element 对象都有一个 attributes 属性,它是一个 NamedNodeMap 对象。听上去挺吓人的,其实不然,因为 NamedNodeMap 是一个行为像字典的对象,所以你已经知道怎么使用它了。 | |
将 NamedNodeMap 视为一个字典,你可以通过 attributes.keys() 获得属性名称的一个列表。这个元素只有一个属性,'id'。 | |
属性名称,像其它 XML 文档中的文本一样,都是以 unicode 保存的。 | |
再次将 NamedNodeMap 视为一个字典,你可以通过 attributes.values() 获取属性值的一个列表。这些值本身是 Attr 类型的对象。你将在下一个例子中看到如何获取对象的有用信息。 | |
仍然把 NamedNodeMap 视为一个字典,你可以通过常用的字典语法和名称访问单个的属性。(那些非常认真的读者将已经知道 NamedNodeMap 类是如何实现这一技巧的:通过定义一个 __getitem__ 专用方法。其它的读者可能乐意接受这一事实:他们不需要理解它是如何工作的就可以有效地使用它。) |
>>> a = bitref.attributes["id"] >>> a <xml.dom.minidom.Attr instance at 0x81d5044> >>> a.name u'id' >>> a.value u'bit'
类似于字典,一个 XML 元素的属性没有顺序。属性可以以某种顺序偶然 列在最初的 XML 文档中,而在 XML 文档解析为 Python 对象时,Attr 对象以某种顺序偶然 列出,这些顺序都是任意的,没有任何特别的含义。你应该总是使用名称来访问单个属性,就像字典的键一样。 |
以上就是 XML 的核心内容。下一章将使用相同的示例程序,但是焦点在于能使程序更加灵活的其它方面:使用输入流处理,使用 getattr 进行方法分发,并使用命令行标识允许用户重新配置程序而无需修改代码。
在进入下一章前,你应该没有困难的完成这些事情:
Python 的最强大力量之一是它的动态绑定,而动态绑定最强大的用法之一是类文件(file-like)对象。
许多需要输入源的函数可以只接收一个文件名,并以读方式打开文件,读取文件,处理完成后关闭它。其实它们不是这样的,而是接收一个类文件对象。
在最简单的例子中,类文件对象 是任意一个带有 read 方法的对象,这个方法带有一个可选的 size 参数,并返回一个字符串。调用时如果没有 size 参数,它从输入源中读取所有东西并将所有数据作为单个字符串返回;调用时如果指定了 size 参数,它将从输入源中读取 size 大小的数据并返回这些数据;再次调用的时候,它从余下的地方开始并返回下一块数据。
这就是从真实文件读取数据的工作方式;区别在于你不用把自己局限于真实的文件。输入源可以是任何东西:磁盘上的文件,甚至是一个硬编码的字符串。只要你将一个类文件对象传递给函数,函数只是调用对象的 read 方法,就可以处理任何类型的输入源,而不需要为处理每种类型分别编码。
你可能会纳闷,这和 XML 处理有什么关系。其实 minidom.parse 就是一个可以接收类文件对象的函数。
>>> from xml.dom import minidom >>> fsock = open('binary.xml') >>> xmldoc = minidom.parse(fsock) >>> fsock.close() >>> print xmldoc.toxml() <?xml version="1.0" ?> <grammar> <ref id="bit"> <p>0</p> <p>1</p> </ref> <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref> </grammar>
首先,你要打开一个磁盘上的文件。这会提供给你一个文件对象。 | |
将文件对象传递给 minidom.parse,它调用 fsock 的 read 方法并从磁盘上的文件读取 XML 文档。 | |
确保处理完文件后调用 close 方法。minidom.parse不会替你做这件事。 | |
在返回的 XML 文档上调用 toxml() 方法,打印出整个文档的内容。 |
哦,所有这些看上去像是在浪费大量的时间。毕竟,你已经看到,minidom.parse 可以只接收文件名,并自动执行所有打开文件和关闭无用文件的行为。不错,如果你知道正要解析的是一个本地文件,你可以传递文件名而且 minidom.parse 可以足够聪明地做正确的事情 (Do The Right Thing™[10]),这一切都不会有问题。但是请注意,使用类文件,会使分析直接从 Internet 上来的 XML 文档变得多么相似和容易!
>>> import urllib >>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') >>> xmldoc = minidom.parse(usock) >>> usock.close() >>> print xmldoc.toxml() <?xml version="1.0" ?> <rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <channel> <title>Slashdot</title> <link>http://slashdot.org/</link> <description>News for nerds, stuff that matters</description> </channel> <image> <title>Slashdot</title> <url>http://images.slashdot.org/topics/topicslashdot.gif</url> <link>http://slashdot.org/</link> </image> <item> <title>To HDTV or Not to HDTV?</title> <link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link> </item> [...snip...]
正如在前一章中所看到的,urlopen 接收一个 web 页面的 URL 作为参数并返回一个类文件对象。最重要的是,这个对象有一个 read 方法,它可以返回 web 页面的 HTML 源代码。 | |
现在把类文件对象传递给 minidom.parse,它顺从地调用对象的 read 方法并解析 read 方法返回的 XML 数据。这与 XML 数据现在直接来源于 web 页面的事实毫不相干。minidom.parse 并不知道 web 页面,它也不关心 web 页面;它只知道类文件对象。 | |
到这里已经处理完毕了,确保将 urlopen 提供给你的类文件对象关闭。 | |
顺便提一句,这个 URL 是真实的,它真的是一个 XML。它是 Slashdot 站点 (一个技术新闻和随笔站点) 上当前新闻提要的 XML 表示。 |
>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>" >>> xmldoc = minidom.parseString(contents) >>> print xmldoc.toxml() <?xml version="1.0" ?> <grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
好吧,所以你可以使用 minidom.parse 函数来解析本地文件和远端 URL,但对于解析字符串,你使用……另一个函数。这就是说,如果你要从文件、URL 或者字符串接收输入,就需要特别的逻辑来判断参数是否是字符串,然后调用 parseString。多不让人满意。
如果有一个方法可以把字符串转换成类文件对象,那么你只要这个对象传递给 minidom.parse 就可以了。事实上,有一个模块专门设计用来做这件事:StringIO。
>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>" >>> import StringIO >>> ssock = StringIO.StringIO(contents) >>> ssock.read() "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>" >>> ssock.read() '' >>> ssock.seek(0) >>> ssock.read(15) '<grammar><ref i' >>> ssock.read(15) "d='bit'><p>0</p" >>> ssock.read() '><p>1</p></ref></grammar>' >>> ssock.close()
>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>" >>> ssock = StringIO.StringIO(contents) >>> xmldoc = minidom.parse(ssock) >>> ssock.close() >>> print xmldoc.toxml() <?xml version="1.0" ?> <grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
那么现在你知道了如何使用同一个函数,minidom.parse,来解析一个保存在 web 页面上、本地文件中或硬编码字符串中的 XML 文档。对于一个 web 页面,使用 urlopen 得到类文件对象;对于本地文件,使用 open;对于字符串,使用 StringIO。现在让我们进一步并归纳一下这些 不同。
def openAnything(source): # try to open with urllib (if source is http, ftp, or file URL) import urllib try: return urllib.urlopen(source) except (IOError, OSError): pass # try to open with native open function (if source is pathname) try: return open(source) except (IOError, OSError): pass # treat source as string import StringIO return StringIO.StringIO(str(source))
openAnything 函数接受单个参数,source,并返回类文件对象。source 是某种类型的字符串;它可能是一个 URL (例如 'http://slashdot.org/slashdot.rdf'),一个本地文件的完整或者部分路径名 (例如 'binary.xml'),或者是一个包含了待解析 XML 数据的字符串。 | |
首先,检查 source 是否是一个 URL。这里通过强制方式进行:尝试把它当作一个 URL 打开并静静地忽略打开非 URL 引起的错误。这样做非常好,因为如果 urllib 将来支持更多的 URL 类型,不用重新编码就可以支持它们。如果 urllib 能够打开 source,那么 return 可以立刻把你踢出函数,下面的 try 语句将不会执行。 | |
另一方面,如果 urllib 向你呼喊并告诉你 source 不是一个有效的 URL,你假设它是一个磁盘文件的路径并尝试打开它。再一次,你不用做任何特别的事来检查 source 是否是一个有效的文件名 (在不同的平台上,判断文件名有效性的规则变化很大,因此不管怎样做都可能会判断错)。反而,只要盲目地打开文件并静静地捕获任何错误就可以了。 | |
到这里,你需要假设 source 是一个其中有硬编码数据的字符串 (因为没有别的可以判断的了),所以你可以使用 StringIO 从中创建一个类文件对象并将它返回。(实际上,由于使用了 str 函数,所以 source 没有必要一定是字符串;它可以是任何对象,你可以使用它的字符串表示形式,只要定义了它的 __str__ 专用方法。) |
现在你可以使用这个 openAnything 函数联合 minidom.parse 构造一个函数,接收一个指向 XML 文档的 source,而且无需知道这个 source 的含义 (可以是一个 URL 或是一个本地文件名,或是一个硬编码 XML 文档的字符串形式),然后解析它。
UNIX 用户已经对标准输入、标准输出和标准错误的概念非常熟悉了。这一节是为其他不熟悉的人准备的。
标准输入和标准错误 (通常缩写为 stdout 和 stderr) 是内建在每一个 UNIX 系统中的管道。当你 print 某些东西时,结果前往 stdout 管道;当你的程序崩溃并打印出调试信息 (例如 Python 中的 traceback (错误跟踪)) 的时候,信息前往 stderr 管道。通常这两个管道只与你正在工作的终端窗口相联,所以当一个程序打印时,你可以看到输出,而当一个程序崩溃时,你可以看到调试信息。(如果你正在一个基于窗口的 Python IDE 上工作,stdout 和 stderr 缺省为你的“交互窗口”。)
>>> for i in range(3): ... print 'Dive in' Dive in Dive in Dive in >>> import sys >>> for i in range(3): ... sys.stdout.write('Dive in') Dive inDive inDive in >>> for i in range(3): ... sys.stderr.write('Dive in') Dive inDive inDive in
正如在例 6.9 “简单计数”中看到的,你可以使用 Python 内置的 range 函数来构造简单的计数循环,即重复某物一定的次数。 | |
stdout 是一个类文件对象;调用它的 write 函数可以打印出你给定的任何字符串。实际上,这就是 print 函数真正做的事情;它在你打印的字符串后面加上一个硬回车,然后调用 sys.stdout.write 函数。 | |
在最简单的例子中,stdout 和 stderr 把它们的输出发送到相同的地方:Python IDE (如果你在一个 IDE 中的话),或者终端 (如果你从命令行运行 Python 的话)。和 stdout 一样,stderr 并不为你添加硬回车;如果需要,要自己加上。 |
stdout 和 stderr 都是类文件对象,就像在第 10.1 节 “抽象输入源”中讨论的一样,但是它们都是只写的。它们都没有 read 方法,只有 write 方法。然而,它们仍然是类文件对象,因此你可以将其它任何 (类) 文件对象赋值给它们来重定向其输出。
[you@localhost kgp]$ python stdout.py Dive in [you@localhost kgp]$ cat out.log This message will be logged instead of displayed
(在 Windows 上,你要使用 type 来代替 cat 显示文件的内容。)
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
#stdout.py import sys print 'Dive in' saveout = sys.stdout fsock = open('out.log', 'w') sys.stdout = fsock print 'This message will be logged instead of displayed' sys.stdout = saveout fsock.close()
重定向 stderr 以完全相同的方式进行,只要把 sys.stdout 改为 sys.stderr。
[you@localhost kgp]$ python stderr.py [you@localhost kgp]$ cat error.log Traceback (most recent line last): File "stderr.py", line 5, in ? raise Exception, 'this error will be logged' Exception: this error will be logged
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
#stderr.py import sys fsock = open('error.log', 'w') sys.stderr = fsock raise Exception, 'this error will be logged'
向标准错误写入错误信息是很常见的,所以有一种较快的语法可以立刻导出信息。
>>> print 'entering function' entering function >>> import sys >>> print >> sys.stderr, 'entering function' entering function
另一方面,标准输入是一个只读文件对象,它表示从前一个程序到这个程序的数据流。这个对于老的 Mac OS 用户和 Windows 用户可能不太容易理解,除非你受到过 MS-DOS 命令行的影响。在 MS-DOS 命令行中,你可以使用一行指令构造一个命令的链,使得一个程序的输出就可以成为下一个程序的输入。第一个程序只是简单地输出到标准输出上 (程序本身没有做任何特别的重定向,只是执行了普通的 print 语句等),然后,下一个程序从标准输入中读取,操作系统就把一个程序的输出连接到一个程序的输入。
[you@localhost kgp]$ python kgp.py -g binary.xml 01100111 [you@localhost kgp]$ cat binary.xml <?xml version="1.0"?> <!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd"> <grammar> <ref id="bit"> <p>0</p> <p>1</p> </ref> <ref id="byte"> <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\ <xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p> </ref> </grammar> [you@localhost kgp]$ cat binary.xml | python kgp.py -g - 10110001
正如你在第 9.1 节 “概览”中看到的,该命令将只打印一个随机的八位字符串,其中只有 0 或者 1。 | |
该处只是简单地打印出整个 binary.xml 文档的内容。(Windows 用户应该用 type 代替 cat。) | |
该处打印 binary.xml 的内容,但是“|”字符,称为“管道”符,说明内容不会打印到屏幕上;它们会成为下一个命令的标准输入,在这个例子中是你调用的 Python 脚本。 | |
为了不用指定一个文件 (例如 binary.xml),你需要指定“-”,它会使得你的脚本从标准输入载入脚本,而不是从磁盘上的特定文件。 (下一个例子更多地说明了这是如何实现的)。所以效果和第一种语法是一样的,在那里你要直接指定语法文件,但是想想这里的扩展性。让我们把 cat binary.xml 换成别的什么东西――例如运行一个脚本动态生成语法――然后通过管道将它导入你的脚本。它可以来源于任何地方:数据库,或者是生成语法的元脚本,或者其他。你根本不需要修改你的 kgp.py 脚本就可以并入这个功能。你要做的仅仅是从标准输入取得一个语法文件,然后你就可以将其他的逻辑分离出来,放到另一程序中去了。 |
那么脚本是如何“知道”在语法文件是“-”时从标准输入读取? 其实不神奇;它只是代码。
def openAnything(source): if source == "-": import sys return sys.stdin # try to open with urllib (if source is http, ftp, or file URL) import urllib try: [... snip ...]
这是 toolbox.py 中的 openAnything 函数,以前在第 10.1 节 “抽象输入源”中你已经检视过了。所有你要做的就是在函数的开始加入 3 行代码来检测源是否是“-”;如果是,返回 sys.stdin。就这么简单!记住,stdin 是一个拥有 read 方法的类文件对象,所以其它的代码 (在 kgp.py 中,在那里你调用了 openAnything) 一点都不需要改动。 |
kgp.py 使用了多种技巧,在你进行 XML 处理时,它们或许能派上用场。第一个就是,利用输入文档的结构稳定特征来构建节点缓冲。
一个语法文件定义了一系列的 ref 元素。每个 ref 包含了一个或多个 p 元素,p 元素则可以包含很多不同的东西,包括 xref。对于每个 xref,你都能找到相对应的 ref 元素 (它们具有相同的 id 属性),然后选择 ref 元素的子元素之一进行解析。(在下一部分中你将看到是如何进行这种随机选择的。)
语法的构建方式如下:先为最小的片段定义 ref 元素,然后使用 xref 定义“包含”第一个 ref 元素的 ref 元素,等等。然后,解析“最大的”引用并跟着 xref 跳来跳去,最后输出真实的文本。输出的文本依赖于你每次填充 xref 时所做的 (随机) 决策,所以每次的输出都是不同的。
这种方式非常灵活,但是有一个不好的地方:性能。当你找到一个 xref 并需要找到相应的 ref 元素时,会遇到一个问题。xref 有 id 属性,而你要找拥有相同 id 属性的 ref 元素,但是没有简单的方式做到这件事。较慢的方式是每次获取所有 ref 元素的完整列表,然后手动遍历并检视每一个 id 属性。较快的方式是只做一次,然后以字典形式构建一个缓冲。
def loadGrammar(self, grammar): self.grammar = self._load(grammar) self.refs = {} for ref in self.grammar.getElementsByTagName("ref"): self.refs[ref.attributes["id"].value] = ref
从创建一个空字典 self.refs 开始。 | |
正如你在第 9.5 节 “搜索元素”中看到的,getElementsByTagName 返回所有特定名称元素的一个列表。你可以很容易地得到所有 ref 元素的一个列表,然后遍历这个列表。 | |
正如你在第 9.6 节 “访问元素属性”中看到的,使用标准的字典语法,你可以通过名称来访问个别元素。所以,self.refs 字典的键将是每个 ref 元素的 id 属性值。 | |
self.refs 字典的值将是 ref 元素本身。如你在第 9.3 节 “XML 解析”中看到的,已解析 XML 文档中的每个元素、节点、注释和文本片段都是一个对象。 |
只要构建了这个缓冲,无论何时你遇到一个 xref 并且需要找到具有相同 id 属性的 ref 元素,都只需在 self.refs 中查找它。
def do_xref(self, node): id = node.attributes["id"].value self.parse(self.randomChildElement(self.refs[id]))
你将在下一部分探究 randomChildElement 函数。
解析 XML 文档时,另一个有用的己技巧是查找某个特定元素的所有直接子元素。例如,在语法文件中,一个 ref 元素可以有数个 p 元素,其中每一个都可以包含很多东西,包括其他的 p 元素。你只要查找作为 ref 孩子的 p 元素,不用查找其他 p 元素的孩子 p 元素。
你可能认为你只要简单地使用 getElementsByTagName 来实现这点就可以了,但是你不可以这么做。getElementsByTagName 递归搜索并返回所有找到的元素的单个列表。由于 p 元素可以包含其他的 p 元素,你不能使用 getElementsByTagName,因为它会返回你不要的嵌套 p 元素。为了只找到直接子元素,你要自己进行处理。
def randomChildElement(self, node): choices = [e for e in node.childNodes if e.nodeType == e.ELEMENT_NODE] chosen = random.choice(choices) return chosen
正如你在例 9.9 “获取子节点”中看到的,childNodes 属性返回元素所有子节点的一个列表。 | |
然而,正如你在例 9.11 “子节点可以是文本”中看到的,childNodes 返回的列表包含了所有不同类型的节点,包括文本节点。这并不是你在这里要查找的。你只要元素形式的孩子。 | |
每个节点都有一个 nodeType 属性,它可以是ELEMENT_NODE, TEXT_NODE, COMMENT_NODE,或者其它值。可能值的完整列表在 xml.dom 包的 __init__.py 文件中。(关于包的介绍,参见第 9.2 节 “包”。) 但你只是对元素节点有兴趣,所以你可以过滤出一个列表,其中只包含 nodeType 是ELEMENT_NODE的节点。 | |
只要拥有了一个真实元素的列表,选择任意一个都很容易。Python 有一个叫 random 的模块,它包含了好几个有用的函数。random.choice 函数接收一个任意数量条目的列表并随机返回其中的一个条目。比如,如果 ref 元素包含了多个 p 元素,那么 choices 将会是 p 元素的一个列表,而 chosen 将被赋予其中的某一个值,而这个值是随机选择的。 |
第三个有用的 XML 处理技巧是将你的代码基于节点类型和元素名称分散到逻辑函数中。解析后的 XML 文档是由各种类型的节点组成的,每一个都是通过 Python 对象表示的。文档本身的根层次通过一个 Document 对象表示。Document 还包含了一个或多个 Element 对象 (表示 XML 标记),其中的每一个可以包含其它的 Element 对象、Text 对象 (表示文本),或者 Comment 对象 (表示内嵌注释)。使用 Python 编写分离各个节点类型逻辑的分发器非常容易。
>>> from xml.dom import minidom >>> xmldoc = minidom.parse('kant.xml') >>> xmldoc <xml.dom.minidom.Document instance at 0x01359DE8> >>> xmldoc.__class__ <class xml.dom.minidom.Document at 0x01105D40> >>> xmldoc.__class__.__name__ 'Document'
暂时假设 kant.xml 在当前目录中。 | |
正如你在第 9.2 节 “包”中看到的,解析 XML 文档返回的对象是一个 Document 对象,就像在 xml.dom 包的 minidom.py 中定义的一样。又如你在第 5.4 节 “类的实例化”中看到的,__class__ 是每个 Python 对象的一个内置属性。 | |
此外,__name__ 是每个 Python 类的内置属性,是一个字符串。这个字符串并不神秘;它和你在定义类时输入的类名相同。(参见第 5.3 节 “类的定义”。) |
好,现在你能够得到任何给定 XML 节点的类名了 (因为每个 XML 节点都是以一个 Python 对象表示的)。你怎样才能利用这点来分离解析每个节点类型的逻辑呢?答案就是 getattr,你第一次见它是在第 4.4 节 “通过 getattr 获取对象引用”中。
def parse(self, node): parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) parseMethod(node)
def parse_Document(self, node): self.parse(node.documentElement) def parse_Text(self, node): text = node.data if self.capitalizeNextWord: self.pieces.append(text[0].upper()) self.pieces.append(text[1:]) self.capitalizeNextWord = 0 else: self.pieces.append(text) def parse_Comment(self, node): pass def parse_Element(self, node): handlerMethod = getattr(self, "do_%s" % node.tagName) handlerMethod(node)
在这个例子中,分发函数 parse 和 parse_Element 只是找到相同类中的其它方法。如果你进行的处理过程很复杂 (或者你有很多不同的标记名称),你可以将代码分散到独立的模块中,然后使用动态导入的方式导入每个模块并调用你需要的任何函数。动态导入将在第 16 章 函数编程中介绍。
Python 完全支持创建在命令行运行的程序,也支持通过命令行参数和短长样式来指定各种选项。这些并非是 XML 特定的,但是这样的脚本可以充分使用命令行处理,看来是时候提一下它了。
如果不理解命令行参数如何暴露给你的 Python 程序,讨论命令行处理是很困难的,所以让我们先写个简单点的程序来看一下。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
#argecho.py import sys for arg in sys.argv: print arg
[you@localhost py]$ python argecho.py argecho.py [you@localhost py]$ python argecho.py abc def argecho.py abc def [you@localhost py]$ python argecho.py --help argecho.py --help [you@localhost py]$ python argecho.py -m kant.xml argecho.py -m kant.xml
关于 sys.argv 需要了解的第一件事情就是:它包含了你正在调用的脚本的名称。你后面会实际使用这个知识,在第 16 章 函数编程中。现在不用担心。 | |
命令行参数通过空格进行分隔。在 sys.argv 列表中,每个参数都是一个独立的元素。 | |
命令行标志,像 --help,在 sys.argv 列表中还保存了它们自己的元素。 | |
为了让事情更有趣,有些命令行标志本身就接收参数。比如,这里有一个标记 (-m) 接收一个参数 (kant.xml)。标记自身和标记参数只是 sys.argv 列表中的一串元素。并没有试图将元素与其它元素进行关联;所有你得到的是一个列表。 |
所以正如你所看到的,你确实拥有了命令行传入的所有信息,但是接下来要实际使用它似乎不那么容易。对于只是接收单个参数或者没有标记的简单程序,你可以简单地使用 sys.argv[1] 来访问参数。这没有什么羞耻的;我一直都是这样做的。对更复杂的程序,你需要 getopt 模块。
def main(argv): grammar = "kant.xml" try: opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) except getopt.GetoptError: usage() sys.exit(2) ... if __name__ == "__main__": main(sys.argv[1:])
那么你传给 getopt 函数的参数是什么呢?好的,第一个只不过是一个命令行标志和参数的原始列表 (不包括第一个元素――脚本名称,你在调用 main 函数之前就已经将它砍掉了)。第二个是脚本接收的短命令行标志的一个列表。
第一个标志和第三个标志是简单的独立标志;你选择是否指定它们,它们做某些事情 (打印帮助) 或者改变状态 (打开调试)。但是,第二个标志 (-g) 必须 跟随一个参数――进行读取的语法文件的名称。实际上,它可以是一个文件名或者一个 web 地址,这时还不知道 (后面会确定),但是你要知道必须要有些东西。所以,你可以通过在 getopt 函数的第二个参数的 g 后面放一个冒号,来向 getopt 说明这一点。
更复杂的是,这个脚本既接收短标志 (像 -h),也接受长标志 (像 --help),并且你要它们做相同的事。这就是 getopt 第三个参数存在的原因:它是指定长标志的一个列表,其中的长标志是和第二个参数中指定的短标志相对应的。
这里有三点要注意:
被搞昏没?让我们看一下真实的代码,看看它在上下文中是否起作用。
def main(argv): grammar = "kant.xml" try: opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) except getopt.GetoptError: usage() sys.exit(2) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit() elif opt == '-d': global _debug _debug = 1 elif opt in ("-g", "--grammar"): grammar = arg source = "".join(args) k = KantGenerator(grammar, source) print k.output()
你已经了解很多基础的东西。让我们回来看看所有片段是如何整合到一起的。
作为开始,这里是一个接收命令行参数的脚本,它使用 getopt 模块。
def main(argv): ... try: opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) except getopt.GetoptError: ... for opt, arg in opts: ...
创建 KantGenerator 类的一个实例,然后将语法文件和源文件传给它,可能在命令行没有指定。
k = KantGenerator(grammar, source)
KantGenerator 实例自动加载语法,它是一个 XML 文件。你使用自定义的 openAnything 函数打开这个文件 (可能保存在一个本地文件中或者一个远程服务器上),然后使用内置的 minidom 解析函数将 XML 解析为一棵 Python 对象树。
def _load(self, source): sock = toolbox.openAnything(source) xmldoc = minidom.parse(sock).documentElement sock.close()
哦,根据这种方式,你将使用到 XML 文档结构的知识建立一个引用的小缓冲,这些引用都只是 XML 文档中的元素。
def loadGrammar(self, grammar): for ref in self.grammar.getElementsByTagName("ref"): self.refs[ref.attributes["id"].value] = ref
如果你在命令行中指定了某些源材料,你可以使用它;否则你将打开语法文件查找“顶层”引用 (没有被其它的东西引用) 并把它作为开始点。
def getDefaultSource(self): xrefs = {} for xref in self.grammar.getElementsByTagName("xref"): xrefs[xref.attributes["id"].value] = 1 xrefs = xrefs.keys() standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs] return '<xref id="%s"/>' % random.choice(standaloneXrefs)
现在你打开了了源材料。它是一个 XML,你每次解析一个节点。为了让代码分离并具备更高的可维护性,你可以使用针对每个节点类型的独立处理方法。
def parse_Element(self, node): handlerMethod = getattr(self, "do_%s" % node.tagName) handlerMethod(node)
你在语法里面跳来跳去,解析每一个 p 元素的所有孩子,
def do_p(self, node): ... if doit: for child in node.childNodes: self.parse(child)
用任意一个孩子替换 choice 元素,
def do_choice(self, node): self.parse(self.randomChildElement(node))
并用对应 ref 元素的任意孩子替换 xref,前面你已经进行了缓冲。
def do_xref(self, node): id = node.attributes["id"].value self.parse(self.randomChildElement(self.refs[id]))
就这样一直解析,最后得到普通文本。
def parse_Text(self, node): text = node.data ... self.pieces.append(text)
把结果打印出来。
def main(argv): ... k = KantGenerator(grammar, source) print k.output()
Python 带有解析和操作 XML 文档非常强大的库。minidom 接收一个 XML 文件并将其解析为 Python 对象,并提供了对任意元素的随机访问。进一步,本章展示了如何利用 Python 创建一个“真实”独立的命令行脚本,连同命令行标志、命令行参数、错误处理,甚至从前一个程序的管道接收输入的能力。
在继续下一章前,你应该无困难地完成所有这些事情:
在讲解如何下载 web 页和如何从 URL 解析 XML时,你已经学习了关于 HTML 处理和 XML 处理,接下来让我们来更全面地探讨有关 HTTP web 服务的主题。
简单地讲,HTTP web 服务是指以编程的方式直接使用 HTTP 操作从远程服务器发送和接收数据。如果你要从服务器获取数据,直接使用 HTTP GET;如果您想发送新数据到服务器,使用 HTTP POST。(一些较高级的 HTTP web 服务 API 也定义了使用 HTTP PUT 和 HTTP DELETE 修改和删除现有数据的方法。) 换句话说,构建在 HTTP 协议中的 “verbs (动作)” (GET, POST, PUT 和 DELETE) 直接映射为接收、发送、修改和删除等应用级别的操作。
这种方法的主要优点是简单,并且许多不同的站点充分印证了这样的简单性是受欢迎的。数据 (通常是 XML 数据) 能静态创建和存储,或通过服务器端脚本和所有主流计算机语言 (包括用于下载数据的 HTTP 库) 动态生成。调试也很简单,因为您可以在任意浏览器中调用网络服务来查看这些原始数据。现代浏览器甚至可以为您进行良好的格式化并漂亮地打印这些 XML 数据,以便让您快速地浏览。
HTTP web 服务上的纯 XML 应用举例:
在后面的几章里,我们将探索使用 HTTP 进行数据发送和接收传输的 API,但是不会将应用语义映射到潜在的 HTTP 语义。(所有这些都是通过 HTTP POST 这个管道完成的。) 但是本章将关注使用 HTTP GET 从远程服务器获取数据,并且将探索几个由纯 HTTP web 服务带来最大利益的 HTTP 特性。
如下所示为上一章曾经看到过的 openanything 模块的更高级版本:
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
import urllib2, urlparse, gzip from StringIO import StringIO USER_AGENT = 'OpenAnything/1.0 +http://diveintopython.org/http_web_services/' class SmartRedirectHandler(urllib2.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_301( self, req, fp, code, msg, headers) result.status = code return result def http_error_302(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_302( self, req, fp, code, msg, headers) result.status = code return result class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): def http_error_default(self, req, fp, code, msg, headers): result = urllib2.HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT): '''URL, filename, or string --> stream This function lets you define parsers that take any input source (URL, pathname to local or network file, or actual data as a string) and deal with it in a uniform manner. Returned object is guaranteed to have all the basic stdio read methods (read, readline, readlines). Just .close() the object when you're done with it. If the etag argument is supplied, it will be used as the value of an If-None-Match request header. If the lastmodified argument is supplied, it must be a formatted date/time string in GMT (as returned in the Last-Modified header of a previous request). The formatted date/time will be used as the value of an If-Modified-Since request header. If the agent argument is supplied, it will be used as the value of a User-Agent request header. ''' if hasattr(source, 'read'): return source if source == '-': return sys.stdin if urlparse.urlparse(source)[0] == 'http': # open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent) if etag: request.add_header('If-None-Match', etag) if lastmodified: request.add_header('If-Modified-Since', lastmodified) request.add_header('Accept-encoding', 'gzip') opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) return opener.open(request) # try to open with native open function (if source is a filename) try: return open(source) except (IOError, OSError): pass # treat source as string return StringIO(str(source)) def fetch(source, etag=None, last_modified=None, agent=USER_AGENT): '''Fetch data and metadata from a URL, file, stream, or string''' result = {} f = openAnything(source, etag, last_modified, agent) result['data'] = f.read() if hasattr(f, 'headers'): # save ETag, if the server sent one result['etag'] = f.headers.get('ETag') # save Last-Modified header, if the server sent one result['lastmodified'] = f.headers.get('Last-Modified') if f.headers.get('content-encoding', '') == 'gzip': # data came back gzip-compressed, decompress it result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read() if hasattr(f, 'url'): result['url'] = f.url result['status'] = 200 if hasattr(f, 'status'): result['status'] = f.status f.close() return result
假如说你想用 HTTP 下载资源,例如一个 Atom feed 汇聚。你不仅仅想下载一次;而是想一次又一次地下载它,如每小时一次,从提供 news feed 的站点获得最新的消息。让我们首先用一种直接而原始的方法来实现它,然后看看如何改进它。
>>> import urllib >>> data = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read() >>> print data <?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3" xmlns="http://purl.org/atom/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en"> <title mode="escaped">dive into mark</title> <link rel="alternate" type="text/html" href="http://diveintomark.org/"/> <-- rest of feed omitted for brevity -->
那么这种方法有何不妥之处吗?当然,在测试或开发中一次性的使用没有什么不妥。我经常这样。我想要 feed 汇聚的内容,我就获取 feed 的内容。这种方法对其他 web 页面同样有效。但是一旦你开始按照 web 服务的方式去思考有规则的访问需求时 (记住,你说你计划每小时一次地重复获取这样的 feed ) 就会发现这样的做法效率实在是太低了,并且对服务器来说也太笨了。
下面先谈论一些 HTTP 的基本特性。
这里有五个你必须关注的 HTTP 重要特性。
User-Agent 是一种客户端告知服务器谁在什么时候通过 HTTP 请求了一个 web 页、feed 汇聚或其他类型的 web 服务的简单途径。当客户端请求一个资源时,应该尽可能明确发起请求的是谁,以便当产生异常错误时,允许服务器端的管理员与客户端的开发者取得联系。
默认情况下 Python 发送一个通用的 User-Agent:Python-urllib/1.15。下一节,您将看到更加有针对性的 User-Agent。
有时资源移来移去。Web 站点重组内容,页面移动到了新的地址。甚至是 web 服务重组。原来位于 http://example.com/index.xml 的 feed 汇聚可能被移动到 http://example.com/xml/atom.xml。或者因为一个机构的扩展或重组,整个域被迁移。例如,http://www.example.com/index.xml 可能被重定向到 http://server-farm-1.example.com/index.xml。
您每次从 HTTP 服务器请求任何类型的资源时,服务器的响应中均包含一个状态代码。状态代码 200 的意思是 “一切正常,这就是您请求的页面”。状态代码 404 的意思是 “页面没找到”。 (当浏览 web 时,你可能看到过 404 errors。)
HTTP 有两种不同的方法表示资源已经被移动。状态代码 302 表示临时重定向;这意味着 “哎呀,访问内容被临时移动” (然后在 Location: 头信息中给出临时地址)。状态代码 301 表示永久重定向;这意味着 “哎呀,访问内容被永久移动” (然后在 Location: 头信息中给出新地址)。如果您获得了一个 302 状态代码和一个新地址,HTTP 规范说您应该使用新地址获取您的请求,但是下次您要访问同一资源时,应该使用原地址重试。但是如果您获得了一个 301 状态代码和一个新地址,您应该从此使用新地址。
当从 HTTP 服务器接受到一个适当的状态代码时,urllib.urlopen 将自动 “跟踪” 重定向,但不幸的是,当它做了重定向时不会告诉你。 你将最终获得所请求的数据,却丝毫不会察觉到在这个过程中一个潜在的库 “帮助” 你做了一次重定向操作。因此你将继续不断地使用旧地址,并且每次都将获得被重定向的新地址。这一过程要往返两次而不是一次:太没效率了!本章的后面,您将看到如何改进这一点,从而适当地且有效率地处理永久重定向。
有些数据随时都在变化。CNN.com 的主页经常几分钟就更新。另一方面,Google.com 的主页几个星期才更新一次 (当他们上传特殊的假日 logo,或为一个新服务作广告时)。 Web 服务是不变的:通常服务器知道你所请求的数据的最后修改时间,并且 HTTP 为服务器提供了一种将最近修改数据连同你请求的数据一同发送的方法。
如果你第二次 (或第三次,或第四次) 请求相同的数据,你可以告诉服务器你上一次获得的最后修改日期:在你的请求中发送一个 If-Modified-Since 头信息,它包含了上一次从服务器连同数据所获得的日期。如果数据从那时起没有改变,服务器将返回一个特殊的 HTTP 状态代码 304,这意味着 “从上一次请求后这个数据没有改变”。这一点有何进步呢?当服务器发送状态编码 304 时,不再重新发送数据。您仅仅获得了这个状态代码。所以当数据没有更新时,你不需要一次又一次地下载相同的数据;服务器假定你有本地的缓存数据。
所有现代的浏览器都支持最近修改 (last-modified) 的数据检查。如果你曾经访问过某页,一天后重新访问相同的页时发现它没有变化,并奇怪第二次访问时页面加载得如此之快——这就是原因所在。你的浏览器首次访问时会在本地缓存页面内容,当你第二次访问,浏览器自动发送首次访问时从服务器获得的最近修改日期。服务器简单地返回 304: Not Modified (没有修改),因此浏览器就会知道从本地缓存加载页面。在这一点上,Web 服务也如此智能。
Python 的 URL 库没有提供内置的最近修改数据检查支持,但是你可以为每一个请求添加任意的头信息并在每一个响应中读取任意头信息,从而自己添加这种支持。
ETag 是实现与最近修改数据检查同样的功能的另一种方法:没有变化时不重新下载数据。其工作方式是:服务器发送你所请求的数据的同时,发送某种数据的 hash (在 ETag 头信息中给出)。hash 的确定完全取决于服务器。当第二次请求相同的数据时,你需要在 If-None-Match: 头信息中包含 ETag hash,如果数据没有改变,服务器将返回 304 状态代码。与最近修改数据检查相同,服务器仅仅 发送 304 状态代码;第二次将不为你发送相同的数据。在第二次请求时,通过包含 ETag hash,你告诉服务器:如果 hash 仍旧匹配就没有必要重新发送相同的数据,因为你还有上一次访问过的数据。
Python 的 URL 库没有对 ETag 的内置支持,但是在本章后面你将看到如何添加这种支持。
最后一个重要的 HTTP 特性是 gzip 压缩。 关于 HTTP web 服务的主题几乎总是会涉及在网络线路上传输的 XML。XML 是文本,而且还是相当冗长的文本,而文本通常可以被很好地压缩。当你通过 HTTP 请求一个资源时,可以告诉服务器,如果它有任何新数据要发送给我时,请以压缩的格式发送。在你的请求中包含 Accept-encoding: gzip 头信息,如果服务器支持压缩,它将返回由 gzip 压缩的数据并且使用 Content-encoding: gzip 头信息标记。
Python 的 URL 库本身没有内置对 gzip 压缩的支持,但是你能为请求添加任意的头信息。Python 还提供了一个独立的 gzip 模块,它提供了对数据进行解压缩的功能。
注意我们用于下载 feed 汇聚的小单行脚本并不支持任何这些 HTTP 特性。让我们来看看如何改善它。
首先,让我们开启 Python HTTP 库的调试特性并查看网络线路上的传输过程。这对本章的全部内容都很有用,因为你将添加越来越多的特性。
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> import urllib >>> feeddata = urllib.urlopen('http://diveintomark.org/xml/atom.xml').read() connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/1.15 ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Wed, 14 Apr 2004 22:27:30 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close
改善你的 HTTP web 服务客户端的第一步就是用 User-Agent 适当地鉴别你自己。为了做到这一点,你需要远离基本的 urllib 而深入到 urllib2。
>>> import httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener() >>> feeddata = opener.open(request).read() connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Wed, 14 Apr 2004 23:23:12 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close
如果你的 Python IDE 仍旧为上一节的例子而打开着,你可以略过这一步,在开启 HTTP 调试时你能看到网络线路上的实际传输过程。 | |
使用 urllib2 获取 HTTP 资源包括三个处理步骤,这会有助于你理解这一过程。 第一步是创建 Request 对象,它接受一个你最终想要获取资源的 URL。注意这一步实际上还不能获取任何东西。 | |
第二步是创建一个 URL 开启器 (opener)。它可以接受任何数量的处理器来控制响应的处理。但你也可以创建一个没有任何自定义处理器的开启器,在这儿你就是这么做的。你将在本章后面探究重定向的部分看到如何定义和使用自定义处理器的内容。 | |
最后一个步骤是,使用你创建的 Request 对象告诉开启器打开 URL。因为你能从获得的信息中看到所有调试信息,这个步骤实际上获得了资源并且把返回数据存储在了 feeddata 中。 |
>>> request <urllib2.Request instance at 0x00250AA8> >>> request.get_full_url() http://diveintomark.org/xml/atom.xml >>> request.add_header('User-Agent', ... 'OpenAnything/1.0 +http://diveintopython.org/') >>> feeddata = opener.open(request).read() connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: OpenAnything/1.0 +http://diveintopython.org/ ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Wed, 14 Apr 2004 23:45:17 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Content-Type: application/atom+xml header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT header: ETag: "e8284-68e0-4de30f80" header: Accept-Ranges: bytes header: Content-Length: 26848 header: Connection: close
既然你知道如何在你的 web 服务请求中添加自定义的 HTTP 头信息,接下来看看如何添加 Last-Modified 和 ETag 头信息的支持。
下面的这些例子将以调试标记置为关闭的状态来显示输出结果。如果你还停留在上一部分的开启状态,可以使用 httplib.HTTPConnection.debuglevel = 0 将其设置为关闭状态。或者,如果你认为有帮助也可以保持为开启状态。
>>> import urllib2 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener() >>> firstdatastream = opener.open(request) >>> firstdatastream.headers.dict {'date': 'Thu, 15 Apr 2004 20:42:41 GMT', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'content-type': 'application/atom+xml', 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'etag': '"e842a-3e53-55d97640"', 'content-length': '15955', 'accept-ranges': 'bytes', 'connection': 'close'} >>> request.add_header('If-Modified-Since', ... firstdatastream.headers.get('Last-Modified')) >>> seconddatastream = opener.open(request) Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\python23\lib\urllib2.py", line 326, in open '_open', req) File "c:\python23\lib\urllib2.py", line 306, in _call_chain result = func(*args) File "c:\python23\lib\urllib2.py", line 901, in http_open return self.do_open(httplib.HTTP, req) File "c:\python23\lib\urllib2.py", line 895, in do_open return self.parent.error('http', req, fp, code, msg, hdrs) File "c:\python23\lib\urllib2.py", line 352, in error return self._call_chain(*args) File "c:\python23\lib\urllib2.py", line 306, in _call_chain result = func(*args) File "c:\python23\lib\urllib2.py", line 412, in http_error_default raise HTTPError(req.get_full_url(), code, msg, hdrs, fp) urllib2.HTTPError: HTTP Error 304: Not Modified
还记得当调试标记设置为开启时所有那些你看到的 HTTP 头信息打印输出吗? 这里便是用编程方式访问它们的方法: firstdatastream.headers 是一个类似 dictionary 行为的对象并且允许你获得任何个别的从 HTTP 服务器返回的头信息。 | |
在第二次请求时,你用第一次请求获得的最近修改时间添加了 If-Modified-Since 头信息。如果数据没被改变,服务器应该返回一个 304 状态代码。 | |
毫无疑问,数据没被改变。你可以从跟踪返回结果看到 urllib2 抛出了一个特殊异常,HTTPError,以响应 304 状态代码。这有点不寻常,并且完全没有任何帮助。毕竟,它不是个错误;你明确地询问服务器如果没有变化就不要发送任何数据,并且数据没有变化,所以服务器告诉你它没有为你发送任何数据。那不是个错误;实际上也正是你所期望的。 |
urllib2 也为你认为是错误的其他条件引发 HTTPError 异常,比如 404 (page not found)。实际上,它将为任何 除了状态代码 200 (OK)、301 (permanent redirect)或 302 (temporary redirect) 之外的状态引发 HTTPError。捕获状态代码并简单返回它,而不是抛出异常,这应该对你很有帮助。为了实现它,你将需要自定义一个 URL 处理器。
这个自定义的 URL 处理器是 openanything.py 的一部分。
class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): def http_error_default(self, req, fp, code, msg, headers): result = urllib2.HTTPError( req.get_full_url(), code, msg, headers, fp) result.status = code return result
urllib2 是围绕 URL 处理器而设计的。每一个处理器就是一个能定义任意数量方法的类。当某事件发生时——比如一个 HTTP 错误,甚至是 304 代码——urllib2 审视用于处理它的 一系列已定义的处理器方法。在此要用到自省,与 第 9 章 XML 处理中为不同节点类型定义不同处理器类似。但是 urllib2 是很灵活的,还可以内省为当前请求所定义的所有处理器。 | |
当从服务器接收到一个 304 状态代码时,urllib2 查找定义的操作并调用 http_error_default 方法。通过定义一个自定义的错误处理,你可以阻止 urllib2 引发异常。取而代之的是,你创建 HTTPError 对象,返回它而不是引发异常。 | |
这是关键部分:返回之前,你保存从 HTTP 服务器返回的状态代码。这将使你从主调程序轻而易举地访问它。 |
>>> request.headers {'If-modified-since': 'Thu, 15 Apr 2004 19:45:21 GMT'} >>> import openanything >>> opener = urllib2.build_opener( ... openanything.DefaultErrorHandler()) >>> seconddatastream = opener.open(request) >>> seconddatastream.status 304 >>> seconddatastream.read() ''
处理 ETag 的工作也非常相似,只不过不是检查 Last-Modified 并发送 If-Modified-Since,而是检查 ETag 并发送 If-None-Match。让我们打开一个新的 IDE 会话。
>>> import urllib2, openanything >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> opener = urllib2.build_opener( ... openanything.DefaultErrorHandler()) >>> firstdatastream = opener.open(request) >>> firstdatastream.headers.get('ETag') '"e842a-3e53-55d97640"' >>> firstdata = firstdatastream.read() >>> print firstdata <?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3" xmlns="http://purl.org/atom/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en"> <title mode="escaped">dive into mark</title> <link rel="alternate" type="text/html" href="http://diveintomark.org/"/> <-- rest of feed omitted for brevity --> >>> request.add_header('If-None-Match', ... firstdatastream.headers.get('ETag')) >>> seconddatastream = opener.open(request) >>> seconddatastream.status 304 >>> seconddatastream.read() ''
在这些例子中,HTTP 服务器同时支持 Last-Modified 和 ETag 头信息,但并非所有的服务器皆如此。作为一个 web 服务的客户端,你应该为支持两种头信息做准备,但是你的程序也应该为服务器仅支持其中一种头信息或两种头信息都不支持而做准备。 |
你可以使用两种不同的自定义 URL 处理器来处理永久重定向和临时重定向。
首先,让我们来看看重定向处理的必要性。
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> request = urllib2.Request( ... 'http://diveintomark.org/redir/example301.xml') >>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /redir/example301.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 301 Moved Permanently\r\n' header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Location: http://diveintomark.org/xml/atom.xml header: Content-Length: 338 header: Connection: close header: Content-Type: text/html; charset=iso-8859-1 connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Thu, 15 Apr 2004 22:06:25 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Content-Length: 15955 header: Connection: close header: Content-Type: application/atom+xml >>> f.url 'http://diveintomark.org/xml/atom.xml' >>> f.headers.dict {'content-length': '15955', 'accept-ranges': 'bytes', 'server': 'Apache/2.0.49 (Debian GNU/Linux)', 'last-modified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'connection': 'close', 'etag': '"e842a-3e53-55d97640"', 'date': 'Thu, 15 Apr 2004 22:06:25 GMT', 'content-type': 'application/atom+xml'} >>> f.status Traceback (most recent call last): File "<stdin>", line 1, in ? AttributeError: addinfourl instance has no attribute 'status'
这不太理想,但很容易改进。实际上当 urllib2 遇到 301 或 302 时的行为并不是我们所期望的,所以让我们来覆盖这些行为。如何实现呢?用一个自定义的处理器,正如你处理 304 代码所做的。
这个类定义在 openanything.py。
class SmartRedirectHandler(urllib2.HTTPRedirectHandler): def http_error_301(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_301( self, req, fp, code, msg, headers) result.status = code return result def http_error_302(self, req, fp, code, msg, headers): result = urllib2.HTTPRedirectHandler.http_error_302( self, req, fp, code, msg, headers) result.status = code return result
这将为我们带来什么?现在你可以用自定义重定向处理器构造一个的 URL 开启器,并且它依然能自动跟踪重定向,也能展示出重定向状态代码。
>>> request = urllib2.Request('http://diveintomark.org/redir/example301.xml') >>> import openanything, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> opener = urllib2.build_opener( ... openanything.SmartRedirectHandler()) >>> f = opener.open(request) connect: (diveintomark.org, 80) send: 'GET /redir/example301.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 301 Moved Permanently\r\n' header: Date: Thu, 15 Apr 2004 22:13:21 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Location: http://diveintomark.org/xml/atom.xml header: Content-Length: 338 header: Connection: close header: Content-Type: text/html; charset=iso-8859-1 connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Thu, 15 Apr 2004 22:13:21 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Content-Length: 15955 header: Connection: close header: Content-Type: application/atom+xml >>> f.status 301 >>> f.url 'http://diveintomark.org/xml/atom.xml'
同样的重定向处理也可以告诉你不该 更新你的地址簿。
>>> request = urllib2.Request( ... 'http://diveintomark.org/redir/example302.xml') >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /redir/example302.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 302 Found\r\n' header: Date: Thu, 15 Apr 2004 22:18:21 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Location: http://diveintomark.org/xml/atom.xml header: Content-Length: 314 header: Connection: close header: Content-Type: text/html; charset=iso-8859-1 connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Thu, 15 Apr 2004 22:18:21 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Content-Length: 15955 header: Connection: close header: Content-Type: application/atom+xml >>> f.status 302 >>> f.url http://diveintomark.org/xml/atom.xml
你要支持的最后一个重要的 HTTP 特性是压缩。许多 web 服务具有发送压缩数据的能力,这可以将网络线路上传输的大量数据消减 60% 以上。这尤其适用于 XML web 服务,因为 XML 数据 的压缩率可以很高。
服务器不会为你发送压缩数据,除非你告诉服务器你可以处理压缩数据。
>>> import urllib2, httplib >>> httplib.HTTPConnection.debuglevel = 1 >>> request = urllib2.Request('http://diveintomark.org/xml/atom.xml') >>> request.add_header('Accept-encoding', 'gzip') >>> opener = urllib2.build_opener() >>> f = opener.open(request) connect: (diveintomark.org, 80) send: ' GET /xml/atom.xml HTTP/1.0 Host: diveintomark.org User-agent: Python-urllib/2.1 Accept-encoding: gzip ' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Thu, 15 Apr 2004 22:24:39 GMT header: Server: Apache/2.0.49 (Debian GNU/Linux) header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT header: ETag: "e842a-3e53-55d97640" header: Accept-Ranges: bytes header: Vary: Accept-Encoding header: Content-Encoding: gzip header: Content-Length: 6289 header: Connection: close header: Content-Type: application/atom+xml
>>> compresseddata = f.read() >>> len(compresseddata) 6289 >>> import StringIO >>> compressedstream = StringIO.StringIO(compresseddata) >>> import gzip >>> gzipper = gzip.GzipFile(fileobj=compressedstream) >>> data = gzipper.read() >>> print data <?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3" xmlns="http://purl.org/atom/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en"> <title mode="escaped">dive into mark</title> <link rel="alternate" type="text/html" href="http://diveintomark.org/"/> <-- rest of feed omitted for brevity --> >>> len(data) 15955
继续上面的例子,f 是一个从 URL 开启器返回的类文件对象。使用它的 read() 方法将正常地获得非压缩数据,但是因为这个数据已经被 gzip 压缩过,所以这只是获得你想要的最终数据的第一步。 | |
好吧,只是先得有点儿凌乱的步骤。Python 有一个 gzip 模块,它能读取 (当然也能写入) 磁盘上的 gzip 压缩文件。但是磁盘上还没有文件,只在内存里有一个 gzip 压缩缓冲区,并且你不想仅仅为了解压缩而写出一个临时文件。那么怎么做来从内存数据 (compresseddata) 创建类文件对象呢?这需要使用 StringIO 模块。你首次看到 StringIO 模块是在上一章,但现在你会发现它的另一种用法。 | |
现在你可以创建 GzipFile 的一个实例,并且告诉它其中的 “文件” 是一个类文件对象 compressedstream。 | |
这是做所有工作的一行:从 GzipFile 中 “读取” 将会解压缩数据。感到奇妙吗?是的,它确实解压缩了数据。gzipper 是一个类文件对象,它代表一个 gzip 压缩文件。尽管这个 “文件” 并非一个磁盘上的真实文件;但 gzipper 还是从你用 StringIO 包装了压缩数据的类文件对象中 “读取” 数据,而它仅仅是内存中的变量 compresseddata。压缩的数据来自哪呢?最初你从远程 HTTP 服务器下载它,通过从用 urllib2.build_opener 创建的类文件对象中 “读取”。令人吃惊吧,这就是所有的步骤。链条上的每一步都完全不知道上一步在造假。 | |
看看吧,实际的数据 (实际为 15955 bytes)。 |
“等等!” 我听见你在叫。“还能更简单吗!” 我知道你在想什么。你在,既然 opener.open 返回一个类文件对象,那么为什么不抛弃中间件 StringIO 而通过 f 直接访问 GzipFile 呢?OK,或许你没想到,但是别为此担心,因为那样无法工作。
>>> f = opener.open(request) >>> f.headers.get('Content-Encoding') 'gzip' >>> data = gzip.GzipFile(fileobj=f).read() Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\python23\lib\gzip.py", line 217, in read self._read(readsize) File "c:\python23\lib\gzip.py", line 252, in _read pos = self.fileobj.tell() # Save current position AttributeError: addinfourl instance has no attribute 'tell'
你已经看到了构造一个智能的 HTTP web 客户端的所有片断。现在让我们看看如何将它们整合到一起。
这个函数定义在 openanything.py 中。
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT): # non-HTTP code omitted for brevity if urlparse.urlparse(source)[0] == 'http': # open URL with urllib2 request = urllib2.Request(source) request.add_header('User-Agent', agent) if etag: request.add_header('If-None-Match', etag) if lastmodified: request.add_header('If-Modified-Since', lastmodified) request.add_header('Accept-encoding', 'gzip') opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) return opener.open(request)
这个函数定义在 openanything.py 中。
def fetch(source, etag=None, last_modified=None, agent=USER_AGENT): '''Fetch data and metadata from a URL, file, stream, or string''' result = {} f = openAnything(source, etag, last_modified, agent) result['data'] = f.read() if hasattr(f, 'headers'): # save ETag, if the server sent one result['etag'] = f.headers.get('ETag') # save Last-Modified header, if the server sent one result['lastmodified'] = f.headers.get('Last-Modified') if f.headers.get('content-encoding', '') == 'gzip': # data came back gzip-compressed, decompress it result['data'] = gzip.GzipFile(fileobj=StringIO(result['data']])).read() if hasattr(f, 'url'): result['url'] = f.url result['status'] = 200 if hasattr(f, 'status'): result['status'] = f.status f.close() return result
>>> import openanything >>> useragent = 'MyHTTPWebServicesApp/1.0' >>> url = 'http://diveintopython.org/redir/example301.xml' >>> params = openanything.fetch(url, agent=useragent) >>> params {'url': 'http://diveintomark.org/xml/atom.xml', 'lastmodified': 'Thu, 15 Apr 2004 19:45:21 GMT', 'etag': '"e842a-3e53-55d97640"', 'status': 301, 'data': '<?xml version="1.0" encoding="iso-8859-1"?> <feed version="0.3" <-- rest of data omitted for brevity -->'} >>> if params['status'] == 301: ... url = params['url'] >>> newparams = openanything.fetch( ... url, params['etag'], params['lastmodified'], useragent) >>> newparams {'url': 'http://diveintomark.org/xml/atom.xml', 'lastmodified': None, 'etag': '"e842a-3e53-55d97640"', 'status': 304, 'data': ''}
第一次获取资源时,你没有 ETag hash 或 Last-Modified 日期,所以你不用使用这些参数。 (它们是可选参数。) | |
你获得了一个 dictionary,它包括几个有用的头信息、HTTP 状态代码和从服务器返回的真实数据。openanything 在内部处理 gzip 压缩;在本级别上你不必关心它。 | |
如果你得到一个 301 状态代码,表示是个永久重定向,你需要把你的 URL 更新为新地址。 | |
第二次获取相同的资源时,你已经从以往获得了各种信息:URL (可能被更新了)、从上一次访问获得的 ETag、从上一次访问获得的 Last-Modified 日期,当然还有 User-Agent。 | |
你重新获取了这个 dictionary,但是数据没有改变,所以你得到了一个 304 状态代码而没有数据。 |
openanything.py 及其函数现在可以完美地工作了。
每个客户端都应该支持 HTTP web 服务的以下 5 个重要特性:
第 11 章 关注 HTTP 上面向文档的web 服务。“输入参数” 是 URL,“返回值” 是需要你来解析的一个实际的 XML 文档。
本章将关注更加结构化的 SOAP web 服务。SOAP 不需要你直接与 HTTP 请求和 XML 文档打交道,而是允许你模拟返回原始数据类型的函数调用。正像你将要看到的,这个描述恰如其份;你可以使用标准 Python 调用语法通过 SOAP 库去调用一个函数,这个函数也自然会返回 Python 对象和值。但揭开这层面纱,SOAP 库实际上执行了一个多个 XML 文档和远程服务器参与的复杂处理过程。
SOAP 的贴切定义很复杂,不要误认为 SOAP 就是用于调用远程函数。有些人觉得应该补充上:SOAP 还允许单向异步的信息通过,以及面向文档的 Web 服务。有这样想法的人是正确的,SOAP 的确是这样,但却不止于此。但这一章的重点在于所谓的 “RPC-style” SOAP――调用远程函数获得返回结果。
你用 Google,对吧?它是一个很流行的搜索引擎。你是否希望能以程序化的方式访问 Google 的搜索结果呢?现在你能做到了。下面是一个用 Python 搜索 Google 的程序。
from SOAPpy import WSDL # you'll need to configure these two values; # see http://www.google.com/apis/ WSDLFILE = '/path/to/copy/of/GoogleSearch.wsdl' APIKEY = 'YOUR_GOOGLE_API_KEY' _server = WSDL.Proxy(WSDLFILE) def search(q): """Search Google and return list of {title, link, description}""" results = _server.doGoogleSearch( APIKEY, q, 0, 10, False, "", False, "", "utf-8", "utf-8") return [{"title": r.title.encode("utf-8"), "link": r.URL.encode("utf-8"), "description": r.snippet.encode("utf-8")} for r in results.resultElements] if __name__ == '__main__': import sys for r in search(sys.argv[1])[:5]: print r['title'] print r['link'] print r['description'] print
你可以在较大的程序中以模块导入并使用它,也可以在命令行上运行这个脚本。在命令行上,需要把查询字符串作为命令行参数使用,之后就会打印出最前面的五个 Google 查询结果,包括:URL、标题和描述信息。
下面是以 “python” 作为命令行参数的查询结果。
C:\diveintopython\common\py> python search.py "python" <b>Python</b> Programming Language http://www.python.org/ Home page for <b>Python</b>, an interpreted, interactive, object-oriented, extensible<br> programming language. <b>...</b> <b>Python</b> is OSI Certified Open Source: OSI Certified. <b>Python</b> Documentation Index http://www.python.org/doc/ <b>...</b> New-style classes (aka descrintro). Regular expressions. Database API. Email Us.<br> docs@<b>python</b>.org. (c) 2004. <b>Python</b> Software Foundation. <b>Python</b> Documentation. <b>...</b> Download <b>Python</b> Software http://www.python.org/download/ Download Standard <b>Python</b> Software. <b>Python</b> 2.3.3 is the current production<br> version of <b>Python</b>. <b>...</b> <b>Python</b> is OSI Certified Open Source: Pythonline http://www.pythonline.com/ Dive Into <b>Python</b> http://diveintopython.org/ Dive Into <b>Python</b>. <b>Python</b> from novice to pro. Find: <b>...</b> It is also available in multiple<br> languages. Read Dive Into <b>Python</b>. This book is still being written. <b>...</b>
与本书中的其他代码不同,本章依赖的库不是 Python 预安装的。
在深入学习 SOAP web 服务之前,你需要安装三个库:PyXML、fpconst 和 SOAPpy。
你要用到的第一个库是 PyXML,它是 XML 库的一个高级组件,提供了比我们在 第 9 章 学习的 XML 内建库更多的功能。
下面是安装 PyXML 的步骤:
访问 http://pyxml.sourceforge.net/,点击 Downloads,下载适合你所使用操作系统的最新版本。
如果你所使用的是 Windows,那么你有多个选择。一定要确保你所下载的 PyXML 和你所使用的 Python 版本匹配。
双击安装程序。如果你下载的是为 Windows 提供的 PyXML 0.8.3,并且你所使用的是 Python 2.3,这个安装程序应该是 PyXML-0.8.3.win32-py2.3.exe。
深入安装过程。
安装完成后,关闭安装程序,没有任何安装成功的昭示 (并没有在开始菜单、快捷栏或桌面出现图标)。因为 PyXML 仅仅是被其他程序调用的 XML 的库集合。
要检验 PyXML 安装得是否正确,可以运行 Python IDE,下面的指令可以看到 XML 库的安装版本。
你所需要安装的第二个库是 fpconst,它是一系列支持 IEEE754 double-precision 特殊值的常量和函数,提供了对 Not-a-Number (NaN), Positive Infinity (Inf) 和 Negative Infinity (-Inf) 等特殊值的支持,而这是 SOAP 数据类型规范的组成部分。
下面是 fpconst 的安装过程:
从 http://www.analytics.washington.edu/statcomp/projects/rzope/fpconst/ 下载 fpconst 的最新版本。
提供了两种格式的下载:.tar.gz 和 .zip。如果你使用的是 Windows 操作系统,下载 .zip 文件;其他情况下应该下载 .tar.gz 文件。
对这个文件进行解压缩。在 Windows XP 上你可以鼠标右键单击这个文件并选择“解压文件”;在较早的 Windows 版本上则需要 WinZip 之类的第三方解压程序。在 Mac OS X 上,可以右键单击压缩文件进行解压。
打开命令提示符窗口并定位到解压目录。
键入 python setup.py install 运行安装程序。
要检验 fpconst 安装得是否正确,运行 Python IDE 并查看版本号。
第三个,也是最后一个需要安装的库是 SOAP 库本身:SOAPpy。
下面是安装 SOAPpy的过程:
访问 http://pywebsvcs.sourceforge.net/ 并选择 SOAPpy 部分中最新的官方发布。
提供了两种格式的下载。如果你使用的是 Windows,那么下载 .zip 文件;其他情况则下载 .tar.gz 文件。
和安装 fpconst 时一样先解压下载的文件.
打开命令提示符窗口并定位到解压 SOAPpy 文件的目录。
键入 python setup.py install 运行安装程序。
要检验 SOAPpy 安装得是否正确,运行 Python IDE 并查看版本号。
调用远程函数是 SOAP 的核心功能。有很多提供公开 SOAP 访问的服务器提供用于展示的简单功能。
最受欢迎的 SOAP 公开访问服务器是 http://www.xmethods.net/。这个例子使用了一个展示函数,可以根据美国邮政编码返回当地气温。
>>> from SOAPpy import SOAPProxy >>> url = 'http://services.xmethods.net:80/soap/servlet/rpcrouter' >>> namespace = 'urn:xmethods-Temperature' >>> server = SOAPProxy(url, namespace) >>> server.getTemp('27502') 80.0
你通过 SOAPProxy 这个代理 (proxy) 类访问远程 SOAP 服务器。这个代理处理了所有的 SOAP 内部事务,其中包括:根据函数名和参数列表创建 XML 请求文档,并将这个请求文档通过 HTTP 发送到远程 SOAP 服务器;解析 XML 返回文档,并创建本地的 Python 返回值。在下一节中你将看到这个 XML 文档。 | |
每个 SOAP 服务都有一个 URL 用以处理所有请求。相同的 URL 可以用于所有的函数请求。每个特定服务则只有一个函数。但稍后你将看到的 Google API 却有多个函数。这个服务的 URL 提供给所有函数分享。每个 SOAP 服务都有一个命名空间 (namespace),这个命名空间是由服务器任意命名的。这不过是为调用 SOAP 方法设置的。它使得服务器让多个不相关的服务共享服务 URL 和路径请求成为可能。这与 Python 中模块相对于包的关系类似。 | |
这里你创建了包含服务 URL 和服务命名空间的 SOAPProxy。此时还不会连接到 SOAP 服务器;仅仅是建立了一个本地 Python 对象。 | |
到此为止,如果你的设置完全正确,应该可以向调用本地函数一样调用远程 SOAP 方法。这和给普通函数传递参数并接收返回值一样,但在背后却隐藏着很多的工作。 |
让我们看一看这些背后的工作。
SOAP 提供了一个很方便的方法用以查看背后的情形。
SOAPProxy 的两个小设置就可以打开查错模式。
>>> from SOAPpy import SOAPProxy >>> url = 'http://services.xmethods.net:80/soap/servlet/rpcrouter' >>> n = 'urn:xmethods-Temperature' >>> server = SOAPProxy(url, namespace=n) >>> server.config.dumpSOAPOut = 1 >>> server.config.dumpSOAPIn = 1 >>> temperature = server.getTemp('27502') *** Outgoing SOAP ****************************************************** <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/1999/XMLSchema"> <SOAP-ENV:Body> <ns1:getTemp xmlns:ns1="urn:xmethods-Temperature" SOAP-ENC:root="1"> <v1 xsi:type="xsd:string">27502</v1> </ns1:getTemp> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ************************************************************************ *** Incoming SOAP ****************************************************** <?xml version='1.0' encoding='UTF-8'?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <SOAP-ENV:Body> <ns1:getTempResponse xmlns:ns1="urn:xmethods-Temperature" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <return xsi:type="xsd:float">80.0</return> </ns1:getTempResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ************************************************************************ >>> temperature 80.0
大部分 XML 请求文档都基于模板文件。忽略所有命名空间声明这些对于所有 SOAP 调用都一成不变的东西。这个 “函数调用” 的核心是<Body> 当中的部分:
<ns1:getTemp xmlns:ns1="urn:xmethods-Temperature" SOAP-ENC:root="1"> <v1 xsi:type="xsd:string">27502</v1> </ns1:getTemp>
这个元素名 getTemp 就是函数名。SOAPProxy 使用 getattr 作为分发器。有别于使用方法名分别调用本地方法,这里使用方法名构造了一个 XML 请求文档。 | |
函数的 XML 元素被存储于一个特别的命名空间,这个命名空间就是你在建立 SOAPProxy 对象时所指定的那个命名空间。也不必为 SOAP-ENC:root 而苦恼,因为它也是基于模板文件的。 | |
函数的参数也被记入 XML 文档。SOAPProxy 查看并确定每个参数的数据类型 (这里是 string 字符串类型)。参数的数据类型记入 xsi:type 属性,并在其后记入实际的字符串值。 |
返回的 XML 文档同样容易理解,重点在于知道应该忽略掉哪些内容。把注意力集中在 <Body> 部分:
SOAPProxy 类本地方法调用并透明地转向到远程 SOAP 方法。正如你所看到的,这是很多的工作,SOAPProxy 快速和透明地完成他们。它没有做到的是提供方法自省的手段。
考虑一下:前面两部分所展现的调用只有一个参数和返回的简单远程 SOAP 方法。服务 URL 和一系列参数及它们的数据类型需要被知道并跟踪。任何的缺失或错误都会导致整体的失败。
这并没有什么可惊讶的。如果我要调用一个本地函数,我需要知道函数所在的包和模块名 (与之对应的则是服务 URL 和命名空间)。我还需要知道正确的函数名以及其函数个数。Python 精妙地不需明示类型,但我还是需要知道有多少个参数需要传递,多少个值将被返回。
最大的区别就在于内省。就像你在 第 4 章 看到的那样,Python 擅长于让你实时地去探索模块和函数的情况。你可以对一个模块中的所有函数进行列表,并不费吹灰之力地明了函数的声明和参数情况。
WSDL 允许你对 SOAP 网络服务做相同的事情。WSDL 是 “网络服务描述语言 (Web Services Description Language)”的缩写。它尽管是为自如地表述多种类型的网络服务而设定,却也经常用于描述 SOAP 网络服务。
一个 WSDL 文件不过就是一个文件。更具体地讲,是一个 XML 文件。通常存储于你所访问的 SOAP 网络服务这个被描述对象所在的服务器上,并没有什么特殊之处。在本章稍后的位置,我们将下载 Google API 的 WSDL 文件并在本地使用它。这并不意味着本地调用 Google,这个 WSDL 文件所描述的仍旧是 Google 服务器上的远程函数。
在 WSDL 文件中描述了调用相应的 SOAP 网络服务的一切:
换言之,一个 WSDL 文件告诉你调用 SOAP 所需要知道的一切。
就像网络服务舞台上的所有事物,WSDL 也经历了一个充满明争暗斗而且漫长多变的历史。我不打算讲述这段令我伤心的历史。还有一些其他的标准提供相同的支持,但 WSDL 还是胜出,所以我们还是来学习一下如何使用它。
WSDL 最基本的功能便是让你揭示 SOAP 服务器所提供的有效方法。
>>> from SOAPpy import WSDL >>> wsdlFile = 'http://www.xmethods.net/sd/2001/TemperatureService.wsdl' >>> server = WSDL.Proxy(wsdlFile) >>> server.methods.keys() [u'getTemp']
好的,你知道这个 SOAP 服务器提供一个方法:getTemp。但是如何去调用它呢?WSDL 也在这方面提供信息。
>>> callInfo = server.methods['getTemp'] >>> callInfo.inparams [<SOAPpy.wstools.WSDLTools.ParameterInfo instance at 0x00CF3AD0>] >>> callInfo.inparams[0].name u'zipcode' >>> callInfo.inparams[0].type (u'http://www.w3.org/2001/XMLSchema', u'string')
WSDL 还允许你自省函数的返回值。
>>> callInfo.outparams [<SOAPpy.wstools.WSDLTools.ParameterInfo instance at 0x00CF3AF8>] >>> callInfo.outparams[0].name u'return' >>> callInfo.outparams[0].type (u'http://www.w3.org/2001/XMLSchema', u'float')
让我们整合一下,通过 WSDL proxy 调用一个 SOAP 网络服务。
>>> from SOAPpy import WSDL >>> wsdlFile = 'http://www.xmethods.net/sd/2001/TemperatureService.wsdl') >>> server = WSDL.Proxy(wsdlFile) >>> server.getTemp('90210') 66.0 >>> server.soapproxy.config.dumpSOAPOut = 1 >>> server.soapproxy.config.dumpSOAPIn = 1 >>> temperature = server.getTemp('90210') *** Outgoing SOAP ****************************************************** <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/1999/XMLSchema"> <SOAP-ENV:Body> <ns1:getTemp xmlns:ns1="urn:xmethods-Temperature" SOAP-ENC:root="1"> <v1 xsi:type="xsd:string">90210</v1> </ns1:getTemp> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ************************************************************************ *** Incoming SOAP ****************************************************** <?xml version='1.0' encoding='UTF-8'?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <SOAP-ENV:Body> <ns1:getTempResponse xmlns:ns1="urn:xmethods-Temperature" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <return xsi:type="xsd:float">66.0</return> </ns1:getTempResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ************************************************************************ >>> temperature 66.0
让我们回到这章开始时你看到的那段代码,获得比当前气温更有价值和令人振奋的信息。
Google 提供了一个 SOAP API,以便通过程序进行 Google 搜索。使用它的前提是,你注册了 Google 网络服务。
访问 http://www.google.com/apis/ 并创建一个账号。唯一的需要是提供一个 E-mail 地址。注册之后,你将通过 E-mail 收到你的 Google API 许可证 (license key)。你需要在调用 Google 搜索函数时使用这个许可证。
还是在 http://www.google.com/apis/ 上,下载 Google 网络 APIs 开发工具包 (Google Web APIs developer kit)。它包含着包括 Python 在内的多种语言的样例代码,更重要的是它包含着 WSDL 文件。
解压这个开发工具包并找到 GoogleSearch.wsdl。将这个文件拷贝到你本地驱动器的一个永久地址。在本章后面位置你会用到它。
你有了开发许可证和 Google WSDL 文件之后就可以和 Google 网络服务打交道了。
>>> from SOAPpy import WSDL >>> server = WSDL.Proxy('/path/to/your/GoogleSearch.wsdl') >>> server.methods.keys() [u'doGoogleSearch', u'doGetCachedPage', u'doSpellingSuggestion'] >>> callInfo = server.methods['doGoogleSearch'] >>> for arg in callInfo.inparams: ... print arg.name.ljust(15), arg.type key (u'http://www.w3.org/2001/XMLSchema', u'string') q (u'http://www.w3.org/2001/XMLSchema', u'string') start (u'http://www.w3.org/2001/XMLSchema', u'int') maxResults (u'http://www.w3.org/2001/XMLSchema', u'int') filter (u'http://www.w3.org/2001/XMLSchema', u'boolean') restrict (u'http://www.w3.org/2001/XMLSchema', u'string') safeSearch (u'http://www.w3.org/2001/XMLSchema', u'boolean') lr (u'http://www.w3.org/2001/XMLSchema', u'string') ie (u'http://www.w3.org/2001/XMLSchema', u'string') oe (u'http://www.w3.org/2001/XMLSchema', u'string')
这里简要地列出了 doGoogleSearch 函数的所有参数:
>>> from SOAPpy import WSDL >>> server = WSDL.Proxy('/path/to/your/GoogleSearch.wsdl') >>> key = 'YOUR_GOOGLE_API_KEY' >>> results = server.doGoogleSearch(key, 'mark', 0, 10, False, "", ... False, "", "utf-8", "utf-8") >>> len(results.resultElements) 10 >>> results.resultElements[0].URL 'http://diveintomark.org/' >>> results.resultElements[0].title 'dive into <b>mark</b>'
results 对象中所加载的不仅仅是实际的搜索结果。它也含有搜索行为自身的信息,比如耗时和总结果数等 (尽管只返回了10条结果)。Google 网页界面中显示了这些信息,通过程序你也同样能获得它们。
>>> results.searchTime 0.224919 >>> results.estimatedTotalResultsCount 29800000 >>> results.directoryCategories [<SOAPpy.Types.structType item at 14367400>: {'fullViewableName': 'Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mark', 'specialEncoding': ''}] >>> results.directoryCategories[0].fullViewableName 'Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mark'
这个搜索耗时 0.224919 秒。这不包括用于发送和接收 SOAP XML 文档的时间,仅仅是 Google 在接到搜索请求后执行搜索所花费的时间。 | |
总共有接近 30,000,000 个结果信息。通过让 start 参数以 10 递增来重复调用 server.doGoogleSearch,你能够获得全部的结果。 | |
对于有些请求,Google 还返回一个 Google Directory 中的类别列表。你可以用这些 URLs 到 http://directory.google.com/ 建立到 directory category 页面的链接。 |
是的,SOAP 网络服务的世界中也不总是欢乐和阳光。有时候也会有故障。
正如你在本章中看到的,SOAP 牵扯了很多层面。SOAP 向 HTTP 服务器发送 XML 文档并接收返回的 XML 文档时需要用到 HTTP 层。这样一来,你在 第 11 章 HTTP Web 服务 学到的调试技术在这里都有了用武之地。你可以 import httplib 并设置 httplib.HTTPConnection.debuglevel = 1 来查看潜在的 HTTP 传输。
在 HTTP 层之上,还有几个可能发生问题的地方。SOAPpy 隐藏 SOAP 语法的本领令你惊叹不已,但也意味着在发生问题时更难确定问题所在。
下面的这些例子是我在使用 SOAP 网络服务时犯过的一些常见错误以及所产生的错误信息。
>>> from SOAPpy import SOAPProxy >>> url = 'http://services.xmethods.net:80/soap/servlet/rpcrouter' >>> server = SOAPProxy(url) >>> server.getTemp('27502') <Fault SOAP-ENV:Server.BadTargetObjectURI: Unable to determine object id from call: is the method element namespaced?> Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 453, in __call__ return self.__r_call(*args, **kw) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 475, in __r_call self.__hd, self.__ma) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 389, in __call raise p SOAPpy.Types.faultType: <Fault SOAP-ENV:Server.BadTargetObjectURI: Unable to determine object id from call: is the method element namespaced?>
错误配置 SOAP 服务的基本元素是 WSDL 着眼解决的问题。WSDL 文件包含服务 URL 和命名空间,所以你应该不会在这里犯错。但是,还有其他可能出错的地方。
>>> wsdlFile = 'http://www.xmethods.net/sd/2001/TemperatureService.wsdl' >>> server = WSDL.Proxy(wsdlFile) >>> temperature = server.getTemp(27502) <Fault SOAP-ENV:Server: Exception while handling service request: services.temperature.TempService.getTemp(int) -- no signature match> Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 453, in __call__ return self.__r_call(*args, **kw) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 475, in __r_call self.__hd, self.__ma) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 389, in __call raise p SOAPpy.Types.faultType: <Fault SOAP-ENV:Server: Exception while handling service request: services.temperature.TempService.getTemp(int) -- no signature match>
Python 所期待的返回值个数与远程函数的实际返回值个数不同是另一种可能的错误。
>>> wsdlFile = 'http://www.xmethods.net/sd/2001/TemperatureService.wsdl' >>> server = WSDL.Proxy(wsdlFile) >>> (city, temperature) = server.getTemp(27502) Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: unpack non-sequence
那么 Google 网络服务方面又如何呢?我曾经犯过的最常见的错误是忘记正确设置应用许可证。
>>> from SOAPpy import WSDL >>> server = WSDL.Proxy(r'/path/to/local/GoogleSearch.wsdl') >>> results = server.doGoogleSearch('foo', 'mark', 0, 10, False, "", ... False, "", "utf-8", "utf-8") <Fault SOAP-ENV:Server: Exception from service object: Invalid authorization key: foo: <SOAPpy.Types.structType detail at 14164616>: {'stackTrace': 'com.google.soap.search.GoogleSearchFault: Invalid authorization key: foo at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe( QueryLimits.java:220) at com.google.soap.search.QueryLimits.validateKey(QueryLimits.java:127) at com.google.soap.search.GoogleSearchService.doPublicMethodChecks( GoogleSearchService.java:825) at com.google.soap.search.GoogleSearchService.doGoogleSearch( GoogleSearchService.java:121) at sun.reflect.GeneratedMethodAccessor13.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.apache.soap.server.RPCRouter.invoke(RPCRouter.java:146) at org.apache.soap.providers.RPCJavaProvider.invoke( RPCJavaProvider.java:129) at org.apache.soap.server.http.RPCRouterServlet.doPost( RPCRouterServlet.java:288) at javax.servlet.http.HttpServlet.service(HttpServlet.java:760) at javax.servlet.http.HttpServlet.service(HttpServlet.java:853) at com.google.gse.HttpConnection.runServlet(HttpConnection.java:237) at com.google.gse.HttpConnection.run(HttpConnection.java:195) at com.google.gse.DispatchQueue$WorkerThread.run(DispatchQueue.java:201) Caused by: com.google.soap.search.UserKeyInvalidException: Key was of wrong size. at com.google.soap.search.UserKey.<init>(UserKey.java:59) at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe( QueryLimits.java:217) ... 14 more '}> Traceback (most recent call last): File "<stdin>", line 1, in ? File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 453, in __call__ return self.__r_call(*args, **kw) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 475, in __r_call self.__hd, self.__ma) File "c:\python23\Lib\site-packages\SOAPpy\Client.py", line 389, in __call raise p SOAPpy.Types.faultType: <Fault SOAP-ENV:Server: Exception from service object: Invalid authorization key: foo: <SOAPpy.Types.structType detail at 14164616>: {'stackTrace': 'com.google.soap.search.GoogleSearchFault: Invalid authorization key: foo at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe( QueryLimits.java:220) at com.google.soap.search.QueryLimits.validateKey(QueryLimits.java:127) at com.google.soap.search.GoogleSearchService.doPublicMethodChecks( GoogleSearchService.java:825) at com.google.soap.search.GoogleSearchService.doGoogleSearch( GoogleSearchService.java:121) at sun.reflect.GeneratedMethodAccessor13.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.apache.soap.server.RPCRouter.invoke(RPCRouter.java:146) at org.apache.soap.providers.RPCJavaProvider.invoke( RPCJavaProvider.java:129) at org.apache.soap.server.http.RPCRouterServlet.doPost( RPCRouterServlet.java:288) at javax.servlet.http.HttpServlet.service(HttpServlet.java:760) at javax.servlet.http.HttpServlet.service(HttpServlet.java:853) at com.google.gse.HttpConnection.runServlet(HttpConnection.java:237) at com.google.gse.HttpConnection.run(HttpConnection.java:195) at com.google.gse.DispatchQueue$WorkerThread.run(DispatchQueue.java:201) Caused by: com.google.soap.search.UserKeyInvalidException: Key was of wrong size. at com.google.soap.search.UserKey.<init>(UserKey.java:59) at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe( QueryLimits.java:217) ... 14 more '}>
在前面的章节中,通过阅读代码,你迅速“深入”,以最快的速度理解了各个程序。既然你已对 Python 有了一定的了解,那么接下来让我们看看程序开发之前 的工作。
在接下来的几章中,你将会编写、调试和优化一系列工具函数来进行罗马数字和阿拉伯数字之间的转换。你已从第 7.3 节 “个案研究:罗马字母”中获知构造和验证罗马数字的机制,现在我们要做的事是退后一步去思考如何将这些机制扩展到一个双向转换的工具。
罗马数字的规则有如下一些有趣的特点:
基于如上所述,你将如何构造罗马数字转换函数呢?
现在你已经定义了你的转换程序所应有的功能,下面一步会有点儿出乎你的意料:你将要开发一个测试组件 (test suite) 来测试你未来的函数以确保它们工作正常。没错:你将为还未开发的程序开发测试代码。
这就是所谓的单元测试,因为这两个转换函数可以被当作一个单元来开发和测试,不用考虑它们可能今后成为一个大程序的一部分。Python 有一个单元测试框架,被恰如其分地称作 unittest 模块。
Python 2.1 和之后的版本已经包含了 unittest。Python 2.0 用户则可以从 pyunit.sourceforge.net下载。 |
单元测试是以测试为核心开发策略的重要组成部分。如果你要写单元测试代码,尽早 (最好是在被测试代码开发之前) 开发并根据代码开发和需求的变化不断更新是很重要的。单元测试不能取代更高层面的功能和系统测试,但在开发的每个阶段都很重要:
这是将被开发并保存为 roman.py 的罗马数字转换程序的完整测试组件 (test suite)。很难立刻看出它们是如何协同工作的,似乎所有类或者方法之间都没有关系。这是有原因的,而且你很快就会明了。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Unit test for roman.py""" import roman import unittest class KnownValues(unittest.TestCase): knownValues = ( (1, 'I'), (2, 'II'), (3, 'III'), (4, 'IV'), (5, 'V'), (6, 'VI'), (7, 'VII'), (8, 'VIII'), (9, 'IX'), (10, 'X'), (50, 'L'), (100, 'C'), (500, 'D'), (1000, 'M'), (31, 'XXXI'), (148, 'CXLVIII'), (294, 'CCXCIV'), (312, 'CCCXII'), (421, 'CDXXI'), (528, 'DXXVIII'), (621, 'DCXXI'), (782, 'DCCLXXXII'), (870, 'DCCCLXX'), (941, 'CMXLI'), (1043, 'MXLIII'), (1110, 'MCX'), (1226, 'MCCXXVI'), (1301, 'MCCCI'), (1485, 'MCDLXXXV'), (1509, 'MDIX'), (1607, 'MDCVII'), (1754, 'MDCCLIV'), (1832, 'MDCCCXXXII'), (1993, 'MCMXCIII'), (2074, 'MMLXXIV'), (2152, 'MMCLII'), (2212, 'MMCCXII'), (2343, 'MMCCCXLIII'), (2499, 'MMCDXCIX'), (2574, 'MMDLXXIV'), (2646, 'MMDCXLVI'), (2723, 'MMDCCXXIII'), (2892, 'MMDCCCXCII'), (2975, 'MMCMLXXV'), (3051, 'MMMLI'), (3185, 'MMMCLXXXV'), (3250, 'MMMCCL'), (3313, 'MMMCCCXIII'), (3408, 'MMMCDVIII'), (3501, 'MMMDI'), (3610, 'MMMDCX'), (3743, 'MMMDCCXLIII'), (3844, 'MMMDCCCXLIV'), (3888, 'MMMDCCCLXXXVIII'), (3940, 'MMMCMXL'), (3999, 'MMMCMXCIX')) def testToRomanKnownValues(self): """toRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman.toRoman(integer) self.assertEqual(numeral, result) def testFromRomanKnownValues(self): """fromRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman.fromRoman(numeral) self.assertEqual(integer, result) class ToRomanBadInput(unittest.TestCase): def testTooLarge(self): """toRoman should fail with large input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000) def testZero(self): """toRoman should fail with 0 input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0) def testNegative(self): """toRoman should fail with negative input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1) def testNonInteger(self): """toRoman should fail with non-integer input""" self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5) class FromRomanBadInput(unittest.TestCase): def testTooManyRepeatedNumerals(self): """fromRoman should fail with too many repeated numerals""" for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) def testRepeatedPairs(self): """fromRoman should fail with repeated pairs of numerals""" for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) def testMalformedAntecedent(self): """fromRoman should fail with malformed antecedents""" for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) class SanityCheck(unittest.TestCase): def testSanity(self): """fromRoman(toRoman(n))==n for all n""" for integer in range(1, 4000): numeral = roman.toRoman(integer) result = roman.fromRoman(numeral) self.assertEqual(integer, result) class CaseCheck(unittest.TestCase): def testToRomanCase(self): """toRoman should always return uppercase""" for integer in range(1, 4000): numeral = roman.toRoman(integer) self.assertEqual(numeral, numeral.upper()) def testFromRomanCase(self): """fromRoman should only accept uppercase input""" for integer in range(1, 4000): numeral = roman.toRoman(integer) roman.fromRoman(numeral.upper()) self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, numeral.lower()) if __name__ == "__main__": unittest.main()
单元测试的基础是构建独立的测试用例 (test case)。一个测试用例只回答一个关于被测试代码的问题。
一个测试用例应该做到:
基于如上原则,让我们构建第一个测试用例。应符合如下要求:
class KnownValues(unittest.TestCase): knownValues = ( (1, 'I'), (2, 'II'), (3, 'III'), (4, 'IV'), (5, 'V'), (6, 'VI'), (7, 'VII'), (8, 'VIII'), (9, 'IX'), (10, 'X'), (50, 'L'), (100, 'C'), (500, 'D'), (1000, 'M'), (31, 'XXXI'), (148, 'CXLVIII'), (294, 'CCXCIV'), (312, 'CCCXII'), (421, 'CDXXI'), (528, 'DXXVIII'), (621, 'DCXXI'), (782, 'DCCLXXXII'), (870, 'DCCCLXX'), (941, 'CMXLI'), (1043, 'MXLIII'), (1110, 'MCX'), (1226, 'MCCXXVI'), (1301, 'MCCCI'), (1485, 'MCDLXXXV'), (1509, 'MDIX'), (1607, 'MDCVII'), (1754, 'MDCCLIV'), (1832, 'MDCCCXXXII'), (1993, 'MCMXCIII'), (2074, 'MMLXXIV'), (2152, 'MMCLII'), (2212, 'MMCCXII'), (2343, 'MMCCCXLIII'), (2499, 'MMCDXCIX'), (2574, 'MMDLXXIV'), (2646, 'MMDCXLVI'), (2723, 'MMDCCXXIII'), (2892, 'MMDCCCXCII'), (2975, 'MMCMLXXV'), (3051, 'MMMLI'), (3185, 'MMMCLXXXV'), (3250, 'MMMCCL'), (3313, 'MMMCCCXIII'), (3408, 'MMMCDVIII'), (3501, 'MMMDI'), (3610, 'MMMDCX'), (3743, 'MMMDCCXLIII'), (3844, 'MMMDCCCXLIV'), (3888, 'MMMDCCCLXXXVIII'), (3940, 'MMMCMXL'), (3999, 'MMMCMXCIX')) def testToRomanKnownValues(self): """toRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman.toRoman(integer) self.assertEqual(numeral, result)
使用有效输入确保函数成功通过测试还不够,你还需要测试无效输入导致函数失败的情形。但并不是任何失败都可以,必须如你预期地失败。
还记得 toRoman 的其他要求吧:
在 Python 中,函数以引发异常的方式表示失败。unittest 模块提供了用于测试函数是否在给定无效输入时引发特定异常的方法。
class ToRomanBadInput(unittest.TestCase): def testTooLarge(self): """toRoman should fail with large input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000) def testZero(self): """toRoman should fail with 0 input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0) def testNegative(self): """toRoman should fail with negative input""" self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1) def testNonInteger(self): """toRoman should fail with non-integer input""" self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
unittest 模块中的 TestCase 类提供了 assertRaises 方法,它接受这几个参数:预期的异常、测试的函数,以及传递给函数的参数。(如果被测试函数有不止一个参数,把它们按顺序全部传递给 assertRaises ,它会把这些参数传给被测的函数。) 特别注意这里的操作:不是直接调用 toRoman 再手工查看是否引发特定异常 (使用 try...except 块捕捉异常),assertRaises 为我们封装了这些。所有你要做的就是把异常 (roman.OutOfRangeError)、函数 (toRoman) 以及 toRoman 的参数 (4000) 传递给 assertRaises ,它会调用 toRoman 查看是否引发 roman.OutOfRangeError 异常。(还应注意到你是把 toRoman 函数本身当作一个参数,而不是调用它,传递它的时候也不是把它的名字作为一个字符串。我提到过吗?无论是函数还是异常, Python 中万物皆对象)。 | |
与测试过大的数相伴的便是测试过小的数。记住,罗马数字不能表示 0 和负数,所以你要分别编写测试用例 ( testZero 和 testNegative)。在 testZero 中,你测试 toRoman 调用 0 引发的 roman.OutOfRangeError 异常,如果没能 引发 roman.OutOfRangeError (不论是返回了一个值还是引发了其他异常),则测试失败。 | |
要求 #3:toRoman 不能接受非整数输入,所以这里你测试 toRoman 在输入 0.5 时引发 roman.NotIntegerError 异常。如果 toRoman 没有引发 roman.NotIntegerError 异常,则测试失败。 |
接下来的两个要求与前三个类似,不同点是他们所针对的是 fromRoman 而不是 toRoman:
要求 #4 与要求 #1 的处理方法相同,即测试一个已知样本中的一个个数字对。要求 #5 与 #2 和 #3的处理方法相同,即通过无效输入确认 fromRoman 引发恰当的异常。
class FromRomanBadInput(unittest.TestCase): def testTooManyRepeatedNumerals(self): """fromRoman should fail with too many repeated numerals""" for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) def testRepeatedPairs(self): """fromRoman should fail with repeated pairs of numerals""" for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) def testMalformedAntecedent(self): """fromRoman should fail with malformed antecedents""" for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
你经常会发现一组代码中包含互逆的转换函数,一个把 A 转换为 B ,另一个把 B 转换为 A。在这种情况下,创建“完备性检测”可以使你在由 A 转 B 再转 A 的过程中不会出现丢失精度或取整等错误。
考虑这个要求:
class SanityCheck(unittest.TestCase): def testSanity(self): """fromRoman(toRoman(n))==n for all n""" for integer in range(1, 4000): numeral = roman.toRoman(integer) result = roman.fromRoman(numeral) self.assertEqual(integer, result)
你已经见到过 range 函数,但这里它以两个参数被调用,返回了从第一个参数 (1) 开始到但不包括 第二个参数 (4000) 的整数列表。因此,1..3999 就是准备转换为罗马数字表示的有效值列表。 | |
我想提一下,这里的 integer 并不是一个 Python 关键字,而只是没有什么特别的变量名。 | |
这里的测试逻辑显而易见:把一个数 (integer) 转换为罗马数字表示的数 (numeral),然后再转换回来 (result) 并确保最后的结果和最初的数是同一个数。如果不是,assertEqual 便会引发异常,测试也便立刻失败。如果所有的结果都和初始数一致,assertEqual 将会保持沉默,整个 testSanity 方法将会最终也保持沉默,测试则将会被认定为通过。 |
最后两个要求和其他的要求不同,似乎既武断而又微不足道:
事实上,它们确实有点武断,譬如你完全可以让 fromRoman 接受小写和大小写混合的输入;但他们也不是完全武断;如果 toRoman 总是返回大写的输出,那么 fromRoman 至少应该接受大写字母输入,不然 “完备性检测” (要求 #6) 就会失败。不管怎么说,只 接受大写输入还是武断的,但就像每个系统都会告诉你的那样,大小写总会出问题,因此事先规定这一点还是有必要的。既然有必要规定,那么也就有必要测试。
class CaseCheck(unittest.TestCase): def testToRomanCase(self): """toRoman should always return uppercase""" for integer in range(1, 4000): numeral = roman.toRoman(integer) self.assertEqual(numeral, numeral.upper()) def testFromRomanCase(self): """fromRoman should only accept uppercase input""" for integer in range(1, 4000): numeral = roman.toRoman(integer) roman.fromRoman(numeral.upper()) self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, numeral.lower())
关于这个测试用例最有趣的一点不在于它测试了什么,而是它不测试什么。它不会测试 toRoman 的返回值是否正确或者一致;这些问题由其他测试用例来回答。整个测试用例仅仅测试大写问题。你也许觉得应该将它并入到完备性测试,毕竟都要遍历整个输入值范围并调用 toRoman。[11]但是这样将会违背一条基本规则:每个测试用例只回答一个问题。试想一下,你将这个测试并入到完备性测试中,然后遇到了测试失败。你还需要进一步分析以便判定测试用例的哪部分出了问题。如果你需要分析方能找出问题所在,无疑你的测试用例在设计上出了问题。 | |
这有一个和前面相似的情况:尽管 “你知道” toRoman 总是返回大写字母,你还是需要把返回值显式地转换成大写字母后再传递给只接受大写的 fromRoman 进行测试。为什么?因为 toRoman 只返回大写字母是一个独立的需求。如果你改变了这个需求,例如改成总是返回小写字母,那么 testToRomanCase 测试用例也应作出调整,但这个测试用例应该仍能通过。这是另外一个基本规则:每个测试用例必须可以与其他测试用例隔离工作,每个测试用例是一个“孤岛”。 | |
注意你并没有使用 fromRoman 的返回值。这是一个有效的 Python 语法:如果一个函数返回一个值,但没有被使用,Python 会直接把这个返回值扔掉。这正是你所希望的,这个测试用例并不对返回值进行测试,只是测试 fromRoman 接受大写字母而不引发异常。 | |
这行有点复杂,但是它与 ToRomanBadInput 和 FromRomanBadInput 测试很相似。 你在测试以特定值 (numeral.lower(),循环中目前罗马数字的小写版) 调用特定函数 (roman.fromRoman) 会确实引发特定的异常 (roman.InvalidRomanNumeralError)。如果 (在循环中的每一次) 确实如此,测试通过;如果有一次不是这样 (比如引发另外的异常或者不引发异常),测试失败。 |
在下一章中,你将看到如何编写可以通过这些测试的代码。
到目前为止,单元测试已经完成,是时候开始编写被单元测试测试的代码了。你将分阶段地完成这个工作,因此开始时所有的单元测试都是失败的,但在逐步完成 roman.py 的同时你会看到它们一个个地通过测试。
这个程序可以在例子目录下的 py/roman/stage1/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass def toRoman(n): """convert integer to Roman numeral""" pass def fromRoman(s): """convert Roman numeral to integer""" pass
这就是如何定义你自己的 Python 异常。异常 (Exception) 也是类,通过继承已有的异常,你可以创建自定义的异常。强烈建议 (但不是必须) 你继承 Exception 来定义自己的异常,因为它是所有内建异常的基类。这里我定义了 RomanError (从 Exception 继承而来) 作为我所有自定义异常的基类。这是一个风格问题,我也可以直接从 Exception 继承建立每一个自定义异常。 | |
OutOfRangeError 和 NotIntegerError 异常将会最终被用于 toRoman 以标示不同类型的无效输入,更具体而言就是 ToRomanBadInput 测试的那些。 | |
InvalidRomanNumeralError 将被最终用于 fromRoman 以标示无效输入,具体而言就是 FromRomanBadInput测试的那些。 | |
在这一步中你只是想定义每个函数的 API ,而不想具体实现它们,因此你以 Python 关键字 pass 姑且带过。 |
重要的时刻到了 (请打起鼓来):你终于要对这个简陋的小模块开始运行单元测试了。目前而言,每一个测试用例都应该失败。事实上,任何测试用例在此时通过,你都应该回头看看 romantest.py ,仔细想想为什么你写的测试代码如此没用,以至于连什么都不作的函数都能通过测试。
用命令行选项 -v 运行 romantest1.py 可以得到更详细的输出信息,这样你就可以看到每一个测试用例的具体运行情况。如果幸运,你的结果应该是这样的:
fromRoman should only accept uppercase input ... ERROR toRoman should always return uppercase ... ERROR fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... FAIL fromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... FAIL toRoman should fail with negative input ... FAIL toRoman should fail with large input ... FAIL toRoman should fail with 0 input ... FAIL ====================================================================== ERROR: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase roman1.fromRoman(numeral.upper()) AttributeError: 'None' object has no attribute 'upper' ====================================================================== ERROR: toRoman should always return uppercase ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase self.assertEqual(numeral, numeral.upper()) AttributeError: 'None' object has no attribute 'upper' ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues self.assertEqual(numeral, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: I != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: NotIntegerError ====================================================================== FAIL: toRoman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with 0 input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ---------------------------------------------------------------------- Ran 12 tests in 0.040s FAILED (failures=10, errors=2)
现在你有了 roman 模块的大概框架,到了开始写代码以通过测试的时候了。
这个文件可以从 py/roman/stage2/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result def fromRoman(s): """convert Roman numeral to integer""" pass
如果你不明了 toRoman 如何工作,在 while 循环的结尾添加一个 print 语句:
while n >= integer: result += numeral n -= integer print 'subtracting', integer, 'from input, adding', numeral, 'to output'
>>> import roman2 >>> roman2.toRoman(1424) subtracting 1000 from input, adding M to output subtracting 400 from input, adding CD to output subtracting 10 from input, adding X to output subtracting 10 from input, adding X to output subtracting 4 from input, adding IV to output 'MCDXXIV'
看来 toRoman 可以运转了,至少手工测试可以。但能通过单元测试吗?啊哈,不,不完全可以。
要记得用 -v 命令行选项运行 romantest2.py 开启详细信息模式。
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... FAIL toRoman should fail with negative input ... FAIL toRoman should fail with large input ... FAIL toRoman should fail with 0 input ... FAIL
事实上,toRoman 的返回值总是大写的,因为 romanNumeralMap 定义的罗马字符都是以大写字母表示的。因此这个测试已经通过了。 | |
好消息来了:这个版本的 toRoman 函数能够通过已知值测试。记住,这并不能证明完全没问题,但至少通过测试多种有效输入考验了这个函数:包括每个单一字符的罗马数字,可能的最大输入 (3999),以及可能的最长的罗马数字 (对应于 3888)。从这点来看,你有理由相信这个函数对于任何有效输入都不会出问题。 | |
但是,函数还没办法处理无效输入,每个无效输入测试都失败了。这很好理解,因为你还没有对无效输入进行检查,测试用例希望捕捉到特定的异常 (通过 assertRaises),而你根本没有让这些异常引发。这是你下一阶段的工作。 |
下面是单元测试结果的剩余部分,列出了所有失败的详细信息,你已经让它降到了 10 个。
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in testFromRomanCase roman2.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in testMalformedAntecedent self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 127, in testRepeatedPairs self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: toRoman should fail with non-integer input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in testNonInteger self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: NotIntegerError ====================================================================== FAIL: toRoman should fail with negative input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in testNegative self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in testTooLarge self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ====================================================================== FAIL: toRoman should fail with 0 input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in testZero self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: OutOfRangeError ---------------------------------------------------------------------- Ran 12 tests in 0.320s FAILED (failures=10)
现在 toRoman 对于有效的输入 (1 到 3999 整数) 已能正确工作,是正确处理那些无效输入 (任何其他输入) 的时候了。
这个文件可以在例子目录下的 py/roman/stage3/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 4000): raise OutOfRangeError, "number out of range (must be 1..3999)" if int(n) <> n: raise NotIntegerError, "non-integers can not be converted" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result def fromRoman(s): """convert Roman numeral to integer""" pass
>>> import roman3 >>> roman3.toRoman(4000) Traceback (most recent call last): File "<interactive input>", line 1, in ? File "roman3.py", line 27, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) >>> roman3.toRoman(1.5) Traceback (most recent call last): File "<interactive input>", line 1, in ? File "roman3.py", line 29, in toRoman raise NotIntegerError, "non-integers can not be converted" NotIntegerError: non-integers can not be converted
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... FAIL toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... FAIL toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
toRoman 仍然能通过已知值测试,这很令人鼓舞。所有第 2 阶段通过的测试仍然能通过,这说明新的代码没有对原有代码构成任何负面影响。 | |
更令人振奋的是所有的无效输入测试现在都通过了。testNonInteger 这个测试能够通过是因为有了 int(n) <> n 检查。当一个非整数传递给 toRoman 时,int(n) <> n 检查出问题并引发 NotIntegerError 异常,这正是 testNonInteger 所期待的。 | |
testNegative 这个测试能够通过是因为 not (0 < n < 4000) 检查引发了 testNegative 期待的 OutOfRangeError 异常。 |
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in testFromRomanCase roman3.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in testMalformedAntecedent self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in testRepeatedPairs self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in testFromRomanKnownValues self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ====================================================================== FAIL: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in testSanity self.assertEqual(integer, result) File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual raise self.failureException, (msg or '%s != %s' % (first, second)) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 12 tests in 0.401s FAILED (failures=6)
全面的单元测试能够告诉你的最重要的事情是什么时候停止编写代码。当一个函数的所有单元测试都通过了,停止编写这个函数。一旦整个模块的单元测试通过了,停止编写这个模块。 |
现在 toRoman 完成了,是开始编写 fromRoman 的时候了。感谢那个将每个罗马数字和对应整数关连的完美数据结构,这个工作不比 toRoman 函数复杂。
这个文件可以在例子目录下的 py/roman/stage4/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Convert to and from Roman numerals""" #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) # toRoman function omitted for clarity (it hasn't changed) def fromRoman(s): """convert Roman numeral to integer""" result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
这和 toRoman 的工作模式很相似。你遍历整个罗马数字数据结构 (一个元组的元组),与前面不同的是不去一个个搜寻最大的整数,而是搜寻 “最大的”罗马数字字符串。 |
如果你不清楚 fromRoman 如何工作,在 while 结尾处添加一个 print 语句:
while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) print 'found', numeral, 'of length', len(numeral), ', adding', integer
>>> import roman4 >>> roman4.fromRoman('MCMLXXII') found M , of length 1, adding 1000 found CM , of length 2, adding 900 found L , of length 1, adding 50 found X , of length 1, adding 10 found X , of length 1, adding 10 found I , of length 1, adding 1 found I , of length 1, adding 1 1972
fromRoman should only accept uppercase input ... FAIL toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... FAIL fromRoman should fail with repeated pairs of numerals ... FAIL fromRoman should fail with too many repeated numerals ... FAIL fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
这儿有两个令人激动的消息。一个是 fromRoman 对于所有有效输入运转正常,至少对于你测试的已知值是这样。 | |
第二个好消息是,完备性测试也通过了。与已知值测试的通过一起来看,你有理由相信 toRoman 和 fromRoman 对于所有有效输入值工作正常。(尚不能完全相信,理论上存在这种可能性:toRoman 存在错误而导致一些特定输入会产生错误的罗马数字表示,并且 fromRoman 也存在相应的错误,把 toRoman 错误产生的这些罗马数字错误地转换为最初的整数。取决于你的应用程序和你的要求,你或许需要考虑这个可能性。如果是这样,编写更全面的测试用例直到解决这个问题。) |
====================================================================== FAIL: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 156, in testFromRomanCase roman4.fromRoman, numeral.lower()) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with malformed antecedents ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 133, in testMalformedAntecedent self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with repeated pairs of numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 127, in testRepeatedPairs self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ====================================================================== FAIL: fromRoman should fail with too many repeated numerals ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage4\romantest4.py", line 122, in testTooManyRepeatedNumerals self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s) File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ---------------------------------------------------------------------- Ran 12 tests in 1.222s FAILED (failures=4)
现在 fromRoman 对于有效输入能够正常工作了,是揭开最后一个谜底的时候了:使它正常工作于无效输入的情况下。这意味着要找出一个方法检查一个字符串是不是有效的罗马数字。这比 toRoman 中验证有效的数字输入困难,但是你可以使用一个强大的工具:正则表达式。
如果你不熟悉正则表达式,并且没有读过 第 7 章 正则表达式,现在是该好好读读的时候了。
如你在 第 7.3 节 “个案研究:罗马字母”中所见到的,构建罗马数字有几个简单的规则:使用字母 M, D, C, L, X, V 和 I。让我们回顾一下:
这个程序可以在例子目录下的py/roman/stage5/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Convert to and from Roman numerals""" import re #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 4000): raise OutOfRangeError, "number out of range (must be 1..3999)" if int(n) <> n: raise NotIntegerError, "non-integers can not be converted" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result #Define pattern to detect valid Roman numerals romanNumeralPattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$' def fromRoman(s): """convert Roman numeral to integer""" if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
这只是 第 7.3 节 “个案研究:罗马字母” 中讨论的匹配模版的继续。十位上可能是XC (90),XL (40),或者可能是 L 后面跟着 0 到 3 个 X 字符。个位则可能是 IX (9),IV (4),或者是一个可能是 V 后面跟着 0 到 3 个 I 字符。 | |
把所有的逻辑编码成正则表达式,检查无效罗马字符的代码就很简单了。如果 re.search 返回一个对象则表示匹配了正则表达式,输入是有效的,否则输入无效。 |
这里你可能会怀疑,这个面目可憎的正则表达式是否真能查出错误的罗马字符表示。没关系,不必完全听我的,不妨看看下面的结果:
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 12 tests in 2.864s OK
当所有测试都通过了,停止编程。 |
尽管你很努力地编写全面的单元测试,但是 bug 还是会出现。我所说的 “bug” 是什么呢?Bug 是你还没有编写的测试用例。
>>> import roman5 >>> roman5.fromRoman("") 0
在前面的章节中你注意到一个空字符串会匹配上那个检查罗马数字有效性的正则表达式了吗?对于最终版本中的正则表达式这一点仍然没有改变。这就是一个 Bug ,你希望空字符串能够像其他无效的罗马数字表示一样引发 InvalidRomanNumeralError 异常。 |
在重现这个 Bug 并修改它之前你应该编写一个会失败的测试用例来说明它。
class FromRomanBadInput(unittest.TestCase): # previous test cases omitted for clarity (they haven't changed) def testBlank(self): """fromRoman should fail with blank string""" self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "")
因为你的代码存在一个 Bug,并且你编写了测试这个 Bug 的测试用例,所以测试用例将会失败:
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... FAIL fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ====================================================================== FAIL: fromRoman should fail with blank string ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in testBlank self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "") File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises raise self.failureException, excName AssertionError: InvalidRomanNumeralError ---------------------------------------------------------------------- Ran 13 tests in 2.864s FAILED (failures=1)
现在 你可以修改这个 Bug了。
这个文件可以在例子目录下的 py/roman/stage6/ 目录中找到。
def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, 'Input can not be blank' if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 13 tests in 2.834s OK
这样编程,并没有令 Bug 修正变得简单。简单的 Bug (就像这一个) 需要简单的测试用例,复杂 Bug 则需要复杂的测试用例。以测试为核心的氛围好像 延长了修正 Bug 的时间,因为你需要先贴切地描述出 Bug (编写测试用例) 然后才去修正它。如果测试用例没能正确通过,你需要思量这个修改错了还是测试用例本身出现了 Bug。无论如何,从长远上讲,这样在测试代码和代码之间的反复是值得的,因为这样会使 Bug 在第一时间就被修正的可能性大大提高。而且不论如何更改,你都可以轻易地重新运行所有 测试用例,新代码破坏老代码的机会也变得微乎其微。今天的单元测试就是明天的回归测试 (regression test)。
尽管你竭尽努力地分析你的客户,并点灯熬油地提炼出精确的需求,但需求还是会是不断变化。大部分客户在看到产品前不知道他们想要什么。即便知道,也不擅于精确表述出他们的有效需求。即便能表述出来,他们在下一个版本一定会要求更多的功能。因此你需要做好更新测试用例的准备以应对需求的改变。
假设你想要扩展罗马数字转换函数的范围。还记得没有哪个字符可以重复三遍以上这条规则吗?呃,现在罗马人希望给这条规则来个例外,用连续出现 4 个 M 字符来表示 4000。如果这样改了,你就可以把转换范围从 1..3999 扩展到 1..4999。但你先要对测试用例进行修改。
这个文件可以在例子目录下的 py/roman/stage7/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
import roman71 import unittest class KnownValues(unittest.TestCase): knownValues = ( (1, 'I'), (2, 'II'), (3, 'III'), (4, 'IV'), (5, 'V'), (6, 'VI'), (7, 'VII'), (8, 'VIII'), (9, 'IX'), (10, 'X'), (50, 'L'), (100, 'C'), (500, 'D'), (1000, 'M'), (31, 'XXXI'), (148, 'CXLVIII'), (294, 'CCXCIV'), (312, 'CCCXII'), (421, 'CDXXI'), (528, 'DXXVIII'), (621, 'DCXXI'), (782, 'DCCLXXXII'), (870, 'DCCCLXX'), (941, 'CMXLI'), (1043, 'MXLIII'), (1110, 'MCX'), (1226, 'MCCXXVI'), (1301, 'MCCCI'), (1485, 'MCDLXXXV'), (1509, 'MDIX'), (1607, 'MDCVII'), (1754, 'MDCCLIV'), (1832, 'MDCCCXXXII'), (1993, 'MCMXCIII'), (2074, 'MMLXXIV'), (2152, 'MMCLII'), (2212, 'MMCCXII'), (2343, 'MMCCCXLIII'), (2499, 'MMCDXCIX'), (2574, 'MMDLXXIV'), (2646, 'MMDCXLVI'), (2723, 'MMDCCXXIII'), (2892, 'MMDCCCXCII'), (2975, 'MMCMLXXV'), (3051, 'MMMLI'), (3185, 'MMMCLXXXV'), (3250, 'MMMCCL'), (3313, 'MMMCCCXIII'), (3408, 'MMMCDVIII'), (3501, 'MMMDI'), (3610, 'MMMDCX'), (3743, 'MMMDCCXLIII'), (3844, 'MMMDCCCXLIV'), (3888, 'MMMDCCCLXXXVIII'), (3940, 'MMMCMXL'), (3999, 'MMMCMXCIX'), (4000, 'MMMM'), (4500, 'MMMMD'), (4888, 'MMMMDCCCLXXXVIII'), (4999, 'MMMMCMXCIX')) def testToRomanKnownValues(self): """toRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman71.toRoman(integer) self.assertEqual(numeral, result) def testFromRomanKnownValues(self): """fromRoman should give known result with known input""" for integer, numeral in self.knownValues: result = roman71.fromRoman(numeral) self.assertEqual(integer, result) class ToRomanBadInput(unittest.TestCase): def testTooLarge(self): """toRoman should fail with large input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000) def testZero(self): """toRoman should fail with 0 input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0) def testNegative(self): """toRoman should fail with negative input""" self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1) def testNonInteger(self): """toRoman should fail with non-integer input""" self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5) class FromRomanBadInput(unittest.TestCase): def testTooManyRepeatedNumerals(self): """fromRoman should fail with too many repeated numerals""" for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s) def testRepeatedPairs(self): """fromRoman should fail with repeated pairs of numerals""" for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'): self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s) def testMalformedAntecedent(self): """fromRoman should fail with malformed antecedents""" for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV', 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'): self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s) def testBlank(self): """fromRoman should fail with blank string""" self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, "") class SanityCheck(unittest.TestCase): def testSanity(self): """fromRoman(toRoman(n))==n for all n""" for integer in range(1, 5000): numeral = roman71.toRoman(integer) result = roman71.fromRoman(numeral) self.assertEqual(integer, result) class CaseCheck(unittest.TestCase): def testToRomanCase(self): """toRoman should always return uppercase""" for integer in range(1, 5000): numeral = roman71.toRoman(integer) self.assertEqual(numeral, numeral.upper()) def testFromRomanCase(self): """fromRoman should only accept uppercase input""" for integer in range(1, 5000): numeral = roman71.toRoman(integer) roman71.fromRoman(numeral.upper()) self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, numeral.lower()) if __name__ == "__main__": unittest.main()
现在你的测试用例和新需求保持一致了,但是你的程序代码还没有,因此几个测试用例的失败是意料之中的事。
fromRoman should only accept uppercase input ... ERROR toRoman should always return uppercase ... ERROR fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ERROR toRoman should give known result with known input ... ERROR fromRoman(toRoman(n))==n for all n ... ERROR toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok
====================================================================== ERROR: fromRoman should only accept uppercase input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in testFromRomanCase numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: toRoman should always return uppercase ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in testToRomanCase numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: fromRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in testFromRomanKnownValues result = roman71.fromRoman(numeral) File "roman71.py", line 47, in fromRoman raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s InvalidRomanNumeralError: Invalid Roman numeral: MMMM ====================================================================== ERROR: toRoman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in testToRomanKnownValues result = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ====================================================================== ERROR: fromRoman(toRoman(n))==n for all n ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in testSanity numeral = roman71.toRoman(integer) File "roman71.py", line 28, in toRoman raise OutOfRangeError, "number out of range (must be 1..3999)" OutOfRangeError: number out of range (must be 1..3999) ---------------------------------------------------------------------- Ran 13 tests in 2.213s FAILED (errors=5)
既然新的需求导致了测试用例的失败,你该考虑修改代码以便它能再次通过测试用例。(在你开始编写单元测试时要习惯一件事:被测试代码永远不会在编写测试用例“之前”编写。正因为如此,你还有一些工作要做,一旦可以通过所有的测试用例,停止编码。)
这个文件可以在例子目录下的 py/roman/stage7/ 目录中找到。
"""Convert to and from Roman numerals""" import re #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) def toRoman(n): """convert integer to Roman numeral""" if not (0 < n < 5000): raise OutOfRangeError, "number out of range (must be 1..4999)" if int(n) <> n: raise NotIntegerError, "non-integers can not be converted" result = "" for numeral, integer in romanNumeralMap: while n >= integer: result += numeral n -= integer return result #Define pattern to detect valid Roman numerals romanNumeralPattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$' def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, 'Input can not be blank' if not re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
你可能会怀疑只需这两处小改动。嘿,不相信我的话,你自己看看吧:
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 13 tests in 3.685s OK
全面的单元测试意味着不必依赖于程序员的一面之词:“相信我!”
全面的单元测试带来的最大好处不是你的全部测试用例最终通过时的成就感;也不是被责怪破坏了别人的代码时能够证明 自己的自信。最大的好处是单元测试给了你自由去无情地重构。
重构是在可运行代码的基础上使之工作得更好的过程。通常,“更好”意味着“更快”,也可能意味着 “使用更少的内存”,或者 “使用更少的磁盘空间”,或者仅仅是“更优雅的代码”。不管对你,对你的项目意味什么,在你的环境中,重构对任何程序的长期良性运转都是重要的。
这里,“更好” 意味着 “更快”。更具体地说,fromRoman 函数可以更快,关键在于那个丑陋的、用于验证罗马数字有效性的正则表达式。尝试不用正则表达式去解决是不值得的 (这样做很难,而且可能也快不了多少),但可以通过预编译正则表达式使函数提速。
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') <SRE_Match object at 01090490> >>> compiledPattern = re.compile(pattern) >>> compiledPattern <SRE_Pattern object at 00F06E28> >>> dir(compiledPattern) ['findall', 'match', 'scanner', 'search', 'split', 'sub', 'subn'] >>> compiledPattern.search('M') <SRE_Match object at 01104928>
在需要多次使用同一个正则表达式的情况下,应该将它进行编译以获得一个 pattern 对象,然后直接调用这个 pattern 对象的方法。 |
这个文件可以在例子目录下的 py/roman/stage8/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
# toRoman and rest of module omitted for clarity romanNumeralPattern = \ re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$') def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, 'Input can not be blank' if not romanNumeralPattern.search(s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s result = 0 index = 0 for numeral, integer in romanNumeralMap: while s[index:index+len(numeral)] == numeral: result += integer index += len(numeral) return result
那么编译正则表达式可以提速多少呢?你自己来看吧:
............. ---------------------------------------------------------------------- Ran 13 tests in 3.385s OK
我还想做另外一个性能优化工作。就正则表达式语法的复杂性而言,通常有不止一种方法来构造相同的表达式是不会令人惊讶的。在 comp.lang.python 上对该模块进行一些讨论后,有人建议我使用 {m,n} 语法来查找可选重复字符。
这个文件可以在例子目录下的 py/roman/stage8/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
# rest of program omitted for clarity #old version #romanNumeralPattern = \ # re.compile('^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$') #new version romanNumeralPattern = \ re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$')
这样的正则表达简短一些 (虽然可读性不太好)。核心问题是,是否能加快速度?
............. ---------------------------------------------------------------------- Ran 13 tests in 3.315s OK
还有另外一个我想做的调整,我保证这是最后一个,之后我会停下来,让这个模块歇歇。就像你多次看到的,正则表达式越晦涩难懂越快,我可不想在六个月内再回头试图维护它。是呀!测试用例通过了,我便知道它工作正常,但如果我搞不懂它是如何 工作的,添加新功能、修正新 Bug,或者维护它都将变得很困难。正如你在 第 7.5 节 “松散正则表达式” 看到的,Python 提供了逐行注释你的逻辑的方法。
该文件可以在例子目录下的 py/roman/stage8/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
# rest of program omitted for clarity #old version #romanNumeralPattern = \ # re.compile('^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$') #new version romanNumeralPattern = re.compile(''' ^ # beginning of string M{0,4} # thousands - 0 to 4 M's (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), # or 500-800 (D, followed by 0 to 3 C's) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), # or 50-80 (L, followed by 0 to 3 X's) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), # or 5-8 (V, followed by 0 to 3 I's) $ # end of string ''', re.VERBOSE)
聪明的读者在学习前一节时想得会更深入一层。现在写的这个程序中最令人头痛的性能负担是正则表达式,但它是必需的,因为没有其它方法来识别罗马数字。但是,它们只有 5000 个,为什么不一次性地构建一个查询表来读取?不必用正则表达式凸现了这个主意的好处。你建立了整数到罗马数字查询表的时候,罗马数字到整数的逆向查询表也构建了。
更大的好处在于,你已经拥有一整套完全的单元测试。你修改了多半的代码,但单元测试还是一样的,因此你可以确定你的新代码与来的代码一样可以正常工作。
这个文件可以在例子目录下的 py/roman/stage9/ 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
#Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class InvalidRomanNumeralError(RomanError): pass #Roman numerals must be less than 5000 MAX_ROMAN_NUMERAL = 4999 #Define digit mapping romanNumeralMap = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100), ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5), ('IV', 4), ('I', 1)) #Create tables for fast conversion of roman numerals. #See fillLookupTables() below. toRomanTable = [ None ] # Skip an index since Roman numerals have no zero fromRomanTable = {} def toRoman(n): """convert integer to Roman numeral""" if not (0 < n <= MAX_ROMAN_NUMERAL): raise OutOfRangeError, "number out of range (must be 1..%s)" % MAX_ROMAN_NUMERAL if int(n) <> n: raise NotIntegerError, "non-integers can not be converted" return toRomanTable[n] def fromRoman(s): """convert Roman numeral to integer""" if not s: raise InvalidRomanNumeralError, "Input can not be blank" if not fromRomanTable.has_key(s): raise InvalidRomanNumeralError, "Invalid Roman numeral: %s" % s return fromRomanTable[s] def toRomanDynamic(n): """convert integer to Roman numeral using dynamic programming""" result = "" for numeral, integer in romanNumeralMap: if n >= integer: result = numeral n -= integer break if n > 0: result += toRomanTable[n] return result def fillLookupTables(): """compute all the possible roman numerals""" #Save the values in two global tables to convert to and from integers. for integer in range(1, MAX_ROMAN_NUMERAL + 1): romanNumber = toRomanDynamic(integer) toRomanTable.append(romanNumber) fromRomanTable[romanNumber] = integer fillLookupTables()
这样有多快呢?
.............
----------------------------------------------------------------------
Ran 13 tests in 0.791s
OK
还记得吗?你原有版本的最快速度是 13 个测试耗时 3.315 秒。当然,这样的比较不完全公平,因为这个新版本需要更长的时间来导入 (当它填充查询表时)。但是导入只需一次,在运行过程中可以忽略。
这个重构的故事的寓意是什么?
单元测试是一个强大的概念,使用得当的话既可以减少维护成本又可以增加长期项目的灵活性。同样重要的是要意识到单元测试并不是“灵丹妙药”,也不是“银弹”。编写好的测试用例很困难,保持其更新更需要磨练 (特别是当顾客对修复严重的 Bug 大呼小叫之时)。单元测试不是其它形式测试的替代品,比如说功能性测试、集成测试以及可用性测试。但它切实可行且功效明显,一旦相识,你会反问为什么以往没有应用它。
这一章涵盖了很多内容,有很多都不是 Python 所特有的。很多语言都有单元测试框架,都要求你理解相同的基本概念:
另外,你应该能够自如地做到如下 Python 的特有工作:
在 第 13 章 单元测试 中,你学会了单元测试的哲学。在 第 14 章 测试优先编程 中你步入了 Python 基本的单元测试操作,在 第 15 章 重构 部分,你看到单元测试如何令大规模重构变得容易。本章将在这些程序样例的基础上,集中关注于超越单元测试本身的更高级的 Python 特有技术。
下面是一个作为简单回归测试 (regression test) 框架运行的完整 Python 程序。它将你前面编写的单独单元测试模块组织在一起成为一个测试套件并一次性运行。实际上这是本书的构建代码的一部分;我为几个样例程序都编写了单元测试 (不是只有 第 13 章 单元测试 中的 roman.py 模块),我的自动构建代码的第一个工作便是确保我所有的例子可以正常工作。如果回归测试程序失败,构建过程当即终止。我可不想因为发布了不能工作的样例程序而让你在下载他们后坐在显示器前抓耳挠腮地为程序不能运转而烦恼。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Regression testing framework This module will search for scripts in the same directory named XYZtest.py. Each such script should be a test suite that tests a module through PyUnit. (As of Python 2.1, PyUnit is included in the standard library as "unittest".) This script will aggregate all found test suites into one big test suite and run them all at once. """ import sys, os, re, unittest def regressionTest(): path = os.path.abspath(os.path.dirname(sys.argv[0])) files = os.listdir(path) test = re.compile("test\.py$", re.IGNORECASE) files = filter(test.search, files) filenameToModuleName = lambda f: os.path.splitext(f)[0] moduleNames = map(filenameToModuleName, files) modules = map(__import__, moduleNames) load = unittest.defaultTestLoader.loadTestsFromModule return unittest.TestSuite(map(load, modules)) if __name__ == "__main__": unittest.main(defaultTest="regressionTest")
把这段代码放在本书其他样例代码相同的目录下运行之,moduletest.py 中的所有单元测试将被找到并一起被运行。
[you@localhost py]$ python regression.py -v help should fail with no object ... ok help should return known result for apihelper ... ok help should honor collapse argument ... ok help should honor spacing argument ... ok buildConnectionString should fail with list input ... ok buildConnectionString should fail with string input ... ok buildConnectionString should fail with tuple input ... ok buildConnectionString handles empty dictionary ... ok buildConnectionString returns known result with known input ... ok fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok fromRoman should fail with blank string ... ok fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok fromRoman should give known result with known input ... ok toRoman should give known result with known input ... ok fromRoman(toRoman(n))==n for all n ... ok toRoman should fail with non-integer input ... ok toRoman should fail with negative input ... ok toRoman should fail with large input ... ok toRoman should fail with 0 input ... ok kgp a ref test ... ok kgp b ref test ... ok kgp c ref test ... ok kgp d ref test ... ok kgp e ref test ... ok kgp f ref test ... ok kgp g ref test ... ok ---------------------------------------------------------------------- Ran 29 tests in 2.799s OK
前五个测试来自于 apihelpertest.py,用以测试 第 4 章 自省的威力 中的样例代码。 | |
接下来的五个测试来自于 odbchelpertest.py,用以测试 第 2 章 第一个 Python 程序 中的样例代码。 | |
其他的测试来自于 romantest.py,你在 第 13 章 单元测试 中深入学习过。 |
从命令行运行 Python 代码时,知道所运行代码在磁盘上的存储位置有时候是有必要的。
这是一个不那么容易想起,但一想起就很容易解决的小麻烦。答案是 sys.argv。正如你在 第 9 章 XML 处理 中看到的,它包含了很多命令行参数。它也同样记录了运行脚本的名字,和你调用它时使用的命令一摸一样。这些信息足以令我们确定文件的位置。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
import sys, os print 'sys.argv[0] =', sys.argv[0] pathname = os.path.dirname(sys.argv[0]) print 'path =', pathname print 'full path =', os.path.abspath(pathname)
进一步地解释 os.path.abspath 是有必要的。它非常灵活,可以接受任何类型的路径名。
>>> import os >>> os.getcwd() /home/you >>> os.path.abspath('') /home/you >>> os.path.abspath('.ssh') /home/you/.ssh >>> os.path.abspath('/home/you/.ssh') /home/you/.ssh >>> os.path.abspath('.ssh/../foo/') /home/you/foo
传递给 os.path.abspath 的路径名和文件名可以不存在。 |
os.path.abspath 不仅构建完整路径名,还能格式化路径名。这意味着如果你正工作于 /usr/ 目录,os.path.abspath('bin/../local/bin') 将会返回 /usr/local/bin。它把路径名格式化为尽可能简单的形式。如果你只是希望简单地返回这样的格式化路径名而不需要完整路径名,可以使用 os.path.normpath。 |
[you@localhost py]$ python /home/you/diveintopython/common/py/fullpath.py sys.argv[0] = /home/you/diveintopython/common/py/fullpath.py path = /home/you/diveintopython/common/py full path = /home/you/diveintopython/common/py [you@localhost diveintopython]$ python common/py/fullpath.py sys.argv[0] = common/py/fullpath.py path = common/py full path = /home/you/diveintopython/common/py [you@localhost diveintopython]$ cd common/py [you@localhost py]$ python fullpath.py sys.argv[0] = fullpath.py path = full path = /home/you/diveintopython/common/py
就像 os 和 os.path 模块的其他函数,os.path.abspath 是跨平台的。如果你是在 Windows (使用反斜杠作为路径符号) 或 Mac OS (使用冒号) 上运行,它们同样工作,只是将获得与我稍有不同的结果。os 的所有函数都是这样的。 |
补充. 一位读者对这个结果并不满意,他希望能够从当前路径运行所有单元测试,而不是从 regression.py 所在目录运行。他建议以下面的代码加以取代:
import sys, os, re, unittest def regressionTest(): path = os.getcwd() sys.path.append(path) files = os.listdir(path)
这个技术允许你在多个项目中重用 regression.py 代码。只需要将这个代码放在一个普通目录中,在运行项目前将路径更改为项目的目录。项目中所有的单元测试被找到并运行,而不仅仅局限于 regression.py 所在目录的单元测试。
你已经熟识了应用列表解析来过滤列表。这里介绍的是达到相同效果的另一种令很多人感觉清晰的实现方法。
Python 有一个内建 filter 函数,它接受两个参数:一个函数和一个列表,返回一个列表。[12] 作为第一个参数传递给 filter 的函数本身应接受一个参数,filter 返回的列表将会包含被传入列表参数传递给 filter 所有可以令函数返回真 (true) 的元素。
都明白了吗?并没有听起来那么难。
>>> def odd(n): ... return n % 2 ... >>> li = [1, 2, 3, 5, 9, 10, 256, -3] >>> filter(odd, li) [1, 3, 5, 9, -3] >>> [e for e in li if odd(e)] >>> filteredList = [] >>> for n in li: ... if odd(n): ... filteredList.append(n) ... >>> filteredList [1, 3, 5, 9, -3]
odd 使用内建的取模 (mod) 函数 “%” 对于为奇数的 n 返回 1;为偶数的返回 0。 | |
filter 接受两个参数:一个函数 (odd) 和一个列表 (li)。它依列表循环为每个元素调用 odd 函数。如果 odd 返回的是真 (记住,Python 认为所有非零值为真),则该元素被放在返回列表中,如若不然则被过滤掉。结果是一个只包含原列表中奇数的列表,出现顺序则和原列表相同。 | |
你可以通过遍历的方式完成相同的工作,正如在 第 4.5 节 “过滤列表” 中看到的。 | |
你可以通过 for 循环的方式完成相同的工作。取决于你的编程背景,这样也许更“直接”,但是像 filter 函数这样的实现方法更清晰。不但编写简单,而且易于读懂。for 循环就好比近距离的绘画:你可以看到所有的细节,但是或许你应该花几秒时间退后几步看一看图画的全景:“啊,你仅仅是要过滤列表!” |
files = os.listdir(path) test = re.compile("test\.py$", re.IGNORECASE) files = filter(test.search, files)
正如你在 第 16.2 节 “找到路径” 中看到的,path 可能包括正在运行脚本的完全或者部分路径名,或者当脚本运行自当前目录时包含一个空的字符串。任何一种情况下,files 都会获得正运行脚本所在目录的文件名。 | |
这是一个预编译的正则表达式。正如你在 第 15.3 节 “重构”中看到的,如果你需要反复使用同一个正则表达式,你应该编译它已获得更快的性能。编译后的对象将含有接受一个待寻找字符串作为参数的 search 方法。如果这个正则表达式匹配字符串,search 方法返回一个包含正则表达式匹配信息的 Match 对象;否则返回 None,这是 Python 空 (null) 值。 | |
对于 files 列表中的每个元素,你将会调用正则表达式编译对象 test 的 search 方法。如果正则表达匹配,方法将会返回一个被 Python 认定为真 (true) 的 Match 对象;如果正则表达不匹配,search 方法将会返回被认定为假 (false) 的 None,元素将被排除。 |
历史注释. Python 2.0 早期的版本不包含 列表解析,因此不能 以列表解析方式过滤,filter 函数是当时唯一的方法。即便是在引入列表解析的 2.0 版,有些人仍然钟情于老派的 filter (和这章稍后将见到的它的伴侣函数 map )。两种方法并存于世,使用哪种方法只是风格问题,map 和 filter 将在未来的 Python 版本中被废止的讨论尚无定论。
>>> def double(n): ... return n*2 ... >>> li = [1, 2, 3, 5, 9, 10, 256, -3] >>> map(double, li) [2, 4, 6, 10, 18, 20, 512, -6] >>> [double(n) for n in li] [2, 4, 6, 10, 18, 20, 512, -6] >>> newlist = [] >>> for n in li: ... newlist.append(double(n)) ... >>> newlist [2, 4, 6, 10, 18, 20, 512, -6]
map 接受一个函数和一个列表作为参数,[13] 并对列表中的每个元素依次调用函数返回一个新的列表。在这个例子中,函数仅仅是将每个元素乘以 2。 | |
使用列表解析的方法你可以做到相同的事情。列表解析是在 Python 2.0 版时被引入的;而 map 则古老得多。 | |
你如果坚持以 Visual Basic 程序员自居,通过 for 循环的方法完成相同的任务也完全可以。 |
>>> li = [5, 'a', (2, 'b')] >>> map(double, li) [10, 'aa', (2, 'b', 2, 'b')]
好了,玩够了。让我们来看一些真实代码。
filenameToModuleName = lambda f: os.path.splitext(f)[0] moduleNames = map(filenameToModuleName, files)
正如你在 第 4.7 节 “使用 lambda 函数” 中所见,lambda 定义一个内联函数。也正如你在 例 6.17 “分割路径名” 中所见,os.path.splitext 接受一个文件名并返回一个元组 (name, extension)。因此 filenameToModuleName 是一个接受文件名,剥离出其扩展名,然后只返回文件名称的函数。 | |
调用 map 将把 files 列出的所有文件名传递给 filenameToModuleName 函数,并且返回每个函数调用结果所组成的列表。换句话说,你剔除掉文件名的扩展名,并将剔除后的文件名存于 moduleNames 之中。 |
如你在本章剩余部分将看到的,你可以将这种数据中心思想扩展到定义和执行一个容纳来自很多单个测试套件的测试的一个测试套件的最终目标。
现在的你,可能正抓耳挠腮地狠想,为什么这样比使用 for 循环和直接调用函数好。这是一个非常好的问题。通常这是一个程序观问题。使用 map 和 filter 强迫你围绕数据进行思考。
就此而言,你从没有数据开始,你所做的第一件事是获得当前脚本的目录路径,并获得该目录中的文件列表。这就是关键的一步,使你有了待处理的真实数据:文件名列表。
当然,你知道你并不关心所有的文件,而只关心测试套件。你有太多数据,因此你需要过滤(filter)数据。你如何知道哪些数据应该保留?你需要一个测试来确定,因此你定义一个测试并把它传给 filter 函数。这里你应用了一个正则表达式来确定,但无论如何构建测试,原则是一样的。
现在你有了每个测试套件的文件名 (且局限于测试套件,因为所有其他内容都被过滤掉了),但是你还需要以模块名来替代之。你有正确数量的数据,只是格式不正确。因此,你定义了一个函数来将文件名转换为模块名,并使用这个函数映射整个列表。从一个文件名,你可以获得一个模块名,从一个文件名列表,你可以获得一个模块名列表。
如果不应用 filter,你也可以使用 for 循环结合一个 if 语句的方法。map 的使用则可以由一个 for 循环和一个函数调用来取代。但是 for 循环看起来像是个繁重的工作。至少,简单讲是在浪费时间,糟糕的话还会隐埋 Bug。例如,你需要弄清楚如何测试这样一个条件:“这个文件是测试套件吗?”这是应用特定的逻辑,没有哪个语言能自动为我们写出其代码。但是一旦你搞清楚了,你还需要费尽周折地定义一个新的空列表,写一个 for 循环以及一个 if 语句并手工地调用 append 将符合条件的元素一个个添加到新列表中,然后一路上注意区分哪个变量里放着过滤后的数据,哪个变量里放着未过滤的老数据。为什么不直接定义测试条件,然后由 Python 为你完成接下来的工作呢?
当然啦,你可以尝试眩一点的做法,去删除列表中的元素而不新建一个列表。但是你以前吃过这样的亏。试图在循环中改变数据结构是很容易出问题的。Python 是一个这样工作的语言吗?用多长时间你才能搞清这一点?你能确定记得你第二次这样尝试的安全性?程序员在和这类纯技术课题较劲的过程中,花费了太多的时间,犯了太多的错误,却并没有什么意义。这样并不可能令你的程序有所进步,只不过是费力不讨好。
我在第一次学习 Python 时是抵触列表解析的,而且我抗拒 filter 和 map 的时间更长。我坚持着我更艰难的生活,固守着类似于 for 循环和 if 语句以及一步步地以代码为中心的编程方式。而且我的 Python 程序看起来很像是 Visual Basic 程序,细化每一个函数中的每一个操作步骤。它们却有着同样的小错误和隐蔽的 Bug。这一切其实都没有意义。
让这一切都远去吧。费力不讨好的编程不重要,数据重要。并且数据并不麻烦,它们不过就是数据。如果多了,就过滤。如果不是我们要的,就映射。聚焦在数据上,摒弃费力的劳作。
好了,大道理谈够了。让我们谈谈动态导入模块吧。
首先,让我们看一看正常的模块导入。import module 语法查看搜索路径,根据给定的名字寻找模块并导入它们。你甚至可以这样做:以逗号分割同时导入多个模块,本章代码前几行就是这样做的。
现在让我们用动态导入做同样的事。
>>> sys = __import__('sys') >>> os = __import__('os') >>> re = __import__('re') >>> unittest = __import__('unittest') >>> sys >>> <module 'sys' (built-in)> >>> os >>> <module 'os' from '/usr/local/lib/python2.2/os.pyc'>
因此 __import__ 导入一个模块,但是是通过一个字符串参数来做到的。依此处讲,你用以导入的仅仅是一个硬编码性的字符串,但它可以是一个变量,或者一个函数调用的结果。并且你指向模块的变量也不必与模块名匹配。你可以导入一系列模块并把它们指派给一个列表。
>>> moduleNames = ['sys', 'os', 're', 'unittest'] >>> moduleNames ['sys', 'os', 're', 'unittest'] >>> modules = map(__import__, moduleNames) >>> modules [<module 'sys' (built-in)>, <module 'os' from 'c:\Python22\lib\os.pyc'>, <module 're' from 'c:\Python22\lib\re.pyc'>, <module 'unittest' from 'c:\Python22\lib\unittest.pyc'>] >>> modules[0].version '2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]' >>> import sys >>> sys.version '2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]'
现在,你应该能够把这一切放在一起,并搞清楚本章大部分样例代码是做什么的。
你已经学习了足够的知识,现在来分析本章样例代码的前七行:读取一个目录并从中导入选定的模块。
def regressionTest(): path = os.path.abspath(os.path.dirname(sys.argv[0])) files = os.listdir(path) test = re.compile("test\.py$", re.IGNORECASE) files = filter(test.search, files) filenameToModuleName = lambda f: os.path.splitext(f)[0] moduleNames = map(filenameToModuleName, files) modules = map(__import__, moduleNames) load = unittest.defaultTestLoader.loadTestsFromModule return unittest.TestSuite(map(load, modules))
让我们一行行交互地看。假定当前目录是 c:\diveintopython\py,其中有包含本章脚本在内的本书众多样例。正如在 第 16.2 节 “找到路径” 中所见,脚本目录将存于 path 变量,因此让我们从这里开始以实打实的代码起步。
>>> import sys, os, re, unittest >>> path = r'c:\diveintopython\py' >>> files = os.listdir(path) >>> files ['BaseHTMLProcessor.py', 'LICENSE.txt', 'apihelper.py', 'apihelpertest.py', 'argecho.py', 'autosize.py', 'builddialectexamples.py', 'dialect.py', 'fileinfo.py', 'fullpath.py', 'kgptest.py', 'makerealworddoc.py', 'odbchelper.py', 'odbchelpertest.py', 'parsephone.py', 'piglatin.py', 'plural.py', 'pluraltest.py', 'pyfontify.py', 'regression.py', 'roman.py', 'romantest.py', 'uncurly.py', 'unicode2koi8r.py', 'urllister.py', 'kgp', 'plural', 'roman', 'colorize.py']
>>> test = re.compile("test\.py$", re.IGNORECASE) >>> files = filter(test.search, files) >>> files ['apihelpertest.py', 'kgptest.py', 'odbchelpertest.py', 'pluraltest.py', 'romantest.py']
>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] >>> filenameToModuleName('romantest.py') 'romantest' >>> filenameToModuleName('odchelpertest.py') 'odbchelpertest' >>> moduleNames = map(filenameToModuleName, files) >>> moduleNames ['apihelpertest', 'kgptest', 'odbchelpertest', 'pluraltest', 'romantest']
正如你在 第 4.7 节 “使用 lambda 函数” 中所见,lambda 快餐式地创建内联单行函数。这里应用你在 例 6.17 “分割路径名” 中已经见过的,标准库的 os.path.splitext 将一个带有扩展名的文件名返回成只包含文件名称的那部分。 | |
filenameToModuleName 是一个函数。lambda 函数并不比你以 def 语句定义的普通函数神奇。你可以如其他函数一样地调用 filenameToModuleName,它也将如你所愿:从参数中剔除扩展名。 | |
现在你可以通过 map 把这个函数应用于单元测试文件列表中的每一个文件。 | |
结果当然如你所愿:以指代模块的字符串构成的一个列表。 |
>>> modules = map(__import__, moduleNames) >>> modules [<module 'apihelpertest' from 'apihelpertest.py'>, <module 'kgptest' from 'kgptest.py'>, <module 'odbchelpertest' from 'odbchelpertest.py'>, <module 'pluraltest' from 'pluraltest.py'>, <module 'romantest' from 'romantest.py'>] >>> modules[-1] <module 'romantest' from 'romantest.py'>
正如你在 第 16.6 节 “动态导入模块” 中所见,你可以通过 map 和 __import__ 的协同工作,将模块名 (字符串) 映射到实际的模块 (像其他模块一样可以被调用和使用)。 | |
modules 现在是一个模块列表,其中的模块和其他模块一样。 | |
该列表的最后一个模块是 romantest 模块,和通过 import romantest 导入的模块完全等价。 |
>>> load = unittest.defaultTestLoader.loadTestsFromModule >>> map(load, modules) [<unittest.TestSuite tests=[ <unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>, <unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>, <unittest.TestSuite tests=[ <apihelpertest.ParamChecks testMethod=testCollapse>, <apihelpertest.ParamChecks testMethod=testSpacing>]>, ... ] ] >>> unittest.TestSuite(map(load, modules))
自省过程是 unittest 模块经常为我们做的一项工作。还记得我们的独立测试模块仅仅调用了看似神奇的 unittest.main() 函数就大刀阔斧地完成了全部工作吗?unittest.main() 实际上创建了一个 unittest.TestProgram 的实例,而这个实例实际上创建了一个 unittest.defaultTestLoader 的实例并以调用它的模块启动它。 (如果你不给出,如何知道调用它的模块是哪一个?通过使用同样神奇的 __import__('__main__') 命令,动态导入正在运行的模块。我可以就 unittest 模块中使用的所有技巧和技术写一本书,但那样我就没法写完这本了。)
regression.py 程序及其输出到现在应该很清楚了。
你现在应该能够很自如地做到如下事情:
我想谈谈名词复数。还有,返回其它函数的函数,高级的正则表达式和生成器 (Generator)。生成器是 Python 2.3 新引入的。但首先还是让我们先来谈谈如何生成名词复数。
如果你还没有看过 第 7 章 正则表达式,现在是个绝佳的机会。这章中假定你已理解了正则表达式的基础内容并迅速深入更高级的应用。
英语是一个吸收很多外来语而令人疯掉的语言,把单数名词变成复数的规则则是复杂而又多变的。有规则,有例外,更有例外的例外。
如果你在英语国家长大或是在正规学校学习了英语,你可能对下面的基本规则很熟悉:
(我知道有很多例外情况,比如:“Man” 变成 “men”,“woman” 变成 “women”,但是,“human” 却变成 “humans”。“Mouse” 变成 “mice”,“louse” 变成 “lice”,但是,“house” 却变成 “houses”。“Knife” 变成 “knives”,“wife” 变成 “wives”,但是 “lowlife” 却变成 “lowlifes”。更不要说那些复数根本就不需要变化的词了,比如 “sheep”, “deer” 和 “haiku”。)
其他的语言当然完全不同。
让我们来设计一个复数化名词的模块吧!从英语名词开始,仅考虑上面的四种规则,但是记得你将来需要不断添加规则,更可能最后添加进更多的语言。
你所针对的单词 (至少在英语中) 是字符串和字符。你还需要规则来找出不同的字符 (字母) 组合,并对它们进行不同的操作。这听起来像是正则表达式的工作。
import re def plural(noun): if re.search('[sxz]$', noun): return re.sub('$', 'es', noun) elif re.search('[^aeioudgkprt]h$', noun): return re.sub('$', 'es', noun) elif re.search('[^aeiou]y$', noun): return re.sub('y$', 'ies', noun) else: return noun + 's'
好啦,这是一个正则表达式,但是它使用了你在 第 7 章 正则表达式 中未曾见过的语法。方括号的意思是 “完全匹配这些字符中的一个”。也就是说,[sxz] 意味着 “s,或者 x,再或者 z”,但只是其中的一个。$ 应该不陌生,它意味着匹配字符串的结尾。也就是说,检查 noun 是否以 s,x,或者 z 结尾。 | |
re.sub 函数进行以正则表达式为基础的替换工作。让我们更具体地看看它。 |
>>> import re >>> re.search('[abc]', 'Mark') <_sre.SRE_Match object at 0x001C1FA8> >>> re.sub('[abc]', 'o', 'Mark') 'Mork' >>> re.sub('[abc]', 'o', 'rock') 'rook' >>> re.sub('[abc]', 'o', 'caps') 'oops'
import re def plural(noun): if re.search('[sxz]$', noun): return re.sub('$', 'es', noun) elif re.search('[^aeioudgkprt]h$', noun): return re.sub('$', 'es', noun) elif re.search('[^aeiou]y$', noun): return re.sub('y$', 'ies', noun) else: return noun + 's'
>>> import re >>> re.search('[^aeiou]y$', 'vacancy') <_sre.SRE_Match object at 0x001C1FA8> >>> re.search('[^aeiou]y$', 'boy') >>> >>> re.search('[^aeiou]y$', 'day') >>> >>> re.search('[^aeiou]y$', 'pita') >>>
>>> re.sub('y$', 'ies', 'vacancy') 'vacancies' >>> re.sub('y$', 'ies', 'agency') 'agencies' >>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') 'vacancies'
正则表达式把 vacancy 变为 vacancies,把 agency 变为 agencies,这正是你想要的。注意,将 boy 变成 boies 是可行的,但是永远不会发生,因为 re.search 首先确定是否应该应用 re.sub。 | |
顺便提一下,可以将两个正则表达式 (一个确定规则适用与否,一个应用规则) 合并在一起成为一个正则表达式。这便是合并后的样子。它的大部分已经很熟悉:你应用的是在 第 7.6 节 “个案研究:解析电话号码” 学过的记忆组 (remembered group) 记住 y 之前的字符。然后再替换字符串,你使用一个新的语法 \1,这意味着:“嘿!记得前面的第一个组吗?把它放这儿”。就此而言,记住了 y 之前的 c ,然后你做替换工作,你将 c 替换到 c 的位置,并将 ies 替换到 y 的位置。(如果你有不止一个组则可以使用 \2 或者 \3 等等。) |
正则表达式替换非常强大,并且 \1 语法使之更加强大。但是将整个操作放在一个正则表达式中仍然晦涩难懂,也不能与前面描述的复数规则直接呼应。你原来列出的规则,比如 “如果单词以 S,X 或者 Z 结尾,结尾追加 ES”。如果你在函数中看到两行代码描述 “如果单词以 S,X 或者 Z 结尾,结尾追加 ES”,更加直观些。
现在你将增加一个抽象过程。你从定义一个规则列表开始:如果这样,就做那个,否则判断下一规则。让我们暂时将程序一部分复杂化以便使另一部分简单化。
import re def match_sxz(noun): return re.search('[sxz]$', noun) def apply_sxz(noun): return re.sub('$', 'es', noun) def match_h(noun): return re.search('[^aeioudgkprt]h$', noun) def apply_h(noun): return re.sub('$', 'es', noun) def match_y(noun): return re.search('[^aeiou]y$', noun) def apply_y(noun): return re.sub('y$', 'ies', noun) def match_default(noun): return 1 def apply_default(noun): return noun + 's' rules = ((match_sxz, apply_sxz), (match_h, apply_h), (match_y, apply_y), (match_default, apply_default) ) def plural(noun): for matchesRule, applyRule in rules: if matchesRule(noun): return applyRule(noun)
这个版本看起来更加复杂 (至少是长了),但做的工作没有变化:试图顺序匹配四种不同规则,并在匹配时应用恰当的正则表达式。不同之处在于,每个独立的匹配和应用规则都在自己的函数中定义,并且这些函数列于 rules 变量这个元组的元组之中。 | |
使用一个 for 循环,你可以根据 rules 元组一次性进行匹配和应用规则两项工作 (一个匹配和一个应用)。for 循环第一轮中,matchesRule 将使用 match_sxz,applyRule 将使用 apply_sxz;在第二轮中 (假设真走到了这么远),matchesRule 将被赋予 match_h,applyRule 将被赋予 apply_h。 | |
记住 Python 中的一切都是对象,包括函数。rules 包含函数;不是指函数名,而是指函数本身。当 matchesRule 和 applyRule 在 for 循环中被赋值后,它们就成了你可以调用的真正函数。因此,在 for 循环第一轮中,这就相当于调用 matches_sxz(noun)。 | |
在 for 循环第一轮中,这就相当于调用 apply_sxz(noun),等等。 |
这个抽象过程有些令人迷惑,试着剖析函数看看实际的等价内容。这个 for 循环相当于:
def plural(noun): if match_sxz(noun): return apply_sxz(noun) if match_h(noun): return apply_h(noun) if match_y(noun): return apply_y(noun) if match_default(noun): return apply_default(noun)
这里的好处在于 plural 函数现在被简化了。它以普通的方法反复使用其它地方定义的规则。获得一个匹配规则,匹配吗?调用并应用规则。规则可以在任意地方以任意方法定义,plural 函数对此并不关心。
现在,添加这个抽象过程值得吗?嗯……还不值。让我们看看如何向函数添加一个新的规则。啊哈,在先前的范例中,需要向 plural 函数添加一个 if 语句;在这个例子中,需要增加两个函数:match_foo 和 apply_foo,然后更新 rules 列表指定在什么相对位置调用这个新匹配和新规则应用。
这其实不过是步入下一节的一个基石。让我们继续。
将每个匹配和规则应用分别制作成函数没有必要。你从来不会直接调用它们:你把它们定义于 rules 列表之中并从那里调用它们。让我们隐去它们的函数名而抓住规则定义的主线。
import re rules = \ ( ( lambda word: re.search('[sxz]$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeioudgkprt]h$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeiou]y$', word), lambda word: re.sub('y$', 'ies', word) ), ( lambda word: re.search('$', word), lambda word: re.sub('$', 's', word) ) ) def plural(noun): for matchesRule, applyRule in rules: if matchesRule(noun): return applyRule(noun)
这与第 2 阶段定义的规则是一样的。惟一的区别是不再定义 match_sxz 和 apply_sxz 之类的函数,而是以 lambda 函数 法将这些函数的内容直接 “嵌入” rules 列表本身。 | |
注意 plural 函数完全没有变化,还是反复于一系列的规则函数,检查第一个匹配规则,如果返回真则调用第二个应用规则并返回值。和前面一样,给定单词返回单词。唯一的区别是规则函数被内嵌定义,化名作 lambda 函数。但是 plural 函数并不在乎它们是如何定义的,只是拿到规则列表,闭着眼睛干活。 |
现在添加一条新的规则,所有你要做的就是直接在 rules 列表之中定义函数:一个匹配规则,一个应用规则。这样内嵌的规则函数定义方法使得没必要的重复很容易被发现。你有四对函数,它们采用相同的模式。匹配函数就是调用 re.search,应用函数就是调用 re.sub。让我们提炼出这些共同点。
让我们精炼出代码中的重复之处,以便更容易地定义新规则。
import re def buildMatchAndApplyFunctions((pattern, search, replace)): matchFunction = lambda word: re.search(pattern, word) applyFunction = lambda word: re.sub(search, replace, word) return (matchFunction, applyFunction)
如果这太费解 (它应该是这样,这是个怪异的东西),可能需要通过了解它的使用来搞明白。
patterns = \ ( ('[sxz]$', '$', 'es'), ('[^aeioudgkprt]h$', '$', 'es'), ('(qu|[^aeiou])y$', 'y$', 'ies'), ('$', '$', 's') ) rules = map(buildMatchAndApplyFunctions, patterns)
我发誓这不是我信口雌黄:rules 被转换成了前面范例中相同的内容。剖析 rules 的定义,你看到的是:
rules = \ ( ( lambda word: re.search('[sxz]$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeioudgkprt]h$', word), lambda word: re.sub('$', 'es', word) ), ( lambda word: re.search('[^aeiou]y$', word), lambda word: re.sub('y$', 'ies', word) ), ( lambda word: re.search('$', word), lambda word: re.sub('$', 's', word) ) )
def plural(noun): for matchesRule, applyRule in rules: if matchesRule(noun): return applyRule(noun)
还不够兴奋吧!我必须承认,在定义 buildMatchAndApplyFunctions 时我跳过了一个微妙之处。让我们回过头再看一下。
def buildMatchAndApplyFunctions((pattern, search, replace)):
>>> def foo((a, b, c)): ... print c ... print b ... print a >>> parameters = ('apple', 'bear', 'catnap') >>> foo(parameters) catnap bear apple
现在,让我们回过头看一看这个元组自动展开技巧的必要性。patterns 是一个元组列表,并且每个元组都有三个元素。调用 map(buildMatchAndApplyFunctions, patterns),这并不 意味着是以三个参数调用 buildMatchAndApplyFunctions。使用 map 映射一个列表到函数时,通常使用单参数:列表中的每个元素。就 patterns 而言,列表的每个元素都是一个元组,所以 buildMatchAndApplyFunctions 总是是以元组来调用,在 buildMatchAndApplyFunctions 中使用元组自动展开技巧将元素赋值给可以被使用的变量。
你已经精炼了所有重复代码,也尽可能地把复数规则提炼到定义一个字符串列表。接下来的步骤是把这些字符串提出来放在另外的文件中,从而可以和使用它们的代码分开来维护。
首先,让我们建立一个包含你需要的所有规则的文本文件。没有什么特别的结构,不过是以空格 (或者制表符) 把字符串列成三列。你把它命名为 rules.en,“en” 是英语的意思。这些是英语名词复数的规则,你以后可以为其它语言添加规则文件。
现在来看看如何使用规则文件。
import re import string def buildRule((pattern, search, replace)): return lambda word: re.search(pattern, word) and re.sub(search, replace, word) def plural(noun, language='en'): lines = file('rules.%s' % language).readlines() patterns = map(string.split, lines) rules = map(buildRule, patterns) for rule in rules: result = rule(noun) if result: return result
在这里你还将使用闭合技术 (动态构建函数时使用函数外部定义的变量),但是现在你把原来分开的匹配函数和规则应用函数合二为一 (你将在下一节中明了其原因)。你很快会看到,这与分别调用两个函数效果相同,只是调用的方法稍有不同。 | |
咱们的 plural 函数现在接受的第二个参数是默认值为 en 的可选参数 language。 | |
你使用 language 参数命名一个文件,打开这个文件并读取其中的内容到一个列表。如果 language 是 en,那么你将打开 rules.en 文件,读取全部内容,以其中的回车符作为分隔构建一个列表。文件的每一行将成为列表的一个元素。 | |
如你所见,文件的每一行都有三个值,但是它们是以空白字符 (制表符或者空格符,这没什么区别) 分割。用 string.split 函数映射列表来创建一个每个元素都是三元素元组的新列表。因此,像 [sxz]$ $ es 这样的一行将被打碎并放入 ('[sxz]$', '$', 'es') 这样的元组。这意味着 patterns 将最终变成元组列表的形式,就像第 4 阶段实打实编写的那样。 | |
如果 patterns 是一个元组列表,那么 rules 就可以通过一个个调用 buildRule 动态地生成函数列表。调用 buildRule(('[sxz]$', '$', 'es')) 返回一个接受单参数 word 的函数。当返回的函数被调用,则将执行 re.search('[sxz]$', word) and re.sub('$', 'es', word)。 | |
因为你现在构建的是一个匹配和规则应用合一的函数,你需要分别调用它们。仅仅是调用函数,如果返回了内容,那么返回的便是复数;如果没有返回 (也就是返回了None),那么该规则未能匹配,就应该尝试其他规则。 |
这里的进步是你把复数规则完全分离到另外的文件中。不但这个文件可以独立于代码单独维护,而且你建立了一个命名规划使 plural 函数可以根据 language 参数使用不同的规则文件。
这里的缺陷是每次调用 plural 函数都需要去读取一次文件。我想我可以在整本书中都不使用 “留给读者去练习”,但是这里:为特定的语言规则文件建立一个缓存机制,并在调用期间规则文件改变时自动刷新留给读者作为练习。祝你顺利。
现在你已准备好探讨生成器 (Generator) 了。
import re def rules(language): for line in file('rules.%s' % language): pattern, search, replace = line.split() yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) def plural(noun, language='en'): for applyRule in rules(language): result = applyRule(noun) if result: return result
这里使用了被称作生成器的技术,我不打算在你看过一个简单例子之前试图解释它。
>>> def make_counter(x): ... print 'entering make_counter' ... while 1: ... yield x ... print 'incrementing x' ... x = x + 1 ... >>> counter = make_counter(2) >>> counter <generator object at 0x001C9C10> >>> counter.next() entering make_counter 2 >>> counter.next() incrementing x 3 >>> counter.next() incrementing x 4
这样你就有了生成连续的 Fibonacci 数的函数了。当然你也可以通过递归做到,但是这里的方法更加易读。并且也与 for 工作得很好。
>>> for n in fibonacci(1000): ... print n, 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
好了,让我们回到 plural 函数看看如何可以把它用起来。
def rules(language): for line in file('rules.%s' % language): pattern, search, replace = line.split() yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) def plural(noun, language='en'): for applyRule in rules(language): result = applyRule(noun) if result: return result
你在第 5 阶段得到的是什么?第 5 阶段中,你读取整个规则文件并在使用第一条规则之前构建一个所有规则组成的列表。现在有了生成器,你可以更舒适地做到这一切:你打开并读取第一条规则,根据它创建函数并使用之,如果它适用则根本不去读取规则文件剩下的内容,也不去建立另外的函数。
这一章中我们探讨了几个不同的高级技术。它们并不都适用于任何情况。
你现在应该能自如应用如下技术:
抽象化,动态构建函数,构建闭合以及应用生成器能够使你的代码更加简单化、可读化、灵活化。你需要在简洁和功能实现两方面进行平衡。
性能优化 (Performance tuning) 是一件多姿多彩的事情。Python 是一种解释性语言并不表示你不应该担心代码优化。但也不必太 担心。
由于代码优化过程中存在太多的不明确因素,以至于你很难清楚该从何入手。
让我们从这里开始:你真的确信你要这样做吗? 你的代码真的那么差吗?值得花时间去优化它吗?在你的应用程序的生命周期中,与花费在等待一个远程数据库服务器,或是等待用户输入相比,运行这段代码将花费多少时间?
第二,你确信已经完成代码编写了吗? 过早的优化就像是在一块半生不熟的蛋糕上撒糖霜。你花费了几小时、几天 (或更长) 时间来优化你的代码以提高性能,却发现它不能完成你希望它做的工作。那是浪费时间。
这并不是说代码优化毫无用处,但是你需要检查一下整个系统,并且确定把时间花在这上面是值得的。在优化代码上每花费一分钟,就意味着你少了增加新功能、编写文档或者陪你的孩子玩或者编写单元测试的一分钟。
哦,是的,单元测试。不必我说,在开始性能优化之前你需要一个完全的单元测试集。你最不需要的就是在乱动你的算法时引入新的问题。
谨记着这些忠告,让我们来看一些优化 Python 代码的技术。我们要研究的代码是 Soundex 算法的实现。Soundex 是一种 20 世纪在美国人口普查中归档姓氏的方法。它把听起来相似的姓氏归在一起,使得在即便错误拼写的情况下调查者仍能查找到。Soundex 今天仍然因差不多的原因被应用着,当然现在用计算机数据库服务器了。大部分的数据库服务器都有 Soundex 函数。
Soundex 算法有几个差别不大的变化版本。这是本章使用的:
比如,我的名字 Pilgrim 被转换为 P942695。没有连续重复,所以这一步不需要做。然后是去除 9,剩下 P4265。太长了,所以你把超出的字符丢弃,剩下 P426。
另一个例子:Woo 被转换为 W99,变成 W9,变成 W,然后以补零成为 W000。
这是 Soundex 函数的第一次尝试:
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
import string, re charToSoundex = {"A": "9", "B": "1", "C": "2", "D": "3", "E": "9", "F": "1", "G": "2", "H": "9", "I": "9", "J": "2", "K": "2", "L": "4", "M": "5", "N": "5", "O": "9", "P": "1", "Q": "2", "R": "6", "S": "2", "T": "3", "U": "9", "V": "1", "W": "9", "X": "2", "Y": "9", "Z": "2"} def soundex(source): "convert string to Soundex equivalent" # Soundex requirements: # source string must be at least 1 character # and must consist entirely of letters allChars = string.uppercase + string.lowercase if not re.search('^[%s]+$' % allChars, source): return "0000" # Soundex algorithm: # 1. make first character uppercase source = source[0].upper() + source[1:] # 2. translate all other characters to Soundex digits digits = source[0] for s in source[1:]: s = s.upper() digits += charToSoundex[s] # 3. remove consecutive duplicates digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d # 4. remove all "9"s digits3 = re.sub('9', '', digits2) # 5. pad end with "0"s to 4 characters while len(digits3) < 4: digits3 += "0" # 6. return first 4 characters return digits3[:4] if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
关于 Python 代码优化你需要知道的最重要问题是,决不要自己编写计时函数。
为一个很短的代码计时都很复杂。处理器有多少时间用于运行这个代码?有什么在后台运行吗?每个现代计算机都在后台运行持续或者间歇的程序。小小的疏忽可能破坏你的百年大计,后台服务偶尔被 “唤醒” 在最后千分之一秒做一些像查收信件,连接计时通信服务器,检查应用程序更新,扫描病毒,查看是否有磁盘被插入光驱之类很有意义的事。在开始计时测试之前,把一切都关掉,断开网络的连接。再次确定一切都关上后关掉那些不断查看网络是否恢复的服务等等。
接下来是计时框架本身引入的变化因素。Python 解释器是否缓存了方法名的查找?是否缓存代码块的编译结果?正则表达式呢? 你的代码重复运行时有副作用吗?不要忘记,你的工作结果将以比秒更小的单位呈现,你的计时框架中的小错误将会带来不可挽回的结果扭曲。
Python 社区有句俗语:“Python 自己带着电池。” 别自己写计时框架。Python 2.3 具备一个叫做 timeit 的完美计时工具。
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
>>> import timeit >>> t = timeit.Timer("soundex.soundex('Pilgrim')", ... "import soundex") >>> t.timeit() 8.21683733547 >>> t.repeat(3, 2000000) [16.48319309109, 16.46128984923, 16.44203948912]
你可以在命令行使用 timeit 模块来测试一个已存在的 Python 程序,而不需要修改代码。在 http://docs.python.org/lib/node396.html 查看文档中关于命令行选项的内容。 |
注意 repeat() 返回一个时间列表。由于 Python 计时器使用的处理器时间的微小变化 (或者那些你没办法根除的可恶的后台进程),这些时间中几乎不可能出现重复。你的第一想法也许是说:“让我们求平均值获得真实的数据。”
事实上,那几乎是确定错误的。你的代码或者 Python 解释器的变化可能缩短耗时,那些没办法去除的可恶后台进程或者其他 Python 解释器以外的因素也许令耗时延长。如果计时结果之间的差异超过百分之几,太多的可变因素使你没法相信结果,如果不是这样则可以取最小值而丢弃其他结果。
Python 有一个方便的 min 函数返回输入列表中的最小值:
>>> min(t.repeat(3, 1000000)) 8.22203948912
timeit 模块只有在你知道哪段代码需要优化时使用。如果你有一个很大的 Python 程序并且不知道你的性能问题所在,查看 hotshot 模块。 |
Soundex 函数的第一件事是检查输入是否是一个空字符串。怎样做是最好的方法?
如果你回答 “正则表达式”,坐在角落里反省你糟糕的直觉。正则表达式几乎永远不是最好的答案,而且应该被尽可能避开。这不仅仅是基于性能考虑,而是因为调试和维护都很困难,当然性能也是个原因。
这是 soundex/stage1/soundex1a.py 检查 source 是否全部由字母构成的一段代码,至少是一个字母 (而不是空字符串):
allChars = string.uppercase + string.lowercase if not re.search('^[%s]+$' % allChars, source): return "0000"
soundex1a.py 表现如何?为了方便,__main__ 部分包含了一段代码:调用 timeit 模块,为三个不同名字分别建立测试,依次测试,并显示每个测试的最短耗时:
if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
那么,应用正则表达式的 soundex1a.py 表现如何呢?
C:\samples\soundex\stage1>python soundex1a.py Woo W000 19.3356647283 Pilgrim P426 24.0772053431 Flingjingwaller F452 35.0463220884
正如你预料,名字越长,算法耗时就越长。有几个工作可以令我们减小这个差距 (使函数对于长输入花费较短的相对时间) 但是算法的本质决定它不可能每次运行时间都相同。
另一点应铭记于心的是,我们测试的是有代表性的名字样本。Woo 是个被缩短到单字符并补零的小样本;Pilgrim 是个夹带着特别字符和忽略字符的平均长度的正常样本;Flingjingwaller 是一个包含连续重复字符并且特别长的样本。其它的测试可能同样有帮助,但它们已经很好地代表了不同的样本范围。
那么那个正则表达式如何呢?嗯,缺乏效率。因为这个表达式测试不止一个范围的字符 (A-Z 的大写范围和 a-z 的小写字母范围),我们可以使用一个正则表达式的缩写语法。这便是 soundex/stage1/soundex1b.py:
if not re.search('^[A-Za-z]+$', source): return "0000"
timeit 显示 soundex1b.py 比 soundex1a.py 稍微快一些,但是没什么令人激动的变化:
C:\samples\soundex\stage1>python soundex1b.py Woo W000 17.1361133887 Pilgrim P426 21.8201693232 Flingjingwaller F452 32.7262294509
在 第 15.3 节 “重构” 中我们看到正则表达式可以被编译并在重用时以更快速度获得结果。因为这个正则表达式在函数中每次被调用时都不变化,我们可以编译它一次并使用被编译的版本。这便是 soundex/stage1/soundex1c.py:
isOnlyChars = re.compile('^[A-Za-z]+$').search def soundex(source): if not isOnlyChars(source): return "0000"
soundex1c.py 中使用被编译的正则表达式产生了显著的提速:
C:\samples\soundex\stage1>python soundex1c.py Woo W000 14.5348347346 Pilgrim P426 19.2784703084 Flingjingwaller F452 30.0893873383
但是这样的优化是正路吗?这里的逻辑很简单:输入 source 应该是非空,并且需要完全由字母构成。如果编写一个循环查看每个字符并且抛弃正则表达式,是否会更快些?
这便是 soundex/stage1/soundex1d.py:
if not source: return "0000" for c in source: if not ('A' <= c <= 'Z') and not ('a' <= c <= 'z'): return "0000"
这个技术在 soundex1d.py 中恰好不及 编译后的正则表达式快 (尽管比使用未编译的正则表达式快[14]):
C:\samples\soundex\stage1>python soundex1d.py Woo W000 15.4065058548 Pilgrim P426 22.2753567842 Flingjingwaller F452 37.5845122774
为什么 soundex1d.py 没能更快?答案来自 Python 的编译本质。正则表达式引擎以 C 语言编写,被编译后则能本能地在你的计算机上运行。另一方面,循环是以 Python 编写,要通过 Python 解释器。尽管循环相对简单,但没能简单到补偿花在代码解释上的时间。正则表达式永远不是正确答案……但例外还是存在的。
恰巧 Python 提供了一个晦涩的字符串方法。你有理由不了解它,因为本书未曾提到它。这个方法便是 isalpha(),它检查一个字符串是否只包含字母。
这便是 soundex/stage1/soundex1e.py:
if (not source) and (not source.isalpha()): return "0000"
在 soundex1e.py 中应用这个特殊方法我们能得到多少好处? 很多。
C:\samples\soundex\stage1>python soundex1e.py Woo W000 13.5069504644 Pilgrim P426 18.2199394057 Flingjingwaller F452 28.9975225902
import string, re charToSoundex = {"A": "9", "B": "1", "C": "2", "D": "3", "E": "9", "F": "1", "G": "2", "H": "9", "I": "9", "J": "2", "K": "2", "L": "4", "M": "5", "N": "5", "O": "9", "P": "1", "Q": "2", "R": "6", "S": "2", "T": "3", "U": "9", "V": "1", "W": "9", "X": "2", "Y": "9", "Z": "2"} def soundex(source): if (not source) and (not source.isalpha()): return "0000" source = source[0].upper() + source[1:] digits = source[0] for s in source[1:]: s = s.upper() digits += charToSoundex[s] digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d digits3 = re.sub('9', '', digits2) while len(digits3) < 4: digits3 += "0" return digits3[:4] if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
Soundex 算法的第二步是依照特定规则将字符转换为数字。做到这点最好的方法是什么?
最明显的解决方案是定义一个以单字符为键并以所对应数字为值的字典,以字典查找每个字符。这便是 soundex/stage1/soundex1e.py 中使用的方法 (目前最好的结果):
charToSoundex = {"A": "9", "B": "1", "C": "2", "D": "3", "E": "9", "F": "1", "G": "2", "H": "9", "I": "9", "J": "2", "K": "2", "L": "4", "M": "5", "N": "5", "O": "9", "P": "1", "Q": "2", "R": "6", "S": "2", "T": "3", "U": "9", "V": "1", "W": "9", "X": "2", "Y": "9", "Z": "2"} def soundex(source): # ... input check omitted for brevity ... source = source[0].upper() + source[1:] digits = source[0] for s in source[1:]: s = s.upper() digits += charToSoundex[s]
你已经为 soundex1e.py 计时,这便是其表现:
C:\samples\soundex\stage1>python soundex1c.py Woo W000 13.5069504644 Pilgrim P426 18.2199394057 Flingjingwaller F452 28.9975225902
这段代码很直接,但它是最佳解决方案吗?为每个字符分别调用 upper() 看起来不是很有效率,为整个字符串调用 upper() 一次可能会好些。
然后是一砖一瓦地建立 digits 字符串。一砖一瓦的建造好像非常欠缺效率。在 Python 内部,解释器需要在循环的每一轮创建一个新的字符串,然后丢弃旧的。
但是,Python 擅长于列表。可以自动地将字符串作为列表来对待。而且使用 join() 方法可以很容易地将列表合并成字符串。
这便是 soundex/stage2/soundex2a.py,通过 map 和 lambda 把所有字母转换为数字:
def soundex(source): # ... source = source.upper() digits = source[0] + "".join(map(lambda c: charToSoundex[c], source[1:]))
太震惊了,soundex2a.py 并不快:
C:\samples\soundex\stage2>python soundex2a.py Woo W000 15.0097526362 Pilgrim P426 19.254806407 Flingjingwaller F452 29.3790847719
匿名 lambda 函数的使用耗费掉了从以字符列表替代字符串争取来的时间。
soundex/stage2/soundex2b.py 使用了一个列表遍历来替代 map 和 lambda:
source = source.upper() digits = source[0] + "".join([charToSoundex[c] for c in source[1:]])
在 soundex2b.py 中使用列表遍历比 soundex2a.py 中使用 map 和 lambda 快,但还没有最初的代码快 (soundex1e.py 中一砖一瓦的构建字符串[15]):
C:\samples\soundex\stage2>python soundex2b.py Woo W000 13.4221324219 Pilgrim P426 16.4901234654 Flingjingwaller F452 25.8186157738
是时候从本质不同的方法来思考了。字典查找是一个普通目的实现工具。字典的键可以是任意长度的字符串 (或者很多其他数据类型) 但这里我们只和单字符键和 单字符值打交道。恰巧 Python 有处理这种情况的特别函数:string.maketrans 函数。
这便是 soundex/stage2/soundex2c.py:
allChar = string.uppercase + string.lowercase charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2) def soundex(source): # ... digits = source[0].upper() + source[1:].translate(charToSoundex)
这儿在干什么?string.maketrans 创建一个两个字符串间的翻译矩阵:第一参数和第二参数。就此而言,第一个参数是字符串 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz,第二个参数是字符串 9123912992245591262391929291239129922455912623919292。看到其模式了?恰好与我们用冗长的字典构建的模式相同。A 映射到 9,B 映射到 1,C 映射到 2 等等。但它不是一个字典。而是一个你可以通过字符串方法 translate 使用的特别数据结构。它根据 string.maketrans 定义的矩阵将每个字符翻译为对应的数字。
timeit 显示 soundex2c.py 比定义字典并对输入进行循环一砖一瓦地构建输出快很多:
C:\samples\soundex\stage2>python soundex2c.py Woo W000 11.437645008 Pilgrim P426 13.2825062962 Flingjingwaller F452 18.5570110168
你不可能做得更多了。Python 有一个特殊函数,通过使用它做到了一个和你的工作差不多的事情。就用它并继续吧!
import string, re allChar = string.uppercase + string.lowercase charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2) def soundex(source): if (not source) or (not source.isalpha()): return "0000" digits = source[0].upper() + source[1:].translate(charToSoundex) digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d digits3 = re.sub('9', '', digits2) while len(digits3) < 4: digits3 += "0" return digits3[:4] if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
Soundex 算法的第三步是去除连续重复字符。怎样做是最佳方法?
这里是我们目前在 soundex/stage2/soundex2c.py 中的代码:
digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d
这里是 soundex2c.py 的性能表现:
C:\samples\soundex\stage2>python soundex2c.py Woo W000 11.437645008 Pilgrim P426 13.2825062962 Flingjingwaller F452 18.5570110168
第一件事是考虑,考察在循环的每一轮都检查 digits[-1] 是否有效率。列表索引代价大吗?如果把上一个数字存在另外的变量中以便检查是否会获益?
这里的 soundex/stage3/soundex3a.py 将回答这个问题:
digits2 = '' last_digit = '' for d in digits: if d != last_digit: digits2 += d last_digit = d
soundex3a.py 并不比 soundex2c.py 运行得快多少,而且甚至可能更会慢些 (差异还没有大到可以确信这一点):
C:\samples\soundex\stage3>python soundex3a.py Woo W000 11.5346048171 Pilgrim P426 13.3950636184 Flingjingwaller F452 18.6108927252
为什么 soundex3a.py 不更快呢?其实 Python 的索引功能恰恰很有效。重复使用 digits2[-1] 根本没什么问题。另一方面,手工保留上一个数字意味着我们每存储一个数字都要为两个 变量赋值,这便抹杀了我们避开索引查找所带来的微小好处。
让我们从本质上不同的方法来思考。如果可以把字符串当作字符列表来对待,那么使用列表遍历遍寻列表便成为可能。问题是代码需要使用列表中的上一个字符,而且使用列表遍历做到这一点并不容易。
但是,使用内建的 range() 函数创建一个索引数字构成的列表是可以的。使用这些索引数字一步步搜索列表并拿出与前面不同的字符。这样将使你得到一个字符串列表,使用字符串方法 join() 便可重建字符串。
这便是 soundex/stage3/soundex3b.py:
digits2 = "".join([digits[i] for i in range(len(digits)) if i == 0 or digits[i-1] != digits[i]])
这样快了吗?一个字,否。
C:\samples\soundex\stage3>python soundex3b.py Woo W000 14.2245271396 Pilgrim P426 17.8337165757 Flingjingwaller F452 25.9954005327
有可能因为目前的这些方法都是 “字符串中心化” 的。Python 可以通过一个命令把一个字符串转化为一个字符列表:list('abc') 返回 ['a', 'b', 'c']。更进一步,列表可以被很快地就地 改变。与其一砖一瓦地建造一个新的列表 (或者字符串),为什么不选择操作列表的元素呢?
这便是 soundex/stage3/soundex3c.py,就地修改列表去除连续重复元素:
digits = list(source[0].upper() + source[1:].translate(charToSoundex)) i=0 for item in digits: if item==digits[i]: continue i+=1 digits[i]=item del digits[i+1:] digits2 = "".join(digits)
这比 soundex3a.py 或 soundex3b.py 快吗?不,实际上这是目前最慢的一种方法[16]:
C:\samples\soundex\stage3>python soundex3c.py Woo W000 14.1662554878 Pilgrim P426 16.0397885765 Flingjingwaller F452 22.1789341942
我们在这儿除了试用了几种 “聪明” 的技术,根本没有什么进步。到目前为止最快的方法就是最直接的原始方法 (soundex2c.py)。有时候聪明未必有回报。
import string, re allChar = string.uppercase + string.lowercase charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2) def soundex(source): if (not source) or (not source.isalpha()): return "0000" digits = source[0].upper() + source[1:].translate(charToSoundex) digits2 = digits[0] for d in digits[1:]: if digits2[-1] != d: digits2 += d digits3 = re.sub('9', '', digits2) while len(digits3) < 4: digits3 += "0" return digits3[:4] if __name__ == '__main__': from timeit import Timer names = ('Woo', 'Pilgrim', 'Flingjingwaller') for name in names: statement = "soundex('%s')" % name t = Timer(statement, "from __main__ import soundex") print name.ljust(15), soundex(name), min(t.repeat())
Soundex 算法的最后一步是对短结果补零和截短长结果。最佳的做法是什么?
这是目前在 soundex/stage2/soundex2c.py 中的做法:
digits3 = re.sub('9', '', digits2) while len(digits3) < 4: digits3 += "0" return digits3[:4]
这里是 soundex2c.py 的表现:
C:\samples\soundex\stage2>python soundex2c.py Woo W000 12.6070768771 Pilgrim P426 14.4033353401 Flingjingwaller F452 19.7774882003
思考的第一件事是以循环取代正则表达式。这里的代码来自 soundex/stage4/soundex4a.py:
digits3 = '' for d in digits2: if d != '9': digits3 += d
soundex4a.py 快了吗?是的:
C:\samples\soundex\stage4>python soundex4a.py Woo W000 6.62865531792 Pilgrim P426 9.02247576158 Flingjingwaller F452 13.6328416042
但是,等一下。一个从字符串去除字符的循环?我们可以用一个简单的字符串方法做到。这便是 soundex/stage4/soundex4b.py:
digits3 = digits2.replace('9', '')
soundex4b.py 快了吗?这是个有趣的问题,它取决输入值:
C:\samples\soundex\stage4>python soundex4b.py Woo W000 6.75477414029 Pilgrim P426 7.56652144337 Flingjingwaller F452 10.8727729362
soundex4b.py 中的字符串方法对于大多数名字比循环快,但是对于短小的情况 (很短的名字) 却比 soundex4a.py 略微慢些。性能优化并不总是一致的,对于一个情况快些,却可能对另外一些情况慢些。就此而言,大多数情况将会从改变中获益,所以就改吧,但是别忘了原则。
最后仍很重要的是,让我们检测算法的最后两步:以零补齐短结果和截短超过四字符的长结果。你在 soundex4b.py 中看到的代码就是做这个工作的,但是太没效率了。看一下 soundex/stage4/soundex4c.py 找出原因:
digits3 += '000' return digits3[:4]
我们为什么需要一个 while 循环来补齐结果?我们早就知道我们需要把结果截成四字符,并且我们知道我们已经有了至少一个字符 (直接从 source 中拿过来的起始字符)。这意味着我们可以仅仅在输出的结尾添加三个零,然后截断它。不要害怕重新理解问题,从不太一样的角度看问题可以获得简单的解决方案。
我们丢弃 while 循环后从 soundex4c.py 中获得怎样的速度?太明显了:
C:\samples\soundex\stage4>python soundex4c.py Woo W000 4.89129791636 Pilgrim P426 7.30642134685 Flingjingwaller F452 10.689832367
最后,还有一件事可以令这三行运行得更快:你可以把它们合并为一行。看一眼 soundex/stage4/soundex4d.py:
return (digits2.replace('9', '') + '000')[:4]
在 soundex4d.py 中把所有代码放在一行可以比 soundex4c.py 稍微快那么一点:
C:\samples\soundex\stage4>python soundex4d.py Woo W000 4.93624105857 Pilgrim P426 7.19747593619 Flingjingwaller F452 10.5490700634
它非常难懂,而且优化也不明显。这值得吗?我希望你有很好的见解。性能并不是一切。你在优化方面的努力应该与程序的可读性和可维护性相平衡。
这一章展示了性能优化的几个重要方面,这里是就 Python 而言,但它们却普遍适用。
最后一点太重要了,这章中你令这个程序提速三倍并且令百万次的调用节省 20 秒。太棒了!现在思考一下:在那百万次的函数调用中,有多少秒花在周边应用程序等待数据库连接?花在磁盘输入/输出上?花在等待用户输入上?不要在过度优化算法上花时间,从而忽略了其它地方可以做的明显改进。开发你编写运行良好的 Python 代码的直觉,如果发现明显的失误则修正它们,并不对其它部分过分操作。
学习 Python 的第一件事就是安装,不是吗?
在 Windows 上,安装 Python 有两种选择。
在 Mac OS X 上,对于安装 Python 有两种选择:安装或不安装。您可能想要安装它。
Mac OS 9 上没有预装任何版本的 Python,安装相对简单,只有一种选择。
在 http://www.python.org/ftp/python/ 选择列出的最新的版本号, 然后选择 其中的rpms/ 目录下载最新的 Python RPM 包。 使用 rpm 命令进行安装,操作如下所示:
如果您运行在 Debian GNU/Linux 上,安装 Python 需要使用 apt 命令。
如果您宁愿从源码创建,可以从 http://www.python.org/ftp/python/下载 Python 的源代码。选择最新的版本,下载.tgz 文件,执行通常的 configure, make, make install 步骤。
既然我们已经安装了 Python,那么我们运行的这个交互 shell 是什么东西呢?
您现在应该已经安装了一个可以工作的 Python 版本了。
这是一个完整的、可执行的 Python 程序。
与其它大多数语言一样 Python 有函数,但是它没有像 C++ 一样的独立的头文件;或者像 Pascal 一样的分离的 interface/implementation 段。在需要函数时,像下面这样声明即可:
可以通过给出一个 doc string (文档字符串) 来文档化一个 Python 函数。
在 Python 中,函数同其它东西一样也是对象。
Python 函数没有明显的 begin 和 end,没有标明函数的开始和结束的花括号。唯一的分隔符是一个冒号 (:),接着代码本身是缩进的。
所有的 Python 模块都是对象,并且有几个有用的属性。您可以使用这些属性方便地测试您所编写的模块。下面是一个使用 if __name__ 的技巧。
Dictionary 是 Python 的内置数据类型之一,它定义了键和值之间一对一的关系。
List 是 Python 中使用最频繁的数据类型。如果您对 list 仅有的经验就是在 Visual Basic 中的数组或 Powerbuilder 中的数据存储,那么就打起精神学习 Python 的 list 吧。
Tuple 是不可变的 list。一旦创建了一个 tuple,就不能以任何方式改变它。
Python 与大多数其它语言一样有局部变量和全局变量之分,但是它没有明显的变量声明。变量通过首次赋值产生,当超出作用范围时自动消亡。
Python 支持格式化字符串的输出 。尽管这样可能会用到非常复杂的表达式,但最基本的用法是将一个值插入到一个有字符串格式符 %s 的字符串中。
Python 的强大特性之一是其对 list 的解析,它提供一种紧凑的方法,可以通过对 list 中的每个元素应用一个函数,从而将一个 list 映射为另一个 list。
您有了一个形如 key=value 的 key-value 对 list,并且想将它们合成为单个字符串。为了将任意包含字符串的 list 连接成单个字符串,可以使用字符串对象的 join 方法。
现在 odbchelper.py 程序和它的输出结果都应该非常清楚了。
下面是一个完整可运行的 Python 程序。大概看一下这段程序,你应该可以理解不少了。用数字标出的行阐述了 第 2 章 第一个 Python 程序 中涉及的一些概念。如果剩下来的代码看起来有点奇怪,不用担心,通过阅读本章你将会理解所有这些。
Python 允许函数参数有缺省值;如果调用函数时不使用参数,参数将获得它的缺省值。此外,通过使用命名参数还可以以任意顺序指定参数。SQL Server Transact/SQL 中的存储过程也可以做到这些;如果你是脚本高手,你可以略过这部分。
Python 有小部分相当有用的内置函数。除这些函数之外,其它所有的函数都被分到了各个模块中。其实这是一个非常明智的设计策略,避免了核心语言变得像其它脚本语言一样臃肿 (咳 咳,Visual Basic)。
你已经知道 Python 函数是对象。你不知道的是,使用 getattr 函数,可以得到一个直到运行时才知道名称的函数的引用。
如你所知,Python 具有通过列表解析 (第 3.6 节 “映射 list”) 将列表映射到其它列表的强大能力。这种能力同过滤机制结合使用,使列表中的有些元素被映射的同时跳过另外一些元素。
在Python 中,and 和 or 执行布尔逻辑演算,如你所期待的一样。但是它们并不返回布尔值,而是返回它们实际进行比较的值之一。
Python 支持一种有趣的语法,它允许你快速定义单行的最小函数。这些叫做 lambda 的函数,是从 Lisp 借用来的,可以用在任何需要函数的地方。
最后一行代码是唯一还没有解释过的,它完成全部的工作。但是现在工作已经简单了,因为所需要的每件事都已经按照需求建立好了。所有的多米诺骨牌已经就位,到了将它们推倒的时候了。
apihelper.py 程序和它的输出现在应该非常清晰了。
下面是一个完整的,可运行的 Python 程序。请阅读模块、类和函数的 doc strings,可以大概了解这个程序所做的事情和工作情况。像平时一样,不用担心你不理解的东西,这就是本章其它部分将告诉你的内容。
Python 有两种导入模块的方法。两种都有用,你应该知道什么时候使用哪一种方法。一种方法,import module,你已经在第 2.4 节 “万物皆对象”看过了。另一种方法完成同样的事情,但是它与第一种有着细微但重要的区别。
Python 是完全面向对象的:你可以定义自已的类,从自已的或内置的类继承,然后从你定义的类创建实例。
在 Python 中对类进行实例化很直接。要对类进行实例化,只要调用类 (就好像它是一个函数),传入定义在 __init__ 方法中的参数。返回值将是新创建的对象。
如你所见,FileInfo 是一个有着像字典一样的行为方式的类。为了进一步揭示这一点,让我们看一看在 UserDict 模块中的 UserDict 类,它是我们的 FileInfo 类的父类。它没有什么特别的,也是用 Python 写的,并且保存在一个 .py 文件里,就像我们其他的代码。特别之处在于,它保存在你的 Python 安装目录的 lib 目录下。
除了普通的类方法,Python 类还可以定义专用方法。专用方法是在特殊情况下或当使用特别语法时由 Python 替你调用的,而不是在代码中直接调用 (像普通的方法那样)。
除了 __getitem__ 和 __setitem__ 之外 Python 还有更多的专用函数。某些可以让你模拟出你甚至可能不知道的功能。
你已经知道了数据属性,它们是被一个特定的类实例所拥有的变量。Python 也支持类属性,它们是由类本身所拥有的。
与大多数的语言不同,一个 Python 函数,方法,或属性是私有还是公有,完全取决于它的名字。
实打实的对象把戏到此为止。你将在 第 12 章 中看到一个真实世界应用程序的专有类方法,它使用 getattr 创建一个到远程 Web 服务的代理。
与许多面向对象语言一样,Python 具有异常处理,通过使用 try...except 块来实现。
Python 有一个内置函数,open,用来打开在磁盘上的文件。open 返回一个文件对象,它拥有一些方法和属性,可以得到被打开文件的信息,以及对被打开文件进行操作。
与其它大多数语言一样,Python 也拥有 for 循环。你到现在还未曾看到它们的唯一原因就是,Python 在其它太多的方面表现出色,通常你不需要它们。
与其它任何 Python 的东西一样,模块也是对象。只要导入了,总可以用全局 dictionary sys.modules 来得到一个模块的引用。
os.path 模块有几个操作文件和目录的函数。这里,我们看看如何操作路径名和列出一个目录的内容。
再一次,所有的多米诺骨牌都放好了。我们已经看过每行代码是如何工作的了。现在往回走一步,看一下放在一起是怎么样的。
在 第 5 章 介绍的 fileinfo.py 程序现在应该完全理解了。
如果你要解决的问题利用字符串函数能够完成,你应该使用它们。它们快速、简单且容易阅读,而快速、简单、可读性强的代码可以说出很多好处。但是,如果你发现你使用了许多不同的字符串函数和 if 语句来处理一个特殊情况,或者你组合使用了 split、join 等函数而导致用一种奇怪的甚至读不下去的方式理解列表,此时,你也许需要转到正则表达式了。
这一系列的例子是由我几年前日常工作中的现实问题启发而来的,当时我需要从一个老化系统中导出街道地址,在将它们导入新的系统之前,进行清理和标准化。(看,我不是只将这些东西堆到一起,它有实际的用处。)这个例子展示我如何处理这个问题。
你可能经常看到罗马数字,即使你没有意识到它们。你可能曾经在老电影或者电视中看到它们 (“版权所有 MCMXLVI” 而不是 “版权所有1946”),或者在某图书馆或某大学的贡献墙上看到它们 (“成立于 MDCCCLXXXVIII”而不是“成立于1888”)。你也可能在某些文献的大纲或者目录上看到它们。这是一个表示数字的系统,它实际上能够追溯到远古的罗马帝国 (因此而得名)。
在前面的章节,你处理了相同字符可以重复三次的情况。在正则表达式中,有另外一个方式来表达这种情况,并且能提高代码的可读性。首先看看我们在前面的例子中使用的方法。
迄今为止,你只是处理过被我称之为“紧凑”类型的正则表达式。正如你曾看到的,它们难以阅读,即使你清楚正则表达式的含义,你也不能保证六个月以后你还能理解它。你真正所需的就是利用内联文档 (inline documentation)。
迄今为止,你主要是匹配整个模式,不论是匹配上,还是没有匹配上。但是正则表达式还有比这更为强大的功能。当一个模式确实 匹配上时,你可以获取模式中特定的片断,你可以发现具体匹配的位置。
这只是正则表达式能够完成工作的很少一部分。换句话说,即使你现在备受打击,相信我,你也不是什么也没见过了。
我经常在 comp.lang.python 上看到关于如下的问题: “ 怎么才能从我的 HTML 文档中列出所有的 [头|图像|链接] 呢?” “怎么才能 [分析|解释|munge] 我的 HTML 文档的文本,但是又要保留标记呢?” “怎么才能一次给我所有的 HTML 标记 [增加|删除|加引号] 属性呢?” 本章将回答所有这些问题。
HTML 处理分成三步:将 HTML 分解成它的组成片段,对片段进行加工,接着将片段再重新合成 HTML。第一步是通过 sgmllib.py 来完成的,它是标准 Python 库的一部分。
为了从 HTML 文档中提取数据,将 SGMLParser 类进行子类化,然后对想要捕捉的标记或实体定义方法。
SGMLParser 自身不会产生任何结果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用相应的一个方法,但是这些方法什么都不做。SGMLParser 是一个 HTML 消费者 (consumer):它接收 HTML,将其分解成小的、结构化的小块。正如您所看到的,在前一节中,您可以定义 SGMLParser 的子类,它可以捕捉特别标记和生成有用的东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser 所丢出来的所有东西的一个类,接着重建整个 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者 (producer)。
我们先偏离一下 HTML 处理的主题,讨论一下 Python 如何处理变量。Python 有两个内置的函数,locals 和 globals,它们提供了基于 dictionary 的访问局部和全局变量的方式。
有另外一种字符串格式化的形式,它使用 dictionary 而不是值的 tuple。
在 comp.lang.python 上的一个常见问题是 “我有一些 HTML 文档,属性值没有用引号括起来,并且我想将它们全部括起来,我怎么才能实现它呢?” [7] (一般这种事情的出现是由于一个项目经理加入到一个大的项目中来,而他又抱着 HTML 是一种标记语言的教条,要求所有的页面必须能够通过 HTML 校验器的验证。而属性值没有被引号括起来是一种常见的对 HTML 规范的违反。) 不管什么原因,未括起来的属性值通过将 HTML 送进 BaseHTMLProcessor 可以容易地修复。
Dialectizer 是 BaseHTMLProcessor 的简单 (和拙劣) 的派生类。它通过一系列的替换对文本块进行了处理,但是它确保在 <pre>...</pre> 块之间的任何东西不被修改地通过。
到了将迄今为止我们已经学过并用得不错的东西放在一起的时候了。我希望您专心些。
Python 向您提供了一个强大工具,sgmllib.py,可以通过将 HTML 结构转变为一种对象模型来进行处理。可以以许多不同的方式来使用这个工具。
处理 XML 有两种基本的方式。一种叫做 SAX (“Simple API for XML”),它的工作方式是,一次读出一点 XML 内容,然后对发现的每一个元素调用一个方法。(如果你读了 第 8 章 HTML 处理,这应该听起来很熟悉,因为这是 sgmllib 工作的方式。) 另一种方式叫做 DOM (“Document Object Model”),它的工作方式是,一次性读入整个 XML 文档,然后使用 Python 类创建一个内部表示形式 (以树结构进行连接)。Python 拥有这两种解析方式的标准模块,但是本章只涉及 DOM。
实际上解析一个 XML 文档是很简单的:只要一行代码。但是,在你接触那行代码前,需要暂时岔开一下,讨论一下包。
正如我说的,实际解析一个 XML 文档是非常简单的:只要一行代码。从这里出发到哪儿去就是你自己的事了。
Unicode 是一个系统,用来表示世界上所有不同语言的字符。当 Python 解析一个 XML 文档时,所有的数据都是以unicode的形式保存在内存中的。
通过一步步访问每一个节点的方式遍历 XML 文档可能很乏味。如果你正在寻找些特别的东西,又恰恰它们深深埋入了你的 XML 文档,有个捷径让你可以快速找到它:getElementsByTagName 。
XML 元素可以有一个或者多个属性,只要已经解析了一个 XML 文档,访问它们就太简单了。
以上就是 XML 的核心内容。下一章将使用相同的示例程序,但是焦点在于能使程序更加灵活的其它方面:使用输入流处理,使用 getattr 进行方法分发,并使用命令行标识允许用户重新配置程序而无需修改代码。
Python 的最强大力量之一是它的动态绑定,而动态绑定最强大的用法之一是类文件(file-like)对象。
UNIX 用户已经对标准输入、标准输出和标准错误的概念非常熟悉了。这一节是为其他不熟悉的人准备的。
kgp.py 使用了多种技巧,在你进行 XML 处理时,它们或许能派上用场。第一个就是,利用输入文档的结构稳定特征来构建节点缓冲。
解析 XML 文档时,另一个有用的己技巧是查找某个特定元素的所有直接子元素。例如,在语法文件中,一个 ref 元素可以有数个 p 元素,其中每一个都可以包含很多东西,包括其他的 p 元素。你只要查找作为 ref 孩子的 p 元素,不用查找其他 p 元素的孩子 p 元素。
第三个有用的 XML 处理技巧是将你的代码基于节点类型和元素名称分散到逻辑函数中。解析后的 XML 文档是由各种类型的节点组成的,每一个都是通过 Python 对象表示的。文档本身的根层次通过一个 Document 对象表示。Document 还包含了一个或多个 Element 对象 (表示 XML 标记),其中的每一个可以包含其它的 Element 对象、Text 对象 (表示文本),或者 Comment 对象 (表示内嵌注释)。使用 Python 编写分离各个节点类型逻辑的分发器非常容易。
Python 完全支持创建在命令行运行的程序,也支持通过命令行参数和短长样式来指定各种选项。这些并非是 XML 特定的,但是这样的脚本可以充分使用命令行处理,看来是时候提一下它了。
你已经了解很多基础的东西。让我们回来看看所有片段是如何整合到一起的。
Python 带有解析和操作 XML 文档非常强大的库。minidom 接收一个 XML 文件并将其解析为 Python 对象,并提供了对任意元素的随机访问。进一步,本章展示了如何利用 Python 创建一个“真实”独立的命令行脚本,连同命令行标志、命令行参数、错误处理,甚至从前一个程序的管道接收输入的能力。
在讲解如何下载 web 页和如何从 URL 解析 XML时,你已经学习了关于 HTML 处理和 XML 处理,接下来让我们来更全面地探讨有关 HTTP web 服务的主题。
假如说你想用 HTTP 下载资源,例如一个 Atom feed 汇聚。你不仅仅想下载一次;而是想一次又一次地下载它,如每小时一次,从提供 news feed 的站点获得最新的消息。让我们首先用一种直接而原始的方法来实现它,然后看看如何改进它。
这里有五个你必须关注的 HTTP 重要特性。
首先,让我们开启 Python HTTP 库的调试特性并查看网络线路上的传输过程。这对本章的全部内容都很有用,因为你将添加越来越多的特性。
改善你的 HTTP web 服务客户端的第一步就是用 User-Agent 适当地鉴别你自己。为了做到这一点,你需要远离基本的 urllib 而深入到 urllib2。
既然你知道如何在你的 web 服务请求中添加自定义的 HTTP 头信息,接下来看看如何添加 Last-Modified 和 ETag 头信息的支持。
你可以使用两种不同的自定义 URL 处理器来处理永久重定向和临时重定向。
你要支持的最后一个重要的 HTTP 特性是压缩。许多 web 服务具有发送压缩数据的能力,这可以将网络线路上传输的大量数据消减 60% 以上。这尤其适用于 XML web 服务,因为 XML 数据 的压缩率可以很高。
你已经看到了构造一个智能的 HTTP web 客户端的所有片断。现在让我们看看如何将它们整合到一起。
openanything.py 及其函数现在可以完美地工作了。
你用 Google,对吧?它是一个很流行的搜索引擎。你是否希望能以程序化的方式访问 Google 的搜索结果呢?现在你能做到了。下面是一个用 Python 搜索 Google 的程序。
与本书中的其他代码不同,本章依赖的库不是 Python 预安装的。
调用远程函数是 SOAP 的核心功能。有很多提供公开 SOAP 访问的服务器提供用于展示的简单功能。
SOAP 提供了一个很方便的方法用以查看背后的情形。
SOAPProxy 类本地方法调用并透明地转向到远程 SOAP 方法。正如你所看到的,这是很多的工作,SOAPProxy 快速和透明地完成他们。它没有做到的是提供方法自省的手段。
就像网络服务舞台上的所有事物,WSDL 也经历了一个充满明争暗斗而且漫长多变的历史。我不打算讲述这段令我伤心的历史。还有一些其他的标准提供相同的支持,但 WSDL 还是胜出,所以我们还是来学习一下如何使用它。
让我们回到这章开始时你看到的那段代码,获得比当前气温更有价值和令人振奋的信息。
是的,SOAP 网络服务的世界中也不总是欢乐和阳光。有时候也会有故障。
SOAP 网络服务是很复杂的,雄心勃勃的它试图涵盖网络服务的很多不同应用。这一章我们接触了它的一个简单应用。
在前面的章节中,通过阅读代码,你迅速“深入”,以最快的速度理解了各个程序。既然你已对 Python 有了一定的了解,那么接下来让我们看看程序开发之前 的工作。
现在你已经定义了你的转换程序所应有的功能,下面一步会有点儿出乎你的意料:你将要开发一个测试组件 (test suite) 来测试你未来的函数以确保它们工作正常。没错:你将为还未开发的程序开发测试代码。
这是将被开发并保存为 roman.py 的罗马数字转换程序的完整测试组件 (test suite)。很难立刻看出它们是如何协同工作的,似乎所有类或者方法之间都没有关系。这是有原因的,而且你很快就会明了。
单元测试的基础是构建独立的测试用例 (test case)。一个测试用例只回答一个关于被测试代码的问题。
使用有效输入确保函数成功通过测试还不够,你还需要测试无效输入导致函数失败的情形。但并不是任何失败都可以,必须如你预期地失败。
你经常会发现一组代码中包含互逆的转换函数,一个把 A 转换为 B ,另一个把 B 转换为 A。在这种情况下,创建“完备性检测”可以使你在由 A 转 B 再转 A 的过程中不会出现丢失精度或取整等错误。
到目前为止,单元测试已经完成,是时候开始编写被单元测试测试的代码了。你将分阶段地完成这个工作,因此开始时所有的单元测试都是失败的,但在逐步完成 roman.py 的同时你会看到它们一个个地通过测试。
现在你有了 roman 模块的大概框架,到了开始写代码以通过测试的时候了。
现在 toRoman 对于有效的输入 (1 到 3999 整数) 已能正确工作,是正确处理那些无效输入 (任何其他输入) 的时候了。
现在 toRoman 完成了,是开始编写 fromRoman 的时候了。感谢那个将每个罗马数字和对应整数关连的完美数据结构,这个工作不比 toRoman 函数复杂。
现在 fromRoman 对于有效输入能够正常工作了,是揭开最后一个谜底的时候了:使它正常工作于无效输入的情况下。这意味着要找出一个方法检查一个字符串是不是有效的罗马数字。这比 toRoman 中验证有效的数字输入困难,但是你可以使用一个强大的工具:正则表达式。
尽管你很努力地编写全面的单元测试,但是 bug 还是会出现。我所说的 “bug” 是什么呢?Bug 是你还没有编写的测试用例。
尽管你竭尽努力地分析你的客户,并点灯熬油地提炼出精确的需求,但需求还是会是不断变化。大部分客户在看到产品前不知道他们想要什么。即便知道,也不擅于精确表述出他们的有效需求。即便能表述出来,他们在下一个版本一定会要求更多的功能。因此你需要做好更新测试用例的准备以应对需求的改变。
全面的单元测试带来的最大好处不是你的全部测试用例最终通过时的成就感;也不是被责怪破坏了别人的代码时能够证明 自己的自信。最大的好处是单元测试给了你自由去无情地重构。
聪明的读者在学习前一节时想得会更深入一层。现在写的这个程序中最令人头痛的性能负担是正则表达式,但它是必需的,因为没有其它方法来识别罗马数字。但是,它们只有 5000 个,为什么不一次性地构建一个查询表来读取?不必用正则表达式凸现了这个主意的好处。你建立了整数到罗马数字查询表的时候,罗马数字到整数的逆向查询表也构建了。
单元测试是一个强大的概念,使用得当的话既可以减少维护成本又可以增加长期项目的灵活性。同样重要的是要意识到单元测试并不是“灵丹妙药”,也不是“银弹”。编写好的测试用例很困难,保持其更新更需要磨练 (特别是当顾客对修复严重的 Bug 大呼小叫之时)。单元测试不是其它形式测试的替代品,比如说功能性测试、集成测试以及可用性测试。但它切实可行且功效明显,一旦相识,你会反问为什么以往没有应用它。
在 第 13 章 单元测试 中,你学会了单元测试的哲学。在 第 14 章 测试优先编程 中你步入了 Python 基本的单元测试操作,在 第 15 章 重构 部分,你看到单元测试如何令大规模重构变得容易。本章将在这些程序样例的基础上,集中关注于超越单元测试本身的更高级的 Python 特有技术。
从命令行运行 Python 代码时,知道所运行代码在磁盘上的存储位置有时候是有必要的。
你已经熟识了应用列表解析来过滤列表。这里介绍的是达到相同效果的另一种令很多人感觉清晰的实现方法。
现在的你,可能正抓耳挠腮地狠想,为什么这样比使用 for 循环和直接调用函数好。这是一个非常好的问题。通常这是一个程序观问题。使用 map 和 filter 强迫你围绕数据进行思考。
好了,大道理谈够了。让我们谈谈动态导入模块吧。
你已经学习了足够的知识,现在来分析本章样例代码的前七行:读取一个目录并从中导入选定的模块。
regression.py 程序及其输出到现在应该很清楚了。
我想谈谈名词复数。还有,返回其它函数的函数,高级的正则表达式和生成器 (Generator)。生成器是 Python 2.3 新引入的。但首先还是让我们先来谈谈如何生成名词复数。
你所针对的单词 (至少在英语中) 是字符串和字符。你还需要规则来找出不同的字符 (字母) 组合,并对它们进行不同的操作。这听起来像是正则表达式的工作。
现在你将增加一个抽象过程。你从定义一个规则列表开始:如果这样,就做那个,否则判断下一规则。让我们暂时将程序一部分复杂化以便使另一部分简单化。
将每个匹配和规则应用分别制作成函数没有必要。你从来不会直接调用它们:你把它们定义于 rules 列表之中并从那里调用它们。让我们隐去它们的函数名而抓住规则定义的主线。
让我们精炼出代码中的重复之处,以便更容易地定义新规则。
你已经精炼了所有重复代码,也尽可能地把复数规则提炼到定义一个字符串列表。接下来的步骤是把这些字符串提出来放在另外的文件中,从而可以和使用它们的代码分开来维护。
现在你已准备好探讨生成器 (Generator) 了。
这一章中我们探讨了几个不同的高级技术。它们并不都适用于任何情况。
由于代码优化过程中存在太多的不明确因素,以至于你很难清楚该从何入手。
关于 Python 代码优化你需要知道的最重要问题是,决不要自己编写计时函数。
Soundex 函数的第一件事是检查输入是否是一个空字符串。怎样做是最好的方法?
Soundex 算法的第二步是依照特定规则将字符转换为数字。做到这点最好的方法是什么?
Soundex 算法的第三步是去除连续重复字符。怎样做是最佳方法?
Soundex 算法的最后一步是对短结果补零和截短长结果。最佳的做法是什么?
这一章展示了性能优化的几个重要方面,这里是就 Python 而言,但它们却普遍适用。
在 Windows 的 ActivePython IDE 中,可以选择 -> (Ctrl-R) 来运行 Python 程序。输出结果将显示在交互窗口中。 |
在 Mac OS 的 Python IDE 中,可以选择 -> (Cmd-R) 来运行 Python 程序,但首先要设置一个重要的选项。在 IDE 中打开 .py 模块,点击窗口右上角的黑色三角,弹出这个模块的选项菜单,然后将 选中。 这个设置是同模块一同保存的,所以对于每个模块您都需要这样做。 |
在 UNIX 兼容的操作系统中 (包括 Mac OS X),可以通过命令行:python odbchelper.py 运行模块。 |
在 Visual Basic 中,函数 (有返回值) 以 function 开始,而子程序 (无返回值) 以 sub 开始。在 Python 中没有子程序。只有函数,所有的函数都有返回值 (尽管可能为 None),并且所有的函数都以 def 开始。 |
在 Java、C++ 和其他静态类型语言中,必须要指定函数返回值和每个函数参数的数据类型。在 Python 中,永远也不需要明确指定任何东西的数据类型。Python 会根据赋给它的值在内部将其数据类型记录下来。 |
三重引号也是一种定义既包含单引号又包含双引号的字符串的简单方法,就像 Perl 中的 qq/.../ 。 |
许多 Python IDE 使用 doc string 来提供上下文敏感的文档信息,所以当键入一个函数名时,它的 doc string 显示为一个工具提示。这一点可以说非常有用,但是它的好坏取决于您书写的 doc string 的好坏。 |
在 Python 中的 import 就像 Perl 中的 require。import 一个 Python 模块后,您就可以使用 module.function 来访问它的函数;require 一个 Perl 模块后,您就可以使用 module::function 来访问它的函数。 |
Python 使用硬回车来分割语句,冒号和缩进来分割代码块。C++ 和 Java 使用分号来分割语句,花括号来分割代码块。 |
与 C 一样,Python 使用 == 做比较,使用 = 做赋值。与 C 不一样,Python 不支持行内赋值,所以不会出现想要进行比较却意外地出现赋值的情况。 |
在 MacPython 上,需要一个额外的步聚来使得 if __name__ 技巧有效。点击窗口右上角的黑色三角,弹出模块的属性菜单,确认 被选中。 |
Python 中的 dictionary 就像 Perl 中的 hash (哈希数组)。在 Perl 中,存储哈希值的变量总是以 % 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。 |
Python 中的 dictionary 像 Java 中的 Hashtable 类的实例。 |
Python 中的 dictionary 像 Visual Basic 中的 Scripting.Dictionary 对象的实例。 |
Dictionary 没有元素顺序的概念。说元素 “顺序乱了” 是不正确的,它们只是序偶的简单排列。这是一个重要的特性,它会在您想要以一种特定的,可重现的顺序 (像以 key 的字母表顺序) 存取 dictionary 元素的时候骚扰您。有一些实现这些要求的方法,它们只是没有加到 dictionary 中去。 |
Python 的 list 如同 Perl 中的数组。在 Perl 中,用来保存数组的变量总是以 @ 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。 |
Python 中的 list 更像 Java 中的数组 (您可以简单地这样理解,但 Python 中的 list 远比 Java 中的数组强大)。一个更好的类比是 ArrayList 类,它可以保存任意对象,并且可以在增加新元素时动态扩展。 |
在 2.2.1 版本之前,Python 没有单独的布尔数据类型。为了弥补这个缺陷,Python 在布尔环境 (如 if 语句) 中几乎接受所有东西,遵循下面的规则:
|
Tuple 可以转换成 list,反之亦然。内置的 tuple 函数接收一个 list,并返回一个有着相同元素的 tuple。而 list 函数接收一个 tuple 返回一个 list。从效果上看,tuple 冻结一个 list,而 list 解冻一个 tuple。 |
当一条命令用续行符 (“\”) 分割成多行时,后续的行可以以任何方式缩进,此时 Python 通常的严格的缩进规则无需遵守。如果您的 Python IDE 自由对后续行进行了缩进,您应该把它当成是缺省处理,除非您有特别的原因不这么做。 |
在 Python 中,字符串格式化使用与 C 中 sprintf 函数一样的语法。 |
join 只能用于元素是字符串的 list;它不进行任何的强制类型转换。连接一个存在一个或多个非字符串元素的 list 将引发一个异常。 |
anystring.split(delimiter, 1) 是一个有用的技术,在您想要搜索一个子串,然后分别处理字符前半部分 (即 list 中第一个元素) 和后半部分 (即 list 中第二个元素) 时,使用这个技术。 |
调用函数时唯一必须做的事情就是为每一个必备参数指定值 (以某种方式);以何种具体的方式和顺序都取决于你。 |
Python 提供了很多出色的参考手册,你应该好好地精读一下所有 Python 提供的必备模块。对于其它大部分语言,你会发现自己要常常回头参考手册或者 man 页来提醒自己如何使用这些模块,但是 Python 不同于此,它很大程度上是自文档化的。 |
lambda 函数是一种风格问题。不一定非要使用它们;任何能够使用它们的地方,都可以定义一个单独的普通函数来进行替换。我将它们用在需要封装特殊的、非重用代码上,避免令我的代码充斥着大量单行函数。 |
在 SQL 中,你必须使用 IS NULL 代替 = NULL 进行 null 值比较。在 Python,你可以使用 == None 或者 is None 进行比较,但是 is None 更快。 |
Python 中的 from module import * 像 Perl 中的 use module ;Python 中的 import module 像 Perl 中的 require module 。 |
Python 中的 from module import * 像 Java 中的 import module.* ;Python 中的 import module 像 Java 中的 import module 。 |
尽量少用 from module import * ,因为判定一个特殊的函数或属性是从哪来的有些困难,并且会造成调试和重构都更困难。 |
在 Python 中的 pass 语句就像 Java 或 C 中的大括号空集 ({})。 |
在 Python 中,类的基类只是简单地列在类名后面的小括号里。不像在 Java 中有一个特殊的 extends 关键字。 |
习惯上,任何 Python 类方法的第一个参数 (对当前实例的引用) 都叫做 self。这个参数扮演着 C++ 或 Java 中的保留字 this 的角色,但 self 在 Python 中并不是一个保留字,它只是一个命名习惯。虽然如此,也请除了 self 之外不要使用其它的名字,这是一个非常坚固的习惯。 |
__init__ 方法是可选的,但是一旦你定义了,就必须记得显示调用父类的 __init__ 方法 (如果它定义了的话)。这样更是正确的:无论何时子类想扩展父类的行为,后代方法必须在适当的时机,使用适当的参数,显式调用父类方法。 |
在 Python 中,创建类的实例只要调用一个类,仿佛它是一个函数就行了。不像 C++ 或 Java 有一个明确的 new 操作符。 |
在 Windows 下的 ActivePython IDE 中,你可以快速打开在你的库路径中的任何模块,使用 -> (Ctrl-L)。 |
Java 和 Powerbuilder 支持通过参数列表的重载,也就是 一个类可以有同名的多个方法,但这些方法或者是参数个数不同,或者是参数的类型不同。其它语言 (最明显如 PL/SQL) 甚至支持通过参数名的重载,也就是 一个类可以有同名的多个方法,这些方法有相同类型,相同个数的参数,但参数名不同。Python 两种都不支持,总之是没有任何形式的函数重载。一个 __init__ 方法就是一个 __init__ 方法,不管它有什么样的参数。每个类只能有一个 __init__ 方法,并且如果一个子类拥有一个 __init__ 方法,它总是 覆盖父类的 __init__ 方法,甚至子类可以用不同的参数列表来定义它。 |
Python 的原作者 Guido 是这样解释方法覆盖的:“子类可以覆盖父类中的方法。因为方法没有特殊的优先级设置,父类中的一个方法在调用同类中的另一方法时,可能其实调用到的却是一个子类中覆盖父类同名方法的方法。 (C++ 程序员可能会这样想:所有的 Python 方法都是虚函数。)”如果你不明白 (它令我颇感困惑),不必在意。我想我要跳过它。[3] |
应该总是在 __init__ 方法中给一个实例的所有数据属性赋予一个初始值。这样做将会节省你在后面调试的时间,不必为捕捉因使用未初始化 (也就是不存在) 的属性而导致的 AttributeError 异常费时费力。 |
在 Python 2.2 之前的版本中,你不可以直接子类化字符串、列表以及字典之类的内建数据类型。作为补偿,Python 提供封装类来模拟内建数据类型的行为,比如:UserString、UserList 和 UserDict。通过混合使用普通和特殊方法,UserDict 类能十分出色地模仿字典。在 Python 2.2 和其后的版本中,你可以直接从 dict 内建数据类型继承。本书 fileinfo_fromdict.py 中有这方面的一个例子。 |
当在一个类中存取数据属性时,你需要限定属性名:self.attribute。当调用类中的其它方法时,你属要限定方法名:self.method。 |
在 Java 中,通过使用 str1 == str2 可以确定两个字符串变量是否指向同一块物理内存位置。这叫做对象同一性,在 Python 中写为 str1 is str2。在 Java 中要比较两个字符串值,你要使用 str1.equals(str2);在 Python 中,你要使用 str1 == str2。某些 Java 程序员,他们已经被教授得认为,正是因为在 Java 中 == 是通过同一性而不是值进行比较,所以世界才会更美好。这些人要接受 Python 的这个“严重缺失”可能要花些时间。 |
其它的面向对象语言仅让你定义一个对象的物理模型 (“这个对象有 GetLength 方法”),而 Python 的专用类方法像 __len__ 允许你定义一个对象的逻辑模型 (“这个对象有一个长度”)。 |
在 Java 中,静态变量 (在 Python 中叫类属性) 和实例变量 (在 Python 中叫数据属性) 两者都是紧跟在类定义之后定义的 (一个有 static 关键字,一个没有)。在 Python 中,只有类属性可以定义在这里,数据属性定义在 __init__ 方法中。 |
在 Python 中没有常量。如果你试图努力的话什么都可以改变。这一点满足 Python 的核心原则之一:坏的行为应该被克服而不是被取缔。如果你真正想改变 None 的值,也可以做到,但当无法调试的时候别来找我。 |
在 Python 中,所有的专用方法 (像 __setitem__) 和内置属性 (像 __doc__) 遵守一个标准的命名习惯:开始和结束都有两个下划线。不要对你自已的方法和属性用这种方法命名;到最后,它只会把你 (或其它人) 搞乱。 |
Python 使用 try...except 来处理异常,使用 raise 来引发异常。Java 和 C++ 使用 try...catch 来处理异常,使用 throw 来引发异常。 |
只要有可能,你就应该使用在 os 和 os.path 中的函数进行文件、目录和路径的操作。这些模块是对平台相关模块的封装模块,所以像 os.path.split 这样的函数可以工作在 UNIX、Windows、Mac OS 和 Python 所支持的任一种平台上。 |
没有一个轻松的方法来确定两个正则表达式是否等价。你能采用的最好的办法就是列出很多的测试样例,确定这两个正则表达式对所有的相关输入都有相同的输出。在本书后面的章节,将更多地讨论如何编写测试样例。 |
Python 2.0 存在一个 bug,即 SGMLParser 完全不能识别声明 (handle_decl 永远不会调用),这就意味着 DOCTYPE 被静静地忽略掉了。这个错误在 Python 2.1 中改正了。 |
在 Windows 下的 ActivePython IDE 中,您可以在 “Run script” 对话框中指定命令行参数。用空格将多个参数分开。 |
HTML 规范要求所有非 HTML (像客户端的 JavaScript) 必须包括在 HTML 注释中,但不是所有的页面都是这么做的 (而且所有的最新的浏览器也都容许不这样做) 。BaseHTMLProcessor 不允许这样,如果脚本嵌入得不正确,它将被当作 HTML 一样进行分析。例如,如果脚本包含了小于和等于号,SGMLParser 可能会错误地认为找到了标记和属性。SGMLParser 总是把标记名和属性名转换成小写,这样可能破坏了脚本,并且 BaseHTMLProcessor 总是用双引号来将属性封闭起来 (尽管原始的 HTML 文档可能使用单引号或没有引号) ,这样必然会破坏脚本。应该总是将您的客户端脚本放在 HTML 注释中进行保护。 |
Python 2.2 引入了一种略有不同但重要的改变,它会影响名字空间的搜索顺序:嵌套的作用域。
在 Python 2.2 版本之前,当您在一个嵌套函数或 lambda 函数中引用一个变量时,Python 会在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后在模块的名字空间。Python 2.2 将只在当前 (嵌套的或 lambda) 函数的名字空间中搜索,然后是在父函数的名字空间 中搜索,接着是模块的名字空间中搜索。Python 2.1 可 以两种方式工作,缺省地,按 Python 2.0 的方式工作。但是您可以把下面一行代码增加到您的模块头部,使您的模块工作起来像 Python 2.2 的方式:from __future__ import nested_scopes |
使用 locals 和 globals 函数,通过提供变量的字符串名字您可以动态地得到任何变量的值。这种方法提供了这样的功能:getattr 函数允许您通过提供函数的字符串名来动态地访问任意的函数。 |
使用 locals 来应用基于 dictionary 的字符串格式化是一种方便的作法,它可以使复杂的字符串格式化表达式更易读。但它需要花费一定的代价。在调用 locals 方面有一点性能上的问题,这是由于 locals 创建了局部名字空间的一个拷贝引起的。 |
一个包是一个其中带有特殊文件 __init__.py 的目录。__init__.py 文件定义了包的属性和方法。其实它可以什么也不定义;可以只是一个空文件,但是必须要存在。如果 __init__.py 不存在,这个目录就仅仅是一个目录,而不是一个包,它就不能被导入或者包含其它的模块和嵌套包。 |
这部分由于某个含义重叠的术语可能让人有点糊涂。在一个 XML 文档中,元素可以有属性,而 Python 对象也有属性。当你解析一个 XML 文档时,你得到了一组 Python 对象,它们代表 XML 文档中的所有片段,同时有些 Python 对象代表 XML 元素的属性。但是表示 (XML) 属性的 (Python) 对象也有 (Python) 属性,它们用于访问对象表示的 (XML) 属性。我告诉过你它让人糊涂。我会公开提出关于如何更明显地区分这些不同的建议。 |
类似于字典,一个 XML 元素的属性没有顺序。属性可以以某种顺序偶然 列在最初的 XML 文档中,而在 XML 文档解析为 Python 对象时,Attr 对象以某种顺序偶然 列出,这些顺序都是任意的,没有任何特别的含义。你应该总是使用名称来访问单个属性,就像字典的键一样。 |
在这些例子中,HTTP 服务器同时支持 Last-Modified 和 ETag 头信息,但并非所有的服务器皆如此。作为一个 web 服务的客户端,你应该为支持两种头信息做准备,但是你的程序也应该为服务器仅支持其中一种头信息或两种头信息都不支持而做准备。 |
Python 2.1 和之后的版本已经包含了 unittest。Python 2.0 用户则可以从 pyunit.sourceforge.net下载。 |
全面的单元测试能够告诉你的最重要的事情是什么时候停止编写代码。当一个函数的所有单元测试都通过了,停止编写这个函数。一旦整个模块的单元测试通过了,停止编写这个模块。 |
当所有测试都通过了,停止编程。 |
在需要多次使用同一个正则表达式的情况下,应该将它进行编译以获得一个 pattern 对象,然后直接调用这个 pattern 对象的方法。 |
传递给 os.path.abspath 的路径名和文件名可以不存在。 |
os.path.abspath 不仅构建完整路径名,还能格式化路径名。这意味着如果你正工作于 /usr/ 目录,os.path.abspath('bin/../local/bin') 将会返回 /usr/local/bin。它把路径名格式化为尽可能简单的形式。如果你只是希望简单地返回这样的格式化路径名而不需要完整路径名,可以使用 os.path.normpath。 |
就像 os 和 os.path 模块的其他函数,os.path.abspath 是跨平台的。如果你是在 Windows (使用反斜杠作为路径符号) 或 Mac OS (使用冒号) 上运行,它们同样工作,只是将获得与我稍有不同的结果。os 的所有函数都是这样的。 |
你可以在命令行使用 timeit 模块来测试一个已存在的 Python 程序,而不需要修改代码。在 http://docs.python.org/lib/node396.html 查看文档中关于命令行选项的内容。 |
timeit 模块只有在你知道哪段代码需要优化时使用。如果你有一个很大的 Python 程序并且不知道你的性能问题所在,查看 hotshot 模块。 |
修订历史 | |
---|---|
修订 5.4b, r2209 | 2008-01-27 |
修订 5.4b | 2007-09-01 |
|
|
修订 5.4 中文版 | 2006-04-25 |
参加了 Dive Into Python 中文版 5.4 版翻译的有:
|
|
修订 5.4 | 2004-05-20 |
|
|
修订 5.3 | 2004-05-12 |
|
|
修订 5.2 | 2004-05-09 |
|
|
修订 5.1 | 2004-05-05 |
|
|
修订 5.0 | 2004-04-16 |
|
|
修订 4.9 | 2004-03-25 |
|
|
修订 4.8 | 2004-03-25 |
|
|
修订 4.7 | 2004-03-21 |
|
|
修订 4.6 | 2004-03-14 |
|
|
修订 4.5 | 2004-03-07 |
|
|
修订 4.4 | 2003-10-08 |
|
|
修订 4.3 | 2003-09-28 |
|
|
修订 4.2.1 | 2003-09-17 |
|
|
修订 4.2 | 2003-09-12 |
|
|
修订 4.1 | 2002-07-28 |
|
|
修订 4.0-2 | 2002-04-26 |
|
|
修订 4.0 | 2002-04-19 |
|
|
修订 3.9 | 2002-01-01 |
|
|
修订 3.8 | 2001-11-18 |
|
|
修订 3.7 | 2001-09-30 |
|
|
修订 3.6.4 | 2001-09-06 |
|
|
修订 3.6.3 | 2001-09-04 |
|
|
修订 3.6.2 | 2001-08-31 |
|
|
修订 3.6 | 2001-08-31 |
|
|
修订 3.5 | 2001-06-26 |
|
|
修订 3.4 | 2001-05-31 |
|
|
修订 3.3 | 2001-05-24 |
|
|
修订 3.2 | 2001-05-03 |
|
|
修订 3.1 | 2001-04-18 |
|
|
修订 3.0 | 2001-04-16 |
|
|
修订 2.9 | 2001-04-13 |
|
|
修订 2.8 | 2001-03-26 |
|
|
修订 2.7 | 2001-03-16 |
|
|
修订 2.6 | 2001-02-28 |
|
|
修订 2.5 | 2001-02-23 |
|
|
修订 2.4.1 | 2001-02-12 |
|
|
修订 2.4 | 2001-02-12 |
|
|
修订 2.3 | 2001-02-09 |
|
|
修订 2.2 | 2001-02-02 |
|
|
修订 2.1 | 2001-02-01 |
|
|
修订 2.0 | 2001-01-31 |
|
|
修订 1.9 | 2001-01-15 |
|
|
修订 1.8 | 2001-01-12 |
|
|
修订 1.71 | 2001-01-03 |
|
|
修订 1.7 | 2001-01-02 |
|
|
修订 1.6 | 2000-12-11 |
|
|
修订 1.5 | 2000-11-22 |
|
|
修订 1.4 | 2000-11-14 |
|
|
修订 1.3 | 2000-11-09 |
|
|
修订 1.2 | 2000-11-06 |
|
|
修订 1.1 | 2000-10-31 |
|
|
修订 1.0 | 2000-10-30 |
|
本书的英文版是用 Emacs 以 DocBook XML 格式写成的, 使用 由 Norman Walsh 编制的 XSL 样式表 的经过客户化的版本由 由 Michael Kay 编制的 SAXON XSLT 解析器 转化为 HTML。在此基础上, 使用 HTMLDoc 将其转化为 PDF; 使用 w3m 将其转化为无格式文本 (plain text)。(本书的简体中文版是使用译者习惯使用的编辑器对 XML 源代码进行翻译, 并使用原书作者提供的创建脚本输出了 html 版和 htmlflat 版,使用 HTML Help Workshop 生成了chm版本。)程序清单和例子使用了由 Just van Rossum 编写的 pyfontify.py 程序的改进版本进行了语法着色, 此程序包含在示例脚本中。
如果您对学习用 DocBook 进行技术方面的写作感兴趣, 可以 下载本书的 XML 源代码 和 创建脚本, 它包含了用于创建本书各种输出格式的经过客户化的 XSL 样式表。您还应该阅读权威著作, DocBook: The Definitive Guide。如果您打算用 DocBook 进行任何正式的写作, 我建议您订阅 DocBook 邮件列表。
Version 1.1, March 2000
Copyright (C) 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
The purpose of this License is to make a manual, textbook, or other written document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.
This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.
We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.
This License applies to any manual or other work that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you".
A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.
A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (For example, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.
The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License.
The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License.
A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup has been designed to thwart or discourage subsequent modification by readers is not Transparent. A copy that is not "Transparent" is called "Opaque".
Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML designed for human modification. Opaque formats include PostScript, PDF, proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML produced by some word processors for output purposes only.
The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.
You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and you may publicly display copies.
If you publish printed copies of the Document numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.
If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a publicly-accessible computer-network location containing a complete Transparent copy of the Document, free of added material, which the general network-using public has access to download anonymously at no charge using public-standard network protocols. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.
It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.
You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:
If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.
You may add a section entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.
You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.
You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice.
The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections entitled "History" in the various original documents, forming one section entitled "History"; likewise combine any sections entitled "Acknowledgements", and any sections entitled "Dedications". You must delete all sections entitled "Endorsements."
You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.
You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.
A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, does not as a whole count as a Modified Version of the Document, provided no compilation copyright is claimed for the compilation. Such a compilation is called an "aggregate", and this License does not apply to the other self-contained works thus compiled with the Document, on account of their being thus compiled, if they are not themselves derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one quarter of the entire aggregate, the Document's Cover Texts may be placed on covers that surround only the Document within the aggregate. Otherwise they must appear on covers around the whole aggregate.
Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License provided that you also include the original English version of this License. In case of a disagreement between the translation and the original English version of this License, the original English version will prevail.
You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.
Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.
To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:
Copyright (c) YEAR YOUR NAME. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with the Invariant Sections being LIST THEIR TITLES, with the Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. A copy of the license is included in the section entitled "GNU Free Documentation License".
If you have no Invariant Sections, write "with no Invariant Sections" instead of saying which ones are invariant. If you have no Front-Cover Texts, write "no Front-Cover Texts" instead of "Front-Cover Texts being LIST"; likewise for Back-Cover Texts.
If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.
1.1 版, 2000 年 5 月
copyright (C) 2000 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
任何人都可以复制和发行这一协议文档的原始副本,但不允许对它进行修改。
This is an unofficial translation of the GNU Free Documentation License (GFDL) into Chinese. It was not published by the Free Software Foundation, and does not legally state the distribution terms for works that uses the GFDL --only the original English text of the GFDL does that. However, I hope that this translation will help Chinese speakers understand the GFDL better.
这是 GNU 自由文档协议的非官方中文翻译。它不由自由软件基金会出版,因此不代表使用 GFDL 的著作的法律合同――只有 GFDL 的英文原文具有此效力。然而我们希望此翻译有助于说中文的人更好地理解 GFDL 。
本协议的目的在于保证一本手册、教程,或其它任何书面文档在如下意义上的“自由”:确保任何人复制、重发行它的自由,不论是否经过修改,不论是否出于商业目的;其次,这一协议保护文档的作者和出版者的信誉不受他人修改的影响。
本协议是一种“copyleft”,这意味着:文档的派生物和文档本身必须遵循同种意义上的自由。本协议是对 GNU 通用公共协议的补充,后者是为自由软件撰写的“copyleft”协议。
我们撰写这一协议的最初目的是用于自由软件的手册,因为自由软件需要自由文档:自由软件应当带有同样自由的文档。但这一协议的适用范围不局限于软件文档;任何书面著作都可以使用本协议,不论它涉及哪一主题,不论它是否会出版为纸质书籍。我们建议那些以指导或参考为目的的文档作品使用本协议。
本协议适用于任何手册或者其它著作,只要版权人声明了以本协议发行。下文所说的“文档”是指任何此类手册或著作。任何公众成员都是协议的受益者,我们将用“你”来称呼。
文档的“修改版”(Modified Version) 指任何包括文档或其一部分的著作,不论是原样复制的,还是加入了修改和/或翻译。
“附属部分” (Secondary Section) 指文档中指定的附录或序言部分,用于专门叙述作者或出版者与文档主题 (或其相关事项) 之间的关系,不包含该主题内的任何具体内容。(例如,如果文档是数学教材的一部分,那么附属部分可能不会讲述任何数学知识。)上面所说的关系可能是与文档主题 (或其相关事项) 之间的历史关联,或对它的法律、商业、哲学、伦理或政治观点。
“不变部分”(Invariant Sections) 是“附属部分”的一个特定子集,由使用本协议的声明根据其标题指定。
“封面文本”(Cover Texts) 是特定的短篇文字,在使用本协议的声明中列为“前封面文本”或“封底文本”。
文档的“透明”(Transparent) 副本指这样的副本:可以由机器阅读;其格式标准是公众可得的,其内容可以用一般的文本编辑器,或 (对于像素图象) 一般的绘图程序 ,或 (对于绘画作品) 广泛使用的图象编辑器直接打开查看和编辑;能直接输入文本格式器,或可自动化翻译为多种能输入文本格式器的格式。如果某一副本以非透明文件格式制作,后者的目的在于阻挠或不鼓励读者进行后期的修改,则该副本不是透明的,即“非透明”(Opaque) 的。
透明副本应当采用的合适格式的例子:不带标记的纯 ASCII 文本、Texinfo 输入格式、LaTeX 输入格式、采用公众可得的 DTD 的 SGML 或 XML,以及遵循标准的简单的为人类修改所设计的 HTML。非透明的格式包括 PostScript、PDF、只能被专有文字处理器阅读和编辑的专有格式、采用公众不可得的 DTD 和/或处理工具的 SGML 或 XML、机器生成的,仅被一些文字处理器用于输出的 HTML。
“扉页”(Title Page)指的是――对于纸版书来说――扉页本身,加上本协议要求出现在扉页中的材料所在的页面。对那些格式上没有此类扉页的作品,“扉页”指的是出现在作品最显著标题附近,正文前的文字。
你可以以任何媒介复制和发行文档――不论是否出于商业目的――只要在每个副本中包含本协议、版权声明和采用本协议的声明,同时不给本协议附加任何其它条件。你不能使用技术手段妨碍或控制你所制作或发行的副本的阅读和继续复制。然而,你可以为你的复制接受偿金。如果你发行了大量的副本,你还要遵循第 3 部分中的条件。
你也可以借出副本――条件和上面所说的相同――还可以公开展示。
如果你出版的印刷的文档副本数超过 100,而且文档的协议声明要求包括封面文本,你必须为每份副本都装上包含清楚易认的封面文本的封面:前封面文本放在前封面,封底文本放在封底。两个封面都必须清楚易认地指出你是这些副本的出版者。前封面必须印上完整的标题,每个单词都同样显著和易认。你可以在封面上添加其它内容。如果某一复制的改动仅限于封面,且保留了原有标题并符合上述条件,那么在其它方面就可以被认为是原样复制。
如果要求包括的封面文本 (前封面的或封底的) 过于冗长以致不能满足易认的属性,你应当把列在前面的内容 (数量尽量合适地) 放在封面上,余下的内容放在后续页面上。
如果你出版或发行的非透明文档的副本数超过 100,你必须或者为每个非透明副本附带一份可机读的透明副本;或者在每个非透明副本内,或附带副本指出一个公众可进入的计算机网络地址,后者包含了一份完整的不带附加材料的透明文档副本,一般的网络使用者使用公共标准的网络接口,无需付费即可匿名下载。如果你选用了后一种办法,你必须在开始发行非透明副本时采取一些谨慎的措施,以确保处在你所指明的地址的透明副本在你发行最后一本副本 (不论是直接发行的,还是通过代理商或零售商发行的) 后至少一年内仍然可用。
请求,但不强制要求,你在重发行大量副本之前联系文档的作者,以便他们能给你提供文档的更新版本。
满足了第 2 和第 3 部分中的要求,你就可以复制和发行文档的修改版,其前提是你精确地把修改版以本协议发布,此时修改版就扮演了文档的角色,本协议所规定的发行和修改条款就对修改版的持有者有效。此外你必须对修改版做到:
如果修改版包括新的序言或附录,且满足附属部分的条件,不包含从文档中复制的材料,你可以 (也可以不) 把它们的一部分或全体指定为不变的。要做到这一点,需要把它们的标题添加到修改版的协议声明的不变部分列表中。这些标题必须和其它章节标题不同。
你可以添加题为“赞誉”的一个章节,只要它只包括不同群体对你的修改版的赞誉――例如,同类作品的对比,或者你的文字已被某个组织或官方机构批准为某个标准的定义的原文。
你可以在修改版封面文本的最后添加一小段――最多五个单词――作为前封面文本,另一小段――最多 25 个单词――作为封底文本。一个实体只能添加 (或整理出) 前封面文本的一段和封底文本的一段。如果文档的某一封面已经包括了你或你所代表的同一个实体之前所添加 (或整理出) 的一段封面文本,你不能再往这一封面上添加;但你可以替换掉旧的文本,只要得到了添加该文本的出版者的明确许可。
文档的作者和出版者使用本协议,并不连同给予此许可:公开使用他们的名字来确保或暗示对修改版的肯定。
你可以把文档以及其它以本协议发布的文档合并起来,前提是符合第 4 部分中关于修改版的条件,并在合并作品中不经修改地包含所有原文档的所有不变部分,并在合并作品的协议声明中把它们全部列为不变部分。
合并作品只需包含本协议的一份副本,重复的不变部分可以合并为一个。如果几个不变部分的标题相同而内容不同,则应在标题的末尾添加其原作者或出版者的名字 (用括号括起来)――如果是已知的――来区分,否则就用一个独有的数字来区分。对合并作品的协议声明的不变部分列表中出现的章节标题作同样的调整。
合并时,你必须把各个原文档的“历史”部分合并为一个“历史”部分;对任何题为“鸣谢”和“献辞”的部分作同样的合并。你必须删除所有题为“赞誉”的部分。
你可以把文档以及其它以本协议发布的文档合为一集,把分立的本协议的副本合为一个包含在合集中,前提是对每个文档的其它所有方面,都应遵循本协议关于原样复制的规定。
你可以从这样的合集中抽取出一个文档,并以本协议单独发行,前提是你把本协议的副本插入到了抽取出的文档中,并在其它所有方面,都应遵循本协议关于原样复制的规定。
文档或其派生物与其它分立文档或著作的汇编,以一个文库或一个大媒体的形式发行,如果其自身没有汇编版权声明,其整体不被视为文档的修改版。这样的汇编称为“聚集”,本协议不约束和文档汇编在一起的其它非派生作品。
如果第 3 部分中关于封面文本的要求对于这些 (聚集中的) 文档副本是可行的,但文档只占聚集的不到 1/4 的部分,那么文档的封面文本可以被置于聚集内部,只围绕在文档自身周围。否则,它们必须出现在聚集的封面上。
翻译被认为是修改的一种,所以你可以依据第 4 分部分发行文档的翻译版。把不变部分替换为翻译版本需要得到版权持有者的特别许可,但你可以包括部分或全部不变部分的翻译版本作为补充。你也可以包括本协议的一份翻译,但同时也要包括本协议的英语版本。如果翻译版本和英语原版有任何出入,当以英语原版为准。
你只能在明确遵循本协议的前提下,对文档进行复制、修改、派生协议或发行。其它任何任何复制、修改、派生协议或发行的企图都是无效的,并会自动终止本协议授予你的权力。然而如果你违反了本协议,那些根据此协议从你处接受了副本或者权力的人们,只要遵循本协议,他们的协议将不会被终止。
自由软件基金会可能会不时出版 GNU 自由文档协议的新的、修订过的版本。这样的新版本在其精神上会与这一版本相似,但为对付新的问题或利害关系,在细节上可能会有所不同。参看 http://www.gnu.org/copyleft/。
协议的每一版本都会被赋予一个以示区别的版本号。如果文档指定和本协议的版本号加上“或者更新的版本”,你可以遵循指定的版本和自由软件基金会出版的新版本 (不包括草稿) 中的任意一个。如果文档没有指定版本号,你可以从自由软件基金会曾经出版过的版本 (不包括草稿) 中任选一个。
要为你所写的文档使用本协议,在文档中包括本协议的一份副本,并把如下版权和协议声明放在扉页后:
Copyright (c) YEAR YOUR NAME.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with the Invariant Sections being LIST THEIR TITLES, with the Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. A copy of the license is included in the section entitled "GNU Free Documentation License".
参考译文:
Copyright (c) YEAR YOUR NAME.
允许在 GNU 自由文档协议 (1.1 版,或自由软件基金会出版的任何更新版本) 的许可下复制、发行且/或修改本文档;本文档的不变部分包括[它们的标题列在这里],前封面文本是[列在这里],封底文本是[列在这里]。该协议的一份副本包含在题为“GNU Free Documentation License”的章节中。
如果没有不变部分,就写“with no Invariant Sections”(没有不变部分) 而不是指明不变部分;如果没有前封面文本,就写“no Front-Cover Texts”(没有前封面文本) 而不是“Front-Cover Texts being LIST”(前封面文本是[列在这里]);封底文本也是一样。
如果你的文档包含有价值的样例程序代码,我们建议你把这些样例用平行的自由软件协议发布,以使得它们可以在自由软件中被使用,例如 GNU 通用公共协议。
Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum (CWI) in the Netherlands as a successor of a language called ABC. Guido is Python's principal author, although it includes many contributions from others. The last version released from CWI was Python 1.2. In 1995, Guido continued his work on Python at the Corporation for National Research Initiatives (CNRI) in Reston, Virginia where he released several versions of the software. Python 1.6 was the last of the versions released by CNRI. In 2000, Guido and the Python core development team moved to BeOpen.com to form the BeOpen PythonLabs team. Python 2.0 was the first and only release from BeOpen.com.
Following the release of Python 1.6, and after Guido van Rossum left CNRI to work with commercial software developers, it became clear that the ability to use Python with software available under the GNU Public License (GPL) was very desirable. CNRI and the Free Software Foundation (FSF) interacted to develop enabling wording changes to the Python license. Python 1.6.1 is essentially the same as Python 1.6, with a few minor bug fixes, and with a different license that enables later versions to be GPL-compatible. Python 2.1 is a derivative work of Python 1.6.1, as well as of Python 2.0.
After Python 2.0 was released by BeOpen.com, Guido van Rossum and the other PythonLabs developers joined Digital Creations. All intellectual property added from this point on, starting with Python 2.1 and its alpha and beta releases, is owned by the Python Software Foundation (PSF), a non-profit modeled after the Apache Software Foundation. See http://www.python.org/psf/ for more information about the PSF.
Thanks to the many outside volunteers who have worked under Guido's direction to make these releases possible.
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Python 是 Guido van Rossum 于 20 世纪 90 年代早期在荷兰的 Stichting Mathematisch Centrum (CWI) 发明的,作为一门叫 ABC 的语言的后继。Guido 是 Python 的主要作者,尽管它包括了很多他人的贡献。CWI 发布的最后版本是 Python 1.2。1995 年,Guido 在 Reston,Virginia 的 Corporation for National Research Initiatives (CNRI) 继续进行 Python 的工作,他在此又发布了这个软件的多个版本。Python 1.6 是 CNRI 发布的最后版本。2000 年,Guido 和 Python 的核心团队转移到了 BeOpen.com,形成了 BeOpen PythonLabs 团队。Python 2.0 第一个也是唯一一个 BeOpen.com 发布的版本。
Python 1.6 发布后,Guido van Rossum 离开了 CNRI,同商业软件开发员一同工作。让 Python 和以 GNU Public License (GPL) 发布的软件一起工作的需求逐渐明朗起来。CNRI 和自由软件基金会 (Free Software Foundation, FSF) 进行了接触,得到对 Python 协议改动措辞的许可。Python 1.6.1 本质上与 Python 1.6 是相同的,只修正了小部分 bug,但却是用不同的协议发布,从而后续的版本都兼容 GPL 了。Python 2.1 是 Python 1.6.1 的衍生物,Python 2.0 也是。
Python 2.0 在 BeOpen.com 发布后,Guido van Rossum 和其它的 PythonLabs 开发者加入了 Digital Creations。从 Python 2.1 及其 alpha 和 beta 发布开始,此后加入的所有知识产权都由 Python 软件基金会 (Python Software Foundation, PSF) 所有,后者是一个非盈利的组织,以 Apache 软件基金会为样板。关于 PSF 的更多信息可以参看 http://www.python.org/psf/。
感谢成千上万的外部志愿者,他们在 Guido 的指引下使得所有这些发布成为可能。
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved.
在此特许以任何目的、不付费地使用、复制、修改和发行本软件及其文档,其前提是上述版权声明出现在所有的副本中,版权声明和此许可声明出现在支持文档中。在未经书面特许,Stichting Mathematisch Centrum 或 CWI 的名字不被用于广告或软件发行的相关宣传事宜。
Stichting Mathematisch Centrum 否认所有对此软件的保证,包括所有暗含的对产品的销路、是否适合于某一用途的保证。无论如何,Stichting Mathematisch Centrum 都不为任何意外的、间接的,或直接的损害或由任何由使用权、数据或利益的损失所造成的损害负担任何责任,不论是否达成了合同,还是由于疏忽大意或者其它的侵权行为,不论和使用或运行本软件是否有关。