Go Home

Dive Into Python

审校 (5.4b):2007 年 6 月—9 月

译文版 (5.4):2005 年 12 月—2006 年 4 月 (update-060425)

英文原版 (5.4):2004 年 5 月 20 日

本书存放在 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 的规定。你可以复制、发行、修改此文档,但请保留此版权信息。

目录

第 1 章 安装 Python

欢迎来到 Python 世界,让我们开始吧。在本章中,将学习适合您的 Python 安装。

1.1. 哪一种 Python 适合您?

学习 Python 的第一件事就是安装,不是吗?

如果您在公网的服务器上有个用户账号,那么您的 ISP 或许已经安装了 Python。 大多数 Linux 发行版在默认安装的情况下就已经提供了 Python。 虽然您可能希望在苹果机上安装一个拥有类 Mac 的图形操作界面,但在 Mac OS X 10.2 或更高的版本上已经包含了一个 Python 的命令行版本。

Windows 环境默认不提供任何版本的 Python,但是不要担心!本章将提供几种 Windows 环境下安装 Python 的方法。

正像您所看到的,Python 可以运行于很多操作系统平台。包括 Windows、Mac OSMac OS X、所有免费的类 UNIX 变种 (如 Linux)。也有运行于 Sun Solaris、AS/400、Amiga、OS/2、BeOS 的版本,甚至是您从来没听说过的其他操作系统平台。

有太多的平台可以运行 Python 了。在一种平台下编写的 Python 程序稍作修改,就可以运行于任何 其他支持的平台。例如,我通常在 Windows 平台上开发 Python 程序,然后适当配置后使之能在 Linux 平台上运行。

回到开始的问题,“哪一种 Python 适合您?” 回答是:哪一个已经安装在您计算机上的均可。

1.2. Windows 上的 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 的最新版本。

过程 1.1. 选项 1:安装 ActivePython

下面描述 ActivePython 的安装过程:

  1. http://www.activestate.com/Products/ActivePython/ 下载 ActivePython

  2. 如果您正在使用 Windows 95、Windows 98 或 Windows ME,还需要在安装 ActivePython 之前下载并安装Windows Installer 2.0

  3. 双击安装程序 ActivePython-2.2.2-224-win32-ix86.msi

  4. 按照安装程序的提示信息一步步地执行。

  5. 如果磁盘空间不足,您可以执行定制安装,不选文档,但是笔者不建议您这样做,除非您实在是挤不出14M空间来。

  6. 在安装完后之后,关闭安装程序,打开 开始->程序->ActiveState ActivePython 2.2->PythonWin IDE。您将看到类似如下的信息:

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.
>>> 

过程 1.2. 选项 2:安装来自 Python.orgPython

  1. http://www.python.org/ftp/python/ 选择最新的 Python Windows 安装程序,下载 .exe 安装文件。

  2. 双击安装程序 Python-2.xxx.yyy.exe。文件名依赖于您所下载的 Python 安装程序文件。

  3. 按照安装程序的提示信息一步步地执行。

  4. 如果磁盘空间不足,可以取消 HTMLHelp 文件、实用脚本 (Tools/)、和/或测试套件 (Lib/test/)。

  5. 如果您没有机器的管理员权限,您可以选择 Advanced Options,然后选择 Non-Admin Install。这只会对登记注册表和开始菜单中创建的快捷方式有影响。

  6. 在安装完成之后,关闭安装程序,打开 开始->程序->Python 2.3->IDLE (Python GUI)。您将看到类似如下的信息:

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
>>> 

1.3. Mac OS X 上的 Python

Mac OS X 上,对于安装 Python 有两种选择:安装或不安装。您可能想要安装它。

Mac OS X 10.2 及其后续版本已经预装了一个 Python 的命令行版本。如果您习惯使用命令行,那么您可以使用它学完本书的三分之一。然而,预安装的版本不带 XML 解析器,所以当您学到 XML 的章节时,您会需要安装完整版。

您还可以安装优于预装版本的最新的包含图形界面 Shell 的完整版本。

过程 1.3. 在 Mac OS X 上运行预装版本的 Python

使用预装的 Python 版本的步骤:

  1. 打开 /Applications 文件夹。

  2. 打开 Utilities 文件夹。

  3. 双击 Terminal 打开一个终端进入命令行窗口。

  4. 在提示符下键入 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% 

过程 1.4. 在 Mac OS X 上安装最新版的 Python

下面介绍下载并安装 Python 最新版本的过程:

  1. http://homepages.cwi.nl/~jack/macpython/download.html 下载 MacPython-OSX 磁盘镜像 。

  2. 下载完毕,双击 MacPython-OSX-2.3-1.dmg 将磁盘镜像挂载到桌面。

  3. 双击安装程序 MacPython-OSX.pkg.

  4. 安装程序将提示要求您的管理员用户名和口令。

  5. 按照安装程序的提示一步步执行。

  6. 安装完毕后,关闭安装程序,打开 /Applications 文件夹。

  7. 打开 MacPython-2.3 文件夹。

  8. 双击 PythonIDE 来运行 Python

MacPython IDE 将显示启动画面将您带进交互 shell。如果交互 shell 没有出现,选择 Window->Python Interactive (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

例 1.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% 

1.4. Mac OS 9 上的 Python

Mac OS 9 上没有预装任何版本的 Python,安装相对简单,只有一种选择。

下面介绍在 Mac OS 9 上安装 Python 的过程:

  1. http://homepages.cwi.nl/~jack/macpython/download.html 下载 MacPython23full.bin

  2. 如果浏览器不能自动解压文件,那么双击 MacPython23full.binStuffit Expander 解压。

  3. 双击安装程序 MacPython23full

  4. 按照安装程序的提示一步步执行。

  5. 安装完毕后,关闭安装程序,打开 /Applications 文件夹。

  6. 打开 MacPython-OS9 2.3 文件夹。

  7. 双击 PythonIDE 来运行 Python

MacPython IDE 将显示启动画面将您带进交互 shell。如果交互 shell 没有出现,选择 Window->Python Interactive (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
>>> 

1.5. RedHat Linux 上的 Python

在类 UNIX 的操作系统 (如 Linux) 上安装二进制包很容易。预编译好的二进制包对大多数 Linux 发行版是可用的。或者您可以通过源码进行编译。

http://www.python.org/ftp/python/ 选择列出的最新的版本号, 然后选择 其中的rpms/ 目录下载最新的 Python RPM 包。 使用 rpm 命令进行安装,操作如下所示:

例 1.2. 在 RedHat Linux 9 上安装

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          1
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       2
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 3
/usr/bin/python2.3
1 仅仅键入 python 运行的是老版本的 Python ――它是缺省安装的版本。它不是我们想要的。
2 截止到笔者写作时,新的版本是 python2.3。您可能会需要修改示例脚本的第一行的路径指向新版本。
3 这是我们刚安装的 Python 新版本的全路径。在 #! 行中 (每个脚本的第一行) 使用它来确保脚本运行在最新版的 Python 下,并且确保敲入的是 python2.3 进入交互shell。

1.6. Debian GNU/Linux 上的 Python

如果您运行在 Debian GNU/Linux 上,安装 Python 需要使用 apt 命令。

例 1.3. 在 Debian GNU/Linux 上安装

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]

1.7. 从源代码安装 Python

如果您宁愿从源码创建,可以从 http://www.python.org/ftp/python/下载 Python 的源代码。选择最新的版本,下载.tgz 文件,执行通常的 configure, make, make install 步骤。

例 1.4. 从源代码安装

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:~$ 

1.8. 使用 Python 的交互 Shell

既然我们已经安装了 Python,那么我们运行的这个交互 shell 是什么东西呢?

Python 扮演着两种角色。首先它是一个脚本解释器,可以从命令行运行脚本,也可以在脚本上双击,像运行其他应用程序一样。它还是一个交互 shell,可以执行任意的语句和表达式。这一点对调试、快速组建和测试相当有用。我甚至知道一些人把 Python 的交互 shell 当作计算器来使用!

在您的计算机平台上启动 Python 的交互 shell,接下来让我们尝试着做些操作:

例 1.5. 初次使用交互 Shell

>>> 1 + 1               1
2
>>> print 'hello world' 2
hello world
>>> x = 1               3
>>> y = 2
>>> x + y
3
1 Python 的交互 shell 可以计算任意的 Python 表达式,包括任何基本的数学表达式。
2 交互 shell 可以执行任意的 Python 语句,包括 print 语句。
3 也可以给变量赋值,并且变量值在 shell 打开时一直有效 (一旦关毕交互 Sheel,变量值将丢失)。

1.9. 小结

您现在应该已经安装了一个可以工作的 Python 版本了。

根据您的运行平台,您可能安装有不止一个 Python 版本。那样的话,您需要知道 Python 的路径。若在命令行简单地键入 python 没有运行您想使用的 Python 版本,则需要输入想要的版本的全路径。

最后祝贺您,欢迎来到 Python 世界。

第 2 章 第一个 Python 程序

大家都很清楚,其他书籍是如何一步步从编程基础讲述到构建完整的可运行程序的,但还是让我们跳过这个部分吧!

2.1. 概览

这是一个完整的、可执行的 Python 程序。

它可能对您来说根本无法理解。别着急,我们将逐行地进行剖析。不过首先把代码通读一遍,看一看是否有些可以理解的内容。

例 2.1. 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)

现在运行一下这个程序,看一看结果是什么。

提示
在 Windows 的 ActivePython IDE 中,可以选择 File->Run... (Ctrl-R) 来运行 Python 程序。输出结果将显示在交互窗口中。
提示
Mac OSPython IDE 中,可以选择 Python->Run window... (Cmd-R) 来运行 Python 程序,但首先要设置一个重要的选项。在 IDE 中打开 .py 模块,点击窗口右上角的黑色三角,弹出这个模块的选项菜单,然后将 Run as __main__ 选中。 这个设置是同模块一同保存的,所以对于每个模块您都需要这样做。
提示
UNIX 兼容的操作系统中 (包括 Mac OS X),可以通过命令行:python odbchelper.py 运行模块。

odbchelper.py 的输出结果:

server=mpilgrim;uid=sa;database=master;pwd=secret

2.2. 函数声明

与其它大多数语言一样 Python 有函数,但是它没有像 C++ 一样的独立的头文件;或者像 Pascal 一样的分离的 interface/implementation 段。在需要函数时,像下面这样声明即可:

def buildConnectionString(params):

首先,函数声明以关键字 def 开始,接着为函数名,再往后为参数,参数放在小括号里。多个参数之间 (这里没有演示)用逗号分隔。

其次,函数没有定义返回的数据类型。Python 不需要指定返回值的数据类型;甚至不需要指定是否有返回值。实际上,每个 Python 函数都返回一个值;如果函数执行过 return 语句,它将返回指定的值,否则将返回 None (Python 的空值)。

注意
Visual Basic 中,函数 (有返回值) 以 function 开始,而子程序 (无返回值) 以 sub 开始。在 Python 中没有子程序。只有函数,所有的函数都有返回值 (尽管可能为 None),并且所有的函数都以 def 开始。

最后需要指出的是,在 Python 中参数,params 不需要指定数据类型。Python 会判定一个变量是什么类型,并在内部将其记录下来。

注意
JavaC++ 和其他静态类型语言中,必须要指定函数返回值和每个函数参数的数据类型。在 Python 中,永远也不需要明确指定任何东西的数据类型。Python 会根据赋给它的值在内部将其数据类型记录下来。

2.2.1. Python 和其他编程语言数据类型的比较

一位博学的读者发给我 Python 如何与其它编程语言的比较的解释:

静态类型语言
一种在编译期间就确定数据类型的语言。大多数静态类型语言是通过要求在使用任一变量之前声明其数据类型来保证这一点的。JavaC 是静态类型语言。
动态类型语言
一种在运行期间才去确定数据类型的语言,与静态类型相反。VBScriptPython 是动态类型的,因为它们确定一个变量的类型是在您第一次给它赋值的时候。
强类型语言
一种总是强制类型定义的语言。JavaPython 是强制类型定义的。您有一个整数,如果不明确地进行转换 ,不能将把它当成一个字符串。
弱类型语言
一种类型可以被忽略的语言,与强类型相反。VBScript 是弱类型的。在 VBScript 中,您可以将字符串 '12' 和整数 3 进行连接得到字符串'123',然后可以把它看成整数 123 ,所有这些都不需要任何的显示转换。

所以说 Python 既是动态类型语言 (因为它不使用显示数据类型声明),又是强类型语言 (因为只要一个变量获得了一个数据类型,它实际上就一直是这个类型了)。

2.3. 文档化函数

可以通过给出一个 doc string (文档字符串) 来文档化一个 Python 函数。

例 2.2. 定义 buildConnectionString 函数的 doc string

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 的好坏。

进一步阅读

2.4. 万物皆对象

也许您没在意,我刚才的意思是 Python 函数有属性,并且这些属性在运行时是可用的。

Python 中,函数同其它东西一样也是对象。

打开您习惯使用的 Python IDE 执行如下的操作:

例 2.3. 访问 buildConnectionString 函数的 doc string

>>> import odbchelper                              1
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> print odbchelper.buildConnectionString(params) 2
server=mpilgrim;uid=sa;database=master;pwd=secret
>>> print odbchelper.buildConnectionString.__doc__ 3
Build a connection string from a dictionary

Returns string.
1 第一行将 odbchelper 程序作为模块导入。模块是指一个可以交互使用,或者从另一 Python 程序访问的代码段。(您在 第 4 章 将会看到多模块 Python 程序的许多例子。) 只要导入了一个模块,就可以引用它的任何公共的函数、类或属性。模块可以通过这种方法来使用其它模块的功能,您也可以在 IDE 中这样做。这是一个很重要的概念,在后面我们将谈得更多。
2 当使用在被导入模块中定义的函数时,必须包含模块的名字。所以不能只使用 buildConnectionString,而应该使用 odbchelper.buildConnectionString。如果您用过 Java 的类,对此应该不感到陌生。
3 访问函数的 __doc__ 属性不像您想象的那样是通过函数调用。
注意
Python 中的 import 就像 Perl 中的 requireimport 一个 Python 模块后,您就可以使用 module.function 来访问它的函数;require 一个 Perl 模块后,您就可以使用 module::function 来访问它的函数。

2.4.1. 模块导入的搜索路径

在我们继续之前,我想简要地提一下库的搜索路径。当导入一个模块时,Python 在几个地方进行搜索。明确地,它会对定义在 sys.path 中的目录逐个进行搜索。它只是一个list (列表),您可以容易地查看它或通过标准的list方法来修改它。(在本章的后面我们将学习更多关于list的知识。)

例 2.4. 模块导入的搜索路径

>>> import sys                 1
>>> sys.path                   2
['', '/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                        3
<module 'sys' (built-in)>
>>> sys.path.append('/my/new/path') 4
1 导入 sys 模块,使得它的所有函数和属性都有效。
2 sys.path 是一个指定当前搜索路径的目录列表。(您的输出结果可能有所不同,这取决于您的操作系统、正在运行的 Python 版本和初始安装的位置。)Python 将搜索这些目录 (按顺序) 来查找一个与您正试着导入的模块名相匹配的 .py 文件。
3 实际上,我没说实话。真实情况要比这更复杂,因为不是所有的模块都保存为 .py 文件。有一些模块 (像 sys),是“内置模块”,它们实际上是置于 Python 内部的。内置模块的行为如同一般的模块,但是它们的 Python 源代码是不可用的,因为它们不是用 Python 写的!(sys 模块是用 C 写的。)
4 在运行时,通过向 sys.path 追加目录名,就可以在 Python 的搜索路径中增加新的目录,然后当您导入模块时,Python 也会在那个目录中进行搜索。这个作用在 Python 运行时一直生效。(在 第 3 章 我们将讨论更多的关于 append 和其它的 list 方法。)

2.4.2. 何谓对象?

Python 中一切都是对象,并且几乎一切都有属性和方法。所有的函数都有一个内置的 __doc__ 属性,它会返回在函数源代码中定义的 doc stringsys 模块是一个对象,它有一个叫作 path 的属性;等等。

我们仍然在回避问题的实质,究竟何谓对象?不同的编程语言以不同的方式定义 “对象” 。 某些语言中,它意味着所有 对象必须 有属性和方法;另一些语言中,它意味着所有的对象都可以子类化。在 Python 中,定义是松散的;某些对象既没有属性也没有方法 (关于这一点的说明在 第 3 章),而且不是所有的对象都可以子类化 (关于这一点的说明在第 5 章)。但是万物皆对象从感性上可以解释为:一切都可以赋值给变量或作为参数传递给函数 (关于这一点的说明在第 4 章)。

这一点太重要了,所以我会在刚开始就不止一次地反复强调它,以免您没注意到:在 Python万物皆对象。字符串是对象。列表是对象。函数是对象。甚至模块也是对象,这一点我们很快会看到。

进一步阅读

2.5. 代码缩进

Python 函数没有明显的 beginend,没有标明函数的开始和结束的花括号。唯一的分隔符是一个冒号 (:),接着代码本身是缩进的。

例 2.5. 缩进 buildConnectionString 函数

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 语句缩进的例子。

例 2.6. if 语句

def fib(n):                   1
    print 'n =', n            2
    if n > 1:                 3
        return n * fib(n - 1)
    else:                     4
        print 'end of the line'
        return 1
1 这是一个名为 fib 的函数,有一个参数 n。在函数内的所有代码都是缩进的。
2 Python 中向屏幕输出内容非常容易,只要使用 print 即可。print 语句可以接受任何数据类型,包括字符串、整数和其它类型,如字典和列表 (我们将在下一章学习)。甚至可以混在一起输出,只需用逗号隔开。所有值都输出到同一行,用空格隔开 (逗号并不打印出来)。所以当用 5 来调用 fib 时,将输出“n = 5”。
3 if 语句是一种的代码块。如果 if 表达式计算为 true,紧跟着的缩进块会被执行,否则进入 else 块执行。
4 当然 ifelse 块可以包含许多行,只要它们都同样缩进。这个 else 块中有两行代码。对于多行代码块没有其它特殊的语法,只要缩进就行了。

在经过一些最初的抗议和几个与 Fortran 的嘲讽的类比之后,您会心平气和地对待代码缩进,并且开始看到它的好处。一个主要的好处就是所有的 Python 程序看上去都差不多,因为缩进是一种语言的要求而不是一种风格。这样就使得阅读和理解他人的 Python 代码容易得多。

注意
Python 使用硬回车来分割语句,冒号和缩进来分割代码块。C++Java 使用分号来分割语句,花括号来分割代码块。

进一步阅读

2.6. 测试模块

所有的 Python 模块都是对象,并且有几个有用的属性。您可以使用这些属性方便地测试您所编写的模块。下面是一个使用 if __name__ 的技巧。

if __name__ == "__main__":

在继续学习新东西之前,有几个重要的观察结果。首先,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__ 技巧有效。点击窗口右上角的黑色三角,弹出模块的属性菜单,确认 Run as __main__ 被选中。

进一步阅读

第 3 章 内置数据类型

让我们用点儿时间来回顾一下您的第一个 Python 程序。但首先,先说些其他的内容,因为您需要了解一下 dictionary (字典)、tuple (元组) 和 list (列表)(哦,我的老天!)。如果您是一个 Perl hacker,当然可以撇开 dictionary 和 list,但是仍然需要注意 tuple。

3.1. Dictionary 介绍

Dictionary 是 Python 的内置数据类型之一,它定义了键和值之间一对一的关系。

注意
Python 中的 dictionary 就像 Perl 中的 hash (哈希数组)。在 Perl 中,存储哈希值的变量总是以 % 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。
注意
Python 中的 dictionary 像 Java 中的 Hashtable 类的实例。
注意
Python 中的 dictionary 像 Visual Basic 中的 Scripting.Dictionary 对象的实例。

3.1.1. Dictionary 的定义

例 3.1. 定义 Dictionary

>>> d = {"server":"mpilgrim", "database":"master"} 1
>>> d
{'server': 'mpilgrim', 'database': 'master'}
>>> d["server"]                                    2
'mpilgrim'
>>> d["database"]                                  3
'master'
>>> d["mpilgrim"]                                  4
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
KeyError: mpilgrim
1 首先我们创建了新 dictionary,它有两个元素,将其赋给变量 d 。每一个元素都是一个 key-value 对;整个元素集合用大括号括起来。
2 'server' 是一个 key,它所关联的值是通过 d["server"] 来引用的,为 'mpilgrim'
3 'database' 是一个 key,它所关联的值是通过 d["database"] 来引用的,为 'master'
4 您可以通过 key 来引用其值,但是不能通过值获取 key。所以 d["server"] 的值为 'mpilgrim',而使用 d["mpilgrim"] 会引发一个异常,因为 'mpilgrim' 不是一个 key。

3.1.2. Dictionary 的修改

例 3.2. 修改 Dictionary

>>> d
{'server': 'mpilgrim', 'database': 'master'}
>>> d["database"] = "pubs" 1
>>> d
{'server': 'mpilgrim', 'database': 'pubs'}
>>> d["uid"] = "sa"        2
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'}
1 在一个 dictionary 中不能有重复的 key。给一个存在的 key 赋值会覆盖原有的值。
2 在任何时候都可以加入新的 key-value 对。这种语法同修改存在的值是一样的。(是的,它可能某天会给您带来麻烦。假设你一次次地修改一个 dictionary,但其间您使用的 key 并未按照您的想法进行改变。您可能以为加入了新值,但实际上只是一次又一次地修改了同一个值。)

请注意新的元素 (key 为 'uid',value 为 'sa') 出现在中间。实际上,在第一个例子中的元素看上去是的有序不过是一种巧合。现在它们看上去的无序同样是一种巧合。

注意
Dictionary 没有元素顺序的概念。说元素 “顺序乱了” 是不正确的,它们只是序偶的简单排列。这是一个重要的特性,它会在您想要以一种特定的,可重现的顺序 (像以 key 的字母表顺序) 存取 dictionary 元素的时候骚扰您。有一些实现这些要求的方法,它们只是没有加到 dictionary 中去。

当使用 dictionary 时,您需要知道:dictionary 的 key 是大小写敏感的。

例 3.3. Dictionary 的 key 是大小写敏感的

>>> d = {}
>>> d["key"] = "value"
>>> d["key"] = "other value" 1
>>> d
{'key': 'other value'}
>>> d["Key"] = "third value" 2
>>> d
{'Key': 'third value', 'key': 'other value'}
1 为一个已经存在的 dictionary key 赋值,将简单覆盖原有的值。
2 这不会为一个已经存在的 dictionary key 赋值,因为在 Python 中是区分大小写的,也就是说 'key''Key' 是不同的。所以这种情况将在 dictionary 中创建一个新的 key-value 对。虽然看上去很相近,但是在 Python 眼里是完全不同的。

例 3.4. 在 dictionary 中混用数据类型

>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'}
>>> d["retrycount"] = 3 1
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 'retrycount': 3}
>>> d[42] = "douglas"   2
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
42: 'douglas', 'retrycount': 3}
1 Dictionary 不只是用于存储字符串。Dictionary 的值可以是任意数据类型,包括字符串、整数、对象,甚至其它的 dictionary。在单个 dictionary 里,dictionary 的值并不需要全都是同一数据类型,可以根据需要混用和匹配。
2 Dictionary 的 key 要严格多了,但是它们可以是字符串、整数或几种其它的类型 (后面还会谈到这一点)。也可以在一个 dictionary 中混用和匹配 key 的数据类型。

3.1.3. 从 dictionary 中删除元素

例 3.5. 从 dictionary 中删除元素

>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master',
42: 'douglas', 'retrycount': 3}
>>> del d[42] 1
>>> d
{'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 'retrycount': 3}
>>> d.clear() 2
>>> d
{}
1 del 允许您使用 key 从一个 dictionary 中删除独立的元素。
2 clear 从一个 dictionary 中清除所有元素。注意空的大括号集合表示一个没有元素的 dictionary。

3.2. List 介绍

List 是 Python 中使用最频繁的数据类型。如果您对 list 仅有的经验就是在 Visual Basic 中的数组或 Powerbuilder 中的数据存储,那么就打起精神学习 Python 的 list 吧。

注意
Python 的 list 如同 Perl 中的数组。在 Perl 中,用来保存数组的变量总是以 @ 字符开始;在 Python 中,变量可以任意取名,并且 Python 在内部会记录下其数据类型。
注意
Python 中的 list 更像 Java 中的数组 (您可以简单地这样理解,但 Python 中的 list 远比 Java 中的数组强大)。一个更好的类比是 ArrayList 类,它可以保存任意对象,并且可以在增加新元素时动态扩展。

3.2.1. List 的定义

例 3.6. 定义 List

>>> li = ["a", "b", "mpilgrim", "z", "example"] 1
>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[0]                                       2
'a'
>>> li[4]                                       3
'example'
1 首先我们定义了一个有 5 个元素的 list。注意它们保持着初始的顺序。这不是偶然。List 是一个用方括号包括起来的有序元素的集合。
2 List 可以作为以 0 下标开始的数组。任何一个非空 list 的第一个元素总是 li[0]
3 这个包含 5 个元素 list 的最后一个元素是 li[4],因为列表总是从 0 开始。

例 3.7. 负的 list 索引

>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[-1] 1
'example'
>>> li[-3] 2
'mpilgrim'
1 负数索引从 list 的尾部开始向前计数来存取元素。任何一个非空的 list 最后一个元素总是 li[-1]
2 如果负数索引使您感到糊涂,可以这样理解:li[-n] == li[len(li) - n]。所以在这个 list 里,li[-3] == li[5 - 3] == li[2]

例 3.8. list 的分片 (slice)

>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[1:3]  1
['b', 'mpilgrim']
>>> li[1:-1] 2
['b', 'mpilgrim', 'z']
>>> li[0:3]  3
['a', 'b', 'mpilgrim']
1 您可以通过指定 2 个索引得到 list 的子集,叫做一个 “slice” 。返回值是一个新的 list,它包含了 list 中按顺序从第一个 slice 索引 (这里为 li[1]) 开始,直到但是不包括第二个 slice 索引 (这里为 li[3]) 的所有元素。
2 如果一个或两个 slice 索引是负数,slice 也可以工作。如果对您有帮助,您可以这样理解:从左向右阅读 list,第一个 slice 索引指定了您想要的第一个元素,第二个 slice 索引指定了第一个您不想要的元素。返回的值为在其间的每个元素。
3 List 从 0 开始,所以 li[0:3] 返回 list 的前 3 个元素,从 li[0] 开始,直到但不包括 li[3]

例 3.9. Slice 简写

>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li[:3] 1
['a', 'b', 'mpilgrim']
>>> li[3:] 2 3
['z', 'example']
>>> li[:]  4
['a', 'b', 'mpilgrim', 'z', 'example']
1 如果左侧分片索引为 0,您可以将其省略,默认为 0。所以 li[:3]例 3.8 “list 的分片 (slice)”li[0:3] 是一样的。
2 同样的,如果右侧分片索引是 list 的长度,可以将其省略。所以 li[3:]li[3:5] 是一样的,因为这个 list 有 5 个元素。
3 请注意这里的对称性。在这个包含 5 个元素的 list 中,li[:3] 返回前 3 个元素,而 li[3:] 返回后 2 个元素。实际上,li[:n] 总是返回前 n 个元素,而 li[n:] 将返回剩下的元素,不管 list 有多长。
4 如果将两个分片索引全部省略,这将包括 list 的所有元素。但是与原始的名为 li 的 list 不同,它是一个新 list,恰好拥有与 li 一样的全部元素。li[:] 是生成一个 list 完全拷贝的一个简写。

3.2.2. 向 list 中增加元素

例 3.10. 向 list 中增加元素

>>> li
['a', 'b', 'mpilgrim', 'z', 'example']
>>> li.append("new")               1
>>> li
['a', 'b', 'mpilgrim', 'z', 'example', 'new']
>>> li.insert(2, "new")            2
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new']
>>> li.extend(["two", "elements"]) 3
>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements']
1 append 向 list 的末尾追加单个元素。
2 insert 将单个元素插入到 list 中。数值参数是插入点的索引。请注意,list 中的元素不必唯一,现在有两个独立的元素具有 'new' 这个值,li[2]li[6]
3 extend 用来连接 list。请注意不要使用多个参数来调用 extend,要使用一个 list 参数进行调用。在本例中,这个 list 有两个元素。

例 3.11. extend (扩展) 与 append (追加) 的差别

>>> li = ['a', 'b', 'c']
>>> li.extend(['d', 'e', 'f']) 1
>>> li
['a', 'b', 'c', 'd', 'e', 'f']
>>> len(li)                    2
6
>>> li[-1]
'f'
>>> li = ['a', 'b', 'c']
>>> li.append(['d', 'e', 'f']) 3
>>> li
['a', 'b', 'c', ['d', 'e', 'f']]
>>> len(li)                    4
4
>>> li[-1]
['d', 'e', 'f']
1 Lists 的两个方法 extendappend 看起来类似,但实际上完全不同。extend 接受一个参数,这个参数总是一个 list,并且把这个 list 中的每个元素添加到原 list 中。
2 在这里 list 中有 3 个元素 ('a''b''c'),并且使用另一个有 3 个元素 ('d''e''f') 的 list 扩展之,因此新的 list 中有 6 个元素。
3 另一方面,append 接受一个参数,这个参数可以是任何数据类型,并且简单地追加到 list 的尾部。在这里使用一个含有 3 个元素的 list 参数调用 append 方法。
4 原来包含 3 个元素的 list 现在包含 4 个元素。为什么是 4 个元素呢?因为刚刚追加的最后一个元素本身是个 list。List 可以包含任何类型的数据,也包括其他的 list。这或许是您所要的结果,或许不是。如果您的意图是 extend,请不要使用 append

3.2.3. 在 list 中搜索

例 3.12. 搜索 list

>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements']
>>> li.index("example") 1
5
>>> li.index("new")     2
2
>>> li.index("c")       3
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: list.index(x): x not in list
>>> "c" in li           4
False
1 index 在 list 中查找一个值的首次出现并返回索引值。
2 index 在 list 中查找一个值的首次 出现。这里 'new' 在 list 中出现了两次,在 li[2]li[6],但 index 只返回第一个索引,2
3 如果在 list 中没有找到值,Python 会引发一个异常。这一点与大部分的语言截然不同,大部分语言会返回某个无效索引。尽管这种处理可能令人讨厌,但它仍然是件好事,因为它说明您的程序会由于源代码的问题而崩溃,好于在后面当您使用无效索引而引起崩溃。
4 要测试一个值是否在 list 内,使用 in。如果值存在,它返回 True,否则返为 False
注意
在 2.2.1 版本之前,Python 没有单独的布尔数据类型。为了弥补这个缺陷,Python 在布尔环境 (如 if 语句) 中几乎接受所有东西,遵循下面的规则:
  • 0 为 false; 其它所有数值皆为 true。
  • 空串 ("") 为 false; 其它所有字符串皆为 true。
  • 空 list ([]) 为 false; 其它所有 list 皆为 true。
  • 空 tuple (()) 为 false; 其它所有 tuple 皆为 true。
  • 空 dictionary ({}) 为 false; 其它所有 dictionary 皆为 true。
这些规则仍然适用于 Python 2.2.1 及其后续版本,但现在您也可以使用真正的布尔值,它的值或者为 True 或者为 False。请注意第一个字母是大写的;这些值如同在 Python 中的其它东西一样都是大小写敏感的。

3.2.4. 从 list 中删除元素

例 3.13. 从 list 中删除元素

>>> li
['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements']
>>> li.remove("z")   1
>>> li
['a', 'b', 'new', 'mpilgrim', 'example', 'new', 'two', 'elements']
>>> li.remove("new") 2
>>> li
['a', 'b', 'mpilgrim', 'example', 'new', 'two', 'elements']
>>> li.remove("c")   3
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: list.remove(x): x not in list
>>> li.pop()         4
'elements'
>>> li
['a', 'b', 'mpilgrim', 'example', 'new', 'two']
1 remove 从 list 中删除一个值的首次出现。
2 remove 仅仅 删除一个值的首次出现。在这里,'new' 在 list 中出现了两次,但 li.remove("new") 只删除了 'new' 的首次出现。
3 如果在 list 中没有找到值,Python 会引发一个异常来响应 index 方法。
4 pop 是一个有趣的东西。它会做两件事:删除 list 的最后一个元素,然后返回删除元素的值。请注意,这与 li[-1] 不同,后者返回一个值但不改变 list 本身。也不同于 li.remove(value),后者改变 list 但并不返回值。

3.2.5. 使用 list 的运算符

例 3.14. List 运算符

>>> li = ['a', 'b', 'mpilgrim']
>>> li = li + ['example', 'new'] 1
>>> li
['a', 'b', 'mpilgrim', 'example', 'new']
>>> li += ['two']                2
>>> li
['a', 'b', 'mpilgrim', 'example', 'new', 'two']
>>> li = [1, 2] * 3              3
>>> li
[1, 2, 1, 2, 1, 2]
1 Lists 也可以用 + 运算符连接起来。list = list + otherlist 相当于 list.extend(otherlist)。但 + 运算符把一个新 (连接后) 的 list 作为值返回,而 extend 只修改存在的 list。也就是说,对于大型 list 来说,extend 的执行速度要快一些。
2 Python 支持 += 运算符。li += ['two'] 等同于 li.extend(['two'])+= 运算符可用于 list、字符串和整数,并且它也可以被重载用于用户自定义的类中 (更多关于类的内容参见 第 5 章)。
3 * 运算符可以作为一个重复器作用于 list。li = [1, 2] * 3 等同于 li = [1, 2] + [1, 2] + [1, 2],即将三个 list 连接成一个。

3.3. Tuple 介绍

Tuple 是不可变的 list。一旦创建了一个 tuple,就不能以任何方式改变它。

例 3.15. 定义 tuple

>>> t = ("a", "b", "mpilgrim", "z", "example") 1
>>> t
('a', 'b', 'mpilgrim', 'z', 'example')
>>> t[0]                                       2
'a'
>>> t[-1]                                      3
'example'
>>> t[1:3]                                     4
('b', 'mpilgrim')
1 定义 tuple 与定义 list 的方式相同,但整个元素集是用小括号包围的,而不是方括号。
2 Tuple 的元素与 list 一样按定义的次序进行排序。Tuples 的索引与 list 一样从 0 开始,所以一个非空 tuple 的第一个元素总是 t[0]
3 负数索引与 list 一样从 tuple 的尾部开始计数。
4 与 list 一样分片 (slice) 也可以使用。注意当分割一个 list 时,会得到一个新的 list ;当分割一个 tuple 时,会得到一个新的 tuple。

例 3.16. Tuple 没有方法

>>> t
('a', 'b', 'mpilgrim', 'z', 'example')
>>> t.append("new")    1
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'append'
>>> t.remove("z")      2
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'remove'
>>> t.index("example") 3
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'index'
>>> "z" in t           4
True
1 您不能向 tuple 增加元素。Tuple 没有 appendextend 方法。
2 您不能从 tuple 删除元素。Tuple 没有 removepop 方法。
3 您不能在 tuple 中查找元素。Tuple 没有 index 方法。
4 然而,您可以使用 in 来查看一个元素是否存在于 tuple 中。

那么使用 tuple 有什么好处呢?

  • Tuple 比 list 操作速度快。如果您定义了一个值的常量集,并且唯一要用它做的是不断地遍历它,请使用 tuple 代替 list。
  • 如果对不需要修改的数据进行 “写保护”,可以使代码更安全。使用 tuple 而不是 list 如同拥有一个隐含的 assert 语句,说明这一数据是常量。如果必须要改变这些值,则需要执行 tuple 到 list 的转换 (需要使用一个特殊的函数)。
  • 还记得我说过 dictionary keys 可以是字符串,整数和 “其它几种类型”吗?Tuples 就是这些类型之一。Tuples 可以在 dictionary 中被用做 key,但是 list 不行。实际上,事情要比这更复杂。Dictionary key 必须是不可变的。Tuple 本身是不可改变的,但是如果您有一个 list 的 tuple,那就认为是可变的了,用做 dictionary key 就是不安全的。只有字符串、整数或其它对 dictionary 安全的 tuple 才可以用作 dictionary key。
  • Tuples 可以用在字符串格式化中,我们会很快看到。
注意
Tuple 可以转换成 list,反之亦然。内置的 tuple 函数接收一个 list,并返回一个有着相同元素的 tuple。而 list 函数接收一个 tuple 返回一个 list。从效果上看,tuple 冻结一个 list,而 list 解冻一个 tuple。

进一步阅读

3.4. 变量声明

现在您已经了解了有关 dictionary、tuple、和 list 的相关知识 (哦,我的老天!),让我们回到 第 2 章 的例子程序 odbchelper.py

Python 与大多数其它语言一样有局部变量和全局变量之分,但是它没有明显的变量声明。变量通过首次赋值产生,当超出作用范围时自动消亡。

例 3.17. 定义 myParams 变量

if __name__ == "__main__":
    myParams = {"server":"mpilgrim", \
                "database":"master", \
                "uid":"sa", \
                "pwd":"secret" \
                }

首先注意缩进。if 语句是代码块,需要像函数一样缩进。

其次,变量的赋值是一条被分成了多行的命令,用反斜线 (“\”) 作为续行符。

注意
当一条命令用续行符 (“\”) 分割成多行时,后续的行可以以任何方式缩进,此时 Python 通常的严格的缩进规则无需遵守。如果您的 Python IDE 自由对后续行进行了缩进,您应该把它当成是缺省处理,除非您有特别的原因不这么做。

严格地讲,在小括号,方括号或大括号中的表达式 (如定义一个 dictionary) 可以用或者不用续行符 (“\”) 分割成多行。甚至在不是必需的时候,我也喜欢使用续行符,因为我认为这样会让代码读起来更容易,但那只是风格问题。

第三,您从未声明过变量 myParams,您只是给它赋了一个值。这点就像是 VBScript 没有设置 option explicit 选项一样。幸运的是,与 VBScript 不同,Python 不允许您引用一个未被赋值的变量,试图这样做会引发一个异常。

3.4.1. 变量引用

例 3.18. 引用未赋值的变量

>>> x
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
NameError: There is no variable named 'x'
>>> x = 1
>>> x
1

迟早有一天您会为此而感谢 Python

3.4.2. 一次赋多值

Python 中比较 “酷” 的一种编程简写是使用序列来一次给多个变量赋值。

例 3.19. 一次赋多值

>>> v = ('a', 'b', 'e')
>>> (x, y, z) = v     1
>>> x
'a'
>>> y
'b'
>>> z
'e'
1 v 是一个三元素的 tuple,并且 (x, y, z) 是一个三变量的 tuple。将一个 tuple 赋值给另一个 tuple,会按顺序将 v 的每个值赋值给每个变量。

这种用法有许多种用途。我经常想要将一定范围的值赋给多个变量。在 C 语言中,可以使用 enum 类型,手工列出每个常量和其所对应的值,当值是连续的时候这一过程让人感到特别繁琐。而在 Python 中,您可以使用内置的 range 函数和多变量赋值的方法来快速进行赋值。

例 3.20. 连续值赋值

>>> range(7)                                                                    1
[0, 1, 2, 3, 4, 5, 6]
>>> (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) 2
>>> MONDAY                                                                      3
0
>>> TUESDAY
1
>>> SUNDAY
6
1 内置的 range 函数返回一个元素为整数的 list。这个函数的简化调用形式是接收一个上限值,然后返回一个初始值从 0 开始的 list,它依次递增,直到但不包含上限值。(如果您愿意,您可以传入其它的参数来指定一个非 0 的初始值和非 1 的步长。也可以使用 print range.__doc__ 来了解更多的细节。)
2 MONDAYTUESDAYWEDNESDAYTHURSDAYFRIDAYSATURDAYSUNDAY 是我们定义的变量。(这个例子来自 calendar 模块。它是一个很有趣的打印日历的小模块,像 UNIXcal 命令。这个 calendar 模块定义了一星期中每天的整数常量表示。)
3 现在每个变量都拥有了自己的值:MONDAY 的值为 0TUESDAY 的值为 1,等等。

您也可以使用多变量赋值来创建返回多个值的函数,只要返回一个包含所有值的 tuple 即可。调用者可以将其视为一个 tuple,或将值赋给独立的变量。许多标准的 Python 库都是这样做的,包括 os 模块,我们将在 第 6 章 中讨论。

3.5. 格式化字符串

Python 支持格式化字符串的输出 。尽管这样可能会用到非常复杂的表达式,但最基本的用法是将一个值插入到一个有字符串格式符 %s 的字符串中。

注意
Python 中,字符串格式化使用与 Csprintf 函数一样的语法。

例 3.21. 字符串的格式化

>>> k = "uid"
>>> v = "sa"
>>> "%s=%s" % (k, v) 1
'uid=sa'
1 整个表达式的值为一个字符串。第一个 %s 被变量 k 的值替换;第二个 %sv 的值替换。字符串中的所有其它字符 (在这个例子中,是等号) 按原样打印输出。

注意 (k, v) 是一个 tuple。我说过它们对某些东西有用。

您可能一直在想,做了这么多工作只不过是为了做简单的字符串连接。您想的不错,只不过字符串格式化不只是连接。它甚至不仅仅是格式化。它也是强制类型转换。

例 3.22. 字符串格式化与字符串连接的比较

>>> uid = "sa"
>>> pwd = "secret"
>>> print pwd + " is not a good password for " + uid      1
secret is not a good password for sa
>>> print "%s is not a good password for %s" % (pwd, uid) 2
secret is not a good password for sa
>>> userCount = 6
>>> print "Users connected: %d" % (userCount, )           3 4
Users connected: 6
>>> print "Users connected: " + userCount                 5
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
TypeError: cannot concatenate 'str' and 'int' objects
1 + 是字符串连接操作符。
2 在这个简单例子中,字符串格式化实现与连接一样的结果。
3 (userCount, ) 是一个只包含一个元素的 tuple。是的,语法有一点奇怪,但是使用它的理由就是:显示地指出它是一个 tuple,而不是其他。实际上,当定义一个 list、tuple 或 dictionary 时,您可以总是在最后一个元素后面跟上一个逗号,但是当定义一个只包含一个元素的 tuple 时逗号是必须的。如果省略逗号,Python 不会知道 (userCount) 究竟是一个只包含一个元素的 tuple 还是变量 userCount 的值。
4 字符串格式化通过将 %s 替换成 %d 即可处理整数。
5 试图将一个字符串同一个非字符串连接会引发一个异常。与字符串格式化不同,字符串连接只能在被连接的每一个都是字符串时起作用。

如同 printfC 中的作用,Python 中的字符串格式化是一把瑞士军刀。它有丰富的选项,不同的格式化格式符和可选的修正符可用于不同的数据类型。

例 3.23. 数值的格式化

>>> print "Today's stock price: %f" % 50.4625   1
50.462500
>>> print "Today's stock price: %.2f" % 50.4625 2
50.46
>>> print "Change since yesterday: %+.2f" % 1.5 3
+1.50
1 %f 格式符选项对应一个十进制浮点数,不指定精度时打印 6 位小数。
2 使用包含“.2”精度修正符的 %f 格式符选项将只打印 2 位小数。
3 您甚至可以混合使用各种修正符。添加 + 修正符用于在数值之前显示一个正号或负号。注意“.2”精度修正符仍旧在它原来的位置,用于只打印 2 位小数。

3.6. 映射 list

Python 的强大特性之一是其对 list 的解析,它提供一种紧凑的方法,可以通过对 list 中的每个元素应用一个函数,从而将一个 list 映射为另一个 list。

例 3.24. List 解析介绍

>>> li = [1, 9, 8, 4]
>>> [elem*2 for elem in li]      1
[2, 18, 16, 8]
>>> li                           2
[1, 9, 8, 4]
>>> li = [elem*2 for elem in li] 3
>>> li
[2, 18, 16, 8]
1 为了便于理解它,让我们从右向左看。li 是一个将要映射的 list。Python 循环遍历 li 中的每个元素。对每个元素均执行如下操作:首先临时将其值赋给变量 elem,然后 Python 应用函数 elem*2 进行计算,最后将计算结果追加到要返回的 list 中。
2 需要注意是,对 list 的解析并不改变原始的 list。
3 将一个 list 的解析结果赋值给对其映射的变量是安全的。不用担心存在竞争情况或任何古怪事情的发生。Python 会在内存中创建新的 list,当对 list 的解析完成时,Python 将结果赋给变量。

让我们回过头来看看位于 第 2 章 的函数 buildConnectionString 对 list 的解析:

["%s=%s" % (k, v) for k, v in params.items()]

首先,注意到你调用了dictionary paramsitems 函数。这个函数返回一个 dictionary 中所有数据的 tuple 的 list。

例 3.25. keys, valuesitems 函数

>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> params.keys()   1
['server', 'uid', 'database', 'pwd']
>>> params.values() 2
['mpilgrim', 'sa', 'master', 'secret']
>>> params.items()  3
[('server', 'mpilgrim'), ('uid', 'sa'), ('database', 'master'), ('pwd', 'secret')]
1 Dictionary 的 keys 方法返回一个包含所有键的 list。这个 list 没按 dictionary 定义的顺序输出 (记住,元素在 dictionary 中是无序的),但它是一个 list。
2 values 方法返回一个包含所有值的 list。它同 keys 方法返回的 list 输出顺序相同,所以对于所有的 nparams.values()[n] == params[params.keys()[n]]
3 items 方法返回一个由形如 (keyvalue) 组成的 tuple 的 list。这个 list 包括 dictionary 中所有的数据。

现在让我们看一看 buildConnectionString 做了些什么。它接收一个 list,params.items(),通过对每个元素应用字符串格式化将其映射为一个新 list。这个新 list 将与 params.items() 一一对应:新 list 中的每个元素都是 dictionary params 中的一个键-值对构成的的字符串。

例 3.26. buildConnectionString 中的 list 解析

>>> 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()]                1
['server', 'uid', 'database', 'pwd']
>>> [v for k, v in params.items()]                2
['mpilgrim', 'sa', 'master', 'secret']
>>> ["%s=%s" % (k, v) for k, v in params.items()] 3
['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret']
1 请注意我们正在使用两个变量对 list params.items() 进行遍历。这是多变量赋值的另一种用法。params.items() 的第一个元素是 ('server', 'mpilgrim'),所以在 list 解析的第一次遍历中,k 将为 'server'v 将为 'mpilgrim'。在本例中,我们忽略了返回 list 中 v 的值,而只包含了 k 的值,所以这个 list 解析最后等于 params.keys()
2 这里我们做着相同的事情,但是忽略了 k 的值,所以这个 list 解析最后等于 params.values()
3 用一些简单的 字符串格式化将前面两个例子合并起来 ,我们就得到一个包括了 dictionary 中每个元素的 key-value 对的 list。这个看上去有点像程序的输出结果,剩下的就只是将这个 list 中的元素接起来形成一个字符串了。

进一步阅读

3.7. 连接 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 将引发一个异常。

例 3.27. odbchelper.py 的输出结果

>>> 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

例 3.28. 分割字符串

>>> li = ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret']
>>> s = ";".join(li)
>>> s
'server=mpilgrim;uid=sa;database=master;pwd=secret'
>>> s.split(";")    1
['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret']
>>> s.split(";", 1) 2
['server=mpilgrim', 'uid=sa;database=master;pwd=secret']
1 splitjoin 正好相反,它将一个字符串分割成多元素 list。注意,分隔符 (“;”) 被完全去掉了,它没有在返回的 list 中的任意元素中出现。
2 split 接受一个可选的第二个参数,它是要分割的次数。(“哦,可选参数……”,您将会在下一章中学会如何在您自己的函数中使用它。)
提示
anystring.split(delimiter, 1) 是一个有用的技术,在您想要搜索一个子串,然后分别处理字符前半部分 (即 list 中第一个元素) 和后半部分 (即 list 中第二个元素) 时,使用这个技术。

3.7.1. 字符串方法的历史注解

当我开始学 Python 时,我以为 join 是 list 的方法,它会使用分隔符作为一个参数。很多人都有同样的感觉:在 join 方法的背后有一段故事。在 Python 1.6 之前,字符串完全没有这些有用的方法。有一个独立的 string 模块包含所有的字符串函数,每个函数使用一个字符串作为它的第一个参数。这些函数被认为足够重要,所以它们移到字符串中去了,这就使得诸如 loweruppersplit 之类的函数是有意义的。但许多核心的 Python 程序员反对新的 join 方法,争论说应该换成是 list 的一个方法,或不应该移动而仅仅保留为旧的 string 模块 (现仍然还有许多有用的东西在里面) 的一部分。我只使用新的 join 方法,但是您还是会看到其它写法。如果它真的使您感到麻烦,您可以使用旧的 string.join 函数来替代。

3.8. 小结

现在 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

在深入下一章学习之前,确保您可以无阻碍地完成下面的事情:

第 4 章 自省的威力

本章论述了 Python 众多强大功能之一:自省。正如你所知道的,Python 中万物皆对象,自省是指代码可以查看内存中以对象形式存在的其它模块和函数,获取它们的信息,并对它们进行操作。用这种方法,你可以定义没有名称的函数,不按函数声明的参数顺序调用函数,甚至引用事先并不知道名称的函数。

4.1. 概览

下面是一个完整可运行的 Python 程序。大概看一下这段程序,你应该可以理解不少了。用数字标出的行阐述了 第 2 章 第一个 Python 程序 中涉及的一些概念。如果剩下来的代码看起来有点奇怪,不用担心,通过阅读本章你将会理解所有这些。

例 4.1. apihelper.py

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

def info(object, spacing=10, collapse=1): 1 2 3
    """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__":                4 5
    print info.__doc__
1 该模块有一个声明为 info 的函数。根据它的函数声明可知,它有三个参数: objectspacingcollapse。实际上后面两个参数都是可选参数,关于这点你很快就会看到。
2 info 函数有一个多行的 doc string,简要地描述了函数的功能。注意这里并没有提到返回值;单独使用这个函数只是为了这个函数产生的效果,并不是为了它的返回值。
3 函数内的代码是缩进形式的。
4 if __name__ 技巧允许这个程序在自己独立运行时做些有用的事情,同时又不妨碍作为其它程序的模块使用。在这个例子中,程序只是简单地打印出 info 函数的 doc string
5 if 语句使用 == 进行比较,而且不需要括号。

info 函数的设计意图是提供给工作在 Python IDE 中的开发人员使用,它可以接受任何含有函数或者方法的对象 (比如模块,含有函数,又比如list,含有方法) 作为参数,并打印出对象的所有函数和它们的 doc string

例 4.2. 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

缺省地,程序输出进行了格式化处理,以使其易于阅读。多行 doc string 被合并到单行中,要改变这个选项需要指定 collapse 参数的值为 0。如果函数名称长于10个字符,你可以将 spacing 参数的值指定为更大的值以使输出更容易阅读。

例 4.3. apihelper.py 的高级用法

>>> 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.

4.2. 使用可选参数和命名参数

Python 允许函数参数有缺省值;如果调用函数时不使用参数,参数将获得它的缺省值。此外,通过使用命名参数还可以以任意顺序指定参数。SQL Server Transact/SQL 中的存储过程也可以做到这些;如果你是脚本高手,你可以略过这部分。

info 函数就是这样一个例子,它有两个可选参数。

def info(object, spacing=10, collapse=1):

spacingcollapse 是可选参数,因为它们已经定义了缺省值。object 是必备参数,因为它没有指定缺省值。如果调用 info 时只指定一个参数,那么 spacing 缺省为 10collapse 缺省为 1。如果调用 info 时指定两个参数,collapse 依然默认为 1

假如你要指定 collapse 的值,但是又想要接受 spacing 的缺省值。在绝大部分语言中,你可能运气就不太好了,因为你需要使用三个参数来调用函数,这势必要重新指定 spacing 的值。但是在 Python 中,参数可以通过名称以任意顺序指定。

例 4.4. info 的有效调用

info(odbchelper)                    1
info(odbchelper, 12)                2
info(odbchelper, collapse=0)        3
info(spacing=15, object=odbchelper) 4
1 只使用一个参数,spacing 使用缺省值 10collapse 使用缺省值 1
2 使用两个参数,collapse 使用缺省值 1
3 这里你显式命名了 collapse 并指定了它的值。spacing 将依然使用它的缺省值 10
4 甚至必备参数 (例如 object,没有指定缺省值) 也可以采用命名参数的方式,而且命名参数可以以任意顺序出现。

这些看上去非常累,除非你意识到参数不过是一个字典。“通常” 不使用参数名称的函数调用只是一个简写的形式,Python 按照函数声明中定义的的参数顺序将参数值和参数名称匹配起来。大部分时间,你会使用“通常”方式调用函数,但是如果你需要,总是可以提供附加的灵活性。

注意
调用函数时唯一必须做的事情就是为每一个必备参数指定值 (以某种方式);以何种具体的方式和顺序都取决于你。

进一步阅读

4.3. 使用 typestrdir 和其它内置函数

Python 有小部分相当有用的内置函数。除这些函数之外,其它所有的函数都被分到了各个模块中。其实这是一个非常明智的设计策略,避免了核心语言变得像其它脚本语言一样臃肿 (咳 咳,Visual Basic)。

4.3.1. type 函数

type 函数返回任意对象的数据类型。在 types 模块中列出了可能的数据类型。这对于处理多种数据类型的帮助者函数 [1] 非常有用。

例 4.5. type 介绍

>>> type(1)           1
<type 'int'>
>>> li = []
>>> type(li)          2
<type 'list'>
>>> import odbchelper
>>> type(odbchelper)  3
<type 'module'>
>>> import types      4
>>> type(odbchelper) == types.ModuleType
True
1 type 可以接收任何东西作为参数――我的意思是任何东西――并返回它的数据类型。整型、字符串、列表、字典、元组、函数、类、模块,甚至类型对象都可以作为参数被 type 函数接受。
2 type 可以接收变量作为参数,并返回它的数据类型。
3 type 还可以作用于模块。
4 你可以使用 types 模块中的常量来进行对象类型的比较。这就是 info 函数所做的,很快你就会看到。

4.3.2. str 函数

str 将数据强制转换为字符串。每种数据类型都可以强制转换为字符串。

例 4.6. str 介绍

>>> str(1)          1
'1'
>>> horsemen = ['war', 'pestilence', 'famine']
>>> horsemen
['war', 'pestilence', 'famine']
>>> horsemen.append('Powerbuilder')
>>> str(horsemen)   2
"['war', 'pestilence', 'famine', 'Powerbuilder']"
>>> str(odbchelper) 3
"<module 'odbchelper' from 'c:\\docbook\\dip\\py\\odbchelper.py'>"
>>> str(None)       4
'None'
1 对于简单的数据类型比如整型,你可以预料到 str 的正常工作,因为几乎每种语言都有一个将整型转化为字符串的函数。
2 然而 str 可以作用于任何数据类型的任何对象。这里它作用于一个零碎构建的列表。
3 str 还允许作用于模块。注意模块的字符串形式表示包含了模块在磁盘上的路径名,所以你的显示结果将会有所不同。
4 str 的一个细小但重要的行为是它可以作用于 NoneNonePython 的 null 值。这个调用返回字符串 'None'。你将会使用这一点来改进你的 info 函数,这一点你很快就会看到。

info 函数的核心是强大的 dir 函数。dir 函数返回任意对象的属性和方法列表,包括模块对象、函数对象、字符串对象、列表对象、字典对象 …… 相当多的东西。

例 4.7. dir 介绍

>>> li = []
>>> dir(li)           1
['append', 'count', 'extend', 'index', 'insert',
'pop', 'remove', 'reverse', 'sort']
>>> d = {}
>>> dir(d)            2
['clear', 'copy', 'get', 'has_key', 'items', 'keys', 'setdefault', 'update', 'values']
>>> import odbchelper
>>> dir(odbchelper)   3
['__builtins__', '__doc__', '__file__', '__name__', 'buildConnectionString']
1 li 是一个列表,所以 dir(li) 返回一个包含所有列表方法的列表。注意返回的列表只包含了字符串形式的方法名称,而不是方法对象本身。
2 d 是一个字典,所以 dir(d) 返回字典方法的名称列表。其中至少有一个方法,keys,看起来还是挺熟悉的。
3 这里就是真正变得有趣的地方。odbchelper 是一个模块,所以 dir(odbchelper) 返回模块中定义的所有部件的列表,包括内置的属性,例如 __name____doc__,以及其它你所定义的属性和方法。在这个例子中,odbchelper 只有一个用户定义的方法,就是在第 2 章中论述的 buildConnectionString 函数。

最后是 callable 函数,它接收任何对象作为参数,如果参数对象是可调用的,返回 True;否则返回 False。可调用对象包括函数、类方法,甚至类自身 (下一章将更多的关注类)。

例 4.8. callable 介绍

>>> import string
>>> string.punctuation           1
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
>>> string.join                  2
<function join at 00C55A7C>
>>> callable(string.punctuation) 3
False
>>> callable(string.join)        4
True
>>> print string.join.__doc__    5
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)
1 string 模块中的函数现在已经不赞成使用了 (尽管很多人现在仍然还在使用 join 函数),但是在这个模块中包含了许多有用的变量,例如 string.punctuation,这个字符串包含了所有标准的标点符号字符。
2 string.join 是一个用于连接字符串列表的函数。
3 string.punctuation 是不可调用的对象;它是一个字符串。(字符串确有可调用的方法,但是字符串本身不是可调用的。)
4 string.join 是可调用的;这个函数可以接受两个参数。
5 任何可调用的对象都有 doc string。通过将 callable 函数作用于一个对象的每个属性,可以确定哪些属性 (方法、函数、类) 是你要关注的,哪些属性 (常量等等) 是你可以忽略、之前不需要知道的。

4.3.3. 内置函数

typestrdir 和其它的 Python 内置函数都归组到了 __builtin__ (前后分别是双下划线) 这个特殊的模块中。如果有帮助的话,你可以认为 Python 在启动时自动执行了 from __builtin__ import *,此语句将所有的 “内置” 函数导入该命名空间,所以在这个命名空间中可以直接使用这些内置函数。

像这样考虑的好处是,你是可以获取 __builtin__ 模块信息的,并以组的形式访问所有的内置函数和属性。猜到什么了吗,现在我们的 Python 有一个称为 info 的函数。自己尝试一下,略看一下结果列表。后面我们将深入到一些更重要的函数。(一些内置的错误类,比如 AttributeError,应该看上去已经很熟悉了。)

例 4.9. 内置属性和内置函数

>>> 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 不同于此,它很大程度上是自文档化的。

进一步阅读

4.4. 通过 getattr 获取对象引用

你已经知道 Python 函数是对象。你不知道的是,使用 getattr 函数,可以得到一个直到运行时才知道名称的函数的引用。

例 4.10. getattr 介绍

>>> li = ["Larry", "Curly"]
>>> li.pop                       1
<built-in method pop of list object at 010DF884>
>>> getattr(li, "pop")           2
<built-in method pop of list object at 010DF884>
>>> getattr(li, "append")("Moe") 3
>>> li
["Larry", "Curly", "Moe"]
>>> getattr({}, "clear")         4
<built-in method clear of dictionary object at 00F113D4>
>>> getattr((), "pop")           5
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'pop'
1 该语句获取列表的 pop 方法的引用。注意该语句并不是调用 pop 方法;调用 pop 方法的应该是 li.pop()。这里指的是方法对象本身。
2 该语句也是返回 pop 方法的引用,但是此时,方法名称是作为一个字符串参数传递给 getattr 函数的。getattr 是一个有用到令人无法致信的内置函数,可以返回任何对象的任何属性。在这个例子中,对象是一个 list,属性是 pop 方法。
3 如果不确信它是多么的有用,试试这个:getattr 的返回值 方法,然后你就可以调用它,就像直接使用 li.append("Moe") 一样。但是实际上你没有直接调用函数;只是以字符串形式指定了函数名称。
4 getattr 也可以作用于字典。
5 理论上,getattr 可以作用于元组,但是由于元组没有方法,所以不管你指定什么属性名称 getattr 都会引发一个异常。

4.4.1. 用于模块的 getattr

getattr 不仅仅适用于内置数据类型,也可作用于模块。

例 4.11. apihelper.py 中的 getattr 函数

>>> import odbchelper
>>> odbchelper.buildConnectionString             1
<function buildConnectionString at 00D18DD4>
>>> getattr(odbchelper, "buildConnectionString") 2
<function buildConnectionString at 00D18DD4>
>>> object = odbchelper
>>> method = "buildConnectionString"
>>> getattr(object, method)                      3
<function buildConnectionString at 00D18DD4>
>>> type(getattr(object, method))                4
<type 'function'>
>>> import types
>>> type(getattr(object, method)) == types.FunctionType
True
>>> callable(getattr(object, method))            5
True
1 该语句返回 odbchelper 模块中 buildConnectionString 函数的引用,第 2 章 第一个 Python 程序 你已经研习过这个方法了。(你看到的这个十六进制地址是我机器上的;你的输出结果会有所不同。)
2 使用 getattr,你能够获得同一函数的同一引用。通常,getattr(object, "attribute") 等价于 object.attribute。如果 object 是一个模块的话,那么 attribute 可能是定义在模块中的任何东西:函数、类或者全局变量。
3 接下来的是你真正用在 info 函数中的东西。object 作为一个参数传递给函数; method 是方法或者函数的名称字符串。
4 在这个例子中,method 是函数的名称,通过获取 type 可以进行验证。
5 由于 method 是一个函数,所以它是可调用的

4.4.2. getattr 作为一个分发者

getattr 常见的使用模式是作为一个分发者。举个例子,如果你有一个程序可以以不同的格式输出数据,你可以为每种输出格式定义各自的格式输出函数,然后使用唯一的分发函数调用所需的格式输出函数。

例如,让我们假设有一个以 HTMLXML 和普通文本格式打印站点统计的程序。输出格式在命令行中指定,或者保存在配置文件中。statsout 模块定义了三个函数:output_htmloutput_xmloutput_text。然后主程序定义了唯一的输出函数,如下:

例 4.12. 使用getattr 创建分发者

import statsout

def output(data, format="text"):                              1
    output_function = getattr(statsout, "output_%s" % format) 2
    return output_function(data)                              3
1 output 函数接收一个必备参数 data,和一个可选参数 format。如果没有指定 format 参数,其缺省值是 text 并完成普通文本输出函数的调用。
2 你可以连接 format 参数值和 "output_" 来创建一个函数名称作为参数值,然后从 statsout 模块中取得该函数。这种方式允许今后很容易地扩展程序以支持其它的输出格式,而且无需修改分发函数。所要做的仅仅是向 statsout 中添加一个函数,比如 output_pdf,之后只要将 “pdf” 作为 format 的参数值传递给 output 函数即可。
3 现在你可以简单地调用输出函数,就像调用其它函数一样。output_function 变量是指向 statsout 模块中相应函数的引用。

你是否发现前面示例的一个 Bug?即字符串和函数之间的松耦合,而且没有错误检查。如果用户传入一个格式参数,但是在 statsout 中没有定义相应的格式输出函数,会发生什么呢?还好,getattr 会返回 None,它会取代一个有效函数并被赋值给 output_function,然后下一行调用函数的语句将会失败并抛出一个异常。这种方式不好。

值得庆幸的是,getattr 能够使用可选的第三个参数,一个缺省返回值。

例 4.13. getattr 缺省值

import statsout

def output(data, format="text"):
    output_function = getattr(statsout, "output_%s" % format, statsout.output_text)
    return output_function(data) 1
1 这个函数调用一定可以工作,因为你在调用 getattr 时添加了第三个参数。第三个参数是一个缺省返回值,如果第二个参数指定的属性或者方法没能找到,则将返回这个缺省返回值。

正如你所看到,getattr 是相当强大的。它是自省的核心,在后面的章节中你将看到它更强大的示例。

4.5. 过滤列表

如你所知,Python 具有通过列表解析 (第 3.6 节 “映射 list”) 将列表映射到其它列表的强大能力。这种能力同过滤机制结合使用,使列表中的有些元素被映射的同时跳过另外一些元素。

过滤列表语法:

[mapping-expression for element in source-list if filter-expression]

这是你所知所爱的列表解析的扩展。前三部分都是相同的;最后一部分,以 if 开头的是过滤器表达式。过滤器表达式可以是返回值为真或者假的任何表达式 (在 Python 中是几乎任何东西)。任何经过滤器表达式演算值为真的元素都可以包含在映射中。其它的元素都将忽略,它们不会进入映射表达式,更不会包含在输出列表中。

例 4.14. 列表过滤介绍

>>> li = ["a", "mpilgrim", "foo", "b", "c", "b", "d", "d"]
>>> [elem for elem in li if len(elem) > 1]       1
['mpilgrim', 'foo']
>>> [elem for elem in li if elem != "b"]         2
['a', 'mpilgrim', 'foo', 'c', 'd', 'd']
>>> [elem for elem in li if li.count(elem) == 1] 3
['a', 'mpilgrim', 'foo', 'c']
1 这里的映射表达式很简单 (只是返回每个元素的值),所以请把注意力集中到过滤器表达式上。由于 Python 会遍历整个列表,它将对每个元素执行过滤器表达式。如果过滤器表达式演算值为真,该元素就会被映射,同时映射表达式的结果将包含在返回的列表中。这里,你过滤掉了所有单字符的字符串,留下了一个由长字符串构成的列表。
2 这里你过滤掉了一个特定值 b。注意这个过滤器会过滤掉所有的 b,因为每次取出 b,过滤表达式都将为假。
3 count 是一个列表方法,返回某个值在列表中出现的次数。你可以认为这个过滤器将从列表中剔除重复元素,返回一个只包含了在原始列表中有着唯一值拷贝的列表。但并非如此,因为在原始列表中出现两次的值 (在本例中,bd) 被完全剔除了。从一个列表中排除重复值有多种方法,但过滤并不是其中的一种。

回到 apihelper.py 中的这一行:

    methodList = [method for method in dir(object) if callable(getattr(object, method))]

这行看上去挺复杂――确实也很复杂――但是基本结构都还是一样的。整个过滤表达式返回一个列表,并赋值给 methodList 变量。表达式的前半部分是列表映射部分。映射表达式是一个和遍历元素相同的表达式,因此它返回每个元素的值。dir(object) 返回 object 对象的属性和方法列表――你正在映射的列表。所以唯一新出现的部分就是在 if 后面的过滤表达式。

过滤表达式看上去很恐怖,其实不是。你已经知道了 callablegetattrin。正如你在前面的部分中看到的,如果 object 是一个模块,并且 method 是上述模块中某个函数的名称,那么表达式 getattr(object, method) 将返回一个函数对象。

所以这个表达式接收一个名为 object 的对象,然后得到它的属性、方法、函数和其他成员的名称列表,接着过滤掉我们不关心的成员。执行过滤行为是通过对每个属性/方法/函数的名称调用 getattr 函数取得实际成员的引用,然后检查这些成员对象是否是可调用的,当然这些可调用的成员对象可能是方法或者函数,同时也可能是内置的 (比如列表的 pop 方法) 或者用户自定义的 (比如 odbchelper 模块的 buildConnectionString 函数)。这里你不用关心其它的属性,如内置在每一个模块中的 __name__ 属性。

进一步阅读

4.6. andor 的特殊性质

Python 中,andor 执行布尔逻辑演算,如你所期待的一样。但是它们并不返回布尔值,而是返回它们实际进行比较的值之一。

例 4.15. and 介绍

>>> 'a' and 'b'         1
'b'
>>> '' and 'b'          2
''
>>> 'a' and 'b' and 'c' 3
'c'
1 使用 and 时,在布尔环境中从左到右演算表达式的值。0''[](){}None 在布尔环境中为假;其它任何东西都为真。还好,几乎是所有东西。默认情况下,布尔环境中的类实例为真,但是你可以在类中定义特定的方法使得类实例的演算值为假。你将会在第 5 章中了解到类和这些特殊方法。如果布尔环境中的所有值都为真,那么 and 返回最后一个值。在这个例子中,and 演算 'a' 的值为真,然后是 'b' 的演算值为真,最终返回 'b'
2 如果布尔环境中的某个值为假,则 and 返回第一个假值。在这个例子中,'' 是第一个假值。
3 所有值都为真,所以 and 返回最后一个真值,'c'

例 4.16. or 介绍

>>> 'a' or 'b'          1
'a'
>>> '' or 'b'           2
'b'
>>> '' or [] or {}      3
{}
>>> def sidefx():
...     print "in sidefx()"
...     return 1
>>> 'a' or sidefx()     4
'a'
1 使用 or 时,在布尔环境中从左到右演算值,就像 and 一样。如果有一个值为真,or 立刻返回该值。本例中,'a' 是第一个真值。
2 or 演算 '' 的值为假,然后演算 'b' 的值为真,于是返回 'b'
3 如果所有的值都为假,or 返回最后一个假值。or 演算 '' 的值为假,然后演算 [] 的值为假,依次演算 {} 的值为假,最终返回 {}
4 注意 or 在布尔环境中会一直进行表达式演算直到找到第一个真值,然后就会忽略剩余的比较值。如果某些值具有副作用,这种特性就非常重要了。在这里,函数 sidefx 永远都不会被调用,因为 or 演算 'a' 的值为真,所以紧接着就立刻返回 'a' 了。

如果你是一名 C 语言黑客,肯定很熟悉 bool ? a : b 表达式,如果 bool 为真,表达式演算值为 a,否则为 b。基于 Pythonandor 的工作方式,你可以完成相同的事情。

4.6.1. 使用 and-or 技巧

例 4.17. and-or 技巧介绍

>>> a = "first"
>>> b = "second"
>>> 1 and a or b 1
'first'
>>> 0 and a or b 2
'second'
1 这个语法看起来类似于 C 语言中的 bool ? a : b 表达式。整个表达式从左到右进行演算,所以先进行 and 表达式的演算。1 and 'first' 演算值为 'first',然后 'first' or 'second' 的演算值为 'first'
2 0 and 'first' 演算值为 False,然后 0 or 'second' 演算值为 'second'

然而,由于这种 Python 表达式单单只是进行布尔逻辑运算,并不是语言的特定构成,这是 and-or 技巧和 C 语言中的 bool ? a : b 语法非常重要的不同。如果 a 为假,表达式就不会按你期望的那样工作了。(你能知道我被这个问题折腾过吗?不止一次?)

例 4.18. and-or 技巧无效的场合

>>> a = ""
>>> b = "second"
>>> 1 and a or b         1
'second'
1 由于 a 是一个空字符串,在 Python 的布尔环境中空字符串被认为是假的,1 and '' 的演算值为 '',最后 '' or 'second' 的演算值为 'second'。噢!这个值并不是你想要的。

and-or 技巧,也就是 bool and a or b 表达式,当 a 在布尔环境中的值为假时,不会像 C 语言表达式 bool ? a : b 那样工作。

and-or 技巧后面真正的技巧是,确保 a 的值决不会为假。最常用的方式是使 a 成为 [a]b 成为 [b],然后使用返回值列表的第一个元素,应该是 ab中的某一个。

例 4.19. 安全使用 and-or 技巧

>>> a = ""
>>> b = "second"
>>> (1 and [a] or [b])[0] 1
''
1 由于 [a] 是一个非空列表,所以它决不会为假。即使 a0 或者 '' 或者其它假值,列表 [a] 也为真,因为它有一个元素。

到现在为止,这个技巧可能看上去问题超过了它的价值。毕竟,使用 if 语句可以完成相同的事情,那为什么要经历这些麻烦事呢?哦,在很多情况下,你要在两个常量值中进行选择,由于你知道 a 的值总是为真,所以你可以使用这种较为简单的语法而且不用担心。对于使用更为复杂的安全形式,依然有很好的理由要求这样做。例如,在 Python 语言的某些情况下 if 语句是不允许使用的,比如在 lambda 函数中。

进一步阅读

4.7. 使用 lambda 函数

Python 支持一种有趣的语法,它允许你快速定义单行的最小函数。这些叫做 lambda 的函数,是从 Lisp 借用来的,可以用在任何需要函数的地方。

例 4.20. lambda 函数介绍

>>> def f(x):
...     return x*2
...     
>>> f(3)
6
>>> g = lambda x: x*2  1
>>> g(3)
6
>>> (lambda x: x*2)(3) 2
6
1 这是一个 lambda 函数,完成同上面普通函数相同的事情。注意这里的简短的语法:在参数列表周围没有括号,而且忽略了 return 关键字 (隐含存在,因为整个函数只有一行)。而且,该函数没有函数名称,但是可以将它赋值给一个变量进行调用。
2 使用 lambda 函数时甚至不需要将它赋值给一个变量。这可能不是世上最有用的东西,它只是展示了 lambda 函数只是一个内联函数。

总的来说,lambda 函数可以接收任意多个参数 (包括可选参数) 并且返回单个表达式的值。lambda 函数不能包含命令,包含的表达式不能超过一个。不要试图向 lambda 函数中塞入太多的东西;如果你需要更复杂的东西,应该定义一个普通函数,然后想让它多长就多长。

注意
lambda 函数是一种风格问题。不一定非要使用它们;任何能够使用它们的地方,都可以定义一个单独的普通函数来进行替换。我将它们用在需要封装特殊的、非重用代码上,避免令我的代码充斥着大量单行函数。

4.7.1. 真实世界中的 lambda 函数

apihelper.py 中的 lambda 函数:

    processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)

注意这里使用了 and-or 技巧的简单形式,它是没问题的,因为 lambda 函数在布尔环境中总是为真。(这并不意味这 lambda 函数不能返回假值。这个函数对象的布尔值为真;它的返回值可以是任何东西。)

还要注意的是使用了没有参数的 split 函数。你已经看到过它带一个或者两个参数的使用,但是不带参数它按空白进行分割。

例 4.21. split 不带参数

>>> s = "this   is\na\ttest"  1
>>> print s
this   is
a	test
>>> print s.split()           2
['this', 'is', 'a', 'test']
>>> print " ".join(s.split()) 3
'this is a test'
1 这是一个多行字符串,通过使用转义字符的定义代替了三重引号\n 是一个回车,\t 是一个制表符。
2 不带参数的 split 按照空白进行分割。所以三个空格、一个回车和一个制表符都是一样的。
3 通过 split 分割字符串你可以将空格统一化;然后再以单个空格作为分隔符用 join 将其重新连接起来。这也就是 info 函数将多行 doc string 合并成单行所做的事情。

那么 info 函数到底用这些 lambda 函数、split 函数和 and-or 技巧做了些什么呢?

    processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)

processFunc 现在是一个函数,但是它到底是哪一个函数还要取决于 collapse 变量。如果 collapse 为真,processFunc(string) 将压缩空白;否则 processFunc(string) 将返回未改变的参数。

在一个不很健壮的语言中实现它,像 Visual Basic,你很有可能要创建一个函数,接受一个字符串参数和一个 collapse 参数,并使用 if 语句确定是否压缩空白,然后再返回相应的值。这种方式是低效的,因为函数可能需要处理每一种可能的情况。每次你调用它,它将不得不在给出你所想要的东西之前,判断是否要压缩空白。在 Python 中,你可以将决策逻辑拿到函数外面,而定义一个裁减过的 lambda 函数提供确切的 (唯一的) 你想要的。这种方式更为高效、更为优雅,而且很少引起那些令人讨厌 (哦,想到那些参数就头昏) 的错误。

lambda 函数进一步阅读

4.8. 全部放在一起

最后一行代码是唯一还没有解释过的,它完成全部的工作。但是现在工作已经简单了,因为所需要的每件事都已经按照需求建立好了。所有的多米诺骨牌已经就位,到了将它们推倒的时候了。

下面是 apihelper.py 的关键

    print "\n".join(["%s %s" %
                      (method.ljust(spacing),
                       processFunc(str(getattr(object, method).__doc__)))
                     for method in methodList])

注意这是一条命令,被分隔成了多行,但是并没有使用续行符 (\)。还记得我说过一些表达式可以分割成多行而不需要使用反斜线吗?列表解析就是这些表达式之一,因为整个表达式包括在方括号里。

现在,让我们从后向前分析。

for method in methodList

告诉我们这是一个列表解析。如你所知 methodListobject所有你关心的方法的一个列表。所以你正在使用 method 遍历列表。

例 4.22. 动态得到 doc string

>>> import odbchelper
>>> object = odbchelper                   1
>>> method = 'buildConnectionString'      2
>>> getattr(object, method)               3
<function buildConnectionString at 010D6D74>
>>> print getattr(object, method).__doc__ 4
Build a connection string from a dictionary of parameters.

    Returns string.
1 info 函数中,object 是要得到帮助的对象,作为一个参数传入。
2 在你遍历 methodList 时,method 是当前方法的名称。
3 通过 getattr 函数,你可以得到 object 模块中 method 函数的引用。
4 现在,很容易就可以打印出方法的 doc string

接下来令人困惑的是 doc string 周围 str 的使用。你可能记得,str 是一个内置函数,它可以强制将数据转化为字符串。但是一个 doc string 应该总是一个字符串,为什么还要费事地使用 str 函数呢?答案就是:不是每个函数都有 doc string ,如果没有,这个 __doc__ 属性为 None

例 4.23. 为什么对一个 doc string 使用 str

>>> >>> def foo(): print 2
>>> >>> foo()
2
>>> >>> foo.__doc__     1
>>> foo.__doc__ == None 2
True
>>> str(foo.__doc__)    3
'None'
1 你可以很容易的定义一个没有 doc string 的函数,这种情况下它的 __doc__ 属性为 None。令人迷惑的是,如果你直接演算 __doc__ 属性的值,Python IDE 什么都不会打印。这是有意义的 (前提是你考虑了这个结果的来由),但是却没有什么用。
2 你可以直接通过 __doc__ 属性和 None 的比较验证 __doc__ 属性的值。
3 str 函数可以接收值为 null 的参数,然后返回它的字符串表示,'None'
注意
SQL 中,你必须使用 IS NULL 代替 = NULL 进行 null 值比较。在 Python,你可以使用 == None 或者 is None 进行比较,但是 is None 更快。

现在你确保有了一个字符串,可以把这个字符串传给 processFunc,这个函数已经定义是一个既可以压缩空白也可以不压缩空白的函数。现在你看出来为什么使用 strNone 转化为一个字符串很重要了。processFunc 假设接收到一个字符串参数然后调用 split 方法,如果你传入 None ,将导致程序崩溃,因为 None 没有 split 方法。

再往回走一步,你再一次使用了字符串格式化来连接 processFunc 的返回值 和 methodljust 方法的返回值。ljust 是一个你之前没有见过的新字符串方法。

例 4.24. ljust 方法介绍

>>> s = 'buildConnectionString'
>>> s.ljust(30) 1
'buildConnectionString         '
>>> s.ljust(20) 2
'buildConnectionString'
1 ljust 用空格填充字符串以符合指定的长度。info 函数使用它生成了两列输出并将所有在第二列的 doc string 纵向对齐。
2 如果指定的长度小于字符串的长度,ljust 将简单地返回未变化的字符串。它决不会截断字符串。

几乎已经完成了。有了 ljust 方法填充过的方法名称和来自调用 processFunc 方法得到的 doc string (可能压缩过),你就可以将两者连接起来并得到单个字符串。因为对 methodList 进行了映射,最终你将获得一个字符串列表。利用 "\n"join 函数,将这个列表连接为单个字符串,列表中每个元素独占一行,接着打印出结果。

例 4.25. 打印列表

>>> li = ['a', 'b', 'c']
>>> print "\n".join(li) 1
a
b
c
1 在你处理列表时,这确实是一个有用的调试技巧。在 Python 中,你会十分频繁地操作列表。

上述就是最后一个令人困惑的地方了。但是现在你应该已经理解这段代码了。

    print "\n".join(["%s %s" %
                      (method.ljust(spacing),
                       processFunc(str(getattr(object, method).__doc__)))
                     for method in methodList])

4.9. 小结

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

在研究下一章前,确保你可以无困难的完成下面这些事情:

  • 可选和命名参数定义和调用函数
  • str 强制转换任意值为字符串形式
  • getattr 动态得到函数和其它属性的引用
  • 扩展列表解析语法实现列表过滤
  • 识别 and-or 技巧并安全地使用它
  • 定义 lambda 函数
  • 将函数赋值给变量然后通过引用变量调用函数。我强调的已经够多了:这种思考方式对于提高对 Python 的理解力至关重要。在本书中你会随处可见这种技术的更复杂的应用。


[1] 帮助者函数,原文是 helper function,也就是我们在前文所看到的诸如 odbchelperapihelper 这样的函数。――译注

第 5 章 对象和面向对象

这一章,和此后的许多章,均讨论了面向对象的 Python 程序设计。

5.1. 概览

下面是一个完整的,可运行的 Python 程序。请阅读模块、类和函数的 doc strings,可以大概了解这个程序所做的事情和工作情况。像平时一样,不用担心你不理解的东西,这就是本章其它部分将告诉你的内容。

例 5.1. 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"]): 1
        print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
        print
1 这个程序的输入要取决于你硬盘上的文件。为了得到有意义的输出,你应该修改目录路径指向你自已机器上的一个 MP3 文件目录。

下面就是从我的机器上得到的输出。你的输出将不一样,除非,由于某些令人吃惊的巧合,你与我有着共同的音乐品味。

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

5.2. 使用 from module import 导入模块

Python 有两种导入模块的方法。两种都有用,你应该知道什么时候使用哪一种方法。一种方法,import module,你已经在第 2.4 节 “万物皆对象”看过了。另一种方法完成同样的事情,但是它与第一种有着细微但重要的区别。

下面是 from module import 的基本语法:

from UserDict import UserDict

它与你所熟知的 import module 语法很相似,但是有一个重要的区别:UserDict 被直接导入到局部名字空间去了,所以它可以直接使用,而不需要加上模块名的限定。你可以导入独立的项或使用 from module import * 来导入所有东西。

注意
Python 中的 from module import *Perl 中的 use modulePython 中的 import modulePerl 中的 require module
注意
Python 中的 from module import *Java 中的 import module.*Python 中的 import moduleJava 中的 import module

例 5.2. import module vs. from module import

>>> import types
>>> types.FunctionType             1
<type 'function'>
>>> FunctionType                   2
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
NameError: There is no variable named 'FunctionType'
>>> from types import FunctionType 3
>>> FunctionType                   4
<type 'function'>
1 types 模块不包含方法,只是表示每种 Python 对象类型的属性。注意这个属性必需用模块名 types 进行限定。
2 FunctionType 本身没有被定义在当前名字空间中;它只存在于 types 的上下文环境中。
3 这个语法从 types 模块中直接将 FunctionType 属性导入到局部名字空间中。
4 现在 FunctionType 可以直接使用,与 types 无关了。

什么时候你应该使用 from module import

  • 如果你要经常访问模块的属性和方法,且不想一遍又一遍地敲入模块名,使用 from module import
  • 如果你想要有选择地导入某些属性和方法,而不想要其它的,使用 from module import
  • 如果模块包含的属性和方法与你的某个模块同名,你必须使用 import module 来避免名字冲突。

除了这些情况,剩下的只是风格问题了,你会看到用两种方式编写的 Python 代码。

小心
尽量少用 from module import * ,因为判定一个特殊的函数或属性是从哪来的有些困难,并且会造成调试和重构都更困难。

进一步阅读关于模块导入技术

5.3. 类的定义

Python 是完全面向对象的:你可以定义自已的类,从自已的或内置的类继承,然后从你定义的类创建实例。

Python 中定义类很简单。就像定义函数,没有单独的接口定义。只要定义类,然后就可以开始编码。Python 类以保留字 class 开始,后面跟着类名。从技术上讲,有这些就够了,因为一个类并非必须从其它类继承。

例 5.3. 最简单的 Python

class Loaf: 1
    pass    2 3
1 这个类的名字是 Loaf,它没有从其它类继承。类名通常是第一个字母大写,如:EachWordLikeThis,但这只是一个习惯,不是一个必要条件。
2 这个类没有定义任何方法或属性,但是从语法上,需要在定义中有些东西,所以你使用 pass。这是一个 Python 保留字,仅仅表示 “向前走,不要往这看”。它是一条什么都不做的语句,当你删空函数或类时,它是一个很好的占位符。
3 你可能猜到了,在类中的所有东西都要缩近,就像位于函数、if 语句,for 循环,诸如此类的代码。第一条不缩近的东西不属于这个类。
注意
Python 中的 pass 语句就像 JavaC 中的大括号空集 ({})。

当然,实际上大多数的类都是从其它的类继承来的,并且它们会定义自已的类方法和属性。但是就像你刚才看到的,除了名字以外,类没有什么必须要具有的。特别是,C++ 程序员可能会感到奇怪,Python 的类没有显示的构造函数和析构函数。Python 类的确存在与构造函数相似的东西:__init__ 方法。

例 5.4. 定义 FileInfo

from UserDict import UserDict

class FileInfo(UserDict): 1
1 Python 中,类的基类只是简单地列在类名后面的小括号里。所以 FileInfo 类是从 UserDict 类 (它是从 UserDict 模块导进来的) 继承来的。UserDict 是一个像字典一样工作的类,它允许你完全子类化字典数据类型,同时增加你自已的行为。{也存在相似的类 UserListUserString ,它们允许你子类化列表和字符串。)[2] 在这个类的背后有一些“巫术”,我们将在本章的后面,随着更进一步地研究 UserDict 类,揭开这些秘密。
注意
Python 中,类的基类只是简单地列在类名后面的小括号里。不像在 Java 中有一个特殊的 extends 关键字。

Python 支持多重继承。在类名后面的小括号中,你可以列出许多你想要的类名,以逗号分隔。

5.3.1. 初始化并开始类编码

本例演示了使用 __init__ 方法来进行 FileInfo 类的初始化。

例 5.5. 初始化 FileInfo

class FileInfo(UserDict):
    "store file metadata"              1
    def __init__(self, filename=None): 2 3 4
1 类也可以 (并且应该) 有 doc strings ,就像方法和函数一样。
2 __init__ 在类的实例创建后被立即调用。它可能会引诱你称之为类的构造函数,但这种说法并不正确。说它引诱,是因为它看上去像 (按照习惯,__init__ 是类中第一个定义的方法),行为也像 (在一个新创建的类实例中,它是首先被执行的代码),并且叫起来也像 (“init”当然意味着构造的本性)。说它不正确,是因为对象在调用 __init__ 时已经被构造出来了,你已经有了一个对类的新实例的有效引用。但 __init__ 是在 Python 中你可以得到的最接近构造函数的东西,并且它也扮演着非常相似的角色。
3 每个类方法的第一个参数,包括 __init__,都是指向类的当前实例的引用。按照习惯这个参数总是被称为 self。在 __init__ 方法中,self 指向新创建的对象;在其它的类方法中,它指向方法被调用的类实例。尽管当定义方法时你需要明确指定 self,但在调用方法时,你 用指定它,Python 会替你自动加上的。
4 __init__ 方法可以接受任意数目的参数,就像函数一样,参数可以用缺省值定义,即可以设置成对于调用者可选。在本例中,filename 有一个缺省值 None,即 Python 的空值。
注意
习惯上,任何 Python 类方法的第一个参数 (对当前实例的引用) 都叫做 self。这个参数扮演着 C++Java 中的保留字 this 的角色,但 selfPython 中并不是一个保留字,它只是一个命名习惯。虽然如此,也请除了 self 之外不要使用其它的名字,这是一个非常坚固的习惯。

例 5.6. 编写 FileInfo

class FileInfo(UserDict):
    "store file metadata"
    def __init__(self, filename=None):
        UserDict.__init__(self)        1
        self["name"] = filename        2
                                       3
1 一些伪面向对象语言,像 Powerbuilder 有一种“扩展”构造函数和其它事件的概念,即父类的方法在子类的方法执行前被自动调用。Python 不是这样,你必须显示地调用在父类中的合适方法。
2 我告诉过你,这个类像字典一样工作,那么这里就是第一个印象。我们将参数 filename 赋值给对象 name 关键字,作为它的值。
3 注意 __init__ 方法从不返回一个值。

5.3.2. 了解何时去使用 self__init__

当定义你自已的类方法时,你必须 明确将 self 作为每个方法的第一个参数列出,包括 __init__。当从你的类中调用一个父类的一个方法时,你必须包括 self 参数。但当你从类的外部调用你的类方法时,你不必对 self 参数指定任何值;你完全将其忽略,而 Python 会自动地替你增加实例的引用。我知道刚开始这有些混乱,它并不是自相矛盾的,因为它依靠于一个你还不了解的区别 (在绑定与非绑定方法之间),故看上去是矛盾的。

噢。我知道有很多知识需要吸收,但是你要掌握它。所有的 Python 类以相同的方式工作,所以一旦你学会了一个,就是学会了全部。如果你忘了别的任何事,也要记住这件事,因为我认定它会让你出错:

注意
__init__ 方法是可选的,但是一旦你定义了,就必须记得显示调用父类的 __init__ 方法 (如果它定义了的话)。这样更是正确的:无论何时子类想扩展父类的行为,后代方法必须在适当的时机,使用适当的参数,显式调用父类方法。

5.4. 类的实例化

Python 中对类进行实例化很直接。要对类进行实例化,只要调用类 (就好像它是一个函数),传入定义在 __init__ 方法中的参数。返回值将是新创建的对象。

例 5.7. 创建 FileInfo 实例

>>> import fileinfo
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") 1
>>> f.__class__                                        2
<class fileinfo.FileInfo at 010EC204>
>>> f.__doc__                                          3
'store file metadata'
>>> f                                                  4
{'name': '/music/_singles/kairo.mp3'}
1 你正在创建 FileInfo 类 (定义在 fileinfo 模块中) 的实例,并且将新创建的实例赋值给变量 f。你传入了一个参数,/music/_singles/kairo.mp3,它将最后作为在 FileInfo__init__ 方法中的 filename 参数。
2 每一个类的实例有一个内置属性,__class__,它是对象的类。(注意这个表示包括了在我机器上的实例的物理地址,你的表示不会一样。)Java 程序员可能对 Class 类熟悉,这个类包含了像 getNamegetSuperclass 之类用来得到一个对象元数据信息的方法。在 Python 中,这类元数据可以直接通过对象本身的属性,像 __class____name____bases__ 来得到。
3 你可以像对函数或模块一样来访问实例的 doc string。一个类的所有实例共享相同的 doc string
4 还记得什么时候 __init__ 方法将它的 filename 参数赋给 self["name"] 吗?哦,答案在这。在创建类实例时你传入的参数被正确发送到 __init__ 方法中 (当我们创建类实例时,我们所传递的参数被正确地发送给 __init__ 方法 (随同一起传递的还有对象的引用,self,它是由 Python 自动添加的)。
注意
Python 中,创建类的实例只要调用一个类,仿佛它是一个函数就行了。不像 C++Java 有一个明确的 new 操作符。

5.4.1. 垃圾回收

如果说创建一个新的实例是容易的,那么销毁它们甚至更容易。通常,不需要明确地释放实例,因为当指派给它们的变量超出作用域时,它们会被自动地释放。内存泄漏在 Python 中很少见。

例 5.8. 尝试实现内存泄漏

>>> def leakmem():
...     f = fileinfo.FileInfo('/music/_singles/kairo.mp3') 1
...     
>>> for i in range(100):
...     leakmem()                                          2
1 每次 leakmem 函数被调用,你创建了 FileInfo 的一个实例,将其赋给变量 f,这个变量是函数内的一个局部变量。然后函数结束时没有释放 f,所以你可能认为有内存泄漏,但是你错了。当函数结束时,局部变量 f 超出了作用域。在这个地方,不再有任何对 FileInfo 新创建实例的引用 (因为除了 f 我们从未将其赋值给其它变量),所以 Python 替我们销毁掉实例。
2 不管我们调用 leakmem 函数多少次,决不会泄漏内存,因为每一次,Python 将在从 leakmem 返回前销毁掉新创建的 FileInfo 类实例。

对于这种垃圾收集的方式,技术上的术语叫做“引用计数”。Python 维护着对每个实例的引用列表。在上面的例子中,只有一个 FileInfo 的实例引用:局部变量 f。当函数结束时,变量 f 超出作用域,所以引用计数降为 0,则 Python 自动销毁掉实例。

Python 的以前版本中,存在引用计数失败的情况,这样 Python 不能在后面进行清除。如果你创建两个实例,它们相互引用 (例如,双重链表,每一个结点有都一个指向列表中前一个和后一个结点的指针),任一个实例都不会被自动销毁,因为 Python (正确) 认为对于每个实例都存在一个引用。Python 2.0 有一种额外的垃圾回收方式,叫做“标记后清除”,它足够聪明,可以正确地清除循环引用。

作为曾经读过哲学专业的一员,让我感到困惑的是,当没有人对事物进行观察时,它们就消失了,但是这确实是在 Python 中所发生的。通常,你可以完全忘记内存管理,让 Python 在后面进行清理。

进一步阅读

5.5. 探索 UserDict:一个封装类

如你所见,FileInfo 是一个有着像字典一样的行为方式的类。为了进一步揭示这一点,让我们看一看在 UserDict 模块中的 UserDict 类,它是我们的 FileInfo 类的父类。它没有什么特别的,也是用 Python 写的,并且保存在一个 .py 文件里,就像我们其他的代码。特别之处在于,它保存在你的 Python 安装目录的 lib 目录下。

提示
在 Windows 下的 ActivePython IDE 中,你可以快速打开在你的库路径中的任何模块,使用 File->Locate... (Ctrl-L)。

例 5.9. 定义 UserDict

class UserDict:                                1
    def __init__(self, dict=None):             2
        self.data = {}                         3
        if dict is not None: self.update(dict) 4 5
1 注意 UserDict 是一个基类,不是从任何其他类继承而来。
2 这就是我们FileInfo 类中进行了覆盖__init__ 方法。注意这个父类的参数列表与子类不同。很好,每个子类可以拥有自已的参数集,只要使用正确的参数调用父类就可以了。这里父类有一个定义初始值的方法 (通过在 dict 参数中传入一个字典),这一方法我们的 FileInfo 没有用上。
3 Python 支持数据属性 (在 JavaPowerbuilder 中叫做 “实例变量”,在 C++ 中叫 “数据成员”),它是由某个特定的类实例所拥有的数据。在本例中,每个 UserDict 实例将拥有一个 data 数据属性。要从类外的代码引用这个属性,需要用实例的名字限定它,instance.data,限定的方法与你用模块的名字来限定函数一样。要在类的内部引用一个数据属性,我们使用 self 作为限定符。习惯上,所有的数据属性都在 __init__ 方法中初始化为有意义的值。然而,这并不是必须的,因为数据属性,像局部变量一样,当你首次赋给它值的时候突然产生
4 update 方法是一个字典复制器:它把一个字典中的键和值全部拷贝到另一个字典。这个操作并不 事先清空目标字典,如果一些键在目标字典中已经存在,则它们将被覆盖,那些键名在目标字典中不存在的则不改变。应该把 update 看作是合并函数,而不是复制函数。
5 这个语法你可能以前没看过 (我还没有在这本书中的例子中用过它)。这是一条 if 语句,但是没有在下一行有一个缩近块,而只是在冒号后面,在同一行上有单条语句。这完全是合法的,它只是当你在一个块中仅有一条语句时的一个简写。(它就像在 C++ 中没有用大括号包括的单行语句。) 你可以用这种语法,或者可以在后面的行写下缩近代码,但是不能对同一个块同时用两种方式。
注意
JavaPowerbuilder 支持通过参数列表的重载,也就是 一个类可以有同名的多个方法,但这些方法或者是参数个数不同,或者是参数的类型不同。其它语言 (最明显如 PL/SQL) 甚至支持通过参数名的重载,也就是 一个类可以有同名的多个方法,这些方法有相同类型,相同个数的参数,但参数名不同。Python 两种都不支持,总之是没有任何形式的函数重载。一个 __init__ 方法就是一个 __init__ 方法,不管它有什么样的参数。每个类只能有一个 __init__ 方法,并且如果一个子类拥有一个 __init__ 方法,它总是 覆盖父类的 __init__ 方法,甚至子类可以用不同的参数列表来定义它。
注意
Python 的原作者 Guido 是这样解释方法覆盖的:“子类可以覆盖父类中的方法。因为方法没有特殊的优先级设置,父类中的一个方法在调用同类中的另一方法时,可能其实调用到的却是一个子类中覆盖父类同名方法的方法。 (C++ 程序员可能会这样想:所有的 Python 方法都是虚函数。)”如果你不明白 (它令我颇感困惑),不必在意。我想我要跳过它。[3]
小心
应该总是在 __init__ 方法中给一个实例的所有数据属性赋予一个初始值。这样做将会节省你在后面调试的时间,不必为捕捉因使用未初始化 (也就是不存在) 的属性而导致的 AttributeError 异常费时费力。

例 5.10. UserDict 常规方法

    def clear(self): self.data.clear()          1
    def copy(self):                             2
        if self.__class__ is UserDict:          3
            return UserDict(self.data)         
        import copy                             4
        return copy.copy(self)                 
    def keys(self): return self.data.keys()     5
    def items(self): return self.data.items()  
    def values(self): return self.data.values()
1 clear 是一个普通的类方法,可以在任何时候被任何人公开调用。注意,clear 像所有的类方法一样 (常规的或专用的),使用 self 作为它的第一个参数。(记住,当你调用方法时,不用包括 self;这件事是 Python 替你做的。) 还应注意这个封装类的基本技术:将一个真正的字典 (data) 作为数据属性保存起来,定义所有真正字典所拥有的方法,并且将每个类方法重定向到真正字典上的相应方法。(你可能已经忘了,字典的 clear 方法删除它的所有关键字和关键字相应的值。)
2 真正字典的 copy 方法会返回一个新的字典,它是原始字典的原样的复制 (所有的键-值对都相同)。但是 UserDict 不能简单地重定向到 self.data.copy,因为那个方法返回一个真正的字典,而我们想要的是返回同一个类的一个新的实例,就像是 self
3 我们使用 __class__ 属性来查看 self 是否是一个 UserDict,如果是,太好了,因为我们知道如何拷贝一个 UserDict:只要创建一个新的 UserDict ,并传给它真正的字典,这个字典已经存放在 self.data 中了。然后你立即返回这个新的 UserDict,你甚至于不需要在下面一行中使用 import copy
4 如果 self.__class__ 不是 UserDict,那么 self 一定是 UserDict 的某个子类 (如可能为 FileInfo),生活总是存在意外。UserDict 不知道如何生成它的子类的一个原样的拷贝,例如,有可能在子类中定义了其它的数据属性,所以我们只能完全复制它们,确定拷贝了它们的全部内容。幸运的是,Python 带了一个模块可以正确地完成这件事,它叫做 copy。在这里我不想深入细节 (然而它是一个绝对酷的模块,你是否已经想到要自已研究它了呢?)。说 copy 能够拷贝任何 Python 对象就够了,这就是我们在这里用它的原因。
5 其余的方法是直截了当的重定向到 self.data 的内置函数上。
注意
Python 2.2 之前的版本中,你不可以直接子类化字符串、列表以及字典之类的内建数据类型。作为补偿,Python 提供封装类来模拟内建数据类型的行为,比如:UserStringUserListUserDict。通过混合使用普通和特殊方法,UserDict 类能十分出色地模仿字典。在 Python 2.2 和其后的版本中,你可以直接从 dict 内建数据类型继承。本书 fileinfo_fromdict.py 中有这方面的一个例子。

如例子中所示,在 Python 中,你可以直接继承自内建数据类型 dict,这样做有三点与 UserDict 不同。

例 5.11. 直接继承自内建数据类型 dict

class FileInfo(dict):                  1
    "store file metadata"
    def __init__(self, filename=None): 2
        self["name"] = filename
1 第一个区别是你不需要导入 UserDict 模块,因为 dict 是已经可以使用的内建数据类型。第二个区别是你不是继承自 UserDict.UserDict ,而是直接继承自 dict
2 第三个区别有些晦涩,但却很重要。UserDict 内部的工作方式要求你手工地调用它的 __init__ 方法去正确初始化它的内部数据结构。dict 并不这样工作,它不是一个封装所以不需要明确的初始化。

进一步阅读

5.6. 专用类方法

除了普通的类方法,Python 类还可以定义专用方法。专用方法是在特殊情况下或当使用特别语法时由 Python 替你调用的,而不是在代码中直接调用 (像普通的方法那样)。

就像你在上一节所看到的,普通的方法对在类中封装字典很有帮助。但是只有普通方法是不够的,因为除了对字典调用方法之外,还有很多事情可以做的。例如,你可以通过一种没有包括明确方法调用的语法来获得设置数据项。这就是专用方法产生的原因:它们提供了一种方法,可以将非方法调用语法映射到方法调用上。

5.6.1. 获得和设置数据项

例 5.12. __getitem__ 专用方法

    def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name") 1
'/music/_singles/kairo.mp3'
>>> f["name"]             2
'/music/_singles/kairo.mp3'
1 __getitem__ 专用方法很简单。像普通的方法 clearkeysvalues 一样,它只是重定向到字典,返回字典的值。但是怎么调用它呢?哦,你可以直接调用 __getitem__,但是在实际中你其实不会那样做:我在这里执行它只是要告诉你它是如何工作的。正确地使用 __getitem__ 的方法是让 Python 来替你调用。
2 这个看上去就像你用来得到一个字典值的语法,事实上它返回你期望的值。下面是隐藏起来的一个环节:暗地里,Python 已经将这个语法转化为 f.__getitem__("name") 的方法调用。这就是为什么 __getitem__ 是一个专用类方法的原因,不仅仅是你可以自已调用它,还可以通过使用正确的语法让 Python 来替你调用。

当然,Python 有一个与 __getitem__ 类似的 __setitem__ 专用方法,参见下面的例子。

例 5.13. __setitem__ 专用方法

    def __setitem__(self, key, item): self.data[key] = item
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__setitem__("genre", 31) 1
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32            2
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
1 __getitem__ 方法一样,__setitem__ 简单地重定向到真正的字典 self.data ,让它来进行工作。并且像 __getitem__ 一样,通常你不会直接调用它,当你使用了正确的语法,Python 会替你调用 __setitem__
2 这个看上去像正常的字典语法,当然除了 f 实际上是一个类,它尽可能地打扮成一个字典,并且 __setitem__ 是打扮的一个重点。这行代码实际上暗地里调用了 f.__setitem__("genre", 32)

__setitem__ 是一个专用类方法,因为它可以让 Python 来替你调用,但是它仍然是一个类方法。就像在 UserDict 中定义 __setitem__ 方法一样容易,我们可以在子类中重新定义它,对父类的方法进行覆盖。这就允许我们定义出在某些方面像字典一样动作的类,但是可以定义它自已的行为,超过和超出内置的字典。

这个概念是本章中我们正在学习的整个框架的基础。每个文件类型可以拥有一个处理器类,这些类知道如何从一个特殊的文类型得到元数据。只要知道了某些属性 (像文件名和位置),处理器类就知道如何自动地得到其它的属性。它的实现是通过覆盖 __setitem__ 方法,检查特别的关键字,然后当找到后加入额外的处理。

例如,MP3FileInfoFileInfo 的子类。在设置了一个 MP3FileInfo 类的 name 时,并不只是设置 name 关键字 (像父类 FileInfo 所做的),它还要在文件自身内进行搜索 MP3 的标记然后填充一整套关键字。下面的例子将展示其工作方式。

例 5.14. 在 MP3FileInfo 中覆盖 __setitem__

    def __setitem__(self, key, item):         1
        if key == "name" and item:            2
            self.__parse(item)                3
        FileInfo.__setitem__(self, key, item) 4
1 注意我们的 __setitem__ 方法严格按照与父类方法相同的形式进行定义。这一点很重要,因为 Python 将替你执行方法,而它希望这个函数用确定个数的参数进行定义。(从技术上说,参数的名字没有关系,只是个数。)
2 这里就是整个 MP3FileInfo 类的难点:如果给 name 关键字赋一个值,我们还想做些额外的事情。
3 我们对 name 所做的额外处理封装在了 __parse 方法中。这是定义在 MP3FileInfo 中的另一个类方法,则当我们调用它时,我们用 self 对其限定。仅是调用 __parse 将只会看成定义在类外的普通方法,调用 self.__parse 将会看成定义在类中的一个类方法。这不是什么新东西,你用同样的方法来引用数据属性
4 在做完我们额外的处理之后,我们需要调用父类的方法。记住,在 Python 中不会自动为你完成,需手工执行。注意,我们在调用直接父类,FileInfo,尽管它没有 __setitem__ 方法。没问题,因为 Python 将会沿着父类树走,直到它找到一个拥有我们正在调用方法的类,所以这行代码最终会找到并且调用定义在 UserDict 中的 __setitem__
注意
当在一个类中存取数据属性时,你需要限定属性名:self.attribute。当调用类中的其它方法时,你属要限定方法名:self.method

例 5.15. 设置 MP3FileInfoname

>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo()                   1
>>> mp3file
{'name':None}
>>> mp3file["name"] = "/music/_singles/kairo.mp3"      2
>>> 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" 3
>>> mp3file
{'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder', 
'name': '/music/_singles/sidewinder.mp3', 'year': '2000', 
'comment': 'http://mp3.com/cynicproject'}
1 首先,我们创建了一个 MP3FileInfo 的实例,没有传递给它文件名。(我们可以不用它,因为 __init__ 方法的 filename 参数是可选的。) 因为 MP3FileInfo 没有它自已的 __init__ 方法,Python 沿着父类树走,发现了 FileInfo__init__ 方法。这个 __init__ 方法手工调用了 UserDict__init__ 方法,然后设置 name 关键字为 filename,它为 None,因为我们还没有传入一个文件名。所以,mp3file 最初看上去像是有一个关键字的字典,name 的值为 None
2 现在真正有趣的开始了。设置 mp3filename 关键字触发了 MP3FileInfo 上的 __setitem__ 方法 (而不是 UserDict 的),这个方法注意到我们正在用一个真实的值来设置 name 关键字,接着调用 self.__parse。尽管我们完全还没有研究过 __parse 方法,从它的输出你可以看出,它设置了其它几个关键字:albumartistgenretitleyearcomment
3 修改 name 关键字将再次经受同样的处理过程:Python 调用 __setitem____setitem__调用 self.__parseself.__parse 设置其它所有的关键字。

5.7. 高级专用类方法

除了 __getitem____setitem__ 之外 Python 还有更多的专用函数。某些可以让你模拟出你甚至可能不知道的功能。

下面的例子将展示 UserDict 一些其他专用方法。

例 5.16. UserDict 中更多的专用方法

    def __repr__(self): return repr(self.data)     1
    def __cmp__(self, dict):                       2
        if isinstance(dict, UserDict):            
            return cmp(self.data, dict.data)      
        else:                                     
            return cmp(self.data, dict)           
    def __len__(self): return len(self.data)       3
    def __delitem__(self, key): del self.data[key] 4
1 __repr__ 是一个专用的方法,在当调用 repr(instance) 时被调用。repr 函数是一个内置函数,它返回一个对象的字符串表示。它可以用在任何对象上,不仅仅是类的实例。你已经对 repr 相当熟悉了,尽管你不知道它。在交互式窗口中,当你只敲入一个变量名,接着按ENTERPython 使用 repr 来显示变量的值。自已用一些数据来创建一个字典 d ,然后用 print repr(d) 来看一看吧。
2 __cmp__ 在比较类实例时被调用。通常,你可以通过使用 == 比较任意两个 Python 对象,不只是类实例。有一些规则,定义了何时内置数据类型被认为是相等的,例如,字典在有着全部相同的关键字和值时是相等的。对于类实例,你可以定义 __cmp__ 方法,自已编写比较逻辑,然后你可以使用 == 来比较你的类,Python 将会替你调用你的 __cmp__ 专用方法。
3 __len__ 在调用 len(instance) 时被调用。len 是一个内置函数,可以返回一个对象的长度。它可以用于任何被认为理应有长度的对象。字符串的 len 是它的字符个数;字典的 len 是它的关键字的个数;列表或序列的 len 是元素的个数。对于类实例,定义 __len__ 方法,接着自已编写长度的计算,然后调用 len(instance)Python 将替你调用你的 __len__ 专用方法。
4 __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__ 方法让一个类表现得像一个函数,允许你直接调用一个类实例。并且存在其它的专用函数,允许类拥有只读或只写数据属性,在后面的章节中我们会更多地谈到这些。

进一步阅读

5.8. 类属性介绍

你已经知道了数据属性,它们是被一个特定的类实例所拥有的变量。Python 也支持类属性,它们是由类本身所拥有的。

例 5.17. 类属性介绍

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            1
<class fileinfo.MP3FileInfo at 01257FDC>
>>> fileinfo.MP3FileInfo.tagDataMap 2
{'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()      3
>>> 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>)}
1 MP3FileInfo 是类本身,不是任何类的特别实例。
2 tagDataMap 是一个类属性:字面的意思,一个类的属性。它在创建任何类实例之前就有效了。
3 类属性既可以通过直接对类的引用,也可以通过对类的任意实例的引用来使用。
注意
Java 中,静态变量 (在 Python 中叫类属性) 和实例变量 (在 Python 中叫数据属性) 两者都是紧跟在类定义之后定义的 (一个有 static 关键字,一个没有)。在 Python 中,只有类属性可以定义在这里,数据属性定义在 __init__ 方法中。

类属性可以作为类级别的常量来使用 (这就是为什么我们在 MP3FileInfo 中使用它们),但是它们不是真正的常量。你也可以修改它们。

注意
Python 中没有常量。如果你试图努力的话什么都可以改变。这一点满足 Python 的核心原则之一:坏的行为应该被克服而不是被取缔。如果你真正想改变 None 的值,也可以做到,但当无法调试的时候别来找我。

例 5.18. 修改类属性

>>> class counter:
...     count = 0                     1
...     def __init__(self):
...         self.__class__.count += 1 2
...     
>>> counter
<class __main__.counter at 010EAECC>
>>> counter.count                     3
0
>>> c = counter()
>>> c.count                           4
1
>>> counter.count
1
>>> d = counter()                     5
>>> d.count
2
>>> c.count
2
>>> counter.count
2
1 countcounter 类的一个类属性。
2 __class__ 是每个类实例的一个内置属性 (也是每个类的)。它是一个类的引用,而 self 是一个类 (在本例中,是 counter 类) 的实例。
3 因为 count 是一个类属性,它可以在我们创建任何类实例之前,通过直接对类引用而得到。
4 创建一个类实例会调用 __init__ 方法,它会给类属性 count1。这样会影响到类自身,不只是新创建的实例。
5 创建第二个实例将再次增加类属性 count。注意类属性是如何被类和所有类实例所共享的。

5.9. 私有函数

与大多数语言一样,Python 也有私有的概念:

  • 私有函数不可以从它们的模块外面被调用
  • 私有类方法不能够从它们的类外面被调用
  • 私有属性不能够从它们的类外面被访问

与大多数的语言不同,一个 Python 函数,方法,或属性是私有还是公有,完全取决于它的名字。

如果一个 Python 函数,类方法,或属性的名字以两个下划线开始 (但不是结束),它是私有的;其它所有的都是公有的。 Python 没有类方法保护 的概念 (只能用于它们自已的类和子类中)。类方法或者是私有 (只能在它们自已的类中使用) 或者是公有 (任何地方都可使用)。

MP3FileInfo 中,有两个方法:__parse__setitem__。正如我们已经讨论过的,__setitem__ 是一个专有方法;通常,你不直接调用它,而是通过在一个类上使用字典语法来调用,但它是公有的,并且如果有一个真正好的理由,你可以直接调用它 (甚至从 fileinfo 模块的外面)。然而,__parse 是私有的,因为在它的名字前面有两个下划线。

注意
Python 中,所有的专用方法 (像 __setitem__) 和内置属性 (像 __doc__) 遵守一个标准的命名习惯:开始和结束都有两个下划线。不要对你自已的方法和属性用这种方法命名;到最后,它只会把你 (或其它人) 搞乱。

例 5.19. 尝试调用一个私有方法

>>> import fileinfo
>>> m = fileinfo.MP3FileInfo()
>>> m.__parse("/music/_singles/kairo.mp3") 1
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'MP3FileInfo' instance has no attribute '__parse'
1 如果你试图调用一个私有方法,Python 将引发一个有些误导的异常,宣称那个方法不存在。当然它确实存在,但是它是私有的,所以在类外是不可使用的。严格地说,私有方法在它们的类外是可以访问的,只是不容易 处理。在 Python 中没有什么是真正私有的;在内部,私有方法和属性的名字被忽然改变和恢复,以致于使得它们看上去用它们给定的名字是无法使用的。你可以通过 _MP3FileInfo__parse 名字来使用 MP3FileInfo 类的 __parse 方法。知道了这个方法很有趣,然后要保证决不在真正的代码中使用它。私有方法由于某种原因而私有,但是像其它很多在 Python 中的东西一样,它们的私有化基本上是习惯问题,而不是强迫的。

进一步阅读

5.10. 小结

实打实的对象把戏到此为止。你将在 第 12 章 中看到一个真实世界应用程序的专有类方法,它使用 getattr 创建一个到远程 Web 服务的代理。

下一章将继续使用本章的例程探索其他 Python 的概念,例如:异常、文件对象 和 for 循环。

在研究下一章之前,确保你可以无困难地完成下面的事情:



[2] 在 2.2 之后已经可以从 dict、list 来派生子类了,关于这一点作者在后文也会提到。――译注

[3] 实际上,这一点并不是那么难以理解。考虑两个类,basechildbase 中的方法 a 需要调用 self.b;而我们又在 child 中覆盖了方法 b。然后我们创建一个 child 的实例,ch。调用 ch.a,那么此时的方法 a 调用的 b 函数将不是 base.b,而是 child.b。――译注

第 6 章 异常和文件处理

在本章中,将研究异常、文件对象、for 循环、ossys 模块等内容。如果你已经在其它编程语言中使用过异常,你可以简单看看第一部分来了解 Python 的语法。但是本章其它的内容仍需仔细研读。

6.1. 异常处理

与许多面向对象语言一样,Python 具有异常处理,通过使用 try...except 块来实现。

注意
Python 使用 try...except 来处理异常,使用 raise 来引发异常。JavaC++ 使用 try...catch 来处理异常,使用 throw 来引发异常。

异常在 Python 中无处不在;实际上在标准 Python 库中的每个模块都使用了它们,并且 Python 自已会在许多不同的情况下引发它们。在整本书中你已经再三看到它们了。

在这些情况下,我们都在简单地使用 Python IDE:一个错误发生了,异常被打印出来 (取决于你的 IDE,可能会有意地以一种刺眼的红色形式表示),这便是。这叫做未处理 异常;当异常被引发时,没有代码来明确地关注和处理它,所以异常被传给置在 Python 中的缺省的处理,它会输出一些调试信息并且终止运行。在 IDE 中,这不是什么大事,但是如果发生在你真正的 Python 程序运行的时候,整个程序将会终止。

然而,一个异常不一定会引起程序的完全崩溃。当异常引发时,可以被处理 掉。有时候一个异常实际是因为代码中的 bug (比如使用一个不存在的变量),但是许多时候,一个异常是可以预见的。如果你打开一个文件,它可能不存在。如果你连接一个数据库,它可能不可连接或没有访问所需的正确的安全证书。如果知道一行代码可能会引发异常,你应该使用一个 try...except 块来处理异常。

例 6.1. 打开一个不存在的文件

>>> fsock = open("/notthere", "r")      1
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
IOError: [Errno 2] No such file or directory: '/notthere'
>>> try:
...     fsock = open("/notthere")       2
... except IOError:                     3
...     print "The file does not exist, exiting gracefully"
... print "This line will always print" 4
The file does not exist, exiting gracefully
This line will always print
1 使用内置 open 函数,我们可以试着打开一个文件来读取 (在下一节有关于 open 的更多内容)。但是那个文件不存在,所以这样就引发 IOError 异常。因为我们没有提供任何显式的对 IOError 异常的检查,Python 仅仅打印出某个关于发生了什么的调试信息,然后终止。
2 我们试图打开同样不存在的文件,但是这次我们在一个 try...except 内来执行它。
3 open 方法引发 IOError 异常时,我们已经准备好处理它了。except IOError: 行捕捉异常,接着执行我们自已的代码块,这个代码块在本例中只是打印出更令人愉快的错误信息。
4 一旦异常被处理了,处理通常在 try...except 块之后的第一行继续进行。注意这一行将总是打印出来,无论异常是否发生。如果在你的根目录下确实有一个叫 notthere 的文件,对 open 的调用将成功,except 子句将忽略,并且最后一行仍将执行。

异常可能看上去不友好 (毕竟,如果你不捕捉异常,整个程序将崩溃),但是考虑一下别的方法。你该不会希望获得一个指向不存在的文件的对象吧?不管怎么样你都得检查它的有效性,而且如果你忘记了,你的程序将会在下面某个地方给出奇怪的错误,这样你将不得不追溯到源程序。我确信你做过这种事;这可并不有趣。使用异常,一发生错误,你就可以在问题的源头通过标准的方法来处理它们。

6.1.1. 为其他用途使用异常

除了处理实际的错误条件之外,对于异常还有许多其它的用处。在标准 Python 库中一个普通的用法就是试着导入一个模块,然后检查是否它能使用。导入一个并不存在的模块将引发一个 ImportError 异常。你可以使用这种方法来定义多级别的功能――依靠在运行时哪个模块是有效的,或支持多种平台 (即平台特定代码被分离到不同的模块中)。

你也能通过创建一个从内置的 Exception 类继承的类定义你自己的异常,然后使用 raise 命令引发你的异常。如果你对此感兴趣,请看进一步阅读的部分。

下面的例子演示了如何使用异常支持特定平台功能。代码来自 getpass 模块,一个从用户获得口令的封装模块。获得口令在 UNIX、Windows 和 Mac OS 平台上的实现是不同的,但是这个代码封装了所有的不同之处。

例 6.2. 支持特定平台功能

  # Bind the name getpass to the appropriate function
  try:
      import termios, TERMIOS                     1
  except ImportError:
      try:
          import msvcrt                           2
      except ImportError:
          try:
              from EasyDialogs import AskPassword 3
          except ImportError:
              getpass = default_getpass           4
          else:                                   5
              getpass = AskPassword
      else:
          getpass = win_getpass
  else:
      getpass = unix_getpass
1 termiosUNIX 独有的一个模块,它提供了对于输入终端的底层控制。如果这个模块无效 (因为它不在你的系统上,或你的系统不支持它),则导入失败,Python 引发我们捕捉的 ImportError 异常。
2 OK,我们没有 termios,所以让我们试试 msvcrt,它是 Windows 独有的一个模块,可以提供在 Microsoft Visual C++ 运行服务中的许多有用的函数的一个API。如果导入失败,Python 会引发我们捕捉的 ImportError 异常。
3 如果前两个不能工作,我们试着从 EasyDialogs 导入一个函数,它是 Mac OS 独有的一个模块,提供了各种各样类型的弹出对话框。再一次,如果导入失败,Python 会引发一个我们捕捉的 ImportError 异常。
4 这些平台特定的模块没有一个有效 (有可能,因为 Python 已经移植到了许多不同的平台上了),所以我们需要回头使用一个缺省口令输入函数 (这个函数定义在 getpass 模块中的别的地方)。注意我们在这里所做的:我们将函数 default_getpass 赋给变量 getpass。如果你读了官方 getpass 文档,它会告诉你 getpass 模块定义了一个 getpass 函数。它是这样做的:通过绑定 getpass 到正确的函数来适应你的平台。然后当你调用 getpass 函数时,你实际上调用了平台特定的函数,是这段代码已经为你设置好的。你不需要知道或关心你的代码正运行在何种平台上;只要调用 getpass,则它总能正确处理。
5 一个 try...except 块可以有一条 else 子句,就像 if 语句。如果在 try 块中没有异常引发,然后 else 子句被执行。在本例中,那就意味着如果 from EasyDialogs import AskPassword 导入可工作,所以我们应该绑定 getpassAskPassword 函数。其它每个 try...except 块有着相似的 else 子句,当我们发现一个 import 可用时,就绑定 getpass 到适合的函数。

进一步阅读

6.2. 与文件对象共事

Python 有一个内置函数,open,用来打开在磁盘上的文件。open 返回一个文件对象,它拥有一些方法和属性,可以得到被打开文件的信息,以及对被打开文件进行操作。

例 6.3. 打开文件

>>> f = open("/music/_singles/kairo.mp3", "rb") 1
>>> f                                           2
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.mode                                      3
'rb'
>>> f.name                                      4
'/music/_singles/kairo.mp3'
1 open 方法可以接收三个参数:文件名、模式和缓冲区参数。只有第一个参数 (文件名) 是必须的;其它两个是可选的。如果没有指定,文件以文本方式打开。这里我们以二进制方式打开文件进行读取。(print open.__doc__ 会给出所有可能模式的很好的解释。)
2 open 函数返回一个对象 (到现在为止,这一点应该不会使你感到吃惊)。一个文件对象有几个有用的属性。
3 文件对象的 mode 属性告诉你文件以何种模式被打开。
4 文件对象的 name 属性告诉你文件对象所打开的文件名。

6.2.1. 读取文件

你打开文件之后,你要做的第一件事是从中读取,正如下一个例子所展示的。

例 6.4. 读取文件

>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.tell()              1
0
>>> f.seek(-128, 2)       2
>>> f.tell()              3
7542909
>>> tagData = f.read(128) 4
>>> tagData
'TAGKAIRO****THE BEST GOA         ***DJ MARY-JANE***            
Rave Mix                      2000http://mp3.com/DJMARYJANE     \037'
>>> f.tell()              5
7543037
1 一个文件对象维护它所打开文件的状态。文件对象的 tell 方法告诉你在被打开文件中的当前位置。因为我们还没有对这个文件做任何事,当前位置为 0,它是文件的起始处。
2 文件对象的 seek 方法在被打开文件中移动到另一个位置。第二个参数指出第一个参数是什么意思:0 表示移动到一个绝对位置 (从文件起始处算起),1 表示移到一个相对位置 (从当前位置算起),还有 2 表示相对于文件尾的位置。因为我们搜索的 MP3 标记保存在文件的末尾,我们使用 2 并且告诉文件对象从文件尾移动到 128 字节的位置。
3 tell 方法确认了当前位置已经移动了。
4 read 方法从被打开文件中读取指定个数的字节,并且返回含有读取数据的字符串。可选参数指定了读取的最大字节数。如果没有指定参数,read 将读到文件末尾。(我们本可以在这里简单地说 read() ,因为我们确切地知道在文件的何处,事实上,我们读的是最后 128 个字节。) 读出的数据赋给变量 tagData,并且当前的位置根据所读的字节数作了修改。
5 tell 方法确认了当前位置已经移动了。如果做一下算术,你会看到在读了 128 个字节之后,位置数已经增加了 128。

6.2.2. 关闭文件

打开文件消耗系统资源,并且其间其它程序可能无法访问它们 (取决于文件模式)。这就是一旦操作完毕就该关闭文件的重要所在。

例 6.5. 关闭文件

>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed       1
False
>>> f.close()      2
>>> f
<closed file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed       3
True
>>> f.seek(0)      4
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()      5
1 文件对象的 closed 属性表示对象是打开还是关闭了文件。在本例中,文件仍然打开着 (closedFalse)。
2 为了关闭文件,调用文件对象的 close 方法。这样就释放掉你加在文件上的锁 (如果有的话),刷新被缓冲的系统还未写入的输出 (如果有的话),并且释放系统资源。
3 closed 属性证实了文件被关闭了。
4 文件被关闭了,但这并不意味着文件对象不再存在。变量 f 将继续存在,直到它超出作用域或被手工删除。然而,一旦文件被关闭,操作它的方法就没有一个能使用;它们都会引发异常。
5 对一个文件已经关闭的文件对象调用 close 不会 引发异常,它静静地失败。

6.2.3. 处理 I/O 错误

现在你已经足能理解前一章的例子程序 fileinfo.py 的文件处理代码了。下面这个例子展示了如何安全地打开文件和读取文件,以及优美地处理错误。

例 6.6. MP3FileInfo 中的文件对象

        try:                                1
            fsock = open(filename, "rb", 0) 2
            try:                           
                fsock.seek(-128, 2)         3
                tagdata = fsock.read(128)   4
            finally:                        5
                fsock.close()              
            .
            .
            .
        except IOError:                     6
            pass                           
1 因为打开和读取文件有风险,并且可能引发异常,所有这些代码都用一个 try...except 块封装。(嘿,标准化的缩近不好吗?这就是你开始欣赏它的地方。)
2 open 函数可能引发 IOError 异常。(可能是文件不存在。)
3 seek 方法可能引发 IOError 异常。(可能是文件长度小于 128 字节。)
4 read 方法可能引发 IOError 异常。(可能磁盘有坏扇区,或它在一个网络驱动器上,而网络刚好断了。)
5 这是新的:一个 try...finally 块。一旦文件通过 open 函数被成功地打开,我们应该绝对保证把它关闭,即使是在 seekread 方法引发了一个异常时。try...finally 块可以用来:在 finally 块中的代码将总是 被执行,甚至某些东西在 try 块中引发一个异常也会执行。可以这样考虑,不管在路上发生什么,代码都会被 “即将灭亡” 地执行。
6 最后,处理我们的 IOError 异常。它可能是由调用 openseekread 引发的 IOError 异常。这里,我们其实不用关心,因为将要做的事就是静静地忽略它然后继续。(记住,pass 是一条不做任何事的 Python 语句。) 这样完全合法,“处理” 一个异常可以明确表示不做任何事。它仍然被认为处理过了,并且处理将正常继续,从 try...except 块的下一行代码开始。

6.2.4. 写入文件

正如你所期待的,你也能用与读取文件同样的方式写入文件。有两种基本的文件模式:

  • 追加 (Append) 模式将数据追加到文件尾。
  • 写入 (write) 模式将覆盖文件的原有内容。

如果文件还不存在,任意一种模式都将自动创建文件,因此从来不需要任何复杂的逻辑:“如果 log 文件还不存在,将创建一个新的空文件,正因为如此,你可以第一次就打开它”。打开文件并开始写就可以了。

例 6.7. 写入文件

>>> logfile = open('test.log', 'w') 1
>>> logfile.write('test succeeded') 2
>>> logfile.close()
>>> print file('test.log').read()   3
test succeeded
>>> logfile = open('test.log', 'a') 4
>>> logfile.write('line 2')
>>> logfile.close()
>>> print file('test.log').read()   5
test succeededline 2
1 你可以大胆地开始创建新文件 test.log 或覆盖现有文件,并为写入目的而打开它。(第二个参数 "w" 的意思是为文件写入而打开。) 是的,它和想象中的一样危险。我希望你不要关心文件以前的内容,因为它现在已经不存在了。
2 你可以使用 open 返回的文件对象的 write 方法向一个新打开的文件添加数据。
3 fileopen 的同义语。这一行语句打开文件,读取内容,并打印它们。
4 碰巧你知道 test.log 存在 (因为你刚向它写完了数据),所以你可以打开它并向其追加数据。("a" 参数的意思是为追加目的打开文件。) 实际上即使文件不存在你也可以这样做,因为以追加方式打开一文件时,如果需要的话会创建文件。但是追加操作从不 损坏文件的现有内容。
5 正如你所看到的,原来的行和你以追加方式写入的第二行现在都在 test.log 中了。同时注意两行之间并没包含回车符。因为两次写入文件时都没有明确地写入回车符,所以文件中没有包含回车符。你可以用 "\n" 写入回车符。因为你没做这项工作,所以你写到文件的所有内容都将显示在同一行上。

进一步阅读

6.3. for 循环

与其它大多数语言一样,Python 也拥有 for 循环。你到现在还未曾看到它们的唯一原因就是,Python 在其它太多的方面表现出色,通常你不需要它们。

其它大多数语言没有像 Python 一样的强大的 list 数据类型,所以你需要亲自做很多事情,指定开始,结束和步长,来定义一定范围的整数或字符或其它可重复的实体。但是在 Python 中,for 循环简单地在一个列表上循环,与 list 解析的工作方式相同。

例 6.8. for 循环介绍

>>> li = ['a', 'b', 'e']
>>> for s in li:         1
...     print s          2
a
b
e
>>> print "\n".join(li)  3
a
b
e
1 for 循环的语法同 list 解析相似。li 是一个 list,而 s 将从第一个元素开始依次接收每个元素的值。
2 if 语句或其它任意缩进块for 循环可以包含任意数目的代码行。
3 这就是你以前没看到过 for 循环的原因:至今我们都不需要它。太令人吃惊了,当你想要的只是一个 join 或是 list 解析时,在其它语言中常常需要使用 for 循环。

要做一个 “通常的” (Visual Basic 标准的) 计数 for 循环也非常简单。

例 6.9. 简单计数

>>> for i in range(5):             1
...     print i
0
1
2
3
4
>>> li = ['a', 'b', 'c', 'd', 'e']
>>> for i in range(len(li)):       2
...     print li[i]
a
b
c
d
e
1 正如你在 例 3.20 “连续值赋值” 所看到的,range 生成一个整数的 list,通过它来控制循环。我知道它看上去有些奇怪,但是它对计数循环偶尔 (我只是说偶尔) 会有用 。
2 我们从来没这么用过。这是 Visual Basic 的思维风格。摆脱它吧。正确遍历 list 的方法是前面的例子所展示的。

for 循环不仅仅用于简单计数。它们可以遍历任何类型的东西。下面的例子是一个用 for 循环遍历 dictionary 的例子。

例 6.10. 遍历 dictionary

>>> import os
>>> for k, v in os.environ.items():      1 2
...     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()]) 3
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim

[...略...]
1 os.environ 是在你的系统上所定义的环境变量的 dictionary。在 Windows 下,这些变量是可以从 MS-DOS 访问的用户和系统变量。在 UNIX 下,它们是在你的 shell 启动脚本中所 export (输出) 的变量。在 Mac OS 中,没有环境变量的概念,所以这个 dictionary 为空。
2 os.environ.items() 返回一个 tuple 的 list:[(key1, value1), (key2, value2), ...]for 循环对这个 list 进行遍历。第一轮,它将 key1 赋给 kvalue1 赋给 v,所以 k = USERPROFILEv = C:\Documents and Settings\mpilgrim。第二轮,k 得到第二个键字 OSv 得到相应的值 Windows_NT
3 使用多变量赋值list 解析,你可以使用单行语句来替换整个 for 循环。在实际的编码中是否这样做只是个人风格问题;我喜欢它是因为,将一个 dictionary 映射到一个 list,然后将 list 合并成一个字符串,这一过程显得很清晰。其它的程序员宁愿将其写成一个 for 循环。请注意在两种情况下输出是一样的,然而这一版本稍微快一些,因为它只有一条 print 语句而不是许多。

现在我们来看看在 第 5 章 介绍的样例程序 fileinfo.pyMP3FileInfofor 循环 。

例 6.11. 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)}                               1
    .
    .
    .
            if tagdata[:3] == "TAG":
                for tag, (start, end, parseFunc) in self.tagDataMap.items(): 2
                    self[tag] = parseFunc(tagdata[start:end])                3
1 tagDataMap 是一个类属性,它定义了我们正在一个 MP3 文件中搜索的标记。标记存储为定长字段,只要我们读出文件最后 128 个字节,那么第 3 到 32 字节总是歌曲的名字,33-62 总是歌手的名字,63-92 为专辑的名字,等等。请注意 tagDataMap 是一个 tuple 的 dictionary,每个 tuple 包含两个整数和一个函数引用。
2 这个看上去复杂一些,但其实并非如此。这里的 for 变量结构与 items 所返回的 list 的元素的结构相匹配。记住,items 返回一个形如 (key, value) 的 tuple 的 list。list 第一个元素是 ("title", (3, 33, <function stripnulls>)),所以循环的第一轮,tag"title"start3end33parseFunc 为函数 stripnulls
3 现在我们已经从一个单个的 MP3 标记中提取出了所有的参数,将标记数据保存起来挺容易。我们从 startendtagdata 进行分片,从而得到这个标记的实际数据,调用 parseFunc 对数据进行后续的处理,接着将 parseFunc 的返回值作为值赋值给伪字典 self 中的键字 tag。在遍历完 tagDataMap 中所有元素之后,self 拥有了所有标记的值,你知道看上去是什么样

6.4. 使用 sys.modules

与其它任何 Python 的东西一样,模块也是对象。只要导入了,总可以用全局 dictionary sys.modules 来得到一个模块的引用。

例 6.12. sys.modules 介绍

>>> import sys                          1
>>> print '\n'.join(sys.modules.keys()) 2
win32api
os.path
os
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat
1 sys 模块包含了系统级的信息,像正在运行的 Python 的版本 (sys.versionsys.version_info),和系统级选项,像最大允许递归的深度 (sys.getrecursionlimit()sys.setrecursionlimit())。
2 sys.modules 是一个字典,它包含了从 Python 开始运行起,被导入的所有模块。键字就是模块名,键值就是模块对象。请注意除了你的程序导入的模块外还有其它模块。Python 在启动时预先装入了一些模块,如果你在一个 Python IDE 环境下,sys.modules 包含了你在 IDE 中运行的所有程序所导入的所有模块。

下面的例子展示了如何使用 sys.modules

例 6.13. 使用 sys.modules

>>> import fileinfo         1
>>> 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"] 2
<module 'fileinfo' from 'fileinfo.pyc'>
1 当导入新的模块,它们加入到 sys.modules 中。这就解释了为什么第二次导入相同的模块时非常的快:Python 已经在 sys.modules 中装入和缓冲了,所以第二次导入仅仅对字典做了一个查询。
2 一旦给出任何以前导入过的模块名 (以字符串方式),通过 sys.modules 字典,你可以得到对模块本身的一个引用。

下面的例子将展示通过结合使用 __module__ 类属性和 sys.modules dictionary 来获取已知类所在的模块。

例 6.14. __module__ 类属性

>>> from fileinfo import MP3FileInfo
>>> MP3FileInfo.__module__              1
'fileinfo'
>>> sys.modules[MP3FileInfo.__module__] 2
<module 'fileinfo' from 'fileinfo.pyc'>
1 每个 Python 类都拥有一个内置的类属性 __module__,它定义了这个类的模块的名字。
2 将它与 sys.modules 字典复合使用,你可以得到定义了某个类的模块的引用。

现在准备好了,看看在样例程序 第 5 章 sys.modules 介绍的 fileinfo.py 中是如何使用的。这个例子显示它的一部分代码。

例 6.15. fileinfo.py 中的 sys.modules

    def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):       1
        "get file info class from filename extension"                             
        subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]        2
        return hasattr(module, subclass) and getattr(module, subclass) or FileInfo 3
1 这是一个有两个参数的函数;filename 是必须的,但 module可选的并且 module 的缺省值包含了 FileInfo 类。这样看上去效率低,因为你可能认为 Python 会在每次函数调用时计算这个 sys.modules 表达式。实际上,Python 仅会对缺省表达式计算一次,是在模块导入的第一次。正如后面我们会看到的,我们永远不会用一个 module 参数来调用这个函数,所以 module 的功能是作为一个函数级别的常量。
2 我们会在后面再仔细研究这一行,在我们了解了 os 模块之后。那么现在,只要相信 subclass 最终为一个类的名字就行了,像 MP3FileInfo
3 你已经了解了 getattr,它可以通过名字得到一个对象的引用。hasattr 是一个补充性的函数,用来检查一个对象是否具有一个特定的属性;在本例中,用来检查一个模块是否有一个特别的类 (然而它可以用于任何类和任何属性,就像 getattr)。用英语来说,这行代码是说,“If this module has the class named by subclass then return it, otherwise return the base class FileInfo (如果这个模块有一个名为 subclass 的类,那么返回它,否则返回基类 FileInfo)”。

6.5. 与目录共事

os.path 模块有几个操作文件和目录的函数。这里,我们看看如何操作路径名和列出一个目录的内容。

例 6.16. 构造路径名

>>> import os
>>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3") 1 2
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.join("c:\\music\\ap", "mahadeva.mp3")   3
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.expanduser("~")                         4
'c:\\Documents and Settings\\mpilgrim\\My Documents'
>>> os.path.join(os.path.expanduser("~"), "Python") 5
'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'
1 os.path 是一个模块的引用;使用哪一个模块要看你正运行在哪种平台上。就像 getpass 通过将 getpass 设置为一个与平台相关的函数从而封装了平台之间的不同。os 通过设置 path 封装不同的相关平台模块。
2 os.pathjoin 函数把一个或多个部分路径名连接成一个路径名。在这个简单的例子中,它只是将字符串进行连接。(请注意在 Windows 下处理路径名是一个麻烦的事,因为反斜线字符必须被转义。)
3 在这个几乎没有价值的例子中,在将路径名加到文件名上之前,join 将在路径名后添加额外的反斜线。当发现这一点时我高兴极了,因为当用一种新的语言创建我自已的工具包时,addSlashIfNecessary 总是我必须要写的那些愚蠢的小函数之一。在 Python不要 写这样的愚蠢的小函数,聪明的人已经为你考虑到了。
4 expanduser 将对使用 ~ 来表示当前用户根目录的路径名进行扩展。在任何平台上,只要用户拥有一个根目录,它就会有效,像 Windows、UNIXMac OS X,但在 Mac OS 上无效。
5 将这些技术组合在一起,你可以容易地为在用户根目录下的目录和文件构造出路径名。

例 6.17. 分割路径名

>>> os.path.split("c:\\music\\ap\\mahadeva.mp3")                        1
('c:\\music\\ap', 'mahadeva.mp3')
>>> (filepath, filename) = os.path.split("c:\\music\\ap\\mahadeva.mp3") 2
>>> filepath                                                            3
'c:\\music\\ap'
>>> filename                                                            4
'mahadeva.mp3'
>>> (shortname, extension) = os.path.splitext(filename)                 5
>>> shortname
'mahadeva'
>>> extension
'.mp3'
1 split 函数对一个全路径名进行分割,返回一个包含路径和文件名的 tuple。还记得我说过你可以使用多变量赋值从一个函数返回多个值吗?对,split 就是这样一个函数。
2 我们将 split 函数的返回值赋值给一个两个变量的 tuple。每个变量接收到返回 tuple 相对应的元素值。
3 第一个变量,filepath,接收到从 split 返回 tuple 的第一个元素的值,文件路径。
4 第二个变量,filename,接收到从 split 返回 tuple 的第二个元素的值,文件名。
5 os.path 也包含了一个 splitext 函数,可以用来对文件名进行分割,并且返回一个包含了文件名和文件扩展名的 tuple。我们使用相同的技术来将它们赋值给独立的变量。

例 6.18. 列出目录

>>> os.listdir("c:\\music\\_singles\\")              1
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3', 
'spinning.mp3']
>>> dirname = "c:\\"
>>> os.listdir(dirname)                              2
['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))] 3
['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))]  4
['cygwin', 'docbook', 'Documents and Settings', 'Incoming',
'Inetpub', 'Music', 'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
1 listdir 函数接收一个路径名,并返回那个目录的内容的 list。
2 listdir 同时返回文件和文件夹,并不指出哪个是文件,哪个是文件夹。
3 你可以使用过滤列表os.path 模块的 isfile 函数,从文件夹中将文件分离出来。isfile 接收一个路径名,如果路径表示一个文件,则返回 1,否则为 0。在这里,我们使用 os.path.join 来确保得到一个全路径名,但 isfile 对部分路径 (相对于当前目录) 也是有效的。你可以使用 os.getcwd() 来得到当前目录。
4 os.path 还有一个 isdir 函数,当路径表示一个目录,则返回 1,否则为 0。你可以使用它来得到一个目录下的子目录列表。

例 6.19. 在 fileinfo.py 中列出目录

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)]            1 2
    fileList = [os.path.join(directory, f) 
               for f in fileList
                if os.path.splitext(f)[1] in fileExtList]  3 4 5
1 os.listdir(directory) 返回在 directory 中所有文件和文件夹的一个 list。
2 使用 f 对 list 进行遍历,我们使用 os.path.normcase(f) 根据操作系统的缺省值对大小写进行标准化处理。normcase 是一个有用的函数,用于对大小写不敏感操作系统的一个补充。这种操作系统认为 mahadeva.mp3mahadeva.MP3 是同一个文件名。例如,在 Windows 和 Mac OS 下,normcase 将把整个文件名转换为小写字母;而在 UNIX 兼容的系统下,它将返回未作修改的文件名。
3 再次用 f 对标准化后的 list 进行遍历,我们使用 os.path.splitext(f) 将每个文件名分割为名字和扩展名。
4 对每个文件,我们查看扩展名是否在我们关心的文件扩展名 list 中 (fileExtList,被传递给 listDirectory 函数)。
5 对每个我们所关心的文件,我们使用 os.path.join(directory, f) 来构造这个文件的全路径名,接着返回这个全路径名的 list。
注意
只要有可能,你就应该使用在 osos.path 中的函数进行文件、目录和路径的操作。这些模块是对平台相关模块的封装模块,所以像 os.path.split 这样的函数可以工作在 UNIX、Windows、Mac OSPython 所支持的任一种平台上。

还有一种获得目录内容的方法。它非常强大,并使用了一些你在命令行上工作时可能已经熟悉的通配符。

例 6.20. 使用 glob 列出目录

>>> os.listdir("c:\\music\\_singles\\")               1
['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')           2
['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')          3
['c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\*\\*.mp3')                  4
1 正如你前面看到的,os.listdir 简单地取一个目录路径,返回目录中的所有文件和子目录。
2 glob 模块,另一方面,接受一个通配符并且返回文件的或目录的完整路径与之匹配。这个通配符是一个目录路径加上“*.mp3”,它将匹配所有的 .mp3 文件。注意返回列表的每一个元素已经包含了文件的完整路径。
3 如果你要查找指定目录中所有以“s”开头并以“.mp3”结尾的文件,也可以这么做。
4 现在考查这种情况:你有一个 music 目录,它包含几个子目录,子目录中包含一些 .mp3 文件。使用两个通配符,仅仅调用 glob 一次就可以立刻获得所有这些文件的一个 list。一个通配符是 "*.mp3" (用于匹配 .mp3 文件),另一个通配符是子目录名本身,用于匹配 c:\music 中的所有子目录。这看上去很简单,但它蕴含了强大的功能。

进一步阅读

6.6. 全部放在一起

再一次,所有的多米诺骨牌都放好了。我们已经看过每行代码是如何工作的了。现在往回走一步,看一下放在一起是怎么样的。

例 6.21. listDirectory

def listDirectory(directory, fileExtList):                                         1
    "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]                          2
    def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):       3
        "get file info class from filename extension"                             
        subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]        4
        return hasattr(module, subclass) and getattr(module, subclass) or FileInfo 5
    return [getFileInfoClass(f)(f) for f in fileList]                              6
1 listDirectory 是整个模块主要的有趣之处。它接收一个 dictionary (在我的例子中如 c:\music\_singles\) 和一个感兴趣的文件扩展名列表 (如 ['.mp3']),接着它返回一个类实例的 list ,这些类实例的行为像 dictionary,包含了在目录中每个感兴趣文件的元数据。并且实现起来只用了几行直观的代码。
2 正如在前一节我们所看到的,这行代码得到一个全路径名的列表,它的元素是在 directory 中有着我们感兴趣的文件后缀 (由 fileExtList 所指定的) 的所有文件的路径名。
3 老学校出身的 Pascal 程序员可能对嵌套函数感到熟悉,但大部分人,当我告诉他们 Python 支持嵌套函数时,都茫然地看着我。嵌套函数,从字面理解,是定义在函数内的函数。嵌套函数 getFileInfoClass 只能在定义它的函数 listDirectory 内进行调用。正如任何其它的函数一样,不需要一个接口声明或奇怪的什么东西,只要定义函数,开始编码就行了。
4 既然你已经看过 os 模块了,这一行应该能理解了。它得到文件的扩展名 (os.path.splitext(filename)[1]),将其转换为大写字母 (.upper()),从圆点处进行分片 ([1:]),使用字符串格式化从其中生成一个类名。所以 c:\music\ap\mahadeva.mp3 变成 .mp3 再变成 MP3 再变成 MP3FileInfo
5 在生成完处理这个文件的处理类的名字之后,我们查阅在这个模块中是否存在这个处理类。如果存在,我们返回这个类,否则我们返回基类 FileInfo。这一点很重要:这个函数返回一个类。不是类的实例,而是类本身。
6 对每个属于我们 “感兴趣文件” 列表 (fileList)中的文件,我们用文件名 (f) 来调用 getFileInfoClass。调用 getFileInfoClass(f) 返回一个类;我们并不知道确切是哪一个类,但是我们并不关心。接着我们创建这个类 (不管它是什么) 的一个实例,传入文件名 (又是 f) 给 __init__ 方法。正如我们在本章的前面所看到的,FileInfo__init__ 方法设置了 self["name"],它将引发 __setitem__ 的调用,而 __setitem__ 在子类 (MP3FileInfo) 中被覆盖掉了,用来适当地对文件进行分析,取出文件的元数据。我们对所有感兴趣的文件进行处理,返回结果实例的一个 list。

请注意 listDirectory 完全是通用的。它事先不知道将得到哪种类型的文件,也不知道哪些定义好的类能够处理这些文件。它检查目录中要进行处理的文件,然后反观本身模块,了解定义了什么特别的处理类 (像 MP3FileInfo)。你可以对这个程序进行扩充,对其它类型的文件进行处理,只要用适合的名字定义类:HTMLFileInfo 用于 HTML 文件,DOCFileInfo 用于 Word .doc 文件,等等。不需要改动函数本身, listDirectory 将会对它们都进行处理,将工作交给适当的类,接着收集结果。

6.7. 小结

第 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

在研究下一章之前,确保你可以无困难地完成下面的事情:

第 7 章 正则表达式

正则表达式是搜索、替换和解析复杂字符模式的一种强大而标准的方法。如果你曾经在其他语言 (如 Perl) 中使用过它,由于它们的语法非常相似,你仅仅阅读一下 re 模块的摘要,大致了解其中可用的函数和参数就可以了。

7.1. 概览

字符串也有很多方法,可以进行搜索 (indexfindcount)、替换 (replace) 和解析 (split),但它们仅限于处理最简单的情况。搜索方法查找单个和固定编码的子串,并且它们总是大小写敏感的。对一个字符串s,如果要进行大小写不敏感的搜索,则你必须调用 s.lower()s.upper()s 转换成全小写或者全大写,然后确保搜索串有着相匹配的大小写。replacesplit方法有着类似的限制。

如果你要解决的问题利用字符串函数能够完成,你应该使用它们。它们快速、简单且容易阅读,而快速、简单、可读性强的代码可以说出很多好处。但是,如果你发现你使用了许多不同的字符串函数和 if 语句来处理一个特殊情况,或者你组合使用了 splitjoin 等函数而导致用一种奇怪的甚至读不下去的方式理解列表,此时,你也许需要转到正则表达式了。

尽管正则表达式语法较之普通代码相对麻烦一些,但是却可以得到更可读的结果,与用一长串字符串函数的解决方案相比要好很多。在正则表达式内部有多种方法嵌入注释,从而使之具有自文档化 (self-documenting) 的能力。

7.2. 个案研究:街道地址

这一系列的例子是由我几年前日常工作中的现实问题启发而来的,当时我需要从一个老化系统中导出街道地址,在将它们导入新的系统之前,进行清理和标准化。(看,我不是只将这些东西堆到一起,它有实际的用处。)这个例子展示我如何处理这个问题。

例 7.1. 在字符串的结尾匹配

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')               1
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')               2
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.') 3
'100 NORTH BROAD RD.'
>>> import re                              4
>>> re.sub('ROAD$', 'RD.', s)              5 6
'100 NORTH BROAD RD.'
1 我的目标是将街道地址标准化,'ROAD' 通常被略写为 'RD.'。乍看起来,我以为这个太简单了,只用字符串的方法 replace 就可以了。毕竟,所有的数据都已经是大写的了,因此大小写不匹配将不是问题。并且,要搜索的串'ROAD'是一个常量,在这个迷惑的简单例子中,s.replace 的确能够胜任。
2 不幸的是,生活充满了特例,并且我很快就意识到这个问题。比如:'ROAD' 在地址中出现两次,一次是作为街道名称 'BROAD' 的一部分,一次是作为 'ROAD' 本身。replace 方法遇到这两处的'ROAD'并没有区别,因此都进行了替换,而我发现地址被破坏掉了。
3 为了解决在地址中出现多次'ROAD'子串的问题,有可能采用类似这样的方法:只在地址的最后四个字符中搜索替换 'ROAD' (s[-4:]),忽略字符串的其他部分 (s[:-4])。但是,你可能发现这已经变得不方便了。例如,该模式依赖于你要替换的字符串的长度了 (如果你要把 'STREET' 替换为 'ST.',你需要利用 s[:-6]s[-6:].replace(...))。你愿意在六月个期间回来调试它们么?我本人是不愿意的。
4 是时候转到正则表达式了。在 Python 中,所有和正则表达式相关的功能都包含在 re 模块中。
5 来看第一个参数:'ROAD$'。这个正则表达式非常简单,只有当 'ROAD' 出现在一个字符串的尾部时才会匹配。字符$表示“字符串的末尾”(还有一个对应的字符,尖号^,表示“字符串的开始”)。
6 利用 re.sub 函数,对字符串 s 进行搜索,满足正则表达式 'ROAD$' 的用 'RD.' 替换。这样将匹配字符串 s 末尾的 'ROAD',而不会匹配属于单词 'ROAD' 一部分的 'ROAD',这是因为它是出现在 s 的中间。

继续我的清理地址的故事。很快我发现,在上面的例子中,仅仅匹配地址末尾的 'ROAD' 不是很好,因为不是所有的地址都包括表示街道的单词 ('ROAD');有一些直接以街道名结尾。大部分情况下,不会遇到这种情况,但是,如果街道名称为 'BROAD',那么正则表达式将会匹配 'BROAD' 的一部分为 'ROAD',而这并不是我想要的。

例 7.2. 匹配整个单词

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)  1
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)  2
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)  3
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s) 4
'100 BROAD RD. APT 3'
1 我真正想要做的是,当 'ROAD' 出现在字符串的末尾,并且是作为一个独立的单词时,而不是一些长单词的一部分,才对他进行匹配。为了在正则表达式中表达这个意思,你利用 \b,它的含义是“单词的边界必须在这里”。在 Python 中,由于字符 '\' 在一个字符串中必须转义,这会变得非常麻烦。有时候,这类问题被称为“反斜线灾难”,这也是 Perl 中正则表达式比 Python 的正则表达式要相对容易的原因之一。另一方面,Perl 也混淆了正则表达式和其他语法,因此,如果你发现一个 bug,很难弄清楚究竟是一个语法错误,还是一个正则表达式错误。
2 为了避免反斜线灾难,你可以利用所谓的“原始字符串”,只要为字符串添加一个前缀 r 就可以了。这将告诉 Python,字符串中的所有字符都不转义;'\t' 是一个制表符,而 r'\t' 是一个真正的反斜线字符 '\',紧跟着一个字母 't'。我推荐只要处理正则表达式,就使用原始字符串;否则,事情会很快变得混乱 (并且正则表达式自己也会很快被自己搞乱了)。
3 (一声叹息) 很不幸,我很快发现更多的与我的逻辑相矛盾的例子。在这个例子中,街道地址包含有作为整个单词的'ROAD',但是它不是在末尾,因为地址在街道命名后会有一个房间号。由于 'ROAD' 不是在每一个字符串的末尾,没有匹配上,因此调用 re.sub 没有替换任何东西,你获得的只是初始字符串,这也不是我们想要的。
4 为了解决这个问题,我去掉了 $ 字符,加上另一个 \b。现在,正则表达式“匹配字符串中作为整个单词出现的'ROAD'”了,不论是在末尾、开始还是中间。

7.3. 个案研究:罗马字母

你可能经常看到罗马数字,即使你没有意识到它们。你可能曾经在老电影或者电视中看到它们 (“版权所有 MCMXLVI” 而不是 “版权所有1946”),或者在某图书馆或某大学的贡献墙上看到它们 (“成立于 MDCCCLXXXVIII”而不是“成立于1888”)。你也可能在某些文献的大纲或者目录上看到它们。这是一个表示数字的系统,它实际上能够追溯到远古的罗马帝国 (因此而得名)。

在罗马数字中,利用7个不同字母进行重复或者组合来表达各式各样的数字。

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

下面是关于构造罗马数字的一些通用的规则的介绍:

  • 字符是叠加的。I 表示 1II 表示 2,而 III 表示 3VI 表示 6 (字面上为逐字符相加,“51”),VII 表示 7VIII 表示 8
  • 含十字符 (IXCM) 至多可以重复三次。对于 4,你则需要利用下一个最大的含五字符进行减操作得到:你不能把 4 表示成 IIII,而应表示为 IV (“51”)。数字 40 写成 XL (比 5010),41 写成 XLI42 写成 XLII43 写成 XLIII,而 44 写成 XLIV (比 5010,然后比 51)。
  • 类似地,对于数字 9,你必须利用下一个含十字符进行减操作得到:8 表示为 VIII,而 9 则表示为 IX (比 101),而不是 VIIII (因为字符 I 不能连续重复四次)。数字 90 表示为 XC900 表示为 CM
  • 含五字符不能重复。数字 10 常表示为X,而从来不用VV来表示。数字 100 常表示为C,也从来不表示为 LL
  • 罗马数字一般从高位到低位书写,从左到右阅读,因此不同顺序的字符意义大不相同。DC 表示 600;而 CD 是一个完全不同的数字 (为 400,也就是比 500100)。CI 表示 101;而IC 甚至不是一个合法的罗马字母 (因为你不能直接从数字100减去1;这需要写成 XCIX,意思是比 10010,然后加上数字 9,也就是比 101的数字)。

7.3.1. 校验千位数

怎样校验任意一个字符串是否为一个有效的罗马数字呢?我们每次只看一位数字,由于罗马数字一般是从高位到低位书写。我们从高位开始:千位。对于大于或等于 1000 的数字,千位由一系列的字符 M 表示。

例 7.3. 校验千位数

>>> import re
>>> pattern = '^M?M?M?$'       1
>>> re.search(pattern, 'M')    2
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')   3
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')  4
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM') 5
>>> re.search(pattern, '')     6
<SRE_Match object at 0106F4A8>
1 这个模式有三部分:
  • ^ 表示仅在一个字符串的开始匹配其后的字符串内容。如果没有这个字符,这个模式将匹配出现在字符串任意位置上的 M,而这并不是你想要的。你想确认的是:字符串中是否出现字符 M,如果出现,则必须是在字符串的开始。
  • M? 可选地匹配单个字符 M,由于它最多可重复出现三次,你可以在一行中匹配 0 次到 3 次字符 M
  • $ 字符限制模式只能够在一个字符串的结尾匹配。当和模式开头的字符 ^ 结合使用时,这意味着模式必须匹配整个串,并且在在字符 M 的前后都不能够出现其他的任意字符。
2 re 模块的关键是一个 search 函数,该函数有两个参数,一个是正则表达式 (pattern),一个是字符串 ('M'),函数试图匹配正则表达式。如果发现一个匹配,search 函数返回一个拥有多种方法可以描述这个匹配的对象,如果没有发现匹配,search 函数返回一个 None,一个 Python 空值 (null value)。你此刻关注的唯一事情,就是模式是否匹配上,于是我们利用 search 函数的返回值了解这个事实。字符串'M' 匹配上这个正则表达式,因为第一个可选的 M 匹配上,而第二个和第三个 M 被忽略掉了。
3 'MM' 能匹配上是因为第一和第二个可选的 M 匹配上,而忽略掉第三个 M
4 'MMM' 能匹配上因为三个 M 都匹配上了。
5 'MMMM' 没有匹配上。因为所有的三个 M 都匹配完了,但是正则表达式还有字符串尾部的限制 (由于字符 $),而字符串又没有结束 (因为还有第四个 M 字符),因此 search 函数返回一个 None
6 有趣的是,一个空字符串也能够匹配这个正则表达式,因为所有的字符 M 都是可选的。

7.3.2. 校验百位数

与千位数相比,百位数识别起来要困难得多,这是因为有多种相互独立的表达方式都可以表达百位数,而具体用那种方式表达和具体的数值有关。

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

因此有四种可能的模式:

  • CM
  • CD
  • 零到三次出现 C 字符 (出现零次表示百位数为 0)
  • D,后面跟零个到三个 C 字符

后面两个模式可以结合到一起:

  • 一个可选的字符 D,加上零到 3 个 C 字符。

这个例子显示如何有效地识别罗马数字的百位数。

例 7.4. 检验百位数

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' 1
>>> re.search(pattern, 'MCM')            2
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')             3
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')         4
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')           5
>>> re.search(pattern, '')               6
<SRE_Match object at 01071D98>
1 这个模式的首部和上一个模式相同,检查字符串的开始 (^),接着匹配千位数 (M?M?M?),然后才是这个模式的新内容。在括号内,定义了包含有三个互相独立的模式集合,由垂直线隔开:CMCDD?C?C?C? (D是可选字符,接着是 0 到 3 个可选的 C 字符)。正则表达式解析器依次检查这些模式 (从左到右),如果匹配上第一个模式,则忽略剩下的模式。
2 'MCM' 匹配上,因为第一个 M 字符匹配,第二和第三个 M 字符被忽略掉,而 CM 匹配上 (因此 CDD?C?C?C? 两个模式不再考虑)。MCM 表示罗马数字1900
3 'MD' 匹配上,因为第一个字符 M 匹配上,第二第三个 M 字符忽略,而模式 D?C?C?C? 匹配上 D (模式中的三个可选的字符 C 都被忽略掉了)。MD 表示罗马数字 1500
4 'MMMCCC' 匹配上,因为三个 M 字符都匹配上,而模式 D?C?C?C? 匹配上 CCC (字符D是可选的,此处忽略)。MMMCCC 表示罗马数字 3300
5 'MCMC' 没有匹配上。第一个 M 字符匹配上,第二第三个 M 字符忽略,接着是 CM 匹配上,但是接着是 $ 字符没有匹配,因为字符串还没有结束 (你仍然还有一个没有匹配的C字符)。C 字符也 匹配模式 D?C?C?C? 的一部分,因为与之相互独立的模式 CM 已经匹配上。
6 有趣的是,一个空字符串也可以匹配这个模式,因为所有的 M 字符都是可选的,它们都被忽略,并且一个空字符串可以匹配 D?C?C?C? 模式,此处所有的字符也都是可选的,并且都被忽略。

哎呀!看看正则表达式能够多快变得难以理解?你仅仅表示了罗马数字的千位和百位上的数字。如果你根据类似的方法,十位数和各位数就非常简单了,因为是完全相同的模式。让我们来看表达这个模式的另一种方式吧。

7.4. 使用 {n,m} 语法

前面的章节,你处理了相同字符可以重复三次的情况。在正则表达式中,有另外一个方式来表达这种情况,并且能提高代码的可读性。首先看看我们在前面的例子中使用的方法。

例 7.5. 老方法:每一个字符都是可选的

>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')    1
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MM')   2
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MMM')  3
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM') 4
>>> 
1 这个模式匹配串的开始,接着是第一个可选的字符 M,第二第三个 M 字符则被忽略 (这是可行的,因为它们都是可选的),最后是字符串的结尾。
2 这个模式匹配串的开始,接着是第一和第二个可选字符 M,而第三个 M 字符被忽略 (这是可行的,因为它们都是可选的),最后匹配字符串的结尾。
3 这个模式匹配字符串的开始,接着匹配所有的三个可选字符 M,最后匹配字符串的结尾。
4 这个模式匹配字符串的开始,接着匹配所有的三个可选字符 M,但是不能够匹配字符串的结尾 (因为还有一个未匹配的字符 M),因此不能够匹配而返回一个 None

例 7.6. 一个新的方法:从 nm

>>> pattern = '^M{0,3}$'       1
>>> re.search(pattern, 'M')    2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')   3
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')  4
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM') 5
>>> 
1 这个模式意思是说:“匹配字符串的开始,接着匹配 0 到 3 个 M 字符,然后匹配字符串的结尾。”这里的 0 和 3 可以改成其它任何数字;如果你想要匹配至少 1 次,至多 3 次字符 M,则可以写成 M{1,3}
2 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的一个,最后是字符串的结尾。
3 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的两个,最后是字符串的结尾。
4 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的三个,最后是字符串的结尾。
5 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的三个,但是没有匹配上 字符串的结尾。正则表达式在字符串结尾之前最多只允许匹配三次 M 字符,但是实际上有四个 M 字符,因此模式没有匹配上这个字符串,返回一个 None
注意
没有一个轻松的方法来确定两个正则表达式是否等价。你能采用的最好的办法就是列出很多的测试样例,确定这两个正则表达式对所有的相关输入都有相同的输出。在本书后面的章节,将更多地讨论如何编写测试样例。

7.4.1. 校验十位数和个位数

现在我们来扩展一下关于罗马数字的正则表达式,以匹配十位数和个位数,下面的例子展示十位数的校验方法。

例 7.7. 校验十位数

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')    1
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')     2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')    3
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')  4
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX') 5
>>> 
1 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着 XL,接着是字符串的结尾。请记住,(A|B|C) 这个语法的含义是“精确匹配 A、B 或者 C 其中的一个”。此处匹配了 XL,因此不再匹配 XCL?X?X?X?,接着就匹配到字符串的结尾。MCML 表示罗马数字 1940
2 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着 L?X?X?X?。在模式 L?X?X?X? 中,它匹配 L 字符并且跳过所有可选的 X 字符,接着匹配字符串的结尾。MCML 表示罗马数字 1950
3 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着是可选的 L 字符和可选的第一个 X 字符,并且跳过第二第三个可选的 X 字符,接着是字符串的结尾。MCMLX 表示罗马数字 1960
4 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着是可选的 L 字符和所有的三个可选的 X 字符,接着匹配字符串的结尾。MCMLXXX 表示罗马数字 1980
5 这个模式匹配字符串的开始,接着是第一个可选字符M,接着是CM,接着是可选的 L字符和所有的三个可选的X字符,接着就未能匹配 字符串的结尾ie,因为还有一个未匹配的X 字符。所以整个模式匹配失败并返回一个 None. 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} 语法表达这个正则表达式会如何呢?这个例子展示新的语法。

例 7.8. 用 {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')             1
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')         2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')  3
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')                4
<_sre.SRE_Match object at 0x008EEB48>
1 这个模式匹配字符串的开始,接着匹配三个可选的 M 字符的一个,接着匹配 D?C{0,3},此处,仅仅匹配可选的字符 D 和 0 个可选字符 C。继续向前匹配,匹配 L?X{0,3},此处,匹配可选的 L 字符和 0 个可选字符 X,接着匹配 V?I{0,3},此处,匹配可选的 V 和 0 个可选字符 I,最后匹配字符串的结尾。MDLV 表示罗马数字 1555
2 这个模式匹配字符串的开始,接着是三个可选的 M 字符的两个,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中的一个,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中的一个,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中的一个,接着匹配字符串的结尾。MMDCLXVI 表示罗马数字 2666
3 这个模式匹配字符串的开始,接着是三个可选的 M 字符的所有字符,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中所有字符,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中所有字符,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中所有字符,接着匹配字符串的结尾。MMMDCCCLXXXVIII 表示罗马数字3888,这个数字是不用扩展语法可以写出的最大的罗马数字。
4 仔细看哪!(我像一个魔术师一样,“看仔细喽,孩子们,我将要从我的帽子中拽出一只兔子来啦!”) 这个模式匹配字符串的开始,接着匹配 3 个可选 M 字符的 0 个,接着匹配 D?C{0,3},此处,跳过可选字符 D 并匹配三个可选 C 字符的 0 个,接着匹配 L?X{0,3},此处,跳过可选字符 L 并匹配三个可选 X 字符的 0 个,接着匹配 V?I{0,3},此处跳过可选字符 V 并匹配三个可选 I 字符的一个,最后匹配字符串的结尾。哇赛!

如果你在第一遍就跟上并理解了所讲的这些,那么你做的比我还要好。现在,你可以尝试着理解别人大规模程序里关键函数中的正则表达式了。或者想象着几个月后回头理解你自己的正则表达式。我曾经做过这样的事情,但是它并不是那么有趣。

在下一节里,你将会研究另外一种正则表达式语法,它可以使你的表达式具有更好的可维持性。

7.5. 松散正则表达式

迄今为止,你只是处理过被我称之为“紧凑”类型的正则表达式。正如你曾看到的,它们难以阅读,即使你清楚正则表达式的含义,你也不能保证六个月以后你还能理解它。你真正所需的就是利用内联文档 (inline documentation)。

Python 允许用户利用所谓的松散正则表达式 来完成这个任务。一个松散正则表达式和一个紧凑正则表达式主要区别表现在两个方面:

  • 忽略空白符。空格符,制表符,回车符不匹配它们自身,它们根本不参与匹配。(如果你想在松散正则表达式中匹配一个空格符,你必须在它前面添加一个反斜线符号对它进行转义。)
  • 忽略注释。在松散正则表达式中的注释和在普通 Python 代码中的一样:开始于一个#符号,结束于行尾。这种情况下,采用在一个多行字符串中注释,而不是在源代码中注释,它们以相同的方式工作。

用一个例子可以解释得更清楚。让我们重新来看前面的紧凑正则表达式,利用松散正则表达式重新表达。下面的例子显示实现方法。

例 7.9. 带有内联注释 (Inline Comments) 的正则表达式

>>> 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)                1
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)        2
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)  3
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')                            4
1 当使用松散正则表达式时,最重要的一件事情就是:必须传递一个额外的参数 re.VERBOSE,该参数是定义在 re 模块中的一个常量,标志着待匹配的正则表达式是一个松散正则表达式。正如你看到的,这个模式中,有很多空格 (所有的空格都被忽略),和几个注释 (所有的注释也被忽略)。如果忽略所有的空格和注释,它就和前面章节里的正则表达式完全相同,但是具有更好的可读性。
2 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的一个,接着匹配 CM,接着是字符 L 和三个可选 X 字符的所有字符,接着是 IX,然后是字符串的结尾。
3 这个模式匹配字符串的开始,接着是三个可选的 M 字符的所有字符,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中所有字符,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中所有字符,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中所有字符,接着匹配字符串的结尾。
4 这个没有匹配。为什么呢?因为没有 re.VERBOSE 标记,所以 re.search 函数把模式作为一个紧凑正则表达式进行匹配。Python 不能自动检测一个正则表达式是为松散类型还是紧凑类型。Python 默认每一个正则表达式都是紧凑类型的,除非你显式地标明一个正则表达式为松散类型。

7.6. 个案研究:解析电话号码

迄今为止,你主要是匹配整个模式,不论是匹配上,还是没有匹配上。但是正则表达式还有比这更为强大的功能。当一个模式确实 匹配上时,你可以获取模式中特定的片断,你可以发现具体匹配的位置。

这个例子来源于我遇到的另一个现实世界的问题,也是在以前的工作中遇到的。问题是:解析一个美国电话号码。客户要能 (在一个单一的区域中) 输入任何数字,然后存储区号、干线号、电话号和一个可选的独立的分机号到公司数据库里。为此,我通过网络找了很多正则表达式的例子,但是没有一个能够完全满足我的要求。

这里列举了我必须能够接受的电话号码:

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

格式可真够多的!我需要知道区号是 800,干线号是 555,电话号的其他数字为 1212。对于那些有分机号的,我需要知道分机号为 1234

让我们完成电话号码解析这个工作,这个例子展示第一步。

例 7.10. 发现数字

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') 1
>>> phonePattern.search('800-555-1212').groups()            2
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')                3
>>> 
1 我们通常从左到右阅读正则表达式。这个正则表达式匹配字符串的开始,接着匹配 (\d{3})\d{3} 是什么呢?好吧,{3} 的含义是“精确匹配三个数字”;这是曾在前面见到过的 {n,m} 语法的一种变形。\d 的含义是 “任何一个数字” (09)。把它们放大括号中意味着要“精确匹配三个数字位,接着把它们作为一个组保存下来,以便后面的调用”。接着匹配一个连字符,接着是另外一个精确匹配三个数字位的组,接着另外一个连字符,接着另外一个精确匹配四个数字为的组,接着匹配字符串的结尾。
2 为了访问正则表达式解析过程中记忆下来的多个组,我们使用 search 函数返回对象的 groups() 函数。这个函数将返回一个元组,元组中的元素就是正则表达式中定义的组。在这个例子中,定义了三个组,第一个组有三个数字位,第二个组有三个数字位,第三个组有四个数字位。
3 这个正则表达式不是最终的答案,因为它不能处理在电话号码结尾有分机号的情况,为此,我们需要扩展这个正则表达式。

例 7.11. 发现分机号

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') 1
>>> phonePattern.search('800-555-1212-1234').groups()             2
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')                      3
>>> 
>>> phonePattern.search('800-555-1212')                           4
>>> 
1 这个正则表达式和上一个几乎相同,正像前面的那样,匹配字符串的开始,接着匹配一个有三个数字位的组并记忆下来,接着是一个连字符,接着是一个有三个数字位的组并记忆下来,接着是一个连字符,接着是一个有四个数字位的组并记忆下来。不同的地方是你接着又匹配了另一个连字符,然后是一个有一个或者多个数字位的组并记忆下来,最后是字符串的结尾。
2 函数 groups() 现在返回一个有四个元素的元组,由于正则表达式中定义了四个记忆的组。
3 不幸的是,这个正则表达式也不是最终的答案,因为它假设电话号码的不同部分是由连字符分割的。如果一个电话号码是由空格符、逗号或者点号分割呢?你需要一个更一般的解决方案来匹配几种不同的分割类型。
4 啊呀!这个正则表达式不仅不能解决你想要的任何问题,反而性能更弱了,因为现在你甚至不能解析一个没有分机号的电话号码了。这根本不是你想要的,如果有分机号,你要知道分机号是什么,如果没有分机号,你仍然想要知道主电话号码的其他部分是什么。

下一个例子展示正则表达式处理一个电话号码内部,采用不同分隔符的情况。

例 7.12. 处理不同分隔符

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') 1
>>> phonePattern.search('800 555 1212 1234').groups()                   2
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()                   3
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')                               4
>>> 
>>> phonePattern.search('800-555-1212')                                 5
>>> 
1 当心啦!你首先匹配字符串的开始,接着是一个三个数字位的组,接着是 \D+,这是个什么东西?好吧,\D 匹配任意字符,除了 数字位,+ 表示“1 个或者多个”,因此 \D+ 匹配一个或者多个不是数字位的字符。这就是你替换连字符为了匹配不同分隔符所用的方法。
2 使用 \D+ 代替 - 意味着现在你可以匹配中间是空格符分割的电话号码了。
3 当然,用连字符分割的电话号码也能够被识别。
4 不幸的是,这个正则表达式仍然不是最终答案,因为它假设电话号码一定有分隔符。如果电话号码中间没有空格符或者连字符的情况会怎样哪?
4 我的天!这个正则表达式也没有达到我们对于分机号识别的要求。现在你共有两个问题,但是你可以利用相同的技术来解决它们。

下一个例子展示正则表达式处理没有 分隔符的电话号码的情况。

例 7.13. 处理没有分隔符的数字

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') 1
>>> phonePattern.search('80055512121234').groups()                      2
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()                  3
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                        4
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')                           5
>>> 
1 和上一步相比,你所做的唯一变化就是把所有的 + 变成 *。在电话号码的不同部分之间不再匹配 \D+,而是匹配 \D* 了。还记得 + 的含义是“1 或者多个”吗? 好的,* 的含义是“0 或者多个”。因此,现在你应该能够解析没有分隔符的电话号码了。
2 你瞧,它真的可以胜任。为什么?首先匹配字符串的开始,接着是一个有三个数字位 (800) 的组,接着是 0 个非数字字符,接着是一个有三个数字位 (555) 的组,接着是 0 个非数字字符,接着是一个有四个数字位 (1212) 的组,接着是 0 个非数字字符,接着是一个有任意数字位 (1234) 的组,最后是字符串的结尾。
3 对于其他的变化也能够匹配:比如点号分隔符,在分机号前面既有空格符又有 x 符号的情况也能够匹配。
4 最后,你已经解决了长期存在的一个问题:现在分机号是可选的了。如果没有发现分机号,groups() 函数仍然返回一个有四个元素的元组,但是第四个元素只是一个空字符串。
5 我不喜欢做一个坏消息的传递人,此时你还没有完全结束这个问题。还有什么问题呢?当在区号前面还有一个额外的字符时,而正则表达式假设区号是一个字符串的开始,因此不能匹配。这个不是问题,你可以利用相同的技术“0或者多个非数字字符”来跳过区号前面的字符。

下一个例子展示如何解决电话号码前面有其他字符的情况。

例 7.14. 处理开始字符

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') 1
>>> phonePattern.search('(800)5551212 ext. 1234').groups()                 2
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()                           3
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')                     4
>>> 
1 这个正则表达式和前面的几乎相同,但它在第一个记忆组 (区号) 前面匹配 \D*,0 或者多个非数字字符。注意,此处你没有记忆这些非数字字符 (它们没有被括号括起来)。如果你发现它们,只是跳过它们,接着只要匹配上就开始记忆区号。
2 你可以成功地解析电话号码,即使在区号前面有一个左括号。(在区号后面的右括号也已经被处理,它被看成非数字字符分隔符,由第一个记忆组后面的 \D* 匹配。)
3 进行仔细的检查,保证你没有破坏前面能够匹配的任何情况。由于首字符是完全可选的,这个模式匹配字符串的开始,接着是 0 个非数字字符,接着是一个有三个数字字符的记忆组 (800),接着是 1 个非数字字符 (连字符),接着是一个有三个数字字符的记忆组 (555),接着是 1 个非数字字符 (连字符),接着是一个有四个数字字符的记忆组 (1212),接着是 0 个非数字字符,接着是一个有 0 个数字位的记忆组,最后是字符串的结尾。
4 此处是正则表达式让我产生了找一个硬东西挖出自己的眼睛的冲动。为什么这个电话号码没有匹配上?因为在它的区号前面有一个 1,但是你认为在区号前面的所有字符都是非数字字符 (\D*)。唉!

让我们往回看一下。迄今为止,正则表达式总是从一个字符串的开始匹配。但是现在你看到了,有很多不确定的情况需要你忽略。与其尽力全部匹配它们,还不如全部跳过它们,让我们采用一个不同的方法:根本不显式地匹配字符串的开始。下面的这个例子展示这个方法。

例 7.15. 电话号码,无论何时我都要找到它

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') 1
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()        2
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                                3
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234')                              4
('800', '555', '1212', '1234')
1 注意,在这个正则表达式的开始少了一个 ^ 字符。你不再匹配字符串的开始了,也就是说,你需要用你的正则表达式匹配整个输入字符串,除此之外没有别的意思了。正则表达式引擎将要努力计算出开始匹配输入字符串的位置,并且从这个位置开始匹配。
2 现在你可以成功解析一个电话号码了,无论这个电话号码的首字符是不是数字,无论在电话号码各部分之间有多少任意类型的分隔符。
3 仔细检查,这个正则表达式仍然工作的很好。
4 还是能够工作。

看看一个正则表达式能够失控得多快?回头看看前面的例子,你还能区别它们么?

当你还能够理解这个最终答案的时候 (这个正则表达式就是最终答案,即使你发现一种它不能处理的情况,我也真的不想知道它了),在你忘记为什么你这么选择之前,让我们把它写成松散正则表达式的形式。

例 7.16. 解析电话号码 (最终版本)

>>> 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()        1
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')                                2
('800', '555', '1212', '')
1 除了被分成多行,这个正则表达式和最后一步的那个完全相同,因此它能够解析相同的输入一点也不奇怪。
2 进行最后的仔细检查。很好,仍然工作。你终于完成了这件任务。

关于正则表达式的进一步阅读

7.7. 小结

这只是正则表达式能够完成工作的很少一部分。换句话说,即使你现在备受打击,相信我,你也不是什么也没见过了。

现在,你应该熟悉下列技巧:

  • ^ 匹配字符串的开始。
  • $ 匹配字符串的结尾。
  • \b 匹配一个单词的边界。
  • \d 匹配任意数字。
  • \D 匹配任意非数字字符。
  • x? 匹配一个可选的 x 字符 (换言之,它匹配 1 次或者 0 次 x 字符)。
  • x* 匹配0次或者多次 x 字符。
  • x+ 匹配1次或者多次 x 字符。
  • x{n,m} 匹配 x 字符,至少 n 次,至多 m 次。
  • (a|b|c) 要么匹配 a,要么匹配 b,要么匹配 c
  • (x) 一般情况下表示一个记忆组 (remembered group)。你可以利用 re.search 函数返回对象的 groups() 函数获取它的值。

正则表达式非常强大,但是它并不能为每一个问题提供正确的解决方案。你应该学习足够多的知识,以辨别什么时候它们是合适的,什么时候它们会解决你的问题,什么时候它们产生的问题比要解决的问题还要多。

 

一些人,遇到一个问题时就想:“我知道,我将使用正则表达式。”现在他有两个问题了。

 
--Jamie Zawinski, in comp.emacs.xemacs  

第 8 章 HTML 处理

8.1. 概览

我经常在 comp.lang.python 上看到关于如下的问题: “ 怎么才能从我的 HTML 文档中列出所有的 [头|图像|链接] 呢?” “怎么才能 [分析|解释|munge] 我的 HTML 文档的文本,但是又要保留标记呢?” “怎么才能一次给我所有的 HTML 标记 [增加|删除|加引号] 属性呢?” 本章将回答所有这些问题。

下面给出一个完整的,可工作的 Python 程序,它分为两部分。第一部分,BaseHTMLProcessor.py 是一个通用工具,它可以通过扫描标记和文本块来帮助您处理 HTML 文件。第二部分,dialect.py 是一个例子,演示了如何使用 BaseHTMLProcessor.py 来转化 HTML 文档,保留文本但是去掉了标记。阅读文档字符串 (doc string) 和注释来了解将要发生事情的概况。大部分内容看上去像巫术,因为任一个这些类的方法是如何调用的不是很清楚。不要紧,所有内容都会按进度被逐步地展示出来。

例 8.1. BaseHTMLProcessor.py

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

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 "&#160;", 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 "&copy;", 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)

例 8.2. dialect.py

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")

例 8.3. dialect.py 的输出结果

运行这个脚本会将 第 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>

8.2. sgmllib.py 介绍

HTML 处理分成三步:将 HTML 分解成它的组成片段,对片段进行加工,接着将片段再重新合成 HTML。第一步是通过 sgmllib.py 来完成的,它是标准 Python 库的一部分。

理解本章的关键是要知道 HTML 不只是文本,更是结构化文本。这种结构来源于开始与结束标记的或多或少分级序列。通常您并不以这种方式处理 HTML ,而是以文本方式 在一个文本编辑中对其进行处理,或以可视的方式 在一个浏览器中进行浏览或页面编辑工具中进行编辑。sgmllib.py 表现出了 HTML结构

sgmllib.py 包含一个重要的类:SGMLParserSGMLParserHTML 分解成有用的片段,比如开始标记和结束标记。在它成功地分解出某个数据为一个有用的片段后,它会根据所发现的数据,调用一个自身内部的方法。为了使用这个分析器,您需要子类化 SGMLParser 类,并且覆盖这些方法。这就是当我说它表示了 HTML 结构 的意思:HTML 的结构决定了方法调用的次序和传给每个方法的参数。

SGMLParserHTML 分析成 8 类数据,然后对每一类调用单独的方法:

开始标记 (Start tag)
是开始一个块的 HTML 标记,像 <html><head><body><pre> 等,或是一个独一的标记,像 <br><img> 等。当它找到一个开始标记 tagnameSGMLParser 将查找名为 start_tagnamedo_tagname 的方法。例如,当它找到一个 <pre> 标记,它将查找一个 start_predo_pre 的方法。如果找到了,SGMLParser 会使用这个标记的属性列表来调用这个方法;否则,它用这个标记的名字和属性列表来调用 unknown_starttag 方法。
结束标记 (End tag)
是结束一个块的 HTML 标记,像 </html></head></body></pre> 等。当找到一个结束标记时,SGMLParser 将查找名为 end_tagname 的方法。如果找到,SGMLParser 调用这个方法,否则它使用标记的名字来调用 unknown_endtag
字符引用 (Character reference)
用字符的十进制或等同的十六进制来表示的转义字符,像 &#160;。当找到,SGMLParser 使用十进制或等同的十六进制字符文本来调用 handle_charref
实体引用 (Entity reference)
HTML 实体,像 &copy;。当找到,SGMLParser 使用 HTML 实体的名字来调用 handle_entityref
注释 (Comment)
HTML 注释,包括在 <!-- ... -->之间。当找到,SGMLParser 用注释内容来调用 handle_comment
处理指令 (Processing instruction)
HTML 处理指令,包括在 <? ... > 之间。当找到,SGMLParser 用处理指令内容来调用 handle_pi
声明 (Declaration)
HTML 声明,如 DOCTYPE,包括在 <! ... >之间。当找到,SGMLParser 用声明内容来调用 handle_decl
文本数据 (Text data)
文本块。不满足其它 7 种类别的任何东西。当找到,SGMLParser 用文本来调用 handle_data
重要
Python 2.0 存在一个 bug,即 SGMLParser 完全不能识别声明 (handle_decl 永远不会调用),这就意味着 DOCTYPE 被静静地忽略掉了。这个错误在 Python 2.1 中改正了。

sgmllib.py 所附带的一个测试套件举例说明了这一点。您可以运行 sgmllib.py,在命令行下传入一个 HTML 文件的名字,然后它会在分析标记和其它元素的同时将它们打印出来。它的实现是通过子类化 SGMLParser 类,然后定义 unknown_starttagunknown_endtaghandle_data 和其它方法来实现的。这些方法简单地打印出它们的参数。

提示
在 Windows 下的 ActivePython IDE 中,您可以在 “Run script” 对话框中指定命令行参数。用空格将多个参数分开。

例 8.4. sgmllib.py 的样例测试

下面是一个片段,来自本书的 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      '

... 略 ...

下面是本章其它部分的路标:

  • 子类化 SGMLParser 来创建从 HTML 文档中抽取感兴趣的数据的类。
  • 子类化 SGMLParser 来创建 BaseHTMLProcessor,它覆盖了所有8个处理方法,然后使用它们从片段中重建原始的 HTML
  • 子类化 BaseHTMLProcessor 来创建 Dialectizer,它增加了一些方法,专门用来处理指定的 HTML 标记,然后覆盖了 handle_data 方法,提供了用来处理 HTML 标记之间文本块的框架。
  • 子类化 Dialectizer 来创建定义了文本处理规则的类。这些规则被 Dialectizer.handle_data 使用。
  • 编写一个测试套件,它可以从 http://diveintopython.org/ 处抓取一个真正的 web 页面,然后处理它。

继续阅读本章,您还可以学习到有关 localsglobals 和基于 dictionary 的字符串格式化的内容。

8.3. 从 HTML 文档中提取数据

为了从 HTML 文档中提取数据,将 SGMLParser 类进行子类化,然后对想要捕捉的标记或实体定义方法。

HTML 文档中提取数据的第一步是得到某个 HTML 文件。如果在您的硬盘里存放着 HTML 文件,您可以使用处理文件的函数将它读出来,但是真正有意思的是从实际的网页得到 HTML

例 8.5. urllib 介绍

>>> import urllib                                       1
>>> sock = urllib.urlopen("http://diveintopython.org/") 2
>>> htmlSource = sock.read()                            3
>>> sock.close()                                        4
>>> print htmlSource                                    5
<!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&nbsp;for&nbsp;experienced&nbsp;programmers</td></tr>

[...略...]
1 urllib 模块是标准 Python 库的一部分。它包含了一些函数,可以从基于互联网的 URL (主要指网页) 来获取信息并且真正取回数据。
2 urllib 模块最简单的使用是提取用 urlopen 函数取回的网页的整个文本。打开一个 URL打开一个文件相似。urlopen 的返回值是像文件一样的对象,它具有一个文件对象一样的方法。
3 使用由 urlopen 所返回的类文件对象所能做的最简单的事情就是 read,它可以将网页的整个 HTML 读到一个字符串中。这个对象也支持 readlines 方法,这个方法可以将文本按行放入一个列表中。
4 当用完这个对象,要确保将它 close,就如同一个普通的文件对象。
5 现在我们将 http://diveintopython.org/ 主页的完整的 HTML 保存在一个字符串中了,接着我们将分析它。

例 8.6. urllister.py 介绍

如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序

from sgmllib import SGMLParser

class URLLister(SGMLParser):
    def reset(self):                              1
        SGMLParser.reset(self)
        self.urls = []

    def start_a(self, attrs):                     2
        href = [v for k, v in attrs if k=='href'] 3 4
        if href:
            self.urls.extend(href)
1 resetSGMLParser__init__ 方法来调用,也可以在创建一个分析器实例时手工来调用。所以如果您需要做初始化,在 reset 中去做,而不要在 __init__ 中做。这样当某人重用一个分析器实例时,可以正确地重新初始化。
2 只要找到一个 <a> 标记,start_a 就会由 SGMLParser 进行调用。这个标记可以包含一个 href 属性,或者包含其它的属性,如 nametitleattrs 参数是一个 tuple 的 list,[(attribute, value), (attribute, value), ...]。或者它可以只是一个有效的 HTML 标记 <a> (尽管无用),这时 attrs 将是个空 list。
3 我们可以通过一个简单的多变量 list 映射来查找这个 <a> 标记是否拥有一个 href 属性。
4 k=='href' 的字符串比较是区分大小写的,但是这里是安全的。因为 SGMLParser 会在创建 attrs 时将属性名转化为小写。

例 8.7. 使用 urllister.py

>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read())         1
>>> usock.close()                     2
>>> parser.close()                    3
>>> for url in parser.urls: print url 4
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


...略...
1 调用定义在 SGMLParser 中的 feed 方法,将 HTML 内容放入分析器中。 [4] 这个方法接收一个字符串,这个字符串就是 usock.read() 所返回的。
2 像处理文件一样,一旦处理完毕,您应该 close 您的 URL 对象。
3 您也应该 close 您的分析器对象,但出于不同的原因。feed 方法不保证对传给它的全部 HTML 进行处理,它可能会对其进行缓冲处理,等待接收更多的内容。只要没有更多的内容,就应调用 close 来刷新缓冲区,并且强制所有内容被完全处理。
4 一旦分析器被 close,分析过程也就结束了。parser.urls 中包含了在 HTML 文档中所有的链接 URL。(如果当您读到此处发现输出结果不一样,那是因为下载了本书的更新版本。)

8.4. BaseHTMLProcessor.py 介绍

SGMLParser 自身不会产生任何结果。它只是分析,分析,再分析,对于它找到的有趣的东西会调用相应的一个方法,但是这些方法什么都不做。SGMLParser 是一个 HTML 消费者 (consumer):它接收 HTML,将其分解成小的、结构化的小块。正如您所看到的,在前一节中,您可以定义 SGMLParser 的子类,它可以捕捉特别标记和生成有用的东西,如一个网页中所有链接的一个列表。现在我们将沿着这条路更深一步。我们要定义一个可以捕捉 SGMLParser 所丢出来的所有东西的一个类,接着重建整个 HTML 文档。用技术术语来说,这个类将是一个 HTML 生产者 (producer)

BaseHTMLProcessor 子类化 SGMLParser,并且提供了全部的 8 个处理方法:unknown_starttagunknown_endtaghandle_charrefhandle_entityrefhandle_commenthandle_pihandle_declhandle_data

例 8.8. BaseHTMLProcessor 介绍

class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        1
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): 2
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          3
        self.pieces.append("</%(tag)s>" % locals())

    def handle_charref(self, ref):          4
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        5
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):            6
        self.pieces.append(text)

    def handle_comment(self, text):         7
        self.pieces.append("<!--%(text)s-->" % locals())

    def handle_pi(self, text):              8
        self.pieces.append("<?%(text)s>" % locals())

    def handle_decl(self, text):
        self.pieces.append("<!%(text)s>" % locals())
1 resetSGMLParser.__init__ 来调用。在