Free Essay

Computer Science

In:

Submitted By wanwan862
Words 44035
Pages 177
大学生创意创新创业教育与实践系列教程

Python程序设计与应用教程

目录
第1章 Python语言概述 9 1.1 Python语言简介 9 1.2 Python与C语言的异同 10 1.3 安装与配置Python环境 12 1.3.1 Windows系统平台 12 1.3.2 Linux系统平台 17 1.3.3 MAC系统平台 20 1.4 Python开发环境 23 本章小结 24
第2章 基本数据类型与变量 25 2.1 整数运算 25 2.2 浮点数算术 27 2.3 复数 29 2.4 字符串 29 2.5 类型转换 32 2.6 变量和值 32 2.7 赋值语句 34 2.7.1 变量引用 36 2.7.2 赋值 38 2.7.3 多重赋值 39 本章小结 41 习题 41
第3章 编写Python程序 43 3.1 IDLE 43 3.1.1 IDLE中编写程序 44 3.1.2 命令行运行程序 55 3.1.3 命令行调用Python 55 3.2 注释 56 本章小结 57 习题 57
第4章 列表、元组和字典 58 4.1 序列 59 4.2 序列基本操作 59 4.2.1 索引 59 4.2.2 切片 60 4.2.3 序列加 60 4.2.4 序列乘 61 4.2.5 In 61 4.2.6 len、max、min 61 4.3 列表 62 4.3.1 列表的创建 62 4.3.2 列表的修改 62 4.3.3 列表的方法 63 4.4 元组 66 4.4.1 元组的创建 66 4.4.2 元组的操作 66 4.4.3 元组的困惑 67 4.5 字典 67 4.5.1 字典的创建 67 4.5.2 字典的操作 68 4.5.3 字典的方法 69 4.6 集合 72 4.6.1 集合的创建 72 4.6.2 集合的基本操作 73 4.6.3 集合的特殊操作 74 4.7 五子棋棋盘 76 4.7.1 五子棋盘的选择:列表 76 4.7.2 棋盘的创建 77 本章小结 78 习题 78
第5章 流程控制语句 80 5.1 布尔逻辑 81 5.2 代码块与缩进 81 5.3 if/else语句 82 5.3.1 单分支条件语句 82 5.3.2 二分支条件语句 83 5.3.3 多分支条件语句 84 5.3.4 条件表达式 86 5.3.5 断言 88 5.4 循环 89 5.4.1 while循环 89 5.4.2 for循环 90 5.4.3 循环嵌套 91 5.4.4 循环遍历字典元素 93 5.4.5 迭代工具 93 5.4.6 循环控制语句 95 5.4.7 循环与else子句 97 5.5 列表推导式 97 5.6 继续五子棋 98 本章小结 102 习题 102
第6章 再讲字符串 105 6.1 字符串操作 105 6.1.1 切片操作 105 6.1.2 格式化操作 107 6.1.3 字符串模板 110 6.1.4 原始字符串操作符 110 6.1.5 Unicode字符串操作符 111 6.2 正则表达式 112 6.2.1 第一个正则表达式 112 6.2.2 正则表达式中的特殊字符 113 6.2.3 匹配任意一个单个字符 114 6.2.4 匹配多个字符串(|) 115 6.2.5 创建字符类([]) 115 6.2.6 在字符串边界或单词边界进行匹配(^$\b\B) 116 6.2.7 分组匹配(()) 119 6.2.8 重复匹配(*, +, ? ,{}) 122 6.2.9 替换与分割 124 6.2.10 正则表达式举例 125 本章小结 126 习题 126
第7章 函数 128 7.1 抽象与函数 129 7.2 创建函数 130 7.3 函数参数 131 7.3.1 函数参数类型 131 7.3.2 修改参数 134 7.4 变量的作用域 136 7.5 递归 138 7.6 函数修饰器 139 7.7 完成五子棋(封装及重构) 141 本章小结 146 习题 147
第8章 I/O操作与文件 151 8.1 字符串格式化 151 8.2 I/O操作 154 8.3 文件打开与关闭 155 8.3.1 打开文件 155 8.3.2 文件关闭 156 8.3.3 文件模式 156 8.3.4 缓冲 157 8.4 文件读和写 158 8.5 处理二进制文件 159 8.6 访问文件系统 162 8.7 文本处理举例----词频统计 167 8.8 五子棋游戏保存读取功能 170 本章小结 171 习题 171
第9章 面向对象编程 173 9.1 类 174 9.2 类的创建 174 9.2.1 创建类 175 9.2.2 类变量和类方法 177 9.2.3 静态方法 180 9.2.4 property修饰器 181 9.2.5 类的初始化 182 9.3 继承 183 9.3.1 继承与重写 184 9.3.2 多重继承 185 9.4 多态 186 9.5 重构五子棋 187 本章小结 193 习题 193
第10章 异常处理 195 10.1 异常 195 10.2 抛出异常 196 10.2.1 raise语句 196 10.2.2 自定义异常类 198 10.3 捕获异常 199 10.3.1 try-except语句 199 10.3.2 捕获多种异常 201 10.3.3 捕获所有异常 203 10.4 finally语句 204 10.5 处理异常的特殊方法 207 10.6 让五子棋更健壮 207 本章小结 211 习题 211
第11章 模块 213 11.1 Python模块 213 11.2 名称空间 215 11.3 模块导入特性 217 11.4 模块内建函数 217 11.4.1 __import__() 218 11.4.2 globals()和locals() 218 11.4.3 dir() 219 11.4.4 reload() 219 11.5 包 220 本章小结 221 习题 221
第12章 Python开发游戏 223 12.1 Pygame介绍 223 12.2 常用模块介绍 224 12.2.1 pygame及pygame.locals模块 224 12.2.2 pygame.surface及pygame.font模块 225 12.2.3 pygame.display模块 225 12.2.4 pygame.sprite模块 225 12.2.5 pygame.mouse模块 225 12.2.6 pygame.event模块 226 12.2.7 pygame.Rect模块 226 12.3 游戏初步设计 226 12.4 进一步完善游戏 230 本章小结 239
第13章 TCP/UDP网络编程 241 13.1 问题引入 241 13.1.1 客户端服务器网络简介 242 13.1.2 客户端服务器网络编程 242 13.2 套接字 242 13.3 网络设计模块 243 13.3.1 Socket模块 243 13.3.2 urllib模块 247 13.3.3 urllib2模块 249 13.3.4 其他常见的模块 251 13.4 UDP编程 251 13.5 TCP编程 254 本章小结 257
第14章 Python爬虫程序 258 14.1 搜索引擎和网络爬虫 258 14.2 一些基本概念 259 14.2.1 URI和URL 259 14.2.2 HTTP协议 260 14.3 准备工作 260 14.3.1 初探urllib2网络库 260 14.3.2 urllib2异常处理 265 14.3.3 urllib2使用细节 269 14.4 一个简单爬虫程序 271 本章小结 275
第15章 访问数据库 276 15.1 数据库基础知识 276 15.1.1 关系型数据库 277 15.1.2 SQL语句 277 15.2 Python与数据库 278 15.3 SQLite介绍 279 15.4 Python使用SQLite 280 15.4.1 导入sqlite3模块 280 15.4.2 创建数据库 281 15.4.3 创建游标 281 15.5 MySQL介绍 285 15.6 Python使用MySQL 286 15.6.1 创建数据库 286 15.6.2 创建游标 287 15.7 编写电子同学录 290 15.7.1 需求分析 290 15.7.2 系统设计 290 15.7.3 数据库的设计 290 15.7.4 页面设计 291 15.7.5 模块实现 293 本章小结 301
第16章 CGI编程 302 16.1 CGI介绍 302 16.2 网页与HTML 303 16.2.1 HTML语言简介 303 16.2.2 HTML标签简介 304 16.3 一个网站的初步实现 304 16.3.1 下载和安装Apache 304 16.3.2 第一个CGI程序 306 16.3.3 GET和POST方法 306 16.3.4 表单提交 307 16.3.5 cgitb调试 310 16.4 个人信息管理系统 311 16.4.1 网站需求说明 311 16.4.2 项目数据库设计 311 16.4.3 用户注册模块 313 16.4.4 用户登录 316 16.4.5 个人信息查看 319 16.4.6 个人信息修改 321 本章总结 326
附录A 比较Python 2和Python 3 327
参考文献 330

第1章 Python语言概述
什么是Python?简单来说,Python就是一种简单的计算机语言及一组与之配套的软件工具和库。它是由Guido van Rossum在20世纪90年代初开发,当前由世界数十位程序员负责维护。与传统流行编程语言比,例如C、java、C#等,Python语言的设计理念是使用尽可能少的代码,完成其他语言相同工作,提升代码的可读性。本书将详细介绍Python语言的背景,语言细节以及相关案例,希望读者通过本书学习,能够在快乐中掌握Python语言基本内容,并且可以学以致用,为今后工作和学习打下基础。
【体系结构】

【案例引入】 五子棋游戏介绍
五子棋是一款对弈类的纯策略型棋类游戏,行棋方式与围棋比较类似,游戏规则是每一轮一方只能摆放一个棋子,若一方的五个棋子或横或竖或斜构成一条直线,则此方胜。由于五子棋产生时间久远,操作简单,因此流行于华人和汉字文化圈的国家以及欧美一些地区。考虑到学习计算机语言容易让人感觉到枯燥,因此本书以五子棋游戏为导向,将五子棋游戏开发与Python语言学习结合在一起,在学习语言时完成五子棋游戏的开发,在完成五子棋开发的过程中,加强对所学Python语言知识的理解,最终达到在游戏中学习的目的,这一直是本书作者所追求的目标。
1.1 Python语言简介
Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/)语言的创始人名叫吉多·范罗苏姆(Guido van Rossum),他在1989年的圣诞节期间,为了消磨时间,决心开发一个新的脚本解释程序,作为ABC语言的一种继承,而Python语言的名字来自于一部BBC电视剧--蒙提·派森的飞行马戏团(Monty Python's Flying Circus)。吉多·范罗苏姆认为Python语言的前任ABC语言之所以失败,究其原因是ABC语言不够开放造成,因此他希望能够将Python语言开放,从而避免这个问题。
与java语言一样,Python同样是一种跨平台语言,支持多种范式,例如面向对象、命令式以及函数式编程等等。它设计有动态类型匹配系统和自动内存管理系统,拥有庞大而全面的标准库,可被当作脚本语言用于处理系统管理任务和编写网络程序,此外它也非常适合完成各种高级任务。利用py2exe、PyPy、PyInstaller之类的工具可以将Python源代码转换成可以脱离Python解释器运行的程序。
Python2.0于2000年10月16日发布,与早期版本相比增加了完整的垃圾回收机制,并且支持Unicode编码。同时,整个开发过程更加透明,社区对开发进度的影响逐渐扩大。8年后,Python3.0发布,但是新一代Python3.0不完全兼容之前的Python2.0源代码,这也使得Python语言整体受到消极影响,不过,Python3.0中很多新特性后来也被移植到旧的Python2.6/2.7版本。目前,比较流行的是Python3.0和Python2.0,本书中使用Python2.0为语言基础(具体版本号为Python2.7.9),后续案例均使用此版本开发。
1.2 Python与C语言的异同
Python语言与C语言,在很多地方都是类似的,毕竟高级程序语言的发展思路,基本上都借鉴了C语言的理念,这种借鉴的好处是读者在学习一种语言后,可以很方便的学习其他语言,但是这两种还是有所区别的,即引号的使用、字符串结束符和代码块表示。下面本节将对这两个不同点进行说明。
1. 引号的区别
在Python中,单引号与双引号的作用是相同的。在C语言中,单引号来标识字符,用双引号来标识字符串。例如,
(1)Python引号使用例子 >>> c='abc123'>>> c'abc123'>>> print cabc123>>> c[0]'a'>>> type(c)<type 'str'>>>> b="abc">>> b'abc'>>> print babc>>> id(b)3085801056L>>> type(b)<type 'str'> |
(2)C语言引号使用实例
1 | #include <stdio.h> | 2 | #include <string.h> | 3 | int main() | 4 | { | 5 | char c='c'; | 6 | char s[10]="hello!"; | 7 | printf("输出字符:%c\n", c); | 8 | printf("输出字符串:%s\n", s); | 9 | } |
输出结果:
chello! |
2. 字符串结束符
Python字符串不是通过Null或者'\0'来结束的,而在C语言中,每个字符串末尾都有一个字符'\0'做结束符,这里的\0是ASCII码的八进制表示,也就是ASCII码为0的Null字符。例:
(1)C语言实例: 1 | #include <stdio.h> | 2 | Int main() | 3 | { | 4 | Int i=1; | 5 | char s[10]="hello!"; | 6 | for(;i<=10;i++) | 7 | { | 8 | if(s[i]=='\0') | 9 | { | 10 | S[i]=’c’; | 11 | Break; | 12 | } | 13 | } | 14 | printf("输出i的值: %d\n",i+1); | 15 | printf("output s[i]: %c\n",s[i]); | 16 | return 0; | 17 | } |
输出结果:
7output s[i]: c |
(2)Python语言实例:
>>> x='abc123'>>> x[0]'a'>>> x[2]'c'>>> x[1]'b'>>> x[3]=='\0'Traceback (most recent call last): File "<stdin>", line 1, in <module>IndexError: string index out of range | |
3. 代码块表示
学过C语言的读者都知道,在C语言中是使用{}来表示代码块的,但是在Python语言中,没有{}这种表示方法,取而代之的是使用缩进来描述代码块。这就使得空格和Tab键再也不是在编程中毫无用处的内容了,它们对于Python语言来说,是表示代码和语法逻辑的关键要素,如果你不小心用错,会直接导致程序无法运行。
Python语言的这种特点曾经引起了不少争议,因为这与其他编程语言来说,从编程习惯上改变比较大,需要花费时间去慢慢适应。但是从另外一个角度来说,使用更为严格的缩进方式,确实能够增加代码的规范程度、整齐、可读性和可维护性。 由于读者刚刚接触Python语言,因此在这里就先列举这3点与C语言的不同,其实Python语言与C语言的不同还有很多,例如三元操作符“?:”和switch…case语句等,但总结一句话,不要被其他语言的思维和习惯所困扰,从本质上理解和掌握Python语言的逻辑和思想,才能无往而不胜。
1.3 安装与配置Python环境
由于Python语言是一种跨平台语言,因此在不同操作系统平台安装的方式多少有些不同,下面将三大系统(Windows、Linux和MAC)下安装与配置Python环境的方法进行介绍。
1.3.1 Windows系统平台
在Windows系统上,安装Python通常有两种方式:
第一种方式是使用ActiveState(http://www.activestate.com/Products/ActivePython/)制作的ActivePython来进行安装,它是专门针对Windows的Python语言套件,其中含了一个完整的Python环境、一个Python的IDE以及一些Python语言的Windows系统扩展,提供了访问Windows内部API接口的服务,以及访问修改Windows注册表信息的能力。但是ActivePython软件并不是免费的,尽管它可以自由下载,通常下载Community Edition就可以满足日常开发工作,如图1.1所示。通过ActivePython安装Python环境的唯一缺点是,其中Python环境的更新速度比较慢,如果你一直想使用最新版本的Python,那么最好还是使用下面介绍的第二种安装方式。

图 1.1 Active Python Community Edition
第二种安装方式是使用Python发布的官方安装程序,下载过程是免费的,并且你可以选择下载的版本。安装方式详细过程如下:
步骤1:登录Python官方网站(https://www.Python.org/),如图1.2所示。

图1.2 Python官方网站
步骤2:在下载页面中选择下载Python安装包,如图1.3所示,本书使用的版本是2.7.9(下载地址为https://www.Python.org/ftp/Python/2.7.9/Python-2.7.9.msi)

图1.3 Python下载页面
步骤3:在下载的文件夹中找到Python安装文件,如图1.4所示。然后双击该文件开始安装。

1.4 Python安装文件
步骤4:双击安装文件后,如图1.5所示,可以再此选择安装和使用的用户,一般不需要更改,直接点击Next即可;

1.5 Python安装用户页面
步骤5:如图1.6所示,再此页面中选择Python环境安装的位置,推荐安装到默认位置,否则有时会产生意外错误。

图1.6 Python安装位置选择
步骤7:如图1.7所示,点击Python左边的下拉按钮,选择安装所有模块,此时所有模块都是白色(没有灰色或者红叉),然后点击Next。

图1.7 Python安装模块选择
步骤8:完成上述步骤后,程序就开始自动安装,如图1.8所示。稍微等待一会,安装完毕后(如图1.9所示)点击Finish,结束安装。

图1.8 Python开始安装

图1.9 Python完成安装
步骤9:安装完成后,可以再菜单->搜索框里输入cmd,再命令行列输入Python如出现如图1.10所示。就证明安装成功。

图1.10 Python运行
步骤10:设置Windows的环境变量Path,具体作如下设置(如图1.11所示):右击“我的电脑”->选择“属性”->选择“高级”->点击“环境 变量”,弹出环境变量对话框->在系统变量中,双击“Path”条目,弹出对话框->在变量值中加入Python的安装路径,例如:路径为 C:\Python;

图1.11 Python环境变量设置
1.3.2 Linux系统平台
除了windows系统外,Python还可以在很多系统上运行,例如Linux和MAC系统。本节重点介绍如何在Linux系统下如何安装Python程序。以最新版的ubuntu14.10系统为例,其默认安装了Python2.7.8,而目前最新的Python版本是2.7.9,所以需要重新安装。Python在Ubuntu系统下安装方法如下:
步骤1:下载Python 2.7.9安装包,网址是https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz,如图1.12所示;

图1.12 Python官网下载Python安装包
步骤2:下载完成之后,把文件放在linux系统中,放到自己容易到的文件夹下面。之后把tgz文件进行解压:可以鼠标操作,也可以用命令:tar -xzvf Python-2.7.9.tgz,解压之后,如图1.13所示,会有一个文件夹;

图1.13 Python安装包及其解压
步骤3:对Python-2.7.9进行编译安装,命令是./configure --prefix=${pwd};运行完之后,在终端中运行命令:sudo make;最后在终端中输入命令:sudo make install。如图1.14所示。至此完成Python的安装。

图1.14 Python安装
步骤4:设置Python环境变量。在终端中输入命令:sudo grdit /etc/profile,然后在打开窗口中(如图1.15所示),末尾加上Python安装路径,命令为: PATH="$PATH:/自己的路径/Python-3.3.3"export PATH |

图1.15 设置环境变量
步骤5:晚上上述步骤后,就可以使用Python了。
1.3.3 MAC系统平台 Python在MAC系统环境下安装方式与Windows系统类似,同样非常简单。以Python2.7.9为例,首先需要访问Python官方网站(https://www.python.org/ftp/python/2.7.9/python-2.7.9-macosx10.6.pkg)下载Mac系统安装程序(python-2.7.9-macosx10.6.pkg)。
然后可以使用浏览器自动打开Python程序,并打开一个Finder窗口展示其内容。(如果没有发生这样的情形,则需要在下载目录中找到磁盘映像,并双击挂载。它可能被命名为Python-2.7.9.dmg之类的名称。)磁盘映像包括一些文本文件(Build.txt、License.txt、ReadMe.txt),以及实际的安装程序包,Python.mpkg。双击Python.mpkg安装程序包以启动 Mac Python 安装程序。 安装程序的第一页(如图1.16所示)就Python本身给出了一段简要描述,然后提示您参阅 ReadMe.txt文件以掌握更多细节。点击 Continue[继续]按钮进入下一步。

图1.16 MAC系统安装Python页面1
接下来的页面实际包含一些重要信息(如图1.17所示):Python 必须安装在Mac OS X 10.3或其后续版本之上。如果仍在使用 Mac OS X 10.2,那就真的需要升级一下了。苹果公司已经不再为(Mac OS X 10.2)操作系统提供安全更新了,而且如果曾经上网的话,您的计算机可能已经处于危险之中了。点击 Continue[继续] 按钮继续前进。

图1.17 MAC系统安装Python页面2
如同所有优秀的安装程序,Python安装程序列出了软件许可协议(如图1.18所示)。Python是开源软件,其许可协议由 Open Source Initiative[开源软件促进会]提供。历史上,Python有过一些所有者和赞助者,每个都在软件许可协议之上留下了痕迹。但最终结果是:Python是开源的,可在任何平台上为任何目的使用它,而无需付费或承担对等义务。再次点击Continue[继续]按钮。

图1.18 MAC系统安装Python页面3
根据苹果安装程序框架的习惯,必须“agree[同意]”软件许可协议以完成安装(如图1.19所示)。由于Python 是开源的,实际上您所“同意”的只是授予您额外的权利,而不是剥夺它们。点击Agree[同意]按钮以继续安装。

图1.19 MAC系统安装Python页面4
下一个画面允许您修改安装位置(如图1.20所示)。必须将Python安装到启动驱动器上,但由于安装程序的限制,它并没有强迫这么做。说实话,从来没有需要过修改安装位置。从该画面中,您还可以自定义安装以剔除特定功能。如果想这么做,点击Customize[自定义]按钮;否则点击 Install[安装]按钮。
如果选择了自定义安装,安装程序将为您提供下列功能:
* Python Framework[Python框架]。这是Python的核心所在,由于必须被安装,它已经被选中并处于无法取消状态。 * GUI Applications[GUI 应用程序]包括 IDLE,即本书通篇将用到的图形化Python Shell。强烈建议保留该选项。 * UNIX command-line tools[UNIX 命令行工具]包括了Python命令行应用程序。同样强烈建议保留该选项。 * Python Documentation[Python 文档]包含了来自docs.Python.org的许多信息。如果使用拨号上网或者互联网访问受限的话,建议保留。 * Shell profile updater[Shell 文档更新程序]控制是否更新shell设置(用于Terminal.app 中)以确保此版本的Python位于Shell的搜索路径当中。您可能不需要修改该项设置。 * Fix system Python[修复系统 Python]不应作变更。

图1.20 MAC系统安装Python页面5
最后在安装所选功能时,安装程序将会显示进度条(如图1.21所示)。假定一切顺利,安装程序将会展示一个很大的绿色对号(如图1.22示),告知安装成功完成。点击 Close[关闭] 按钮退出该安装程序。

图1.21 MAC系统安装Python页面6

图1.22 MAC系统安装Python页面7
1.4 Python开发环境
随着Python语言的发展,目前已经出现了很多种开发环境,例如IDLE(第二章介绍),Eclipse等,由于篇幅有限,本节中只介绍其中一种Eclipse开发环境,IDLE开发环境会在第二章中详细介绍。
首先,使用Eclipse对Python进行开发需要在系统中安装Eclipse软件(网址https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/luna/SR1a/eclipse-java-luna-SR1a-win32-x86_64.zip),最新版本的Eclipse代号为LUNA,如图1.23所示。

图1.23 Eclipse LUNA
安装Eclipse完毕后,打开Eclipse程序,如图1.24所示。在打开界面,依次点击Help和Eclipse Marketplace。然后在搜索框中输入“pydev”,然后在搜索结果中安装Pydev-Python IDE for Eclipse3.9.2。如图1.25所示。

图1.24 Eclipse界面

图1.25 Eclipse市场 完成Pydev安装后,就可以使用Eclipse开发Python程序了。可以在Eclipse的新建项目中找到Python项目,如图1.26所示。

图1.26 Eclipse中新建Python项目
本章小结
本章着重介绍了Python语言的发展历史和由来,并与当前几种比较流行的编程语言,例如C、JAVA、C++、C#等,进行了比较和分析,重点比较了Python语言和C语言的异同点。另外还介绍了Python语言的编程思想,即简练、高效、可读,这也是为什么Python可以这么流行的原因。接下来介绍了Python在Windows系统、Linux系统以及MAC系统下如何安装和设置环境变量的方法,方便读者能够尽快搭建Python环境。最后介绍了Python开发环境,即Eclipse软件。希望读者阅读完本章后对Python语言有个初步的了解。

第2章 基本数据类型与变量
上一章对Python语言的发展历史和安装环境进行介绍,读者大致了解了Python语言的特点,以及如何安装Python程序和搭建环境等内容。本章将介绍Python的基本内容,即Python的基本数据类型和变量,这是学习Python的基础,包括数据类型、表达方式以及变量的使用。
【体系结构】

【本章重点】 (1) 掌握Python的基本数据类型和变量的表达方式。 (2) 熟悉Python数据的基本运算规则以及变量赋值问题。 (3) 灵活运用变量与值进行实际操作。
2.1 整数运算
与其他编程语言类似,Python中的整数分为整型和长整型。其中整型的取值范围从 -2147483648至2147483647,有符号位32位长,可表达的最大数为231-1。在数字前加0x或0X前缀表示十六进制数,在数字前加前缀0表示八进制数。Python支持任意长度的长整型,其最大值和最小值由可用的内存来决定。通常情况下,在数字的尾部加上“L”或“l”,但由于数字1和小写L较为相似,习惯用“L”表示。如:long=2312231223122312L。在Python语言中,整数的运算非常简单,Python支持对整数直接进行四则混合运算,运算的规则和在数学上的运算法则完全一致。最基本的算术运算,加(+),减(-),乘(*),除(/),幂运算(**)。从上例中读者也许已经发现有一个结果和预期不同,即7/8+1=1,为什么呢? >>> print 3+2+16 | >>> print 6-2*22 | >>> print 4/2+13 | >>> print 7/8+11 | >>> print 9-2**31 |
原因是,在Python中,整数与整数之间传统除法(/)运算,结果即使除不尽,也直接舍去余数得到一个整数。这也正是与数学的不同之处。如果想要知道余数是多少怎么办?Python提供了一个求余的运算符号“%”,如下例所示:
>>> print 10%31 |
那么如果只想用小数的形式表达出结果怎么办?来看下面的例子:
>>> print 6/51 | >>> print 6/5.01.2 | >>> print 3.0+14.0 |
根据上述计算结果,当整数遇到浮点数,就自动归为浮点数那一类,这是因为Python语言是动态的,它不会强制改变数据类型,由于浮点数的数据类型级别要高于整型,所以当整型与浮点型的数据共同运算时,会自动转换为等值的浮点型去计算,所得结果再被还原为原始结果。一般情况下,Python的数据类型会向精度更高的类型方向转换。
当面对现实中的实际问题时,计算就可能不会这么简单了,也许需要用一个很长的表达式来计算结果,此时就应该考虑优先级的问题了。与传统数学一样,括号可以提高运算的优先级,但是要注意,在Python中进行算术运算,只能使用小括号(),允许嵌套。动手试一试下面的举例: >>> print (1+2)*39 | >>> print 9-2*(1+3)1 | >>> print (4+3)/(2*(5-2))1 |
总体来说,算术运算符的优先级为:小括号>求幂运算符>乘、除、取模运算符>加、减运算符。
Python支持标准的比较运算符:< <= > >= == !=
通常比较运算符运算后会返回一个“真”、“假”逻辑值,在Python中也称布尔值(bollean)。(布尔类型在此处不做详细介绍,会在第四章对此类型进行讲述)举例如下:
>>> print 2>=3False | >>> print 3==5False | >>> print 2+3 != 1+4False | >>> print 3*2>9/(1+2) True |
在此需要注意的是,算术运算符的优先级要高于比较运算符的优先级,所以当混合运算时,要先计算算术运算符连接的数值,再将数值结果进行比较。最后,整数类型还有一种位运算。位运算符整理如下:
&:按位与 |:按位或 ^:按位异或 ~:取反 <<:左移 >>:右移 |
先来看看每种位运算符的使用方法:
>>> print 1&31 | >>> print 1|33 | >>> print 1^32 | >>> print ~3-4 | >>> print 1<<24 | >>> print 5>>21 |
首先要说明的是位运算都是对其补码进行运算,而Python的整数类型在计算机中用二进制表示是32位的。(长整型位宽不确定,补码做无限延展处理,这里不做过多叙述)。
按位与:1的补码是00000000 00000000 00000000 00000001,3的补码是00000000 00000000 00000000 00000011,进行按位与运算时,将二者的补码逐位相与,得到00000000 00000000 00000000 00000001,这是便1的补码。(按位或和按位异或与其道理相同)
取反:3的补码是00000000 00000000 00000000 00000011,取反后是11111111 11111111 11111111 11111100,这正好是-4的补码。
左移:左移n位与平时所常用的十进制中的乘10的n次方。例如1的补码00000000 00000000 00000000 00000001,左移2位变为00000000 00000000 00000000 00000100,这是4的补码,相当于乘以2的平方。(右移n位同理相当于除以二的n次方)
2.2 浮点数算术
Python中的浮点数在计算机中的存储格式与C语言中的double(双精度数浮点数)是一致的。所谓双精度浮点数,是指每个浮点数占64位(8个字节),Python支持直接使用十进制或科学计数法来表示浮点数。
十进制浮点数数: 3.2 17.328 -1.23456 -2.00000001 |
科学计数法浮点数:
4.25e3 3.1416E-19 -7.00001e25 123.345E-3 |
需要注意的是,用科学计数法表示浮点数时,其数值由一个小数点以及后面的指数(可选)组成,其中“e”或“E”后面的指数可正(+)可负(-),一般正指数可省略“+”。
在上一节中,已经讲述了整数的加(+)、减(-)、乘(*)、除(/)、幂运算(**)、取余(%)等运算,这些运算法则同样适用于浮点数,在此就不赘述了,读者可以动手操作一下。但需要注意的是,与整数除法相比,浮点除法才是真正的除法。除此之外,浮点数的运算还增加了一个除法运算符(//),这个运算符与整数除法相类似,即无论能否除尽,都将舍去余数部分。来看下面举例(注意:与整数除法相比,浮点除法才是真正的除法。): >>> print 3.124+1.254.374 | >>> print 2.0*3-4.501.5 | >>> print (5.50+4.5)/(2*(3.55-2.05))3.33333333333 | >>> print 4.36%2.00.36 | >>> print 13.0//4.13.0 |

表2.1 整数与浮点数的除法比较 整数与整数(/) | >>> print 11/4 # ==>2 | 整数与浮点数(/) | >>> print 11.0/4 # ==>2.75 | 浮点数与浮点数(/) | >>> print 11.0/4.0 # ==>2.75 | 浮点数与浮点数(//) | >>> print 11.0//4.0 # ==>2.0 | 整数与整数(/) | >>> print 11/4 # ==>2 | 整数与浮点数(/) | >>> print 11.0/4 # ==>2.75 | 浮点数与浮点数(/) | >>> print 11.0/4.0 # ==>2.75 |
对浮点数进行运算时,运算符的优先级与数学基本一致,需要注意的是幂运算,现举几个示例来看一下它的特殊性:
>>> print -2.0**2-4 | >>> print 2.0**2.14.28709385015 | >>> print (-2.5)**26.25 | >>> print 2.0+2**-12.5 |
从上面例子可以发现,当操作数在幂运算符左侧时,“**”比一元运算符“-”优先级高;操作数在“**”右侧时,一元运算符“-”比“**”优先级低。(最后一个运算相当于给-1加了一个括号,变为2+1**(-1)。
2.3 复数
随着时代的进步,实数集已不能满足人类探索科学的需要。例如,求解一元二次方程时,若判别式小于0则无实数解。因此人类将数集再次扩充,复数概念便由此诞生。复数被定义为a+bi的形式,其中a和b均为实数,i是虚数单位(即-1开根)。而在运算上,复数也满足四则运算等性质。本小节将针对Python中的复数进行详细讲述。首先,要明确复数的几个规定:
(1) 复数是由实数部分和虚数部分共同组成的。 (2) 虚数部分不能脱离了实数部分而单独存在。 (3) Python中复数的表达形式:real+imagj。 (4) 实数部分和虚数部分均以浮点数类型存在。
规则看起来有点抽象,请读者按照下面的举例具体理解:
>>> print (2.1+3.2j)+(1.4+4.3j)(3.5+7.5j) | >>> print (8.0+4.2j)/(1-2j)(-0.08+4.04j) | >>> print ((1+2j)+1)*(2-1j)(6+2j) |
掌握了复数在Python中如何显示,下面一个表列举了复数的三种内建属性:
表2.2 复数的内建属性 属性 | 解释 | n.real | 返回所求复数实数部分的值 | n.imag | 返回所求复数虚数部分的值 | n.conjugate() | 返回所求复数的共轭复数 |
下例是列举出如何显示复数的属性:
>>> n=2+6j | >>> print n(2+6j) | >>> print n.real2.0 | >>> print n.imag6.0 | >>> print n.conjugate()(2-6j) |
这也就证明了实数部分和虚数部分都是以浮点数的类型存在的,即使你以整数去表达它们,它们的性质也不会改变。
2.4 字符串
所谓字符串就是一个或多个字符连接起来的序列。字符串是Python中最基本数据类型之一,几乎每一个程序中都会用到它。在Python中规定,存在于''或""中的任意文本即为字符串(''或""必须是在英文状态下的符号)。请亲自动手输入如下小程序:
>>> print "Hello,world!"Hello,world! | >>> print 'Hello,world!'Hello,world! |
两种引号引用的字符串所得到的结果一模一样。那么,Python语言为何要支持这两种引号?请看下面的示例:
>>> print 'Let's go!'SyntaxError: invalid syntax | >>> print "Lily said:"Good morning!"to Mary "SyntaxError: invalid syntax |
可以看出程序报错了,原因是字符串本身就包含了''或"",导致Python无法辨别该如何匹配,也就不能成功将字符串输出。遇到这种情况,''和""就很好地发挥了作用,将二者交叉使用便可以解决这个错误:
>>> print "Let's go!"Let's go! | >>> print 'Lily said:"Good morning!"to Mary'Lily said:"Good morning!"to Mary |
可是,如果遇到了一个很复杂的字符串,字符串本身既包含""又包含''怎么办?那么就引入了一个新的符号——转义符(\)。
【例2-4】请输出字符串:Mary said:"I'm a girl". >>> print "Mary said:\"I\'m a girl\"."Mary said:"I'm a girl". |
程序分析:由于字符串本身含有双引号"",所以为避免因Python无法识别结束符而引起的错误,用转义符\'和\"告诉Python不要对它们进行匹配。转义符不但可以“转义”单、双引号,还可以转义一些其他符号,这里提供了一些常用的转义符:
表2.3 常用的转义字符 转义字符 | 解释 | \n | 相当于换行符 | \\ | 相当于\字符本身 | \ | 在行尾时,相当于续行符 | \' | 相当于一个单引号 | \" | 相当于一个双引号 | \a | 相当于响铃 | \b | 相当于退格键(Backspace) | \v | 相当于纵向制表符 | \t | 相当于横向制表符 | \r | 相当于回车 | \f | 相当于换页 | \oyy | 八进制数yy代表的字符 | \xyy | 十进制数yy代表的字符 | \other | 其它的字符以普通格式输出 |
转义字符的确很有帮助,但是如果字符串特别长,例如,输出一条文件存储路径:
>>> print "path:C:\\Program Files\\Microsoft Games\\Heart\\bins"path:C:\Program Files\Microsoft Games\Heart\bins |
需要注意的是反斜线“\”不可以用在字符串的末尾处,这样做会出现错误,会使Python迷惑这到底是不是结尾。当遇到类似这种情况时,就要对很多字符进行转义,这样做非常麻烦,为了简化操作,Python又定义了一个raw字符串。raw字符串规定,字符串中的内容无需转义,它的作用就是将字符串中的内容还原成最初的样子。但是,raw字符串不能表示带有""或''的字符串,例如输出下面的字符串:
>>> print r'Mary said:"I\'m a girl".\n'Mary said:"I\'m a girl".\n |
如上例所示,虽然对'进行了转义,但还是会输出一个反斜杠\。而换行符也失去了作用,被当做普通字符串输出。
随着程序代码的增多,如果都写在一行代码上就会繁乱复杂,没有条理,此时如果想要分多行输出,一直用换行符(\n)就太不方便了。Python中的'''...'''即可表达多行字符串: >>> print '''paragraph1...paragraph2...paragraph3'''paragraph1paragraph2paragraph3 |
当然也可以将三个单引号换成三个双引号""",因为这种特殊的引号,中间的字符串可以随意使用''和"",不再需要对其进行转义:
>>> print '''Lily:"Hi Mary,this is Lily."...Mary:"Hello Lily,how are you today?"...Lily:"I'm fine,thank you!" '''Lily:"Hi Mary,this is Lily."Mary:"Hello Lily,how are you today?"Lily:"I'm fine,thank you!" |
为了灵活运用字符串,还可以将raw字符串与'''...'''结合起来,变为r'''...''',这种表达方式可以同时实现raw字符串和'''...'''的功能:
>>> print r'''first code:"\n"...second code:"\t"...third code:"\\" '''first code:"\n"second code:"\t"third code:"\\" |
众所周知,计算机只能对数字进行处理,现在所用的文本,也都是先全部转化为数字,然后再进行处理。至于ASCII编码,它是单字节字符的数据集,可以表示的字符类型不足以满足人们的要求,所以Python便有了Unicode字符串。Unicode字符串可以表示从欧洲到亚洲的字符集,把这些语言汇集成一个统一的编码中,这样在实际编写程序中便不会再出现乱码的情况。Unicode字符串实际上与普通字符串没有太大的区别,只是在要写的字符前面添加一个“u”:
>>> print u"你好,Python。"你好,Python。 |
2.5 类型转换
由于Python是一种动态语言,在设置变量时无需事先声明变量类型,Python会自动判断,但如果想将某个类型变量转换成其他类型,就需要使用相应类型转换方法。所谓类型转换是一种内建函数,需要被转换的对象即为函数的参数,在转换时相当于新建了一个对象,把原有的内容复制到新生成对象中去。表2.4列出了常用的类型转换函数和解释:
表2.4 类型转换 转换函数 | 解释 | int(n[,base]) | 将n转换为一个整数(base为待转换的进制数,如缺省,则默认转换为十进制数) | long(n[,base]) | 将n转换为一个长整数(base为待转换的进制数,如缺省,则默认转换为十进制数) | float(n) | 将n转换为一个浮点数(默认保留一位小数) | str(n) | 将n转换为一个字符串 | list(s) | 将s转换为一个列表 | tuple(s) | 将s转换为一个元组 | unichr(n) | 将一个整数n转换为一个Unicode字符 | chr(n) | 将一个整数n转换为一个字符 | ord(x) | 将一个字符x转换为与它对应的整数值 | hex(n) | 将一个整数n转换为与它对应的十六进制的字符串 | oct(n) | 将一个整数n转换为与它对应的八进制的字符串 |
以str(n)函数为例进行说明:
>>> n=2.6>>> mystr=u'我正在学习Python'>>> print mystr+nTypeError: coercing to Unicode: need string or buffer, float found |
很显然这个程序出错了,原因是“+”这个符号除了有加法的意义,还起到一个连接的作用,但是浮点数不能和字符串用“+”相连接,如果想要输出“我正在学习Python2.6”,就要将“2.6”转换为字符串,再进行连接输出,所以使用类型转换作如下改动:
>>> n=2.6>>> mystr=u'我正在学习Python'>>> print mystr+str(n)我正在学习Python2.6 |
这样将浮点数用str(n)的类型转换之后便可以正常输出预期的结果。
2.6 变量和值
变量,对读者来说并不陌生,早在学习方程时就已经接触到了变量。所谓变量,就是没有固定的值,可以变化的量。这一节将详细的介绍Python是如何定义变量和怎样给变量赋值。
在使用变量前,首先要起一个变量名,就像每个人都有自己的名字一样,给变量一个名字之后,就可以通过名字找到这个变量的值。在Python中,定义变量名的规则和大多数语言的规则相同,即变量名由大、小写英文字母、数字和下划线(_)组成,但是不能以数字开头。而且,Python语言对字母大小写敏感,比如:abc和Abc是两个不同的变量。
以下是一些正确的变量名:
abc word_1 n _num exap_a_001 |
以下是一些不正确的变量名:
1word hello&world this-word return 12 |
特别要注意的是,不能以关键字作为变量名,也不能以数字开头,不然会出现错误:
>>> 1num=1File "<stdin>", line 1SyntaxError:invalid syntax | >>> class='hello'File "<stdin>", line 1SyntaxError:invalid syntax |
表2.5 Python的主要关键字 False | class | finally | is | return | None | continue | for | lambda | try | True | def | from | while | and | del | global | not | with | as | elif | if | or | yield | assert | else | import | pass | break | except | in | raise |
了解了如何定义一个变量之后,就要考虑为这个变量赋值,和C语言相同,“=”已不再是数学符号中单纯的“等于”,Python赋予了它新的功能---即赋值符号。“=”右面的值会赋给“=”左面的变量:
>>> sum=0>>>print sum0 | >>> word_1='Hello'>>> print word_1Hello |
接触过C语言或Java语言的读者也许会困惑,Python变量不需要声明变量类型吗?的确,对于像C或Java这样的静态语言来讲,在使用变量之前要先声明变量类型,但是Python是动态的,变量的类型是由赋予它的值来决定的:
>>> a=1>>> a=1.001>>> a="abc">>> print aabc |
从上面的举例可以看出,第一次为变量“a”赋值是整型,第二次赋值是浮点数,第三次赋值是一个字符串,而最后输出时只保留了最后一次的赋值,而且输出的是一个字符串,这就证明了,在Python中,无需声明变量类型,Python会自动根据变量值来判定。除了给变量赋一个值以外,变量之间也可以相互赋值,请看下面例子:
【例2-6】给变量a赋值,再利用a给另一个变量b赋值,使b的值与a的值相同,最后赋予变量a一个新的值,并用输出语句验证结果。 >>> a=1>>> b=a>>> print b1 | b的值已验证,下面验证变量a的值。 >>> print a1 | a的值也已验证,下面赋予a一个新的值。 >>> a="Python">>> print aPython |
程序分析:在上例中读者会发现,a作为一个变量将值赋给了一个新的变量b,而自身的值并未消失,这相当于创建了一个副本,将a的值复制给b,这时变量b就拥有了和a相同的值,但当一个新的值再次赋给变量a时,就将先前的值覆盖掉了。
也可以先将“=”右边的表达式进行计算,然后再赋值给变量,表达式中的变量可以是“=”左边的变量本身: >>> sum=3>>> num=2>>> sum=num*sum>>> print sum6 |
这个例子就很好的结合了变量和赋值,分别为两个变量赋值,再利用两个变量名对两个数值进行计算,最后再次赋值给变量并输出。这也可以让你更加深刻的理解“=”与传统的“等于”的区别,因为在传统数学中sum=sum*num是不可能成立的(除非num=1),请牢记“=”在Python中是赋值符号。
2.7 赋值语句
上一节中讲到了变量,如果读者接触过C语言,那么应该很清楚,在C语言中要想使用变量,首先必须对变量进行声明,定义该变量的所属类型。但Python则不需这么繁琐,因为Python是动态类型编程语言。Python规定,变量是没有类型之分的,变量的使用不需要提前声明和定义,只需对变量进行赋值,赋值的同时该变量即被创建。这里也体现了Python相对于其他编程语言的优势之处。下面就来详细地讲解一下Python的赋值语句。
赋值语句的基本形式是:变量=值,即用“=”来给变量赋值,等号右边的部分可以是整数,浮点数,字符串,表达式等。下面举一些例子(#号后面部分为注释部分,不被程序执行): >>>A=1 #整数赋值; | >>>A | 1 | >>>A=A+1 #表达式赋值,将A自身加1; | >>>A | 2 | >>>y=2+4 #表达式赋值; | >>>y | 6 | >>>x=3.14 #浮点数赋值; | >>>x | 3.14 | >>>str="How are you? " #字符串赋值; | >>>str | "How are you? " |
变量只有被创建和赋值之后才能被使用,否则会引发错误,如图2.1所示。

图2.1 没创建即使用变量引发错误示意图
还有一种赋值形式叫增量赋值,它简化了前面一种赋值方式,将等号和运算符号写在一起,举例说明:
>>>Y=3 | >>>Y+=4 | >>>Y | 7 |
上例中语句Y+=4是Y=Y+4的简化,这种简化的写法使代码更紧凑,提高了代码的可读性。但是两种赋值方式并不完全等价,具体差别之处在2.7.2节讲解。下面表格中列出了常用的增量赋值符号。
表2.6 常用增量赋值符号表 增量赋值符 | 简化于 | A+=B | A=A+B | A-=B | A=A-B | A*=B | A=A*B | A/=B | A=A/B | A%=B | A=A%B | A**=B | A=A**B |
要注意的一点是,Python中赋值语句是没有返回值的。因此,例如下面的代码是存在错误的:
>>>print X=1 | SyntaxError:invalid syntax | >>>Y=(X=1) | SyntaxError:invalid syntax |
2.7.1 变量引用
前面讲到了运用赋值语句为变量赋值,那么赋值语句的原理到底是什么呢?变量是如何引用值的呢?接下来请读者带着这些疑惑来认真阅读下面的内容来找出答案。
首先介绍一个概念--对象。在Python中,对象是最基本的概念。使用到的整数,浮点数,字符串以及之后要讲到的元组、列表、字典等数据结构都是作为对象存在的。每个对象都有两个标准的头部信息,一个是用来标识这个对象的类型的类型标识符,另一个是记录该对象引用次数的计数器。例如,对象3,2.15,“English”,看到的只是这些对象的值是3,2.15,English,但是其实还包含一个隐含的信息是它们的类型分别是整数,浮点数,字符串。因此说,Python中变量是没有类型的,类型的概念是属于对象的。用id()函数可以返回对象的内存地址。 >>>N=7 | >>>id(N) #不同的电脑,不同的内存使用情况,这个值会有所不同 | 30637240L | >>>fruit="apple" | >>>id(fruit) | 45948040L |
给变量赋值并不是简单地将一个值赋给该变量。看似简简单单的一行赋值语句,实际上做了很多幕后的工作。当使用一个赋值语句时,若等号右边的对象在该赋值语句之前未被创建,则该赋值语句首先做的工作是创建这个对象;同样地,若等号左边的变量在该赋值语句之前未被创建,则此时创建这个变量。最后就是将创建好的对象和变量联系起来即引用。引用就是将对象和变量绑定起来。更形象地说,变量是C语言中指针的概念,通过引用将变量指向了一个对象的内存空间,不管这个对象是刚刚创建的还是之前就存在的,都是将该对象的引用赋给变量,也可说是变量引用了该对象,而不是将该对象的值直接赋给变量。当变量引用了某个对象后,在该变量之后的使用中都会被当前引用的对象所替换。而对象被变量引用后,对象内部的引用计数器也会相应地增加1。
下面是对赋值语句的过程做的一个总结: (1) 创建一个对象,前提是该对象未被创建; (2) 创建一个变量,前提是该变量未被创建; (3) 将对象的引用值赋给变量,引用计数器值增加1;
以X=1为例来深刻地理解赋值语句的过程。
>>>X=1 | >>>Y=X | >>>X | 1 | >>>Y | 1 | >>>X=2.2 | >>>X | 2.2 | >>>Y | 1 |
程序分析:执行第1行代码,Python做的工作如下:
(1) 创建一个对象,该对象的类型为整型,值为1; (2) 创建一个变量,变量名为X; (3) 将对象1的引用值赋给变量X,对象1的引用计数器值增加为1; (4) 再执行第2行代码,Python做的工作如下: (5) 变量X被其引用的对象1替换; (6) 创建一个变量,变量名为Y; (7) 将对象1的引用值赋给Y,对象1的引用计数器值增加为2;
执行完这两行代码后的结果是,变量X和变量Y共同引用了对象1,这种多个变量引用同一个对象的过程称为共享引用。用图来形象地表示这种引用关系,如图2.3所示。

图2.2 执行第1条语句后形象图

图2.3 执行第2条语句后形象图
也许有部分读者对上例中的8~11行会有些许疑惑。其实只要真正理解了前面讲的赋值语句实际是对象值引用的原理,就不难看懂这几行代码。执行第7行代码后,Python做的工作如下:
(1) 创建第二个对象,该对象的类型为浮点型,值为2.2; (2) 将对象2.2的引用值赋给X,此时,变量X撤销对整型对象1的引用,转而引用浮点型对象2.2,相应地,对象1的引用计数器值减少为1,对象2.2的引用计数器值增加为1;
结束第7行的代码后,X引用对象2.2,那么,此时变量Y呢?虽然执行了第二行代码Y=X,但是,读者不要将此时的“=”和彼时的“=”混淆了,此时的“=”是“赋值号”,不再是曾经的“等于号”,因此,并不能理解为Y和X是等价的,X=2.2,所以Y=2.2。正确的理解为X=2.2后,只是改变了变量X的引用对象,它改变不了变量Y的引用对象,因此,执行完第7行代码后,变量Y的引用对象依然是对象1。结束第7行代码后情况如图2.4所示。

图2.4 执行第7条代码后变量结果
2.7.2 赋值
通过上一节知道赋值语句是使变量建立对象的引用值,而不是复制对象,即赋值,不复制。看下面例子。
>>>R=[2,4,6,8] | >>>S=R | >>>R | [2,4,6,8] | >>>S | [2,4,6,8] | >>>id(R) | 50677832L | >>>id(S) | 50677832L | >>>R[2]=5 | >>>R | [2,4,5,8] | >>>S | [2,4,5,8] |
首先简单介绍一下例子中一对方括号([])形式的数据结构——列表,详细内容后续章节会介绍。列表是多个元素的集合体,每个元素可以是任意相同或不同的类型,每个元素被分配一个序列号,称为索引。索引从0开始,第一个序号为0,第二个为1,依次类推。可以利用索引来引用列表中的元素,例如例子中的列表R=[2,4,6,8],第10行中使用到的R[2]即是指列表R的第3个元素(注意索引是从0开始)。列表与前面讲到的数据类型如整型,浮点型,字符串等不同之处是,列表是可变的,当对列表中某个元素进行重新赋值时,改变了这个列表对象,而不是创建了一个新的列表对象。而整型,浮点型,字符串是不可变的,当变量引用这些对象后,重新赋值,是创建了一个新对象,而不是改变了原来的对象。
回到例子中,第1行和第2行代码使列表R和列表S引用同一个列表对象,也可称为共享同一个列表对象,而当执行第11行代码R[2]=5后,将列表R的第3个元素6重新赋值为5,此时是使原列表[2,4,6,8]变成了[2,4,5,8],虽然从代码中没有明确地对列表S重新赋值,但是列表R的重新赋值改变了R和S的共享对象引用值,所以S的值也随之改变。
但是如果将赋值语句理解成是复制,那么结果应该是这样的:通过S=R,先将对象[2,4,6,8]复制一遍来创建一个一模一样的副本对象,R引用的是原本对象,而S引用的是副本对象,当R重新对第三个元素赋值时只是改变了原本对象,副本对象是不会受影响的,所以最后两条输出语句应该是
>>>R | [2,4,5,8] | >>>S | [2,4,6,8] |
可能有些读者会认为复制的说法也说的通啊,为什么是错的呢?看一下代码的第7~10行,可以看到,列表R和S的内存地址是相同的,都是50677832L,这就有力地证明了赋值语句是引用,不是复制。
这里补充讲解一下普通赋值与增量赋值存在的区别。根据前面讲解的内容对对象,列表等概念有简单的了解,知道了列表对象是可变的,而整型,浮点型,字符串等对象是不可变的。两种方式对不可变对象的处理是一样的,都会创建一个新对象。而对可变对象的处理上确实不同的。增量赋值会将可变对象就地修改,而不是创建新的对象。举例说明如下: >>>L=[1,2,3] | >>>id(L) | 43200712L | >>>L=L+[4] | >>>id(L) #普通赋值方式创建了新列表对象,ID值改变 | 43186440L | >>>L+=[5] | >>>id(L) #增量赋值方式就地修改列表对象,不创建新对象,ID值不变 | 43186440L |
2.7.3 多重赋值
有些时候,需要对多个变量进行赋值,此时就需要利用Python语言中的多重赋值功能。在Python中,一次给多个变量同时赋值,称为多重赋值。多重赋值减少了代码的行数,提高了代码的质量与可读性。
>>>X=Y=Z=22 | >>>X | 22 | >>>Y | 22 | >>>Z | 22 |
此例中先创建了一个整数对象,该对象的值为22,变量X,Y,Z同时引用同一个对象22。这种赋值方式也称为链式赋值。下面的例子也是多重赋值的一种形式。
>>>X,Y,Z=2, "python",3.56 | >>>X | 2 | >>>Y | ‘python’ | >>>Z | 3.56 |
程序分析:这个例子中,创建了三个对象分别为整型对象2,字符串对象Python,浮点型对象3.56,同时将这三个对象分别赋给X,Y,Z三个变量。使用这种赋值方式时一定要注意,等号左边的变量数和等号右边的对象数一定要一致,否则会引发错误,如图2.5所示。

图2.5 等号左右参数数量不一致引发错误示意图
多重赋值语句还可以用来在不需要引入辅助变量的情况下直接交换两个变量的值。请看下面的例子:
>>>a,b=5,10 | >>>a | 5 | >>>b | 10 | >>>a,b=b,a | >>>a | 10 | >>>b | 5 |
程序分析:第1行代码创建了两个整型对象5和10,并分别赋给了两个变量a和b。根据2.8.1节中讲到的,在一个变量引用了某个对象之后,在之后的这个变量的使用中都会被当前引用的对象所替换,当执行第6行代码时,等号右边的b和a分别被其引用的对象10和5替换,再通过赋值符号赋给了变量a和b,进行了两个变量间的值的交换。
实际上,针对不同的数据结构,赋值语句的形式也有所不同,本节只是通过几种常见的形式来介绍赋值语句的原理,在接下来的学习当中,读者还会接触到更多的数据类型及其赋值方法。
本章小结
本章介绍了基本数据类型的表达方式、数据的算术运算、以及变量的定义和赋值。详细描述了数据间运算的规则,讲解了字符串如何正确的在Python中输出。介绍了定义变量名需要注意的事项,为避免错误列举了Python中的关键字。Python是动态的,所以使用变量无需事先声明变量类型。本章知识点较繁琐,并且均为基础内容,需要读者反复熟悉为今后灵活使用Python打下坚实基础。
习题
一.填空题 1. Python变量名可以由__________、__________和__________组成,其中不能以__________开头。 2. 在进行位运算时,实际上是对整数的进行运算,字宽为__________位。 3. 当不同类型的数据混合运算时,将自动向__________的方向转换。 4. 定义一个变量,并对它赋值:a=1.234,变量a的类型是__________。 5. 复数是由__________和__________共同组成的。这两个部分的数据类型是__________。 6. 如果现用“... ”定义一个字符串,但是字符串本身刚好也包含"",应当用__________转义字符使得字符串可以正常输出。 7. 整数之间的除法运算“7/2”的值是__________。 8. 现有定义“a=1,b=a,a=2”,则a、b的值分别为__________、__________。 9. 现要对两个整数进行位运算“3&6”,计算结果是__________。 10. 现有定义“mystr=123,mystr=(6-8j),mystr="Python"”,最后mystr的变量类型是__________。
二.选择题
1. 以下哪个不是Python的关键字() a) A,import B.Hello C.del D.True 2. 以下哪个是合法的变量名() b) A.3number B.My_str_0 C.Python$ D.class 3. 表达式“(6+7)/((6-4)*2)”的值是() c) A.3.25 B.3 C.3.0 D.0.25 4. 表达式“(3+4.0)+(1-2j)”的结果是() d) A.(8-2j) B.(8.0-2.0j) C.(8-2.0j) D.(8.0-2j) 5. 下列给出的字符串哪个是换行符() e) A.\n B.\f C.\\ D.\t" 6. 先要将值23232323232323232323赋给变量x,则x的类型是() f) A.字符串 B.长整型 C.浮点型 D.无法表示 7. 表达式“7.0//3.0”的结果是() g) A.1.0 B.2 C.2.0 D.2.33333333333 8. 位运算“~3”的结果正确的是() h) A.4 B.-3 C.-4 D.1 9. 现要输出一句话“I'm Lily”,以下哪种是不正确的() A. print "I'm Lily" B. print 'I'm Lily' C. print """I'm Lily""" D. print 'I\'m Lily' 10. 现有“a=2,print a”,若想将输出结果变为一个字符串类型,要怎样操作() i) A. int(a) B. str(a) C. chr(a) D. list(a)
三.编程题
1. 编写一个程序,输出字符串:The man said:"I'm the boss". 2. 编写一个程序,将0xFF用16进制,10进制,8进制输出(提示:%x,%d,%o) 3. 编写一个程序,给定一个圆的半径,输出圆的面积。 4. 编写一个程序,给定一个操场的长和宽,根据给定值,输出操场的面积(例如,给定长为200m,宽为170m)。 5. 编写一个程序,给定一个用3种方法实现两个数的交换。

第3章 编写Python程序
通过上一章的学习,相信读者已经对Python语言的基本数据类型与变量有了一定了解,但是要想对这些枯燥的知识点有更深刻的理解与记忆,就要亲自动手在程序中运用它们。那么如何编写Python程序呢?本章将首先介绍一种编写Python程序的工具,并且详细地讲解该工具的使用方法和技巧。此外,本章还介绍了用命令行调用Python和运行Python程序的方法。建议读者认真学习并掌握。
【体系结构】

【本章重点】 (1) 掌握IDLE的使用方法和技巧; (2) 掌握使用命令行调用和执行Python程序; (3) 掌握Python的注释方法。 3.1 IDLE
IDLE是Python创初人Guido van Rossum使用Python和Tkinter创建的一个IDE(Integrated Development Environment,集成开发环境),并且是Python缺省的IDE。IDLE不仅提供了一个功能完善的代码编辑器,还提供了一个Python shell解释器和调试器,允许在代码编辑器完成编码后,在shell中实验运行并且用调试器进行调试。初学者可以利用IDLE方便地创建、运行、测试和调试Python程序。IDLE是和Python一起安装的(回忆第一章),在Python安装时,选中“Tck/Tk”选项(在如图3.1所示,默认该组件是被安装的),那么就可以把IDLE安装到电脑中。如果操作系统是windows系统的话,那么可以通过“开始”菜单→所有程序→Python2.7→IDLE(Python GUI)打开它,如图3.2所示。

图3.1 安装时IDLE所属组件

图3.2 IDLE所在位置图 3.1.1 IDLE中编写程序
1. Shell交互解释器
打开IDLE会弹出IDLE的shell交互解释器窗口,在窗口中显示如下所示的几行信息:
Python 2.7.8 (default, Jun 30 2014, 16:08:48) [MSC v.1500 64 bit (AMD64)] on win32 | Type "copyright", "credits" or "license()" for more information. | >>> |
开头两行指出了运行的是哪个Python版本,这里使用的是Python 2.7.8版本,这也是目前使用较广泛的版本。第三行的“>>>”是Python shell的提示符,它提示从这里输入Python语句。试着在提示符后输入下面的命令来完成经典的“Hello world”程序。 >>>print "Hello world" |
按下回车键,可以看见屏幕打印输出了字符串“Hello world”,并另起一行显示了提示符,提示可以继续输入。 Hello world | >>> |
可以看到字符串“Hello world”前面是没有“>>>”提示符的,这是因为该字符串是解释器生成的。因此,根据是否有“>>>”提示符,就可以分辨出哪些内容是用户输入的,哪些内容是解释器生成的。此外,shell还可以作为计算器进行一些简单的算数运算。如下例所示。 >>>23+153 | 176 | >>>13*64 | 832 | >>>3**6 | 729 |
前面的例子中输入了正确的Python语句,解释器也相应地作出了正确的输出。但是,如若不小心输入了错误的信息,解释器会做何反应呢?例如在提示符后依次输入如下信息:
>>>print "I Love China" | >>>print I Love China |
根据第二章字符串的介绍,输出字符串时要给字符串加上双引号或单引号,如果没有会发生语法错误。如下所示:
>>>print "I Love China" | I Love China | >>>print I Love China | SyntaxError: invalid syntax |
这是解释器对输入的错误信息进行的错误提示,SyntaxError代表这是一个语法错误,invalid syntax指出输入的内容是无效的语法。
有时因为用户的粗心大意,或者对知识点掌握地不够全面,常常会输入错误的代码。对于一些简单的错误,IDLE会及时辨认并且做出反应,提供错误提示以便用户及时修改。下面列出了一些在编程中常常会出现的错误。
(1)试图改变字符串的值。字符串是不可变的,尝试修改字符串的值会引发TypeError。 >>>str="abc" | >>>str[0]="d" | Traceback (most recent call last): | File "<pyshell#2>", line 1, in <module> | str[0]="s" | TypeError: 'str' object does not support item assignment |
(2)在for、while、if、elif、else、def、class等语句后面忘记添加“:”会导致SyntaxError。
>>>i=0 | >>>while i<4 | SyntaxError: invalid syntax |
(3)试图连接非字符串值与字符串值,会导致TypeError。
>>>num=12 | >>>print "I have"+num+"apples" | Traceback (most recent call last): | File "<pyshell#6>", line 1, in <module> | print "I have"+num+"apples" | TypeError: cannot concatenate 'str' and 'int' objects |
(4)将等于号“==”错写成赋值号“=”,会导致SyntaxError。
>>>flag=True | >>>if flag=False: | SyntaxError: invalid syntax |
(5)变量或函数没有定义就使用和变量名或函数名拼写错误都会导致NameError。
>>>p | Traceback (most recent call last): | File "<pyshell#9>", line 1, in <module> | p | NameError: name 'p' is not defined |
(6)试图使用Python关键字作为变量名会导致SyntaxError。
>>>for=2 | SyntaxError: invalid syntax |
(7)错误地使用缩进量会导致IndentationError。
>>>r=1 | >>>if i<3: | i+=1 | File "<pyshell#17>", line 2 | i+=1 | ^ | IndentationError: expected an indented block |
(8)方法名拼写错误会导致AttributeError。
>>> string="This is my friend" | >>> string=string.upperr() | Traceback (most recent call last): | File "<pyshell#20>", line 1, in <module> | string=string.upperr() | AttributeError: 'str' object has no attribute 'upperr' |
2. 调试器
对于简单的错误,解释器可以检测出并指示出错误所在的位置,同时会停止程序执行。但是对于较复杂的逻辑错误,解释器就显得力不从心了。为了解决这个问题,IDLE提供了调试器。利用调试器,可以分析被调试程序的数据,并跟踪程序的执行流程,找出代码中隐蔽的错误。
IDLE的调试器的使用方法如下:在Debug菜单中选择Debugger,此时Debugger为选中状态,在该选项前会显示一个对号。进行这个操作后会打开调试器Debug Control窗口,如图3.3所示。打开的同时会在Python shell窗口中输出“[DEBUG ON]”以显示调试器为开启状态。关闭调试器和打开调试器的方法相同,在Python shell窗口中的Debug菜单中选择Debugger后,调试器Debug Control窗口就会相应地关闭,Debugger选项前的对号也会消失,同时,Python shell窗口中输出“[DEBUG OFF]”以显示调试器为关闭状态。

图3.3 调试器界面
从图3.3可以看到,左上角有一行按钮组,共5个按钮,此时它们是灰色的,即不可使用状态,但当运行某个程序,开始具体调试程序的时候,这些按钮就会变为可使用状态。下面介绍一下每个按钮的作用:
* Go:运行程序直到下一个断点处停止。断点可以理解为暂停的控制点,即程序运行到该断点处会停止运行的控制点。断点可以自己手动设置,在代码中将光标放置在想要设置断点所在的行尾处,鼠标右键选择Set Breakpoint即可。 * Step:一步一步逐条执行程序,当遇到函数时会进入到函数内部。 * Over:如果程序当前运行到的一行要调用函数,那么如果按Over按钮会直接运行完整函数得到函数的结果,而不会进入函数内部。如果按下Step按钮将会进入函数内部并将函数逐条执行。 * Out:如果当前位于某个函数内部,按下Out按钮将会执行完该函数中剩余代码并跳出该函数。 * Quit:取消此次调试。
按钮组后面还有4个复选框选项,它们的含义如下:
* Stack:显示当前运行状态模块,控制图中白色空白区域的显示,该复选框默认是选中的。 * Source:显示源代码模块。在源代码处用灰色背景显示出当前运行所在的行。一般情况下可以直接看Stack部分,Source可以不勾选。 * Locals:显示局部对象,该复选框默认是选中的。 * Globals:显示全局对象。
如果调试到一半时想要退出,请点击Quit按钮结束本次调试,而不要直接点击调试器窗口右上角的“X”,因为这可能会带来一点麻烦,当再次点击Debug→Debugger进行调试时,会弹出下面图3.4这个框。

图3.4 不可调试警告
以下面这个例子来讲解如何使用调试器来调试程序:
1 | #coding=utf-8 | 2 | #计算1~100内所有偶数的和 | 3 | def isEven(x): | 4 | if x%2: | 5 | return False | 6 | else: | 7 | return True | 8 | for i in range(100): | 9 | sum=0 | 10 | if isEven(i): | 11 | sum=sum+i | 12 | print sum |
首先打开调试器,然后运行该程序后,会看到shell中输出结果竟然为0!很显然,这个结果是错误的,调到Debug Control窗口,如图3.5所示。蓝色背景行代码为程序可执行的第一条代码。

图3.5 调试程序窗口
此时,点击Step按钮单步逐条执行该程序,发现每次程序进入到for循环内后,都先将sum重新赋值为0,导致每次for循环中累积的偶数的和都被清零,达不到求全部偶数和的目的。因此,正确的代码应该是将初始化sum变量放到for循环之前,而不是之内。将代码修改正确后运行该程序,shell中显示正确的结果如下:
2450 |
通过该例子还可以看到,解释器是从上往下进行扫描,首先找到名字为'__main__'中的模块,然后从其下面调用的函数处去执行。
3. 代码编辑器
在IDLE的shell中,每输入一行命令,交互解释器会立刻生成相应结果。这种方式对于测试来说是很方便的。但是对于这种方式一旦程序被执行后,代码就消失了,要想重新运行,只能从头开始输入。因此,这种方式对于较多代码的程序就不是很适用。要想永久保存代码,需要将代码保存到文件中,这时,就要使用到IDLE的代码编辑器了。接下来以“Hello world”程序为例讲述一下如何使用IDLE的代码编辑器。
(1)新建与保存文件
在shell窗口的菜单栏点击File,选择New file来创建一个新的Python文件,此时弹出一个代码编辑窗口,在这里就可以编辑相应的代码了。在编写代码之前,最好先将文件保存一下,点击File,选择Save(也可以用快捷键Ctrl+s)弹出了另存为窗口,选择想要将此文件保存的位置,这里将该文件保存到E盘下新建的python文件夹中,并且为这个文件起一个适当的名字,这里起名为hello.py(注意:Python文件一定要以.py为后缀名),如图3.6所示,然后点击保存按钮即可。

图3.6 保存文件时另存为窗口图
现在在hello.py文件中输入代码print “Hello world”,保存一下。运行方式有两种,一种是点击Run→Run Module。另一种快捷方式是按下F5。可以根据个人习惯选择不同的运行方式。此时,按下F5运行后,可以看到shell窗口弹出,显示出和上面讲到的在shell窗口中输入命令并运行后显示的一样的结果。
(2)打开文件
如果想要运行已经在其他文本编辑器中编辑好的Python文件,可以在shell窗口中直接打开该文件并运行。在File菜单中选择open选项,会弹出打开窗口,找到要运行的Python程序,点击打开按钮后,就会弹出显示该Python程序代码的编辑器窗口。此时,可以对该程序进行修改,保存后运行,即可在shell窗口中显示结果。
(3)自动缩进
在C语言里,用花括号{}来将复合语句包围起来作为一个代码块,有时会在不同的代码块层之间添加一些缩进,增强程序的层次性、美观性与可读性,但是这并不是必须的。也就是说即使全篇幅都没有缩进,代码只要正确就能正常运行,不会产生语法错误。然而,Python语言将缩进提升到了一个语法的高度,复合语句不是用花括号{}之类的来表示,而是用缩进来表示。这种处理方式有利于统一代码的风格,也避免了由于花括号不匹配而引发错误的情况。
IDLE很清楚Python的缩进语法,只要输入正确的代码,按下回车,IDLE会自动地进行缩进,而无须用户手动缩进。如图3.7所示(图中代码中涉及的while关键字会在后面章节中讲到)。

图3.7 IDLE代码编辑器缩进情况显示图
默认地,IDLE一级的缩进为4个空格。当然,如果用户对默认的缩进量不满意,也可以进行更改。只需要在“Format”菜单中选择“New indent width”项,输入想要的缩进量后点击“OK”即可如图3.8所示。

图3.8 修改缩进量窗口图
但是要注意是所有没有嵌套的语句都在第一列,也就是说必须在最左边,前面不能有空格。如果不是这样,当运行该Python文件时会弹出Syntax Error警告框。
自动缩进功能在shell中也同样适用,在“>>>”提示符后输入如if、else、for、while等语句,输入“:”并回车后,光标会自动停在下一行的指定位置处。当输入完本代码块后,如想要退出此代码块返回到与if、else、for、while等语句同一层代码级中,需要连续按下两次回车,此时“>>>”提示符才会再次出现。
(4)中文编码
IDLE还对代码中的中文显示做了一些限定。若直接在代码中输入中文字符而不事先声明,按下F5试图运行代码时,会弹出警告窗口,如图3.9所示。

图3.9 中文编码警告框图
警告窗口用来提示用户对中文字符进行编码声明,指定代码保存时使用的字符集,点击OK按钮编辑器会自动将文件作为cp936编码文件保存。若点击Edit my file按钮,编辑器会在文件顶部添加一行“# -*- coding: cp936 -*-”代码来对中文字符进行声明。
中文字符常用的编码格式有cp936和utf-8,其中cp936多用于windows系统,而utf-8多用于Linux系统。代码格式可以不按照警告框所示,下面这种写法也是可以的:
# coding = cp936 | # coding = utf-8 | 4. 语法高亮显示
IDLE使用不同的颜色来区分显示不同的代码,称为语法高亮显示。默认地,Python关键字用橘黄色显示,字符串用绿色显示,内置函数(在后面章节会学到)用紫色显示,注释用红色显示,生成的结果用蓝色显示。如若不喜欢这些颜色搭配,可以在IDLE的首选项中自己设置心仪的颜色。这种语法高亮显示的方法有很多好处,它将原本单调乏味的代码变得多彩活泼起来,同时这种将不同含义的代码以不同颜色显示的方法,提高了代码的可读性。更重要的一个好处是,它降低了代码出错率。举个例子,Python有一些预留的关键字,并且Python不建议为变量命名时与这些关键字重复,避免引起某些错误。所以,当输入的变量名显示为橘黄色时,这说明该变量名与某个关键字冲突,就要考虑为该变量换一个名字了。
5. 快捷键
IDLE还提供了很多快捷键来帮助用户更方便地编写代码,以节省时间。例如,Alt+p(previous)组合可以回退到上一条输入的语句,相应地,Alt+n(next)组合可以移至下一条输入的语句。通过这两组快捷键可以轻松自如地在自己输入过的代码之间转换,而无须重新从键盘输入。另一个方便实用的快捷键是Tab键,当只键入某个关键字的前几个字母后,按下Tab键,会看到屏幕中显示出一个列表,这里提供了可能需要的命令如图3.10所示。

图3.10 使用Tab键后显示列表图
表3.1整理了在使用IDLE时经常会使用到的快捷键,熟记它们会大大提高自己的编程效率。
表3.1 常用的IDLE的快捷键 快捷键 | 功能 | Alt+p | 回退至上一条命令 | Alt+n | 移至下一条命令 | Tab | 提供与已输入字母匹配的关键字列表 | Ctrl+n | 打开一个新的编辑器窗口 | Ctrl+o | 打开一个文件 | Ctrl+s | 保存当前文件 | Ctrl+z | 撤销最后一次操作 | F5 | 运行当前Python程序 | Alt+3 Alt+4 | 将选中内容变为注释 取消注释 | Ctrl+[ Ctrl+] | 凸出代码 缩进代码 | Alt+/ | 对出现过的单词自动补齐 | F1 | 获取Python文档 | 6. 获取帮助命令
Python提供了很多内置函数和模块等,要想把这些全部记住显然不是一个简单的事情,因此,IDLE体贴地为用户提供了获取帮助的命令help()(不仅IDLE,其他的shell解释器都提供了help()函数获取帮助),如图3.11所示。

图3.11 help()查询界面图
输入help()后会看到如图3.11所示的一大段蓝色文本。它的主要内容是告诉用户如何通过help()进行查询。在最后一行的help>后输入想要查询的内容并回车,会得到想要的。这里以print语句为例。如图3.12所示。

图3.12 print语句解释界面图
查询所得内容详细地介绍了print语句的使用格式和注意事项以及一些特殊情况。如果的英语足够好的话,help()命令会是编程的得力助手。当得到了想查询的内容后想退出查询界面回到shell命令输入状态,只需输入quit即可。另一种使用help()的方法是在括号中填写参数。这里要注意的是参数的填写形式,针对不同的参数写法也不尽相同。
(1)查看所有的关键字。 >>>Help('keywords') |
回车后会返回Python中所有的关键字。
(2)查看所有的模块 >>>help('modules') |
回车后会返回Python中所有的模块。
(3)查看一个数据类型。 >>>help('int') |
回车后会返回整型的方法及详细说明。
(4)查看一个模块。 >>>help('sys') |
回车后会返回该模块的帮助文档。
(5)查看关键字。 >>>help('for') |
回车后会返回for关键字的详细说明
(6)查看列表 >>>L=['a','b','c'] | >>>help(L) |
回车后会返回列表的全部操作方法。要想只查找某个方法的说明可以如下输入:
>>>help(L . append) | 7. 清屏问题
在shell交互解释器中运行了较多条命令后,细心的读者会发现,当前输入行总是在窗口的最下方,执行过的命令不会消失,依然在窗口中并且占用了绝大部分的窗口面积,这对用户的视觉感受带来的一定影响。但是,遗憾的是IDLE对于这个问题并没有提供很好的解决办法,它没有类似Linux系统中的clear命令,能够清除屏幕上的历史命令使光标返回到窗口首行。想要清除掉窗口中的历史命令,可以一直按回车键,还可以输入一个打印一系列空白行的循环函数。但是,这两种方法也仅仅是能使窗口内历史命令消失,光标依然是在窗口最底行。
8. 其他常用的Python的IDE
IDLE是Python软件包自带的一个集成开发环境。它对于Python初学者来说是一个简单易学的首选工具,但是实际上IDLE只具有一般的IDE功能,它的功能不够强大不足以用来作为项目开发的工具。当然Python的IDE并不局限于IDLE,它还有很多其他的开发环境可供程序开发者选择,下面简单介绍一些Python的常用的IDE。
* PyCharm
PyCharm是由著名的JetBrains公司开发的。PyCharm不仅具有一般IDE具备的功能,比如,调试、语法高亮、项目管理、代码跳转、智能提示、自动完成,还提供了一些更高级的功能,如项目代码导航,Python重构,集成版本控制,集成单元测试,并且支持Django开发,同时支持Google App引擎等等。 * PythonWin
正如名称所示,PythonWin是针对Win32用户的。PythonWin是最早出现的Python开发工具之一,采用C++开发的。获得 PythonWin 的最简单方法是下载 ActivePython 2.0 发行版。PythonWin的发行版本包括Windows应用程序接口和COM组件模型,可以编辑和调试程序。 * Eclipse+Pydev
就目前而言,Eclipse+Pydev的组合当属最优秀的开源IDE,但是Pydev插件不是免费的。Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。Eclipse结合Pydev插件即可进行Python项目开发。需要注意的是,由于Eclipse是Java语言编写的,要使用Eclipse首先要安装Java运行环境,并且Python插件安装完成后,还要配置解释器才可以使用,详见第一章内容。 * WingIDE
与其他类似的IDE相比,wingIDE最大的特色是可以调试django应用。使用Wing IDE,总体的界面就像增强的集成开发环境IDE,使用了与许多TK和XWindow界面类似的“多窗口”排列方式。Wing IDE可运行在Windows,Linux和Mac操作系统上,只需要简单的几步就能使用。它提供一个源码分析器和浏览器、项目管理能力以及文本编辑器和调试器。 3.1.2 命令行运行程序
除了使用IDLE运行Python程序,另一种常用的方法是用命令行运行。前提是Python已经被加入到环境变量里。在windows系统中,打开“开始”菜单,在搜索框中输入cmd,点击cmd.exe,就可以打开命令行窗口。运行程序之前,先要进入到Python文件存放的目录中。这里输入E:后回车进入到E盘下,再输入命令cd python进入到python文件夹,此时已经到了hello.py文件所在的目录,执行命令python hello.py,即可看到输出结果如图3.13所示。

图3.13 命令行运行程序界面 3.1.3 命令行调用Python
不仅可以用命令行运行Python程序,还可以用命令行调用Python。同样前提是Python已经被加入到环境变量里。在window系统中,根据上一小节所讲步骤打开命令行窗口,输入Python命令,按下回车即可,如图3.14所示。此时,熟悉的“>>>”提示符出现了,可以像在IDLE中一样输入命令并执行。

图3.14 命令行调用Python界面
在Linux和Mac系统中,在shell窗口或终端窗口中直接输入Python即可。
当使用命令行调用Python时,可以给解释器一些选项,这些选项直接接在python后面,中间加一个空格。不同的选项具有不同的功能。下表提供了部分可供选择的选项及其功能。 选项 | 功能 | -d | 提供调试输出 | -S | 不导入site模块以在启动时查找Python路径 | -v | 冗余输出 | -O | 生成优化的字节码(生成.pyo文件) | -Q opt | 除法选项 | -m mod | 将一个模块以脚本形式运行 | -c cmd | 运行以命令行字符串形式提交的Python脚本 | file | 从给定的文件运行Python脚本 |
如果想退出命令行,在windows系统中使用Ctrl+Z组合键,如果是在Linux或Mac系统中则使用Ctrl+D组合键。将Python脚本作为其它程序的一部分运行时,从命令行调用Python方法作为首选。
3.2 注释
代码中注释也是不可或缺的部分,能够帮助程序员自己和他人快速理解某段代码的含义。不同的编程语言所使用的注释符号也不尽相同。Python有两种注释方法:
#号注释符:仅注释#号所在的一行,#号之后内容为注释内容,注释内容不被程序执行。如下例所示: print "Hello world" #打印输出字符串"Hello world" |
引号注释符:如果需要注释的内容超过一行,可以使用三个双引号“"""”或三个单引号“'''”将注释内容包围起来。如下图所示:
""" | print "Hello world" | print "Hello world" | print "Hello world" | print "Hello world" | 上面四行代码不会被执行 | """ | 本章小结
本章介绍了Python自带的集成开发环境IDLE,详细地讲解了IDLE的组成以及其各种功能的使用方法,还简单地介绍了其他的一些比较流行好用的Python的IDE,为读者提供了更多练习Python编程的工具的选择。本章还讲解了使用命令行运行和调用Python的方法。在最后对于Python的注释做了简单的介绍。本章的重点还是在于如何用IDLE编写Python程序,望读者认真学习,并且在课后时间多多练习。
习题 * 填空题 1. IDLE由____________、____________和____________组成。 2. shell的提示符是____________。 3. IDLE的语法高亮显示中,关键字用____________色显示,字符串用____________色显示,内置函数用____________色显示,注释用____________色显示,生成的结果用____________色显示。 4. ____________快捷键可以回退到上一条输入的语句,____________快捷键可以移至下一条输入的语句。 5. 想要查看关键字if的详细说明,应使用命令____________。 6. 使用命令行执行test.py程序时,应输入的命令为____________。 * 选择题 1. IDLE的一级缩进空格数是( )。
A.4 B.3 C.2 D.1 2. 以下不是Python的注释符号的是( )。 A. # B.// C.''' D.""" 3. 在Python中试图改变字符串的值会引发( )异常。
A.TypeError B.SyntaxError C.NameError D.AttributeError 4. Python程序保存时必须以( )为后缀名。
A.cpp B.c C.java D.py 5. Python变量或函数没有定义就使用和变量名或函数名拼写错误都会导致( )异常。
A.TypeError B.SyntaxError C.NameError D.AttributeError

第4章 列表、元组和字典 通过前面几章的学习,读者可能对如何使用Python编程有了一定体会,但涉及比较复杂的编程时,还是受到诸多限制。例如前面章节程序都是用一个变量存储一个值,如a=3。基于此种思想,当存储某个班级每个同学课程成绩时,读者可能会使用多个变量进行保存,例如a1=90,a2=85,a3=70……,显然这种方法很不方便。
因此,Python语言使用一些些数据结构方式,例如本章所要介绍的列表、元组和字典,对上述问题进行求解。本章中将为读者介绍上述几种常用的数据结构类型和它们的运算方法。
【体系结构】

【本章重点】
(1)掌握序列、映射和集合的基本概念,理解它们之间的区别与联系;
(2)理解列表与元组的区别和元组存在的意义,掌握它们的基本操作和内建方法;
(3)理解字典、集合与序列的不同,掌握字典的基本操作和方法,掌握集合的操作和各种操作符;
(4)理解为什么选择列表作为棋盘的载体,掌握对棋盘进行抽象的方法及棋盘和棋子状态的表示方法
【案例引入】
本书将以五子棋游戏为例,在使用Python语言进行开发的过程中,让读者理解和应用所学每一章知识,力求在最终完成五子棋游戏的同时,深入掌握Python语言的各个知识点,达到理论和实际相结合的目的。
五子棋游戏是一个老少皆宜的娱乐活动,其基本要素是棋盘和棋子,即需要使用Python语言存储棋盘信息和玩家的下棋信息。具体来说,通过学习本章内容,读者会学到Python记录不同信息的方法。
4.1 序列 在Python语言中,序列是最基本的数据结构类型之一。序列中的每个元素可以是任何类型(当然也可以是序列),每个元素被分配一个序号,即元素位置(也称为索引)。第一个元素的序号规定为0,第二个为1,依次类推。 Python中的序列包括6种:列表、元组、字符串、Unicode字符串、buffer对象和xrange对象。其中最常用的是列表、元组和字符串。字符串将在后面章节详细讲解,本章重点讨论列表和元组。 在表示形式上,列表使用中括号[]而元组使用圆括号()。 >>> lsa=[1,2,3] #lsa是列表>>> tup=(1,2,3) #tup是元组 | 列表和元组的区别不仅表现在它们的表示形式上,它们的本质区别在于,列表是可以修改的而元组不能。如下例所示: >>> lsa=[1,2,3] #lsa是列表>>> tup=(1,2,3) #tup是元组>>> lsa[0]=2>>> tup[0]=2Traceback (most recent call last):File "<pyshell#9>", line 1, in <module>tup[0]=2TypeError: 'tuple' object does not support item assignment |
程序分析:上面代码中,lsa[0]表示序列lsa中第一个元素,tup[0]同理。可以看到当尝试修改元组中的元素时,程序会报错终止。它们的这种特性也决定了列表的使用比元组更灵活,而且大部分情况下列表都可以代替元组。既然如此,为什么还要元组呢?元组的不可改变性,确保元组在程序中不会被另一个应用修改,而列表就没有这样的保证。同时,有些地方列表是无法使用的,如字典中的键值。
4.2 序列基本操作 在Python语言中序列类型虽然有6种,但它们之间存在一些通用操作,这些操作是最基本的也是最常用的。在本节中将以列表为例来演示序列的基本操作。
4.2.1 索引 序列中每个元素都有编号(同样从0开始编号),这些元素可以通过编号来进行访问和读取。使用方法为,
序列[编号]
如下例所示: >>> a=[1,2,3,4]>>> a[0]1 |
如果想读取倒数第几个元素,但是不知道序列的长度时,也可以从最后同一个元素开始计数,如果使用这种方式,最后一个元素的编号是-1,第二个为-2,依次类推,如下例所示:
>>> a[-1]4 |
4.2.2 切片
在Python中可以使用索引每次读取一个值,也可以使用切片读取序列中的一段值。使用方法,
序列名字[开始编号:结束编号后一个:步长]
其中默认步长为1。如下例所示:
>>> a=[1,2,3,4,5]>>> a[1:3][2, 3]>>> a[1:-1][2, 3, 4]>>> a[1:] # : 后未给定值表示[2, 3, 4, 5]>>> a[:-1] # : 前未给定元素表示从第一个元素开始[1, 2, 3, 4]>>> a[:] # : 前后都为给值,表示读取整个序列[1, 2, 3, 4, 5] | | 注意:使用切片所得到的结果也是序列,其类型不变
4.2.3 序列加
可以使用“+”将相同类型的序列连接在一起。使用方法,
序列+序列
要求前后两个序列是相同类型。如下例所示:
>>> a=[1,2,3]>>> b=[2,3,4]>>> a+b # a,b均是列表[1, 2, 3, 2, 3, 4]>>> a=[1,2,3]>>> b=(1,2,3)>>> a+b # a是列表,b是元组,不能连接Traceback (most recent call last): File "<pyshell#32>", line 1, in <module> a+bTypeError: can only concatenate list (not "tuple") to list |
4.2.4 序列乘 可以使用“*”使序列重复n次得到一个新的序列。使用方法:
序列*n,其中n表示重复次数
如下例所示: >>> a=[1,2,3]>>> a*3[1, 2, 3, 1, 2, 3, 1, 2, 3] |
4.2.5 In 可以使用“in”检查某个元素是否在序列中。使用方法:
元素a in 序列A,如果a在A中,则返回True,否则返回False 如下例所示: >>> a=[1,2,3]>>> 1 in aTrue>>> 4 in aFalse |
4.2.6 len、max、min len,max和min是Python内建的几个常用的函数,分别返回序列的长度,最大值,最小值。函数的概念在第8章将会讲到,所以读者在此了解即可。 使用方法:len(序列),max(序列),min(序列) 如下例所示: >>> num=[1,2,3,4,5]>>> len(num)5>>> max(num)5>>> min(num)1>>> max(1,2,55)55>>> min(2,4,-1)-1 | 注意:max,min也可以接受多个数字作为参数,如上面最后两个例子。
4.3 列表 上一节中已经介绍了一些序列的基本操作,本节将会讨论序列的一种——列表,这也是Python中最常用的容器类型。本节将详细介绍它的创建和使用方法。作为最常用的数据结构类型,它的使用方法十分灵活,学会它是学习Python的重要一步。
4.3.1 列表的创建 通过前面的例子,读者已经知道如何创建一个列表,即“[]”。创建列表方式如下所示: >>> [1,2,3][1, 2, 3]>>> [1] #创建一个元素的列表[1]>>> [] #创建空列表[] |
创建列表还有另外的一种方式:通过list函数将其它类型的序列转化为列表。有些时候可能要修改一个元组里面的一个元素的值,当然元组不支持修改操作,这时可以将元组转化为列表。如下例所示:
>>> tup=(1,2,3)>>> tup=list(tup)>>> tup[1, 2, 3] |
4.3.2 列表的修改 前面已经多次提到,列表相比于元组和字符串的区别在于它能够被随意修改,这一节将详细介绍它的修改方法。
(1)单个元素修改
修改单个元素的值是很简单的事情,只需要索引找到这个元素,再使用赋值语句给它一个新值就可以了。如下例所示: >>> list_a = [1,2,3,4]>>> list_a[0]=2 #使用索引找到0号元素>>> list_a[2, 2, 3, 4] #改变后的列表 |
(2)切片修改
使用索引可以找到一个元素,并可以修改这个元素的值;使用切片可以找到一段元素,当然也可以一起修改这一段元素的值。如下例所示: >>> list_a = [1,2,3,4]>>> list_a[0:2]=[5,6]>>> list_a[5, 6, 3, 4] | Python中可以使用不定长的序列来修改选中的切片。这意味着把一段长度为0的序列赋值给一段长度不为0的切片等于删除,把长度不为0的序列赋值给长度为0的切片等于添加。如下例所示: >>> list_a = [1,2,3,4]>>> list_a[1:3]=[] #相当于删除1~2号元素>>> list_a[1, 4]>>> list_a[0:0]=[999,888] #相当于添加两个元素>>> list_a[999, 888, 1, 4]>>> list_a[1:3]=list("hello") #段替换>>> list_a[999, 'h', 'e', 'l', 'l', 'o', 4] |
(3)元素删除
删除的方法除了使用长度的0的序列来替换要删除的一段外,还可以使用del语句。与修改类似,也可以用来删除多个元素。如下例所示: >>> list_a = [1,2,3,4] >>> del list_a[0] #删除一个元素>>> list_a[2, 3, 4]>>> del list_a[0:2] #删除一段元素>>> list_a[4] |
4.3.3 列表的方法 在Python中,一切皆对象。既然都是对象,涉及对象的方法自然必不可少。简单的讲,方法是与对象密切相关的函数,后面章节会详细介绍这部分内容。在这里简单打个比方,如果对象是根棍子,由于它的特性决定了它有不同的使用方法,比如说可以用它来打人,可以用它来在地上写字,可以用来当拐杖等等,而方法的概念就和这里棍子的使用方法十分类似。 Python中,调用对象的方法的方式如下:
对象.方法(参数)
和函数调用不同的是,在调用方法时需要指明这个方法是属于哪个函数。具体方法是在前面写上对象名字加个点“.”。下面将介绍Python为列表提供的内建方法。可以从这些方法的英文本意来理解该方法的操作意图。
(1)append方法
使用append方法可以在一个列表后面追加新的元素: >>> list_a=[1,2,3]>>> list_a.append(4) #在列表后面添加一个元素4>>> list_a[1, 2, 3, 4] |
(2)count方法
使用count方法可以查看某个元素在列表中出现的次数: >>> list_a=[1,2,3,4,1,2,1]>>> list_a.count(1)3 |
(3)extend方法
正如extend的本意一样,extend方法能用其它列表拓展原有列表,其它列表的元素被
添加到原有列表后面:
>>> list_a = [1,2,3]>>> list_b = [4,5,6]>>> list_a.extend(list_b) #使用列表list_b拓展list_a>>> list_a[1, 2, 3, 4, 5, 6] #list_b中的元素被添加到list_a后面 |
这里读者可能想到,使用“加”操作一样可以达到这种效果。它和extend的区别在于“加”操作会得到一个新的列表,元列表不会改变,而extend方法会改变原列表。例如:
>>> list_a =[1,2,3]>>> list_b =[4,5,6]>>> list_a+list_b #使用“加”操作[1, 2, 3, 4, 5, 6]>>> list_a #list_a本身并未改变[1, 2, 3]>>> list_a.extend(list_b) #使用extend方法>>> list_a #list_a已改变[1, 2, 3, 4, 5, 6] |
(4)index方法
返回某个元素的索引,如果这个元素不存在,会引发错误(更专业的术语是“异常”,这将会在后面章节有更详细的介绍): >>> list_a = [1,2,3,88,4,5,6]>>> list_a.index(88) #返回88的索引3>>> list_a.index(99) #99不存在,返回错误Traceback (most recent call last): File "<pyshell#17>", line 1, in <module> list_a.index(99)ValueError: 99 is not in list |
(5)insert方法
insert方法的作用是在序列的某个位置插入一个元素: >>> list_a=[1,2,3,4]>>> list_a.insert(2,'Hello') #在2号位置插入‘Hello’>>> list_a[1, 2, 'Hello', 3, 4]>>> list_a.insert(10,'world') #10号位置不存在,‘world’直接插入到序列末>>> list_a[1, 2, 'Hello', 3, 4, 'world'] |
(6)pop方法
pop方法的作用是移除列表某个位置的元素并返回该元素,如果没指定要移除元素的索引,pop方法默认移除最后一个元素。 >>> list_a=[1,2,3,4] >>> list_a.pop() #移除最后一个元素4>>> list_a[1, 2, 3]>>> list_a.pop(0) #移除0号元素1>>> list_a[2, 3] |
(7)remove方法
remove方法可以移除序列中第一个与参数匹配的元素,注意是第一个而不是所有。 >>> list_a = [1,2,88,3,4,88,5,6]>>> list_a.remove(88) #删除第一个88>>> list_a #第二个88没有被删除[1, 2, 3, 4, 88, 5, 6] |
(8)reverse方法
reverse函数可以将列表改为倒序。 >>> list_a = [1,2,3,4]>>> list_a.reverse()>>> list_a[4, 3, 2, 1] |
(9)sort方法
调用sort方法可以对列表进行排序。 >>> list_a=[4,6,2,7,1,8]>>> list_a.sort()>>> list_a[1, 2, 4, 6, 7, 8] | sort方法的默认排序方式是升序,但如果想进行降序排列或者更复杂的排序怎么办?其实sort方法还可以传入其它参数,它们是key,cmp和reserve。 key和cmp参数都是接受一个函数,这两个函数在比较前调用,作用于元素上返回一个结果,元素进行比较时根据这个结果进行比较。其中key是带一个参数的函数,用来为每个元素提取比较值。cmp是带两个参数的比较函数,形式为cmp(e1,e2),返回值为负数时代表e1<e2;为0时代表e1==e2;为整数时代表e1>e2。reverse接收一个一个bool型变量,为True时将序列反序。key和cmp的默认值为None,reverse的默认值为False。比如字符串列表排序时根据字符串的长度进行排序: >>> names = ["Judy","Peter","Perkins"] #名字列表>>> names.sort(key=len) #按名字长度排序>>> names['Judy', 'Peter', 'Perkins'] |
4.4 元组 元组作为序列的一员,它不同于列表,它的灵活性也因为它的不可变性而受到限制,但它在Python中仍然拥有不可替代的地位。在这一节中将为读者更进一步介绍它和它的使用方法。在本节最后也将更详细为说明元组和列表的区别。
4.4.1 元组的创建 和列表的创建一样,创建元组同样有两种方法。一种是通过“()”直接创建,但这和使用“[]”创建列表有许多不同之处: >>> (1,2,3) #使用括号创建元组(1, 2, 3)>>> 1,2,3 #括号其实可以省略(1, 2, 3)>>> 1 #创建一个元素时必须额外加一个逗号1>>> 1,(1,) | 在列表中逗号只起到分隔的作用,但在元组中却并非如此,有时是否有一个逗号直接决定了一个对象是否是元组,如上面创建一个元素的例子。 创建元组的另外一种方法是使用tuple函数:将其它种类的序列转化外元组: >>> tuple("Hello")('H', 'e', 'l', 'l', 'o') |
4.4.2 元组的操作 因为元组的不可改变性,元组的操作相比于列表简单很多,它只支持序列的基本操作(4.2节中有介绍),不支持其它有意图修改元组元素的操作。尽管在本书中已多次强调元组是不可修改的,但有时看到下面代码不免困惑: >>> book = ("Python","20150101",["Author1","Author2","Author3"])>>> book('Python', '20150101', ['Author1', 'Author2', 'Author3'])>>> book[2][0]="Rosen" #修改 >>> book('Python', '20150101', ['Rosen', 'Author2', 'Author3'])>>> book[2].append("Author4") #添加>>> book('Python', '20150101', ['Rosen', 'Author2', 'Author3', 'Author4']) | 程序分析:表面上看,book元组的元素确实变了,这就和前面所讲的“元组不可修改”相矛盾。事实上,“元组不可修改”是指元组的每个元素指向永远不变,即指向a就不能改成指向b,指向列表对象就不能改成指向其它对象。在上例中,元组第三个元素的指向是一个作者列表,在修改时并没有修改这个指向,所以上面代码是没有问题的。
4.4.3 元组的困惑 通过前面的学习,读者可能发现,列表和元组事实上是极为相似的,只要在使用元组时不对它进行修改,就相当于在使用元组,那么元组存在的意义是什么呢?其实虽然元组没有列表那么常用,但它的不可变性是在有些时候显得格外重要。例如在程序中以列表的形式传递一个对象的集合,可能在程序的某个地方不小心修改了这个列表,这样的错误往往是致命的。如果使用元组的话,这样的情况这不会发生。也就是说,元组提供了一种完整性约束,这种约束在开发大型程序的时候带来极大的方便。 事实上,在Python中元组往往是必不可少的。在4.1节中提到过,可以使用元组作为字典(本章中后续章节会讲到)的键,而列表不行。因为在字典中,要求键不能改变。同时Python中许多内建函数和方法的返回值是元组。
4.5 字典
在前面几节中介绍的序列是一种通过索引来访问元素的数据结构类型,在本节中将会介绍一种新的数据结构类型——映射。映射类型和序列类型的不同在于,映射类型中的元素是无序的,也就是说在映射中没有索引这一概念。Python为映射类型引入“键”和“值”的概念,每个键和一个值对应,可以通过特定的键访问特定的值。键可以是数字,字符串甚至是元组。如果以这种键/值的方式来理解序列,序列的键就是一组有序的数字。
4.5.1 字典的创建 在Python语言中,可以通过以下方式即可创建一个字典: >>> phone_book={"WangLei":"110","YaoJiaNing":"120","Rosen":"119"}>>> phone_book{'Rosen': '119', 'WangLei': '110', 'YaoJiaNing': '120'} | 和列表和元组不同,字典是使用大括号({})括起来。字典中每一项(键值对)由逗号隔开,每个键和它的值之间用冒号隔开。字典中的键是唯一的,而值没有这个要求。如果有相同的键,出现在后面的键的值会覆盖前面的。 >>> phone_book={"WangLei":"110","YaoJiaNing":"120","Rosen":"119","WangLei":"114"}>>> phone_book{'Rosen': '119', 'WangLei': '114', 'YaoJiaNing': '120'} | 字典的创建也可使用dict函数: >>> dict([("WangLei","110"),("YaoJiaNing","120"),("Rosen","119")]) #方法1{'Rosen': '119', 'WangLei': '110', 'YaoJiaNing': '120'}>>> dict(WangLei="110",YaoJiaNing="120",Rosen="119") #方法2{'Rosen': '119', 'WangLei': '110', 'YaoJiaNing': '120'} | 方法1中给dict传入的参数是每个元素是“(键,值)”这种形式的序列,方法2使用的是关键字参数的形式。 也可以给dict传入一个字典,这样将会得到一个传入字典的副本。 >>> phone_book={"WangLei":"110","YaoJiaNing":"120","Rosen":"119"}>>> phone_book_copy=dict(phone_book)>>> phone_book_copy{'Rosen': '119', 'WangLei': '110', 'YaoJiaNing': '120'} |
4.5.2 字典的操作 (1)查找 字典的查找方法和序列很像,区别在于序列通过索引查找元素,字典通过“键”: >>> phone_book={"WangLei":"110","YaoJiaNing":"120","Rosen":"119"}>>> phone_book["WangLei"]'110' | (2)修改 字典修改只需先通过键找到要修改的元素,然后给它赋新值即可。特别的地方在于,如果没有这个元素,执行完这条语句后相当于在字典中添加一项。 >>> phone_book={"WangLei":"110","YaoJiaNing":"120","Rosen":"119"}>>> phone_book["WangLei"]="114">>> phone_book{'Rosen': '119', 'WangLei': '114', 'YaoJiaNing': '120'}>>> phone_book["HuLaoShi"]="122" #”HuLaoShi”不在,相当于添加>>> phone_book{'Rosen': '119', 'HuLaoShi': '122', 'WangLei': '114', 'YaoJiaNing': '120'} |
(3)删除
del语句同样可以用来删除字典中的项: >>> phone_book={"LiuYe":"95555","ZhangCan":"95588","LiaoPengYu":"95566"}>>> del phone_book["LiuYe"]>>> phone_book{'ZhangCan': '95588', 'LiaoPengYu': '95566'} |
(4)len
和序列中len方法一样,len方法同样可以返回字典的项的数量 >>> phone_book={"ShenLian":"95533","ZhouYu":"95599","SunChang":"95568"}>>> len(phone_book)3 |
(5)in
使用in关键字可以检查一个元素是否在字典中,如果在返回True,否则返回False。
特别的,对字典使用in时只会在字典的键中查找这个元素。
>>> phone_book={"ZhouYu":"95599","SunChang":"95568","LiuZhuo":"95577"}>>> "Rosen" in phone_bookFalse>>> "ZhouYu" in phone_bookTrue |
4.5.3 字典的方法 和列表一样,字典也有许多方法。这些方法非常有用,同学习列表的方法一样,联系方法名英文本意和对应方法的功能是一个很有用的方法。
(1)clear
使用clear方法可以清除字典中所有的项。 >>> book={"name":"Python","publishdate":"20150101","author":"XXX"}>>> book.clear()>>> book{} |
(2)copy
调用copy方法会得到一个具有相同键值对的新字典: >>> book={"name":"Python","publishdate":"20150101","author":["X","Y","Z"]}>>> new_book=book.copy()>>> new_book{'name': 'Python', 'publishdate': '20150101', 'author': ['X', 'Y', 'Z']} |
但值得注意的是,使用这种方法得到的新字典不是原字典的副本,它们是完全相同的(这里的相同指的是键的指向相同)。那么,如果原地修改一个字典的某个元素,另一个字典也会随之改变。
>>> book={"name":"Python","publishdate":"20150101","author":["X","Y","Z"]}>>> new_book=book.copy()>>> new_book["author"].append("W") >>> new_book{'name': 'Python', 'publishdate': '20150101', 'author': ['X', 'Y', 'Z', 'W']}>>> book{'name': 'Python', 'publishdate': '20150101', 'author': ['X', 'Y', 'Z', 'W']} | copy方法的复制方法实际上是一种浅复制方法,要想避免这样的情况发生,需要使用深复制方法——deepcopy。如果使用deepcopy,结果截然不同: >>> book={"name":"Python","publishdate":"20150101","author":["X","Y","Z"]}>>> from copy import deepcopy>>> new_book=deepcopy(book)>>> new_book["author"].append("W")>>> new_book{'name': 'Python', 'publishdate': '20150101', 'author': ['X', 'Y', 'Z', 'W']}>>> book{'name': 'Python', 'publishdate': '20150101', 'author': ['X', 'Y', 'Z']} |
(3)get
访问字典元素时,可以直接通过键进行值的查找,但这会有一个问题:当这个键不存在时,程序会出错: >>> dic = {}>>> dic["name"] #不存在“name”键Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> dic["name"]KeyError: 'name' | get是一种更为宽松的查找方法,当键不存在时,返回None。 >>> dic={}>>> dic.get("name") #不存在返回None>>> dic["name"]="Rosen">>> dic.get("name")'Rosen'>>> dic.get("phone","Unknown") #使用“Unknown”代替None' Unknown ' |
(4)setdefault
与get类似的方法是setdefault,setdefault方法同样可以用来查询,键不存在时的默认返回值同样也可以自定义。两者之间的区别在于,当键不存在时,自定义的值会和该键会组成一个新项被添加到字典中。 >>> dic={"name":"XXX","phone":"110"}>>> dic.setdefault("name")'XXX'>>> dic.setdefault("address","mars")'mars'>>> dic #”address”:”mars”被添加{'phone': '110', 'name': 'XXX', 'address': 'mars'} |
(5)has_key
has_key方法的作用是检查一个键是否在字典中,如果在返回True,否则返回False。相信读者已经很快想到它的作用其实和in操作是一样的。 >>> dic={"name":"XXX","phone":"110"}>>> dic.has_key("name")True>>> dic.has_key("address")False |
(6)items
items方法的作用是以列表的形式返回字典所有项。使用dict时,如果传入参数是列表,那么每一项都应该是“(键,值)”这种形式,其实,在这里返回的列表每一项也是这种形式。 >>> dic={"name":"XXX","phone":"110"}>>> dic.items()[('phone', '110'), ('name', 'XXX')] |
(7)keys、values
keys和values方法和items方法的作用相似,只不过keys方法返回的是字典的键,values返回的是字典的值: >>> dic={"name":"XXX","phone":"110"}>>> dic.keys()['phone', 'name']>>> dic.values()['110', 'XXX'] |
(8)pop
字典和列表一样,同样pop方法,因为字典里面的项是无序的,所以没有默认移除最后一个一说。在使用pop方法时需指定一个键,pop方法会返回这个键说对应的值,然后移除这个项,如果指定的键不存在会引发错误: >>> dic={"name":"XXX","phone":"110"}>>> dic.pop("name")'XXX'>>> dic{'phone': '110'}>>> dic.pop("Hello")Traceback (most recent call last): File "<pyshell#26>", line 1, in <module> dic.pop("Hello")KeyError: 'Hello' |
(9)popitem
其实Python提供的一种更简单的方法——使用popitem,调用popitem时不必给它传入参数,它会随机返回字典中的一项(因为字典是无序的),并删除。 >>> dic={"name":"XXX","phone":"110"}>>> while dic: #当dic不为空时 print dic.popitem() #调用popitem方法,输出返回值('phone', '110')('name', 'XXX')>>> dic{} |
(10)delete
如果要一个一个删除字典中的项,可能想到的方法是先使用keys方法获取,再使用del语句进行删除: >>> dic={"name":"XXX","phone":"110"}>>> dic_keys=dic.keys() #获取键列表>>> for k in dic_keys: #for语句 del dic[k]>>> dic #dic中的项已全部删除{} |
(11)update
数据的更新是经常且必要的,但一项一项地修改数据不是一种好的处理方法。update方法提供了一种很好的解决方案:使用新字典更新原字典。使用这种方法时,新字典中有而原字典中没有的项会被添加到原字典中,新字典中有原字典也有的项的值会被新字典的值代替: >>> dic1={"name":"XXX","phone":"110"}>>> dic2={"name":"XXX","phone":"120","address":"mars"}>>> dic1.update(dic2) #使用dic2更新dic1>>> dic1{'phone': '120', 'name': 'XXX', 'address': 'mars'} |
4.6 集合 和数学上的集合一样,Python中的集合也同样具有两个重要特性:无序和唯一。Python中的集合中的元素同字典中的项一样,都是无序的,但它也没有字典中“键”的概念。在创建集合对象时,相同的元素会被去除,只留下一个。 Python中的集合有两种类型:可变集合(set)和不可变集合(frozenset)。两者的名字很好的解释了这两种集合的区别:可变集合(set)支持添加和删除操作,不可变集合则不允许。就像元组一样,不可变集合可以作为字典的键。
4.6.1 集合的创建 和前面介绍的几种数据类型不同,集合没有特别的语法格式,所以它不支持像列表那样使用中括号[]就可以创建。聪明的读者可能想到,是否可用类似于list,tuple和dict这样的函数创建集合呢?对的,可以使用set函数创建可变集合,使用frozenset函数创建不可变集合: >>> set("Hello") #可变集合set(['H', 'e', 'l', 'o']) #去掉了一个‘l’>>> frozenset("Hello") #不可变集合frozenset(['H', 'e', 'l', 'o']) | 需要注意的是,给set传入的参数对象中,每个元素必须是不可变对象,例如字符串或元组,如果传入含有可改变的元素的对象,例如列表,就会引发错误。 >>> set(["Hello",(1,2,3),["world"]])Traceback (most recent call last): File "<pyshell#74>", line 1, in <module> set(["Hello",(1,2,3),["world"]])TypeError: unhashable type: 'list' |
4.6.2 集合的基本操作 和前面介绍的序列和字典一样,集合也支持使用len函数返回集合的大小,使用in检查一个元素是否在集合中,使用max和min同样可以返回集合中的最大值最小值。由于集合中的元素是无序的,也没有“键”这个概念,集合不能通过索引和键访问元素。 >>> list_a=[1,2,"number",(3,4)]>>> set_a=set(list_a)>>> len(set_a) #len方法返回长度4>>> 2 in set_a #in操作检查元素是否在集合True>>> 0 in set_aFalse>>> max(set_a) #返回最大值(3, 4)>>> min(set_a) 1 |
(1)访问元素
Python中只能通过循环遍历集合中的所有元素: >>> set_a = set(["Python","is","a","magic","language"])>>> for item in set_a:print itemPythonaismagiclanguage |
(2)添加元素
可以使用add方法向集合中添加元素: >>> set_a = set(["Python","is","a","magic","language"])>>> set_a.add("!") >>> set_aset(['a', '!', 'magic', 'language', 'Python', 'is']) |
同字典类型一样,集合也支持使用另外的集合来更新原集合(update)。当然,在更新的过程中同样会进行去重处理。
>>> set_a=set([1,2,3,4])>>> set_aset([1, 2, 3, 4])>>> set_b=set([3,4,5,6])>>> set_bset([3, 4, 5, 6])>>> set_a.update(set_b) >>> set_aset([1, 2, 3, 4, 5, 6]) #经过去重处理 |
(3)删除元素
可以使用remove方法删除集合中的元素: >>> set_a=set("hello")>>> set_aset(['h', 'e', 'l', 'o'])>>> set_a.remove("h")>>> set_aset(['e', 'l', 'o']) |
使用remove方法删除元素时,如果这个元素不存在,会引发错误,使用discard方法则不会:
>>> set_a.remove("t")Traceback (most recent call last): File "<pyshell#153>", line 1, in <module> set_a.remove("t")KeyError: 't'>>> set_a.discard("t")>>> |
注意:上面提到的添加和删除操作只适用于可变集合(set),不可变集合则不支持这两操作。
4.6.3 集合的特殊操作 数学上的集合支持一些特有的操作,如交,并,差等,Python中的集合同样也支持这些操作。可能读者已经看出,in操作事实上就是集合操作的“属于(∈)”操作。下面介绍集合的其它操作。
(1)等价、不等价
正如数学上的,Python中两个集合等价当且仅当一个集合里的每个成员同时也是另一个集合里的成员。在Python中可以使用“==”和“!=”判断两个集合是否等价。 >>> set("Python")==set("python")False>>> set("Python")!=set("python")True |
(2)子集、超集
在Python中可以使用<,<=,>,>=判断前面一个集合是否是后面集合的严格子集,子集,严格超集,超集。严格子集,严格超集相比于子集和超集,排除了集合等价的情况。 >>> set("Hello")<set("HelloWorld") #严格子集True>>> set("HelloWorld")<set("HelloWorld") #严格子集不包含等价False>>> set("HelloWorld")<=set("HelloWorld") #子集True |
(3)并
Python中使用“|”操作符表示集合操作中的“∪”操作,执行该操作后会得到一个新集合,该集合中的每个元素都至少是其中一个集合的成员,当然在执行该操作的过程中会进行去重处理: >>> set("Hello")|set("Python")set(['e', 'H', 'l', 'o', 'n', 'P', 't', 'h', 'y']) |
对于可变集合,Python支持“|=”操作对集合进行原地修改:
>>> set_a=set("Hello")>>> set_a|=set("Python")>>> set_aset(['e', 'H', 'l', 'o', 'n', 'P', 't', 'h', 'y']) |
(4)交
Python中“&”操作符表示集合操作中的“∩”操作,执行该操作后同样会得到一个新集合,该集合中的每个元素同时是两个集合中的成员。 >>> set("Hello")&set("Python")set(['o']) |
对于可变集合,使用“&=”操作符可对集合进行原地修改:
>>> set_a=set("Hello")>>> set_a&=set("Python")>>> set_aset(['o']) |
(5)差
Python中“-”操作符表示集合操作中的“-”操作,集合S和集合T做差运算后得到集合C,C集合中的元素只属于集合S,而不属于集合T。 >>> set((1,2,3,4))-set((3,4))set([1, 2]) |
对于可变集合,使用“-=”操作符可对集合进行原地修改:
>>> set_a=set([1,2,3,4])>>> set_a-=set([3,4])>>> set_aset([1, 2]) |
(6)对称差分
Python中的“^”操作符表示集合操作中的对称差分,集合S和T做对称差分后得到集合C,C集合中的元素要么属于S要么属于T,不存在一个元素同时属于S和T。 >>> set((1,2,3,4))^set((3,4,5,6))set([1, 2, 5, 6]) | 对于可变集合,可以使用“^=”进行原地修改 >>> set_a=set([1,2,3,4])>>> set_a^=set([3,4,5,6])>>> set_aset([1, 2, 5, 6]) | 需要注意的是,上面讲到的并、交、差和对称差分操作会产生一个新集合,上面例子中使用的都是可变集合,结果自然是可变集合。如果可变集合(set)和不可变集合(frozenset)运算,得到的新集合的类型和左操作数相同,如下例所示。 >>> set_a=set(["hello"])>>> set_b=frozenset(["python"])>>> set_a | set_b #和set_a类型相同,为setset(['python', 'hello'])>>> set_b | set_a #和set_b类型相同,为frozensetfrozenset(['python', 'hello']) | (7)集合的内建方法 其实每一种集合操作,Python都为其内建了一个方法。使用方法或是使用操作符通常是由读者的喜好决定的,不过使用操作符通常是方便的。操作符和方法的对照如表4.1所示:
表4.1集合操作符和方法对照表
操作符 | 方法 | 说明 | <>||=&&=--=^^= | s.issubset(t)s.issuperset(t)s.union(t)s.update(t)s.intersection(t)s.intersection_update(t)s.difference(t)s.difference_update(t)s.symmetric_difference(t)s.symmetric_difference_update(t) | 如果s是t的子集返回True,否则返回False如果s是t的超集返回True,否则返回False返回一个新集合,该集合是s和t的并把t中的成员并入s中返回一个新集合,该集合是s和t的交s被修改为s和t的交集返回一个新集合,该集合为s和t的差s被修改为s和t的差返回一个新集合,该集合为s和t的对称差分s被修改为s与t的对称差分 |
4.7 五子棋棋盘 五子棋游戏是棋类游戏中比较简单的,但是可别小看这个游戏,能够完美运行需要很多知识作为基础。由于所学知识有限,本节中只会介绍对棋盘进行抽象的方法,并根据抽象创建棋盘。
4.7.1 五子棋盘的选择:列表 (1)棋盘的抽象 棋盘是五子棋游戏的基础,毕竟这个游戏就是在这样一个方形网格状的区域上进行的。事实上,这样一个棋盘在程序中就是一种能存储棋子状态(是否落子)和位置的数据结构。对于程序设计的初学者来说,这里可能存在一个误会:一般看到的棋盘界面和这里讲的棋盘是不同的东西,前者由游戏视觉设计师设计的,而后者是游戏程序中针对棋盘抽象出来数据结构。 在这一章中已介绍过多种数据结构:列表、元组、字典和集合,哪种比较适合棋盘呢?元组肯定是不可以,因为伴随着落子,棋盘的状态一定会改变,而元组是一种是不能改变的类型。 事实上,列表、字典和可变集合都可以作为棋盘的载体。可以将“(x,y,z)”当做集合的元素,其中x,y组成棋子的坐标,z表示棋子的有无和属方,如果集合中存在一个坐标说明这个坐标的位置上有棋子,并且z值标明这个棋子的属方,否则没有这个棋子。也可以将“(x,y):z”作为字典的项,x,y,z意义同上。但是每次走棋都需要对棋盘进行更新(重新绘制),此时还需要对当前步骤棋子位置是否已存在棋子进行判断,这如何操作呢?答案是使用列表,利用列表特性不仅会解决这个问题而且将会显得更优雅。 (2)棋子的状态 前面讲过,列表是个容器类型,它里面的元素可以是任何对象,是否想过如果列表里的对象是列表会有什么情况发生?这其实拓展了列表的维度——从一维到二维,这时如果想访问到列表中列表的元素需要二重索引:第一重返回一个列表,第二重返回里面列表的元素。 >>> chessboard=[[0,1],[2,3],[4,5],[6,7]]>>> chessboard[1][1] #两重索引3 | 相信读者已发现,二重索引其实可以当坐标使用。如果棋盘有n行n列,那么这个棋盘可以抽象为一个包含n个列表的列表,其中每个列表包含n个元素。这样一个数据结构可以完整描述整个棋盘,那么输出棋盘时就不需要每次判断当前位置上是否有棋子了。那么如何记录每个位置上的棋子状态呢?棋盘中每个位置有三种状态:无子、己方棋子和敌方棋子,那么可以用三个值来区别这三种状态:0表示无子,1表示有己方棋子,2表示有敌方棋子,并且当下棋时读取该位置上的状态信息来判断是否能够行棋。 (3)棋盘的状态 除了要记录棋子的状态,棋盘的状态也需记录,即该谁落子。可以看出棋盘的状态只有两种,和棋子的状态的处理方法相同,也可以用一个who变量来记录,这个变量有两个值:False表示该玩家1落子,True表示该玩家2落子。之所以选用逻辑值来作为状态,因为可以使用not语句来改变状态,比较方便: >>> who=False>>> whoFalse>>> who=not who #not取反>>> whoTrue | 如果使用其它值,如0和1,那么状态的改变需要两条语句,who=0和who=1。而使用上面的方法只需一条即可:who=not who。
4.7.2 棋盘的创建 游戏开始时首先会创建一个棋盘,并对其进行初始化。这里可能需要提前透露一下后面的知识:循环(下章会讲到)。有时需要一些重复的操作,如果一条一条语句地写出来,代码会显得冗长。有时候这些操作甚至无法全部写出来,因为很多操作可能需要执行百万千万次。循环的使用可以使这些语句变成一条,创建棋盘便是个例子: 1 | maxx=10 | 2 | maxy=10 | 3 | qipan=[] | 4 | for i in range(maxx): #for循环语句 | 5 | qipan.append([]) | 6 | for j in range(maxy): #for循环语句 | 7 | qipan[i].append(0) | 程序一开始设定棋盘的规模:maxx,maxy分别表示棋盘的行数和列数。在第三行初始一个空列表棋盘,4~7行使用for循环往空列表中添加每一行每一列。 在构建棋盘时使用了两重循环,第一重构建行,第二重构建列,在执行的时候外面的循环每执行一次,里面的循环就要执行完整一圈。那么4~7行的代码可以解读为,每次为每行添加一个空列表,然后循环为这行添加maxy列,直到maxx行全部构建完毕。棋盘创建后用户的每次落子都会更新棋盘,程序需要执行这些操作并输出棋盘,这些操作涉及到的知识会在后面章节中详细讲解。
本章小结
本章介绍了Python中序列的概念和基本操作,重点讲解了列表、元组和字典三种不同的序列。通过讲解和比较不同序列的创建方式,操作和方法等异同点,使读者深入理解了它们之间的联系和各自的使用方式。之后讲解了集合的概念和它的操作,详细介绍了与数学中集合的各种操作相对应的操作方法。最后,以五子棋中绘制棋盘模块为例,使读者在具体应用中更深刻理解各种序列的使用。
习题
* 填空题 1. Python常用的序列类型包括__________、__________和__________三种。__________是Python中唯一的映射类型。 2. Python中的可变数据类型有____________________________________;不可变数据类型有__________________________________________。 3. 对于列表[1,2,3,4,5,6,7.8,9],以下代码输出分别为a[3:5]=_______________、a[:-4]=_______________、a[-3:]=______________和a[-5:7]=______________。 4. 对于列表a=[1,2,3,4,3,2,1],调用a.index(3)方法会输出______________。 5. 字典的方法中,copy与deepcopy的区别为____________________________________。 6. 对于集合A和B,在A集合中但不在B集合中的元素组成集合C,则C=__________。 * 选择题
1.以下不能创建一个字典的语句是( )
A. dict1 = {} B. dict2 = { 3 : 5 }
C. dict3 = dict( [2 , 5] ,[ 3 , 4 ] ) D. dict4 = dict( ( [1,2],[3,4] ) )
2.下面不能创建一个集合的语句是( )
A. s1 = set () B. s2 = set (“abcd”)
C. s3 = (1, 2, 3, 4) D. s4 = frozenset( (3,2,1) ) 3. 设s = ”Happy New Year”, 则s[3:8]的值为( )
A. 'ppy Ne' B. 'py Ne'
C. 'ppy N' D. 'py New' 4. 下面那些不是序列的基本操作( ) A. in B. count C. max D. min 5. 对于集合A和B,A>=B表示( ) A. A是B的子集 B. A是B的严格子集 C. B是A的子集 D. B是A的严格子集 6. 设a = [1,2,3,4,5,6,7,8],则a[::3]的值为( ) A. [1,2,3,4] B. [4,5,6,7,8] C. [1,4,7] D. [3,6] * 上机题 1. 把列表a=[1,2,3,4,5,6,7,8,9,10]分为只含有奇数和偶数的两个列表。 2. 给定两个长度相同的列表,用这两个列表中的所有数据组成一个字典。比如列表[1,2,3]和[‘a’,’b’,’c’],组成{1:’a’,2:’b’,3:’c’}。 3. 颠倒字典phone_a={"Alice":120,"Bob":110,"John":119}中的键值 4. 输出同时出现在集合a=set([1,2,23,14,64,3,6]), b=set([23,4,6,8])中的偶数 5. 使用列表为自己班级建立一个信息表,列表中每个元素为一个存储学生信息的字典,学号,姓名,性别,手机作为键。可以通过学号查询学生信息。

第5章 流程控制语句
在之前的章节已经介绍了Python的数据类型和常用的集合类型,本章将开始讲解Python中的流程控制语句和语法,主要介绍两种重要的控制结构——选择结构和循环结构。
选择结构又称分支结构,当程序需要做出一个决定和判断时,需要利用选择结构对不同的因素进行判断,从而采取不同的操作。程序中的某条语句是否执行,需要通过一个或几个条件来判断,而每个条件都是由表达式所构成。如果有多个条件,需要通过逻辑运算符来连接。另外,本章还将介绍条件运算符的使用和两种重要的选择结构语句,即if-else语句和elif语句。
除了根据条件判断执行分支以外,有时需要根据条件或按特定数量重复运行某段代码来完成特定任务。尽管此类问题同样可以使用顺序结构和选择结构的方式,通过将重复部分顺序运行的方式进行求解,但这样书写程序代码会显得杂乱无章,不利于语言的精简与理解,而且程序执行效率较低。因此,针对此类问题,Python语言中使用循环结构进行设计。本章后半部分将介绍循环语句for语句和while语句,循环控制语句break和continue语句的功能和使用。希望读者对python中的选择流程控制和循环流程控制部分能够深入理解。
【体系结构】

【本章重点】
(1)区分Python语言中的对象对应的逻辑值“真”和“假”。
(2)掌握条件运算符及真值表。
(3)掌握if/else和if/elif/else的结构和使用。
(4)掌握布尔运算符和惰性求值。
(5)掌握for语句和while语句的使用方法。
(6)掌握break、continue等循环控制语句的使用方法。
(7)掌握else语句对for语句和while语句的作用。
(8)掌握列表推导式。
【案例引入】五子棋游戏——棋盘绘制和判胜
在五子棋游戏设计中,棋盘可被认为是一个二维的列表结构,如果只用print语句画出棋盘,会十分麻烦而且笨拙。因此通过本章的学习,读者将学会使用循环结构解决画五子棋棋盘的问题。此外,在游戏进程中,如果一方玩家率先完成五颗棋子相连则胜利,同时游戏结束。通过本章的学习,读者也将学会如果使用选择结构和逻辑表达式来判断游戏玩家是否胜利。
5.1 布尔逻辑
在介绍选择和循环结构前,先了解什么是计算机的布尔逻辑。布尔值,也叫真值。名字来源于在布尔逻辑上做过大量研究的George Boole。布尔值在逻辑上分为真和假两种。在Python中,标准的布尔值有两个,分别是True和False(与C语言完全一致)。以下的值在作为布尔表达式的时候,会被认为是假:
None False 0 ""(空字符串) ()(空元组) [](空列表) {}(空字典)
即这些值可等价于布尔表达式。除了这些之外的所有值在作为布尔表达式的时候都被解释为真,和True等价。所有的值都可以转化成为布尔值。Python语言提供的bool函数可以将其他类型的值转化成为布尔值:
>>> | bool("String type") | True | >>> | bool(5) | True | >>> | bool(0) | False | >>> | bool("") | False |
由于所有值都可以当成布尔值使用,所以在使用这些值作为布尔表达式的时候不需要显式地调用bool函数进行转化,Python会自动转化这些值。有了布尔值作为保证,程序就可以根据布尔值对当前程序状态以及下一步运行方式进行判断。
5.2 代码块与缩进
由于Python语言与其他计算机语言(例如C语言)在编写程序代码方面有一定的差异,因此本章先介绍Python对于代码层次的组织方式和规范。这也是在学习后面的章节内容前所必须掌握的概念。
一个Python程序,是由多个或并列或嵌套的代码块组成的;代码块不是一种语句,而是多个语句组成的一组语句;一个代码块中的每行都有相同的缩进量,这一点是与传统编程语言最大的区别之处。下面一段伪代码解释了Python代码中缩进的工作方式: this is a statement in block onethis is another statement in block one: this is another statement in block two still in block twoescape the block one, now is in block two |
很多语言使用特殊的单词(如Pascal语言的begin)或者字符(如C语言的{)来表示一个语句块的开始,用对应的单词(Pascal的end)或字符(C语言的})来表示这个语句块的结束。在Python中,使用冒号(:)来表示一个语句块的开始,然后在块中每一个语句都向右缩进相同缩进量(如4个空格)。当缩进量回退到之前使用冒号的语句相同的缩进时,当前代码块闭合,即表示当前代码块结束。
代码块通过缩进对齐表达代码逻辑的方式,因为没有如大括号或begin/end的额外的字符,程序变得更加简洁,可读性也更高。而且缩进完全能够清楚地表达一个语句属于哪个代码块。
对于学过其他语言(如C语言、Java语言)的人,使用缩进可能会感到困惑或诧异,甚至对所编代码的正确性感到不安,然而Python的两大特色就是简洁和可读性好,同时使用缩进的方式也能从语法方面提高程序员的编码风格。
5.3 if/else语句
与其他编程语言类似,Python条件语句是通过一个表达式的执行结果对应的真值(True或者False)来决定是否执行代码块。
5.3.1 单分支条件语句
可以通过图5.1来简单了解Python单分支条件语句的执行过程:

图5.1 单分支条件语句流程图
Python的关键字if用于条件控制,基本形式为:
1 | if 判断条件: | 2 | 代码块1… | 3 | 代码块2… |
其中"判断条件"成立时,则执行if语句内的代码块1;如果不成立,执行之后的代码块2。
【例5-1】用户输入学生成绩,根据分数打印学生评级结果。
问题分析:百分制学生成绩的一般评级标准为90~100为A,80~89为B,70~79为C,60~69为D,小于60分的为E。可以通过判断学生成绩区间,来给出学生成绩对应的等级。
1 | score = input("输入学生分数: ") | 2 | if score >= 90: | 3 | print "A" | 4 | if 90 > score >= 80: | 5 | print "B" | 6 | if 80 > score >= 70: | 7 | print "C" | 8 | if 70 > score >= 60: | 9 | print "D" | 10 | if 60 > score: | 11 | print "E" |
运行结果:
输入学生分数:88 | B |
从例5-1可以看出,当python程序满足判断条件时,则执行该条件下的程序代码,否则跳过该语句,继续执行后续代码。
5.3.2 二分支条件语句
二分支条件控制语句的执行流程如下图5.2所示:

图5.2 二分支条件语句流程图
Python使用关键字if/else实现二分支条件控制,基本形式为:
1 | if 判断条件: | 2 | 代码块1 | 3 | else 判断条件: | 4 | 代码块2 |
与单分支条件语句不同,在当判断条件不成立时执将执行else语句下的代码块2,具体例子如下:
1 | # coding=utf8 | 2 | # if 基本用法 | 3 | | 4 | name = "alice" | 5 | if name == "alice": # 判断变量否为"python" | 6 | print "welcome alice" # 并输出欢迎信息 | 7 | else: | 8 | print "welcome stranger" # 条件不成立 |
运行结果:
welcome alice |
5.3.3 多分支条件语句
根据一个条件的结果控制一段代码块的执行可用if语句,若条件失败时执行另一代码块可用else语句。
如果需要检查多个条件,并在不同条件下执行不同代码块,就可以使用elif子句,它是具有条件判断功能的else子句,相当于”else if”。if/elif的基本形式是: 1 | if 判断条件1: | 2 | 执行代码块1…… | 3 | elif 判断条件2: | 4 | 执行代码块2…… | 5 | elif 判断条件3: | 6 | 执行代码块3…… | 7 | …更多elif子句 | 8 | else: | 9 | 执行代码块4…… |
【例5-2】用户输入学生成绩,根据分数打印学生评级,使用多分支语句重新实现。
问题分析:使用if/elif语句,先判断是否为A级(分数大于等于90),如果不成立时,只需判断是否大于等于80即可判断是否为B级,以此类推,可减少一半的比较判断。 1 | score = input("输入学生分数: ") | 2 | if score >= 90: | 3 | print "A" | 4 | elif score >= 80: | 5 | print "B" | 6 | elif score >= 70: | 7 | print "C" | 8 | elif score >= 60: | 9 | print "D" | 10 | else: | 11 | print "E" |
运行结果:
输入学生分数:88 | B |
熟悉C语言的读者可能知道C语言中的switch语句,但是Python语言并不支持switch语句。对于多个条件判断的情况,可以使用if/elif/else来实现。
如果判断需要两个条件需同时判断时,可以使用or(或),表示两个条件有一个成立时判断条件成功;使用and(与)时,表示只有两个条件同时成立的情况下,判断条件才成功。当需要判断多个条件时,可连续使用and和or联立多个条件表达式。 1 | # coding=utf8 | 2 | # if语句多个条件 | 3 | | 4 | num = 9 | 5 | if num >= 0 and num <= 10: # 判断值是否在0~10之间 | 6 | print "small number" | 7 | | 8 | num = 10 | 9 | if num < 0 or num > 10: # 判断值是否在小于0或大于10 | 10 | print "negative or big number" | 11 | else: | 12 | print num | 13 | | 14 | num = 8 | 15 | # 判断值是否在0~5或者10~15之间 | 16 | if (num >= 0 and num <= 5) or (num >= 10 and num <= 15): | 17 | print "sucess" | 18 | else: | 19 | print "failed" |
运行结果:
small number | 10 | failed |
当if有多个条件时可使用括号来区分判断的先后顺序,括号中的判断优先执行,此外and和or的优先级低于>(大于)、<(小于)等关系运算符,即大于和小于在没有括号的情况下会比与或要优先判断。也可以在同一行的位置上使用if条件判断语句,如下实例:
1 | #!/usr/bin/python | 2 | | 3 | num = 10 | 4 | | 5 | if (num == 10): print "Value is 10" | 6 | | 7 | print "Good bye!" |
运行结果:
Value is 10 | Good bye! | 除了多分支条件语句,选择结构还有一种重要的应用场景,即选择结构的嵌套使用。嵌套的形式如下: 1 | if 表达式1: | 2 | 代码块1 | 3 | if 表达式2: | 4 | 代码块2 | 5 | else: | 6 | 代码块3 | 7 | else: | 8 | 代码块4 |
【例5-3】选择结构嵌套应用。读入一个年份,输出该年是否是闰年。
问题分析:当年份能被4整除但不能被100整除,或能被400整除时,该年份就是闰年。 1 | year = raw_input("请输入年份:") | 2 | year = int(year) | 3 | | 4 | if year % 4 == 0: | 5 | if year % 400 == 0: | 6 | print "闰年" | 7 | elif year % 100 == 0: | 8 | print "平年" | 9 | else: | 10 | print "闰年" | 11 | else: | 12 | print "平年" |
运行结果:
请输入年份:2004 | 闰年 | 对于例5-3,判断一个年份为闰年的条件是“被4整除但不能被100整除,或能被400整除”,可以把这个条件拆分为“被4整除但不能被100整除”和“能被400整除”,只要满足其中一个条件就是闰年。那么,可以使用布尔运算符联立条件: 1 | year = raw_input("请输入年份:") | 2 | year = int(year) | 3 | | 4 | if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0: | 5 | print "闰年" | 6 | else: | 7 | print "平年" |
5.3.4 条件表达式 if语句中条件表达式有许多种形式,它们是if语句的关键成分,接下来详细介绍各种形式的条件表达式。
(1)关系运算符
关系运算符是条件表达式中最基本的运算符,它们用来比较两个对象之间的关系,如表5.1所示。 表5.1 关系运算符属性 运算符 | 功 能 | 运算元数 | 结合性 | 示 例 | > | 大于 | 2 | 左结合 | a>3 | < | 小于 | 2 | 左结合 | a<b | == | 等于 | 2 | 左结合 | a==b | >= | 大于或等于 | 2 | 左结合 | a>=b | <= | 小于或等于 | 2 | 左结合 | a<=b | != | 不等于 | 2 | 左结合 | a!=b | is | 同一个对象 | 2 | 左结合 | a is b | is not | 不是相同对象 | 2 | 左结合 | a is not b | in | 判断成员关系 | 2 | 左结合 | a in b | not in | 判断非成员关系 | 2 | 左结合 | a not in b | 由关系运算符连接的两个操作单元所组成的表达式为关系表达式,其一般格式为: 表达式1 关系运算符 表达式2 下面的比较表达式是有效的: (1)3<0,表达式不成立,结果为False。 (2)7>6,表达式成立,结果为True。 (3)若x=2,y=4,z=-5,则关系表达式x+3>y+z成立,结果为True。 (4)若x=2,y=4,则关系表达式x!=3>y按照优先级顺序,先计算3>y,结果为False,再计算x!=False,结果为True。 关系运算符也可以用来比较字符型数据,字符型数据按ASCII码值的大小比较。例如:'a'>'A'结果为1,'A'>'B'结果为False。
在Python中,比较运算可以连续使用,即类似0 < 5 > 3,1 < 2 < 3,都是合法的表达式。
(2)布尔运算符
Python的布尔运算符有三个,分别是and,or和not。 * and是一个二元表达式,它连接两个布尔表达式,并且只有在表达式两边均为真时返回真,否则返回假。 * or也是一个二元表达式,它所连接的两个布尔表达式中,只要有一个为真时返回真,否则返回假。 * not是一个一元表达式,当与它相连的值为真时返回假,否则返回真。
这三个运算符的具体用法如下:
>>> | 1 and 9 > 5 | True | >>> | 9 > 5 and False | False | >>> | 0 or 1 | True | >>> | 0 or False | True | >>> | not 1 | False | >>> | not False | True |
其中and和or运算符有个比较有意思的特性:只有在需要求值时才会进行求值。
例如表达式x and y,需要两个变量均为真时才为真,所以如果x为假时,表达式直接返回x的值(即假)而不会去计算y的值;如果x为真时,表达式会直接返回y的值(因为此时y决定了整个表达式的值)。 再比如表达式x or y,当x为真时表达式直接返回x而不会计算y的值;当x为假时表达式直接返回y的值。 这种特性被称之为惰性求值,避免了一些无用的操作,同时也可作为一种高效的代码技巧使用。假设有三个变量x,y和z,如果需求是:若y不为假,则把y赋值给x,否则把z赋值给x,使用if表达式的话可以这么写: 1 | if y: | 2 | x = y | 3 | else: | 4 | x = z |
而如果使用or的惰性求值特性,可以用一行代码实现这个需求:
1 | x = y or z |
5.3.5 断言 为了使程序能够正确的执行,程序代码中往往有许多判断语句来判断执行到某一步时的状态或结果是否正确,如果出现问题,可以让程序在判断位置提前结束,并给出错误提示信息,便于修复程序。编写代码时,总是会做出一些假设,这些假设用于确保程序执行到某一步时的状态是正确的。断言就是用于在代码中捕捉这些假设。
比较简单的做法是使用if语句进行判断,并根据判断的结果决定是否终结程序或者让程序继续进行下去。在大多数语言中,都提供了一种叫断言的机制(或是语法或是函数),Python也不例外,Python使用关键字assert来执行断言。
>>> | num = 50 | >>> | num > 100 | False | >>> | num > 20 | True | >>> | assert num > 20 #断言num大于20 | >>> | assert 50 < num < 100 #断言num在50和100之间 | Traceback (most recent call last): | File “<stdin>”, line 1, in ? | AssertionError: The age must be realistic | assert断言可以发现问题时抛出异常,使程序提前退出。不过,仅仅是退出程序并不能很好的定位程序的问题,所以,assert语句可以在条件判断后加上字符串,断言失败时将会显示提供的字符串。 >>> | num = -1 | >>> | assert num > 0, “num should be positive!” | Traceback (most recent call last): | File “<stdin>”, line 1, in ? | AssertionError: num should be positive! |
5.4 循环
通过上述内容,读者应该已经了解if语句的使用方法、场景和条件,即在条件为真时执行一次对应代码块。但是有时候如果需要重复多次执行一段代码块,例如,如果需要打印从1到10的整数,尽管可以使用10个print语句来完成,可是如果要打印1到100甚至更多的话,这种方法太不方便了。这时就需要用到循环控制结构。Python语言中循环语句有两种,即while语句和for语句。下面将对这两种循环方式进行详细说明。
5.4.1 while循环
在Python语言中,while循环语句在判断条件为真时,重复执行一段代码,它的基本形式是:
1 | while 判断条件condition | 2 | 执行代码块block… |
图5.3展示了while循环的执行过程:

图5.3 while语句流程图 while的执行步骤是: 1. 计算condition; 2. 如果condition为真,执行while语句内的代码块,然后继续步骤1; 3. 如果condition为假,结束while语句,执行后续代码。
【例5-4】打印从1到100这100个整数。
问题分析:打印100个整数,可以先声明一个变量,初始值为1,然后使用while语句迭代执行打印,并在打印之后改变变量的值,使变量增加1。其中while语句的判断条件是变量的值小于等于100。 1 | x = 1 | 2 | while x <= 100: #当x小于等于100时执行 | 3 | print x | 4 | x += 1 |
运行结果:
1 | 2 | 3 | 4 | … | 100 |
这里需要注意的是,由于Python语言语句块完全依赖缩进表示,因此同一个语句块中的语句一定要设置成相同缩进。
【例5-5】使用while语句实现拉兹猜想。
问题分析:拉兹猜想又名3n+1猜想或冰雹猜想,是指对每一个正整数,如果它是奇数,则对它乘3再加1;如果它是偶数,则对它除以2,如此循环,最终都能得到1。
1 | num = input("输入初始值:") | 2 | while num != 1: | 3 | if num % 2 == 0: | 4 | num = num / 2 | 5 | else: | 6 | num = num * 3 + 1 | 7 | print num |
运行结果:
输入初始值:10 | 5 | 16 | 8 | 4 | 2 | 1 |
5.4.2 for循环 上一节内容讲解了如何使用while循环语句在判断条件为真时重复执行一段代码块,本节将介绍for循环语句。for循环用于为一个集合(list数组,dict字典,set集合类可迭代对象)中的每个元素执行一次循环体内代码块。例如: 1 | nums = [1, 2, 3, 4, 5] | 2 | for item in nums: | 3 | print item |
由于迭代一个范围内的数字是十分常见的操作,Python提供了一个内建的函数,可以返回包含一个范围内的数值的数组。例如:
>>> | range(10) | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] | >>> | range(1, 10) | [1, 2, 3, 4, 5, 6, 7, 8, 9] |
当range接受一个参数时,返回0到该参数(不包含该参数)的数组,当接受两个参数时,返回对应范围内的数组,但不包含第二个参数。所以打印1到100的数字可以用for配合range函数来实现:
1 | for item in range(1, 101): #1到101,不包含101 | 2 | print item |
【例5-6】求10的阶乘。
问题分析:求10的阶乘,就是求1*2*3*…*10,可声明一个变量初始值为1,然后将它乘以1的积赋值给该变量,再乘以2并将积赋值给该变量,使用for和range迭代执行10次。 1 | x = 1 | 2 | for i in range(1, 11): | 3 | x *= i | 4 | print x |
运行结果:
3628800 | 如果需要按一定步长来迭代一个范围内的数字,也可以使用range函数实现,当range函数接受3个参数时,range函数返回相应范围的数组,并且不包含第二个参数,其中步长为第三个参数。例如: >>> | range(1, 10, 2) | [1, 3, 5, 7, 9] | >>> | range(10, 1,-1) | [10, 9, 8, 7, 6, 5, 4, 3, 2] |
【例5-7】对1到100内的偶数求和。
问题分析:可以声明一个变量,代表累加和,初始值为0。然后直接对1到100的数值进行迭代,当值为偶数时与变量相加并更新变量。不过可以用更好的方法,由于range函数提供按步长生成数组,可以使用range直接生成所有偶数。 1 | x = 0 | 2 | for i in range(1, 101, 2): | 3 | x += i | 4 | print x |
运行结果:
2500 |
5.4.3 循环嵌套
到目前为止已经学习了两种循环语句,可以解决一些简单的循环问题。但在实际问题中,依靠单一循环结构往往无法完成任务,需要多种循环结构相互嵌套来解决。例如,学校对各班期末考试成绩进行检查,首先使用循环的方式对各个班级进行选择,然后在被选中的班级中再使用循环结构对班级中每位同学的成绩进行选择,这实际上构成了一个两层循环结构,也就是循环嵌套。
一个循环体内又包含另一个完整的循环结构,称为循环的嵌套。内嵌的循环中还可以嵌套循环,这就是多层循环。循环嵌套从结构上分为内层循环与外层循环,且内层循环仍然可以继续嵌套循环,多层循环被允许嵌套在一起使用,这也是很多语言的循环嵌套语句的特点。两种循环(while循环和for循环)可以互相嵌套。
【例5-8】 求一个数值矩阵的所有数值之和。
问题分析:要求数值矩阵之和,就需要对矩阵中的所有元素进行迭代,可以选择两层嵌套结构,外层为矩阵的第一维,内层为矩阵的第二维。
1 | broad = [[9, 7, 3, 6, 5], | 2 | [10, 2, 4, 6, 7], | 3 | [0, 5, 3, 2, 9], | 4 | [7, 3, 5, 6, 1],] | 5 | sum = 0 | 6 | for i in range(len(broad)): | 7 | for j in range(len(broad[i])): | 8 | sum += broad[i][j] | 9 | print sum |
运行结果:
100 |
程序分析:嵌套循环的执行顺序是,首先进行最外层for循环,i从0到3每次进行迭代,然后进入循环体,执行第二层for循环,j从0到(第i个列表的长度 - 1)每次进行迭代,通过i和j可以唯一定位到一个board中的元素,然后把每个值都加到sum中。
【例5-9】 打印9-9乘法表。
问题分析:使用循环嵌套结构打印9-9乘法表,一共9行,第一行打印1 * 1,第二行打印1 * 2到2 * 2,第三行打印1 * 3到3 * 3,依次类推。 1 | for x in range(1, 10): | 2 | for y in range(1, x + 1): | 3 | print "%d*%d=%2d" % (x, y, x*y), | 4 | print '' |
运行结果:
1*1= 1 | 2*1= 2 2*2= 4 | 3*1= 3 3*2= 6 3*3= 9 | 4*1= 4 4*2= 8 4*3=12 4*4=16 | 5*1= 5 5*2=10 5*3=15 5*4=20 5*5=25 | 6*1= 6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36 | 7*1= 7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49 | 8*1= 8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64 | 9*1= 9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81 |
当然,不只有for语句可以使用循环的嵌套结构,本章介绍的while语句也可以使用循环的嵌套。两种循环语句均可以用作外循环或者内循环,它们之间可以互相嵌套,这根据读者习惯而定。
5.4.4 循环遍历字典元素 for语句除了可以迭代列表元素外,还可以迭代字典的所有键,使用方法和迭代列表一样: 1 | dic = {"k1": 1, "k2": 2, "k3": 3} | 2 | for key in dic: | 3 | print key, ": ", dic[key] |
运行结果:
k1: 1 | k2: 2 | k3: 3 | for语句还有一个特性叫序列解包,当需要迭代的为序列对象(列表或元组)时,可以将序列元素同时赋值给不同的变量进行迭代,当然,要求赋值的变量数与序列元素个数相同。所以配合字典的函数items,上述程序可以换种方式实现,提高可读性: 1 | dic = {"k1": 1, "k2": 2, "k3": 3} | 2 | for key, value in dic.items(): | 3 | print key, ": ", value |
运行结果:
k1: 1 | k2: 2 | k3: 3 |
5.4.5 迭代工具
Python提供了许多非常有用的迭代函数,这些函数可以使代码更加简洁。
(1)翻转迭代 reversed函数提供将列表顺序翻转的功能,返回的是一个叫迭代器的对象,迭代器的意义在现在可以先不去考虑,需要知道的是reversed的返回结果可以用于for语句迭代,或者使用list函数将迭代器转换成列表;如果reversed翻转的是一个字符列表或字符串的话,可以使用join函数将返回结果拼接起来。 >>> | list(reversed(range(1, 10))) | [9, 8, 7, 6, 5, 4, 3, 2, 1] | >>> | list(reversed("hello, world")) | ["d", "l", "r", "o", "w", " ", ",", "o", "l", "l", "e", "h"] |
(2)并行迭代
zip函数可以合并两个序列,可以方便的处理需要同时迭代两个列表的情况。例如有一个物品列表和一个价格列表: 1 | products = ["apple", "banana", "orange"] | 2 | prices = [50, 40, 25] | 如果想要同时打印产品和对应的价格,可以如此做: 1 | for i in range(len(products)): | 2 | print products[i], ": ", prices[i] | 运行结果: apple: 50 | banana: 40 | orange: 25 | 这段程序通过下标索引实现并行迭代,Python提供的内建函数zip也能达到相同的效果,而且使代码的可读性更好。zip接受连个列表参数,并返回合并这两个列表后的列表: >>> | zip(products, prices) | [("apple", 50), ("banana", 40), ("orange", 25)] |
所以上述打印产品名和对应价格的函数可以如此实现:
1 | for product, price in zip(products, prices): | 2 | print product, ": ", price | 注意到上面的代码,zip合并的是两个长度相同的列表,如果zip合并的是两个长度不等的列表,zip根据长度较短的那个列表进行合并,即zip返回的列表长度与较短的列表长度相等,而较长的那个列表的后一部分将被忽略: >>> | a = [1, 3, 5, 7, 9] | [1, 3, 5, 7, 9] | >>> | b = [2, 4, 6, 8] | [2, 4, 6, 8] | >>> | zip(a, b) | [(1, 2), (3, 4), (5, 6), (7, 8)] | (3)编号迭代 在迭代列表时,有时需要既获得列表元素又同时获取该元素对应的下标,通过之前的学习,可以轻松的实现这一需求: 1 | lis = ["a", "b", "c"] | 2 | for i in range(len(lis)): | 3 | print i, ": ", lis[i] |
运行结果:
1: a | 2: b | 3: c | 这个方法很简洁,而且正确,不过可读性较差。另一种方法是使用Python的内建函数enumerate,它能给列表“打上标号”。 1 | lis = ["a", "b", "c"] | 2 | for index, value in enumerator(lis): | 3 | print index, ": ", value |
运行结果:
1: a | 2: b | 3: c |
5.4.6 循环控制语句
前面学习了for语句和while语句,并知道while语句是在某一条件成立时循环执行一段代码块,而for语句是迭代一个集合的元素并执行一段代码块。然而有时可能需要提前结束一次迭代,进行新的一轮迭代,或者跳出循环,执行循环后的代码。下面将介绍两种循环控制语句,即break语句和continue语句。
(1)break语句
在Python中的break语句的作用是结束当前循环然后跳转到循环后的下一条语句。需要注意的是,break只会退出break语句所在的最内层循环,也就是说,当程序为多层嵌套的循环结构时,break语句只会跳出其所在的循环,而外层循环将继续进行迭代。具体参考下面的例子。
【例5-10】 求不大于1000的最大斐波那契数。
问题分析:斐波那契数列,又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、……在数学上,斐波纳契数列以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。要求不大于1000的最大斐波那契数,可根据公式一直循环下去,直到值大于1000时结束循环。
1 | a, b = 0, 1 | 2 | while True: | 3 | a, b = b, a + b | 4 | if b > 1000: | 5 | break | 6 | print a |
运行结果:
987 |
程序分析:首先定义两个变量a和b,分别是斐波那契数列的F0和F1,然后使用while语句循环迭代,将b的值赋值给a,并且把a+b的和复制给b,这样a和b的值更新成了F1和F2,以此类推,由于b始终是a所代表的斐波那契数的下一个值,当b大于1000时,使用break退出循环,这样a的值就是最后要求的值。
【例5-11】 打印英文藏头诗的藏头。
问题分析: 打印英文藏头诗的藏头,可以从前往后分析每句话,获取每句话的第一个单词;由于之后的句子已经没有用了,可以在得到每句的藏头后使用break退出当前循环。 1 | poem = ["M is for the million things she gave me", | 2 | "O means only that she\"s growing old", | 3 | "T is for the tears she shed to save me", | 4 | "H is for her heart of purest gold", | 5 | "E is for her eyes, with love-light shining", | 6 | "R means right, and right she\"ll always be",] | 7 | head = [] | 8 | for line in poem: | 9 | for index in range(len(line)): | 10 | if line[index] == " ": | 11 | head.append(line[: index]) | 12 | break | 13 | | 14 | print "".join(head) #拼接head字符串数组 |
运行结果:
MOTHER |
程序分析:使用for迭代诗的每行并赋值给line,从左到右迭代line的每个字母,当遇到第一个空格时,截断第一个单词并添加到head列表,同时使用break退出当前行的迭代;由于break只会退出所在的最内层循环,所以外层迭代继续,这样就取出了每行的头单词,合并后打印即可。
(2)continue语句 在循环体中,如果遇到某种情况希望提前结束本次循环,并继续进行下次循环时,可以使用continue语句;continue语句与break语句的不同之处在于,break将结束本次循环并跳出循环,而continue仅仅是提前结束当前这次循环,循环将继续进行下一次循环。下例子展示了continue循环控制语句的使用方式和效果。
【例5-12】 一个正整数矩阵中,求满足该行数值之和小于100的行的奇数数值之和。
问题分析: 对于每行,可以先判断该行是否满足属于该行的数值之和小于100,由于都是正整数,可以在累加过程中如果累加和大于100直接break退出;然后判断,若和小于100,再求当前行的所有奇数之和,并加到结果中,否则使用continue结束当前此迭代,继续下一次迭代。 1 | maz = [[12, 13, 20, 9, 30, 7], | 2 | [11, 22, 33, 21, 44], | 3 | [30, 31, 25, 66, 1], | 4 | [12, 34, 56, 7]] | 5 | result = 0 | 6 | for lis in maz: | 7 | tmp = 0 | 8 | for num in lis: | 9 | tmp += num | 10 | if tmp >= 100: | 11 | break | 12 | if tmp >= 100: | 13 | continue | 14 | for num in lis: | 15 | if num % 2 == 1: | 16 | result += num | 17 | | 18 | print result |
运行结果:
29 |
程序分析:在每行的迭代中,声明变量tmp用于记录当前行的数值之和,由于矩阵都是正整数,当tmp大于等于100时可以直接退出求和,然后判断tmp是否大于等于100,如果不小于100,使用continue结束当前迭代继续下一次迭代,否则将该行的所有奇数加到result中去。
5.4.7 循环与else子句 在循环中使用break语句,一般是在循环中发现某种特定的条件符合。但是,如果需要在循环之后判断该条件是否符合,就需要额外的标识来记录。
【例5-13】 判断一个数列中是否有奇数。
问题分析: 循环判断数列中的每一个数,当出现奇数时退出循环;为了记录是否找到了奇数,需要声明一个变量记录。 1 | lis = [2, 4, 6, 8, 0, 10, 12] | 2 | flag = False | 3 | for num in lis: | 4 | if num % 2 == 1: | 5 | flag = True | 6 | break | 7 | | 8 | if not flag: | 9 | print "All num is even" |
运行结果:
All num is even | 如上例,运行结果打印了“All num is even”,说明没有执行5,6行的代码,即程序在循环体中没有被break退出。如果循环体没有被break退出,则执行特定的一段代码,这种情景非常的普遍。Python提供了一种很简单的方式,那就是在循环中增加一个else子句,如果循环正常执行,而不是被break语句结束,则else子句内的代码将被执行。使用else子句重写刚才的例子: 1 | lis = [2, 4, 6, 8, 0, 10, 12] | 2 | for num in lis: | 3 | if num % 2 == 1: | 4 | flag = True | 5 | break | 6 | else: | 7 | print "All num is even" |
如果break没有被执行,说明数列中所有的数字都是偶数,打印“All num is even”。for语句和while语句的循环体中都可以使用continue和break语句,以及else子句。
5.5 列表推导式
列表推导式是利用其它集合类对象(列表,元组,集合和字典)来创建新列表的一种方法,它的工作方式和for循环很相似,语法也非常的简洁:
>>> | [2 * x for x in range(10)] | [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] | >>> | [k for k in {"key": "value", 4: 5}] | ["key", 4] |
列表推导式中还可以增加一个if部分来进行筛选:
>>> | [2 * x for x in range(10) if x % 3 == 0] | [0, 6, 12, 18] | >>> | [k for k in {"key": "value", 4: 5} if type(k) is str] | ["key"] |
也可以增加多个for语句部分:
>>> | [(x, y) for x in range(2) for y in range(2)] | [(0, 0), (0, 1), (1, 0), (1, 1)] |
上面这个例子和下面这段代码是等价的:
1 | result = [] | 2 | for x in range(2): | 3 | for y in range(2): | 4 | result.append((x, y)) |
5.6 继续五子棋
通过本章前面内容的学习,读者已经对Python语言的选择结构和循环结构有所了解,本节将利用前面所学知识,完成五子棋相关工作。
(1)记录状态
随着五子棋游戏的进行,黑白双方会交替往棋盘下棋子,那么就需要不断地记录和更新棋盘的状态。由于使用0作为没有没有任何一方的棋子落在棋盘的初始状态,那么就需要再选两个值分别表示黑白双方在棋盘上落子的状态,不妨分别设为1和2。
那么一个合法的棋盘状态应该是二维矩阵上的每个点都是0、1或2这三个值中的一个。黑白双方轮流下子,读取棋子位置,更新棋盘状态: 1 | who = True | 2 | while True: | 3 | t = raw_input("请输入棋子位置(x,y), 现在由" + \ | 4 | ("〇" if who else "乂") + "方下子:").split(",") | 5 | if len(t) == 2: | 6 | x = int(t[0]) | 7 | y = int(t[1]) | 8 | if qipan[x][y] == 0: | 9 | qipan[x][y] = 1 if who else 2 | 10 | who = not who | 11 | else: | 12 | print "当前位置已有棋子,请重新下子" | 13 | else: | 14 | print "输入位置有误,请输入要下的位置,如 1, 1" |
(2)显示棋盘
要比较直观的显示棋盘,只需要将二维棋盘一行一行的打印出来即可。为了更好的显示效果,将没有棋子落在某位置的0状态显示为”十”,将白方的子显示为”〇”,将黑方的子显示为”乂”。同时,为了棋盘更好的定位,在棋盘的第一行和第一列显示行号。 1 | #coding:utf-8 | 2 | maxx = 10 | 3 | maxy = 10 | 4 | qipan = [[0, 0, 0, 0, 1, 0, 0, 2, 0, 0], | 5 | [0, 1, 2, 1, 1, 0, 2, 0, 0, 0], | 6 | [0, 0, 0, 0, 1, 1, 0, 2, 0, 0], | 7 | [0, 0, 0, 0, 2, 0, 0, 1, 0, 0], | 8 | [0, 0, 0, 1, 1, 1, 2, 0, 0, 0], | 9 | [0, 0, 0, 2, 0, 0, 0, 2, 0, 0], | 10 | [0, 0, 1, 2, 0, 2, 2, 0, 1, 0], | 11 | [0, 0, 0, 2, 0, 0, 0, 1, 0, 0], | 12 | [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], | 13 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],] | 14 | | 15 | print(" 〇 一 二 三 四 五 六 七 八 九 ") | 16 | for i in range(maxx): | 17 | print i, | 18 | for j in range(maxy): | 19 | if qipan[i][j]==0: | 20 | print "十", | 21 | elif qipan[i][j]==1: | 22 | print "〇", | 23 | elif qipan[i][j]==2: | 24 | print "乂", | 25 | print "\n" | 如果运行上述代码,将显示出一个完整的并且已经有部分黑白子的棋盘:

(3)棋局判胜 当棋盘有连续5个位置被落同一方的棋子时(横向、竖向或者斜向),对应的一方获胜,游戏结束。所以每次黑白中的一方下棋子,都需要判断一下棋盘状态,检查棋局是否满足胜利条件。 如果每次都检查整个棋盘状态,复杂度很高,会相当的费时间。其实对于一个还不满足胜利条件的棋局,当一个棋子落下时,受影响的状态只有与这个棋子相连的对应的横向、竖向和斜向。所以每次只需判断新下的棋子的周围的棋盘即可。 1 | xPoint = 3 #新下子的x轴位置 | 2 | yPoint = 4 #新下子的y轴位置 | 3 | t = 1 #新下子的状态,1表示白方 | 4 | #横向 | 5 | count = 0 | 6 | x = xPoint | 7 | y = yPoint | 8 | while (x >= 0 and t == qipan[x][y]): #向左统计连续棋子个数 | 9 | count += 1 | 10 | x -= 1 | 11 | x = xPoint | 12 | y = yPoint | 13 | while (x < maxx and t == qipan[x][y]): #向右统计连续棋子个数 | 14 | count += 1 | 15 | x += 1 | 16 | if (count > 5): print “横向五子相连,胜利!” | 17 | | 18 | #纵向 | 19 | count = 0 | 20 | x = xPoint | 21 | y = yPoint | 22 | while (y >= 0 and t == qipan[x][y]): #向上统计连续棋子个数 | 23 | count += 1 | 24 | y -= 1 | 25 | y = yPoint | 26 | while (y < maxy and t == qipan[x][y]): #向下统计连续棋子个数 | 27 | count += 1 | 28 | y += 1 | 29 | if (count > 5): print “纵向五子相连,胜利!” | 30 | | 31 | #/ 斜向 | 32 | count = 0 | 33 | x = xPoint | 34 | y = yPoint | 35 | while (x >= 0 and y < maxy and t == qipan[x][y]): #向左下统计 | 36 | count += 1 | 37 | x -= 1 | 38 | y += 1 | 39 | x = xPoint | 40 | y = yPoint | 41 | while (x < maxx and y >= 0 and t == qipan[x][y]): #向右上统计 | 42 | count += 1 | 43 | x += 1 | 44 | y -= 1 | 45 | if (count > 5): print “斜向五子相连,胜利!” | 46 | | 47 | #\ 斜向 | 48 | count = 0 | 49 | x = xPoint | 50 | y = yPoint | 51 | while (x >= 0 and y >= 0 and t == qipan[x][y]): #左上统计 | 52 | count += 1 | 53 | x -= 1 | 54 | y -= 1 | 55 | x = xPoint | 56 | y = yPoint | 57 | while (x < maxx and y < maxy and t == qipan[x][y]): #右下统计 | 58 | count += 1 | 59 | x += 1 | 60 | y += 1 | 61 | if (count > 5): print “斜向五子相连,胜利!” | 对于新下的棋子,分别判断横向,竖向和斜向的连续相同状态的棋子个数,如果数量大于5,则满足胜利条件,否则棋局未结束,需要继续进行。
本章小结
本章首先学习了Python语言选择流程控制结构的一些基础知识,理解Python语言中“真”与“假”的含义和表示,掌握关系和逻辑运算符及其表达式的书写方式,学习了关系运算和逻辑运算在基本的if语句中的使用方法。借助并列if/elif/else语句和嵌套if/elif/else语句结构实现更复杂的逻辑选择判断。本章还介绍了条件运算符的使用方法。读者需要善于运用条件表达式进行简单逻辑关系选择,以及逻辑表达式中短路特性的原理和使用,合理缩减代码。最后学习的是断言部分,它是一个开发较大程序甚至工程时常用的工具和技巧,需要熟练掌握。
本章还介绍了Python语言循环流程控制结构的一些基础知识,需要熟练掌握并灵活应用for、while循环语句的使用方法;理解for语句和while语句的差别;理解else语句在循环语句中的应用。在几种循环语句熟练掌握的情况下,读者应深刻理解循环嵌套并能在实际问题中有所应用。读者在应用循环语句的同时,在遇到需要循环终止或跳过本次循环的情况时,要灵活使用break语句与continue语句。熟练地掌握循环语句,利于编程人员缩减代码,同样也能减少编程人员因代码繁多造成不必要的程序错误。

习题 * 填空题 1. 在Python语言中____________值表示逻辑值”假”。 2. 使用”and”连接的表达式,按照从左到右的顺序计算,遇到一个____________,后面的部分将不再计算。 3. 请写出表达式”x<5 or x>6”的等价表达式____________。 4. ____________语句的功能是终止当前循环,____________语句的功能是跳出本次循环继续下次循环。 5. 在内外两层循环嵌套使用时,break语句的使用是终止____________层循环。 * 选择题 1. 下面程序的运行后,b的值是( )。 a=5 b=0 if a==b: b=b+3 else: b= a+5
A. 10 B.5 C.8 D.3 2. 下列表达式中,值为真的是()。
A.3>5 B.5&11 C.!(12>0 || 4<1) D.4 > 3 ? 0 : 5 3. break语句在循环中的作用是( )。
A. 结束本次循环继续下次循环
B. 终止程序
C. 终止本次循环
D. 结束选择结构语句 4. continue语句在循环中的作用是( )。
A. 结束本次循环继续下次循环
B. 终止程序
C. 终止本次循环
D. 结束选择结构语句 5. 这段程序运行后,输出的结果是( )。 for i in range(2): for j in range(1): print i, j,
A. 0 0 1 0 B. 0 0 0 1 C. 0 1 0 1 D. 1 0 0 1 6. 这段程序运行后,输出的结果是( )。 for i in range(4): if i==1: break elif i==3: continue else: print i,
A. 0 B. 0 2 C. 0 1 2 D. 0 1 2 4 5;
三、上机题
1. 输入一个数,输出大于它的最小偶数。 2. 输入两个数据,判断前一个数是否是后一个数的倍数。 3. 求1,3,5,7,9,…,99之和。 4. 输出1~100之间的所有偶数都输出。 5. 从键盘输入两个整数,求其最大公约数和最小公倍数。 6. 从键盘输入一个正整数,将该正整数前后倒置后输出。 7. 随意从键盘输入一个正整数n,求n!的值(使用while语句完成)。 8. 水仙花数是各位数字立方之和等于数字本身的三位整数,求出所有的水仙花数并输出。 9. 打印杨辉三角形(至少打印出前10行)。 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 …………

第6章 再讲字符串
前面章节对Python语言的字符串做了初步介绍,本章将详细的介绍字符串的各种相关操作,以及正则表达式的应用。熟练使用Python字符串的各种操作,将大大提高使用Python的工作效率。正则表达式会使字符串的匹配操作更加便捷,在编写处理字符串的程序时,经常会面对如何查找符合某些复杂规则字符串的问题,而正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
【体系结构】

【本章重点】
(1)掌握字符串各种操作函数;
(2)熟悉re模块正则表达式的方法;
(3)掌握正则表达式特殊符号;
6.1 字符串操作
字符串操作是编程语言中的一项基本操作,比如对一个文本字符串,要得到其中子序列,就需要使用字符串切片操作;再比如,在一个银行系统中,系统输出账户余额时,输出格式应该为“您的账户余额:<数字>”,这里就需要使用格式输出。Python语言所提供的字符串操作方法将大大简化编程复杂度,缩短开发周期。下面列出最常用的几种字符串的操作方法,
(1)切片操作能简单快捷的提取字符串中的一段内容;
(2)字符串的格式化操作能方便的输出被格式化过的字符串,同时也方便地产生大量的相同格式的字符串;
(3)字符串模板更进一步简化了格式化操作,更容易进行字符串模板替换;
(4)原始字符串操作符提供了所见即所得的字符,禁止了字符的转义;
(5)Unicode字符串操作符则使得Python字符串国际化,提高了对其他字符的兼容性。
6.1.1 切片操作
字符串的切片操作就是在字符串中按规则地提取一段字符串,达到简化字符串的目的。Python语言中切片操作主要有两种形式:
(1)s[i:j] 字符串s从索引i到索引j(不包含索引j)的切片;
(2)s[i:j:k] 字符串s从索引i到索引j(不包含索引j)的步长为k的切片;
下面分别讲解这两种形式的切片操作。
(1)s[i:j]形式 * 如果i或j是负的,则索引是相对于所述字符串的末尾,i,j会被len(s) + i,len(s) + j被取代的。请注意-0仍然是0。字符串s的从i到j的切片被定义为字符串s的从i到j-1的子串; * 如果i或者j大于len(s),那么i或者j的值将为len(s); * 如果i被省略或者被设置为None,那么i的值为0。如果j被省略或者被设置为None,那么j的值为len(s)。如果i大于或者等于j,这个切片为空字符串。
以下是s[i:j]形式切片操作的示例:
>>> | s = "ABCDEFGXYZ" | >>> | print s[:] | ABCDEFGXYZ | >>> | print s[:4] | ABCD | >>> | print s[5:] | FGXYZ | >>> | print s[0:3] | ABC | >>> | print s[0:100] | ABCDEFGXYZ |
(2) s[i:j:k]形式 * 如果i或j是负的,则索引是相对于所述字符串的末尾,i,j会被len(s) + i,len(s)+j被取代的。请注意-0仍然是0。字符串s的从i到j步长为k的切片被定义为字符串序列,其序列的索引为x = i + n * k(其中0 <=n < (j-i)/k)。也就是,索引为i,i + k,i + 2 * k,i + 3 * k,…的并且要小于索引j的所有索引组成的字符串。 * 如果i或者j大于len(s),则i,j的值被设置为len(s)。 * 如果i,j被省略或者设置为None时,当k大于0时,i为0,j为len(s);当k小于0时,i为 -1,j为-(len(s) +1)。注意,k不能为0,如果k被省略或者为None时,k的值设置为1。
以下是s[i:j:k]形式切片操作的示例:
>>> | s = "ABCDEFGXYZ" | >>> | print s[::] | ABCDEFGXYZ | >>> | print s[::2] | ACEGY | >>> | print s[0:6:3] | AD | >>> | print s[::-1] | ZYXGFEDCBA | >>> | print s[-1:-3:-1] | ZY | >>> | print s[9:6:-1] | ZYX | >>> | print s[-1:-10:-2] | ZXFDB |
【例6-1】输入两个字符串a,b,判断字符串a是否是b字符串的子串。
问题分析:遍历b字符串,利用切片操作判断b的子串是否与字符串a相同。 1 | a = raw_input("input string a:") #输入字符串a | 2 | b = raw_input("input string b:") #输入字符串b | 3 | la = len(a) #得到字符串a的长度 | 4 | lb = len(b) #得到字符串b的长度 | 5 | ok = False #设置标记变量 | 6 | for i in range(0, lb): #遍历字符串b所有子串 | 7 | if a == b[i: i + la]: #检查b的子串与字符串a是否相等 | 8 | ok = True #若想等则设置标记变量为True,并跳出循环 | 9 | break; | 10 | if ok: #根据标记变量的值,输出结果 | 11 | print "string a is the substring of string b" | 12 | else: | 13 | print "string a is not the substring of string b" |
运行结果:
input string a:ABC | input string b:XYZABC | string a is the substring of string b |
程序分析:例6-1输入两个字符串a,b,分别求出字符串a,b的长度la,lb,然后定义了一个标记变量ok,并循环枚举字符串b的所有位置,判断从每个位置开始的与字符串a长度相同的字符串b的子串是否与字符串a相同,若有一个相同,则说明找到了在字符串b中找到了包含字符串a的一个子串。
6.1.2 格式化操作
Python支持格式化字符串的输出,Python字符串格式化使用字符串格式化操作符(%),只适用于字符串类型,格式化操作的一般格式为:
format % values
其中,符号%称为格式化操作符,在%的左侧的format中放置一个字符串(格式化字符串),在%的右侧values中放置希望格式化的值。format的格式化字符串里面包含%的格式化字符串。格式化操作符的右操作数values可以是任何参数,Python支持两种格式的values输入参数。第一种是元组,这基本上是一种C语言的printf函数的转换参数集;第二种形式是字典形式。在这种形式里面,键是作为格式化字符串出现的,相对应的值作为参数在进行转化时提供格式字符串,格式字符串既可以跟print语句一起来向终端用户输出数据,又可以用来合并字符串形成新字符串:
>>> | format = "hello %s, %s enough!" | >>> | values = ('world' ,happy) | >>> | print format % values | hello world, happy enough! |
如果values中的值与模板串中的待替换部分类型不一致,Python将会自动进行类型转换,如果转换不能进行,则将报错:
>>> | format = "int : %d , str : %s , str : %s" | >>> | values = (1.0, ["in list", "i am list"],"i am str" ) | >>> | print format % values | int : 1 , str : ['in list', 'i am list'] , str : i am str | >>> | values = ("1", ["in list", "i am list"], "i am str" ) | >>> | values = ("1", ["in list", "i am list"], "i am str" ) | Traceback (most recent call last): | File "<stdin>", line 1, in <module> | TypeError: %d format: a number is required, not str | 格式化字符串的“%s”部分称为转换说明符(conversion specifier),它们标记了需要插入转换值的位置。其中“s”表示值会被格式化为字符串。其他转换说明符请参见表6.1。如果要在格式化字符串里面包括百分号%,那么必须使用%%,这样Python就不会将%误认为是转换说明符。
表6.1 字符串格式化符号 格式化字符 | 名 称 | %c | 转换成字符(ASCII码值,或者长度为一的字符串) | %r | 优先用repr()函数进行字符串转换 | %s | 优先用str()函数进行字符串转换 | %d | 转成有符号十进制 | %u | 转成无符号十进制 | %o | 转换成无符号八进制 | %x/%X | 转成无符号十六进制数 | %e/%E | 转成科学记数法 | %f/%F | 转成浮点型 | %% | 输出% | 简单的转换只需要写出转换类型,使用起来简单易写。格式化操作十六进制输出举例: >>> | "%x" % 100 | '64' | >>> | "%X" % 110 | '6E' |
格式化操作浮点型和科学记数法形式输出举例:
>>> | "%f" % 1000005 | '1000005.000000' | >>> | "%e" % 1000005 | '1.000005e+06' |
格式化操作整型和字符串输出举例:
>>> | "We are at %d%%" % 100 | 'We are at 100%' | >>> | s = "%s is %d years old" % ('Li',20) | >>> | print s | Li is 20 years old |
【例6-2】good数的定义为:若一个四位整数的每个数字的四次方的和与这个四位数相等,则称此数为good数。输出所有的good数,并且按照给定格式输出:若一个good数为abcd,则输出格式为:abcd=a^4+b^4+c^4+d^4。
问题分析:枚举所有的四位数,检验其是否是good数,若是good数则按照给定格式输出,输出使用格式化操作,使输出更加方便。 1 | for i in range(1000, 10000): | 2 | a = i / 1000 | 3 | b = i / 100 % 10 | 4 | c = i % 100 / 10 | 5 | d = i % 10 | 6 | if a ** 4 + b ** 4 + c ** 4 + d ** 4 == i: | 7 | print "%d=%d^4+%d^4+%d^4+%d^4"%(i, a, b, c, d) |
运行结果:
1634=1^4+6^4+3^4+4^4 | 8208=8^4+2^4+0^4+8^4 | 9474=9^4+4^4+7^4+4^4 |
程序分析:例6-2通过循环枚举每一个四位数,将四位数的每一位数字提取出来,然后
将每一位数字的四次方相加,并判断其值是否与当前的四位数字相等,若相等则按照格式输出。转换说明符还可以包括字段宽度,精度以及对齐等功能,请参见表6.2。
表6.2 格式化操作符辅助指令 符号 | 作用 | * | 定义宽度或者小数点精度 | - | 左对齐 | + | 在正数前面显示加号(+) | 0 | 显示的数字前面填充‘0’而不是默认的空格 | m.n | m是显示的最小总宽度,n是小数点后的位数 | 注意:字段宽度是转换后的值所保留的最小字符的个数,精度则是结果中应该包含的小数位数。格式化操作对于小数和字符串使用精度指令举例: >>> | "%.3f" % 123.12345 | '123.123' | >>> | "%.5s" % "hello world" | 'hello' | 格式化操作使用加号指令举例: >>> | "%+d" % 4 | '+4' | >>> | "%+d" % -4 | '-4' | 格式化操作使用最小宽度指令举例: >>> | from math import pi | >>> | '%-10.2f' % pi | '3.14 ' | >>> | '%10.2f' % pi | ' 3.14' | Python语言的格式化操作中,不仅可以使用元组,还可以使用字典格式化字符串,在每个转换说明符中的符号“%”后面,加上用圆括号括起来的键值,后面再跟上其他说明指令即可: >>> | student = {"name":"LiMing","age":18,"gender":"male"} | >>> | print "My name is %(name)s, age is %(age)d , gender is %(gender)s" % student | 'My name is LiMing, age is 18 , gender is male' | 除了增加字符串键之外,转换说明符还是像以前一样工作。当以这种的方式使用字典的时候,只要所有给出的键都能在字典中找到,就可以获得任意数量的转换说明符。这类字符串格式化在模板系统中经常用到。
6.1.3 字符串模板 字符串格式化操作符是Python处理这类问题的主要手段。然而它也不是完美的,其中的一个缺点是不够直观,即使是现在使用字典形式转换的程序员也会偶尔出现遗漏转换类型符号的错误,例如,用了%(lang)而不是正确的%(lang)s。为了保证字符串被正确的转换,程序员必须明确的记住转换类型参数。而字符串模板的优势是不用去记住所有的相关细节,就可以完成对原有参数的直接替换。 新式的字符串Template对象存在string模块中,新式的字符串Template对象使用美元符号“$”定义待替换的参数,使用substitute()方法和safe_substitute()方法进行参数替换。substitute()方法更为严谨,在substitute()缺少的情况下它会报一个KeyError的异常出来;而safe_substitute()在缺少key时,直接原封不动的把参数字符串显示出来。 使用字符串模板字典格式化字符串示例: >>> | from string import Template | >>> | s = Template('There are ${how_many} nodes in the ${tree}') | >>> | print s.substitute(how_many = 32, tree = "splay_tree") | There are 32 nodes in the splay_tree | 在substitute() 缺少参数的情况下,将会报错: >>> | from string import Template | >>> | s = Template('There are ${how_many} nodes in the ${tree}') | >>> | print s.substitute(how_many = 32) | File "<stdin>", line 1, in <module> | File "C:\Python27\lib\string.py", line 172, in substitute | val = mapping[named] | KeyError: 'tree' | 使用safe_substitute()代替substitute(),将不会引发异常报错: >>> | from string import Template | >>> | s = Template('There are ${how_many} nodes in the ${tree}') | | print s.safe_substitute(how_many = 30) | >>> | There are 30 nodes in the ${tree} |
6.1.4 原始字符串操作符 有时程序员不希望输出的字符串被转义字符干扰,比如在写文件路径时,不希望将“\”作为转义字符,在这种情况下,原始字符串操作符就很有必要了。原始字符串操作符出现的目的,是为了不让特殊字符进行转义,而全部按照字面意思使用。原始字符串操作符是r或者R,在紧靠普通字符串前,加上r或者是R,则字符串就是原始字符串了。原始字符串里,所有的字符都是直接按照字面的意思来使用,不会将字符转义。 原始字符串的这个特性让许多工作变得非常的方便,比如正则表达式的创建(详见7.2节),url的创建,文件路径的创建。这些字符串中会包含很多转义字符,特殊符号,如果不使用原始字符串,则字符串将会按照转义字符进行转换,与想要输出字面而不被转义字符串的本意不符。使用原始字符串操作符示例: >>> | '\n' #输出换行符号 | | '\n' | >>> | print '\n' #输出换行符号 | | | >>> | r'\n' #输出换行符号的原始字符串 | | '\\n' | >>> | print r'\n' #输出换行符号的原始字符串 | | \n | >>> | len('\n') #输出换行符号的长度 | | 1 | >>> | len(r'\n') #输出换行符号的原始字符串的长度 | | 2 |
6.1.5 Unicode字符串操作符 (1)Unicode字符串 Unicode支持世界上的多种语言。在Unicode之前,用的都是ASCII,ASCII码非常简单,每个字符都是以七位二进制数的方式存贮在计算机内,ASCII字符只能表示95个可打印字符,这对需要成千上万的字符的非欧洲语系的语言来说仍然太少,Unicode 通过使用一个或多个字节来表示一个字符的方法突破了ASCII的限制。 (2)在Python中使用Unicode字符串 Python处理Unicode字符串跟处理ASCII字符串基本一致。Python默认所有字面上的字符串都用ASCII编码,通过在字符串前面加一个'u'或'U'前缀的方式声明Unicode字符串,这个前缀告诉Python后面的字符串要编码成Unicode字符串。例如:当原始字符串操作符,和Unicode字符串操作符混合使用时,只需要将Unicode 操作符和前面讨论过的原始字符串操作符连接在一起就可以,但要注意Unicode操作符必须出现在原始字符串操作符前面。使用Unicode字符串操作符示例: >>> | '\nabc' #普通字符串 | | '\nabc' | >>> | u'\nabc' #Unicode字符串 | | u'\nabc' | >>> | u'刘备' #包含中文的Unicode字符串 | | u'\u5218\u5907' | >>> | U'卓越' #使用大写U的Unicode字符串 | | u'\u5353\u8d8a' | >>> | ur'Hello\nWorld!' #原始字符串操作符与Unicode字符串操作符混用 | | u'Hello\\nWorld!' |
6.2 正则表达式
当前计算机的主要工作是处理文本数据。例如:文字处理,网页表单填写,关键字搜索等等。由于不知道这些需要计算机编程处理文本或数据的具体内容,所以能把这些文本或数据以某种可被计算机识别和处理的模式表达出来是非常有用的。例如,需要要检索出邮箱中的所有主题带有“学习”二字的邮件,如果邮箱程序提供这种自动搜索的功能将大大简化用户的操作,否则需要用户查看每一封邮件,手动的进行检索,这将大大浪费用户的时间和精力。这就引出一个问题:如何通过编程使计算机具有在文本中检索某种模式的能力。
正则表达式为高级文本模式匹配,以及搜索-替代等功能提供了基础。正则表达式是一些由字符和特殊符号组成的字符串,它们描述了这些字符和字符的某种重复方式,因此能按某种模式匹配一个有相似特征的字符串的集合,因此能按某模式匹配一系列有相似特征的字符串。Python通过标准库的re模块支持正则表达式。
6.2.1 第一个正则表达式
Python语言中的re模块提供了对正则表达式的支持,re模块中常用的有正则表达式对象和匹配对象。正则表达式对象是存储模式串的,利用正则表达式对象实现进行模式匹配搜索等功能;匹配对象是用于存储进行模式匹配之后所得到的信息。下面先简要介绍几个常用函数:
(1)compile(pattern,flags=0) re模块下的函数,对模式pattern进行编译,flags是可选标志符,并返回一个正则表达式对象。 (2)match(string, flags=0)
正则表达式对象中的方法,尝试用正则表达式模式pattern在字符串string开始位置进行匹配,flags 是可选标志符,如果匹配成功,则返回一个匹配对象,否则返回None。
(3)search(string, flags=0)
正则表达式对象中的方法,在字符串string 中查找正则表达式模式pattern 的第一次出现,flags 是可选标志符,如果匹配成功,则返回一个匹配对象;否则返回None。
(4)findall(string,flags=0)
正则表达式对象中的方法,在字符串string 中查找正则表达式模式pattern 的所有(非重复)出现,返回一个匹配对象的列表;flag是可选标志符。
(5)group()
匹配对象中的方法,返回全部匹配对象。
下面使用re模块,展示几个简单的正则表达式的示例。判断字符串是否由“hello”开始。
>>> | import re | >>> | pattern = re.compile('hello') #得到匹配字符串'hello'的正则表达式对象 | >>> | res = pattern.match('hello world') #使用match方法进行匹配 | >>> | if res: | >>> | print res.group() #使用gruop方法得到匹配部分 | >>> | else : | >>> | print 'not match' | hello | 查找字符串是否包含“hello”示例: >>> | import re | >>> | pattern = re.compile('hello') #得到匹配字符串'hello'的正则表达式对象 | >>> | res = pattern.search('world hello') #使用search方法进行搜索 | >>> | if res: | >>> | print res.group() #使用gruop方法得到匹配部分 | >>> | else : | >>> | print 'not match' | hello |
注意:match是从待匹配字符串的开始位置与模式串进行匹配,而search函数是从待匹配字符串的任意位置与模式串进行匹配,并且只查找第一个与模式串匹配的部分。
查找字符串中包含“hello”的所有部分: >>> | import re | >>> | pattern = re.compile('hello') #得到匹配字符串'hello'的正则表达式对象 | >>> | res = pattern.findall('world hello world hello') #使用findall方法得到所有匹配部分 | >>> | print res | ['hello', 'hello'] |
请注意:findall返回的是与模式字符串匹配的所有部分,并返回一个包含所有部分的列表。若没有与模式字符串匹配的部分,则返回空列表。
6.2.2 正则表达式中的特殊字符
在6.2.1节中,仅仅介绍了只由普通字符构成的正则表达式,而在正则表达式的实际应用中,使用最广泛的是元字符,这是一种特殊字符,它们赋予了正则表达式强大的功能和灵活性。正则表达式中常用的字符见表6.3:
表6.3 常用的正则表达式字符 符号 | 说明 | 表达式举例 | 相匹配字符串 | 普通字符 | 匹配自身 | abc | abc | . | 匹配除换行符外的任意字符 | a.c | axc | \ | 转义字符,使后一个字符改变原来的意义 | a\.ca\\c | a.ca\c | […] | 匹配括号中出现的任意一个字符 | x[awb]y | xwy | […x-y…] | 匹配从字符x到字符y的任意一个字符 | x[a-d]yx[a-d5-9]y | xdyx6y | [^…] | 不匹配括号中出现的任意一个字符 | xy[^012b-f] | xy3xyg | re1| re2 | 匹配正则表达式re1,re2中的任意一个 | hello|world | world | ^ | 从字符串的起始位置开始匹配 | ^abc | abc | $ | 从字符串的结束位置开始匹配 | abc$ | abc | (…) | 被括起来的表达式将作为分组,每个分组有一个编号,第一个分组编号为1,从表达式左边开始每遇到一个分组的左括号‘(’编号加1,表达式中的|仅在该组中有效 | (abc){2}a(123|456)c | abcabca456c | (?P<name>…) | 被括起来的表达式将作为分组,同时除了原有编号外再指定一个额外的别名name | (?P<id>abc){2} | abcabc | \<number> | 引用编号为<number>分组 | ([0-6])xy\1\1 | 1xy11 | (?P=name) | 引用别名为<name>的分组匹配的字符串 | (?P<id>[0-6])xy(?P=id)(?P=id) | 3xy33 | * | 匹配前一个字符或分组零次或多次 | xy[a-c0-2]* | xyabc01xy | + | 匹配前一个字符或分组一次或多次 | xy[a-c0-2]+ | xyabc210xy0 | ? | 匹配前一个字符或分组零次或一次 | xy[a-c0-2]? | xybxy | {n} | 匹配前一个字符或分组恰好n次 | xy[a-c0-2]{2} | xya0 | {m, n} | 匹配前一个字符或分组m到n次,m和n可以省略:若m省略,则匹配0至n次;若n省略,则匹配m至无限次 | xy[a-c0-2]{2,4} | xyac2bxyacb | \d | 匹配任何数字,和[0-9]意义相同(\D与\d的意义相反,匹配任何不是数字的字符,与[^0-9]意义相同) | abc\dabc\D | abc1abcX | \w | 匹配任何数字字母字符,和[A-Za-z0-9]意义相同(\W与\w的意义相反,匹配任何数非字字母字符) | abc\w*abc\W* | abc01Azabc### | \s | 匹配任何空白字符,如:空格,\t,\r,\n,\f,\v等字符(\S与\s的意义相反,匹配任何数非空白字符) | abc\sxyzabc\Sxyz | abc xyzabclxyz | \A | 仅从字符串起始位置进行匹配 | \Aabc | abc | \Z | 仅从字符串结束位置进行匹配 | abc | abc\Z | \b | 匹配一个单词边界 | gg \bfrAm\b | gg frAm | \B | 匹配一个非单词边界 | gg\BfrAm | ggfrAm |
6.2.3 匹配任意一个单个字符 符号“.”匹配除换行符外的任意一个单个字符(Python 的正则表达式有一个编译标识 (DOTALL),该标识能去掉这一限制,使符号“.”在匹配时包括换行符)。无论是字母、数字、不包括“\n”的空白符、可打印的字符、还是非打印字符,或是一个符号,符号“.”都可以与之匹配。使用符号“.”匹配任意字符方法: >>> | import re | >>> | pattern = re.compile(r'ab.xy') | >>> | res = pattern.match(r'ab0xy') | >>> | if res: | >>> | print res.group() | ab0xy |
当匹配符号“.”本身时,既可以直接使用符号“.”或者转义符号“\.”,但是转义符号“\.”只能匹配符号“.”,如下示例:
>>> | import re | >>> | pattern = re.compile(r'ab\.xy.01.qq') | >>> | | >>> | res = pattern.match(r'ab.xy.01Sqq') | >>> | | >>> | if res: | >>> | print res.group() | ab.xy.01Sqq |
6.2.4 匹配多个字符串(|) 符号“|”,即键盘上的竖杠,表示一个或操作,它的意思是选择被符号“|”分隔的多个不同的正则表达式中的一个进行匹配。正则表达式模式有了这个符号,正则表达式的灵活性增强了,使得它可以匹配不止一个字符串: >>> | import re | >>> | pattern = re.compile(r'abc|xyz') | >>> | res = pattern.match(r'abc') | >>> | if res: | >>> | print res.group() | abc | >>> | res = pattern.match(r'xyz') | >>> | if res: | >>> | print res.group() | xyz |
本示例中,正则表达式“abc|xyz”匹配字符串“abc”或者匹配字符串“xyz”。符号“|”会将正则表达式整体分割为几个部分,因此若不希望对正则表达式整体分割,则需要使用子组与“|”联合使用,这一部分将在6.2.7节讲述。
6.2.5 创建字符类([]) 尽管符号“.”可用来匹配任意字符,但有时候可能需要匹配某些特殊的字符。因此,符号“[]”被发明出来。使用符号“[]”的正则表达式会匹配符号“[]”里的任何一个字符: >>> | import re | >>> | pattern = re.compile(r'[ab][xy][01]') | >>> | res = pattern.match(r'ax0') | >>> | if res: | >>> | print res.group() | ax0 |
正则表达式“[ab][xy][01]”匹配的是一个包含三个字符的字符串:第一个字符是 “a” 或 “b”,接下来是 “x”或 “y”,最后是“0” 或“1”,此正则表达式所匹配的所有字符串为:“ax0”, “ax1”, “ay0”, “ay1”, “bx0”, “bx1”, “by0”, “by1”。因为方括号功能是对方括号中的字符选择且只选择一个字符进行匹配,若只允许匹配 字符串“ax0”和字符串“bx1”,用方括号不能实现这一限定要求,此时需要限定更为严格的正则表达式,可以使用符号“|”:“[ab0|bx1] ”。
方括号除匹配单个字符外,还可以支持所指定的字符范围。方括号里一对符号中间的连字符“-”用来表示一个字符的范围,例如,A-Z, a-z, 或 0-9 分别代表大写字母、小写字母和十进制数字。示例如下: >>> | import re | >>> | pattern = re.compile(r'[a-z][0-9]') | >>> | res = pattern.match(r'a9') | >>> | if res: | >>> | print res.group() | a9 |
本示例中,正则表达式“[a-z][0-9]”匹配的是第一个字符是小写英文字母,第二个字符为数字的长度为2的字符串。另外,如果在左方括号后第一个字符是上箭头符号“^”,就表示不匹配指定字符集里的任意字符:
>>> | import re | >>> | pattern = re.compile(r'[^0-9][0-9][0-9]') | >>> | res = pattern.match(r'A09') | >>> | if res: | >>> | print res.group() | A09 |
本示例中,正则表达式“[^0-9][0-9][0-9]”匹配的是第一个字符是非数字,第二个字符为数字,第三个字符为数字的长度为3的字符串。
6.2.6 在字符串边界或单词边界进行匹配(^$\b\B)
(1) 在字符串的边界进行匹配
有时候在字符匹配时,希望从字符串的开头或结尾开始匹配正则表达式模式。若从字符串的开头开始匹配一个字符串,需要使用符号“^”或字符“\A”; 若从字符串的结尾开始匹配一个字符串,需要使用符号“$”或字符“\Z”。用这些符号的模式与在之前介绍讲的符号是不同的,之前的符号是用于匹配字符,而本小节所介绍的符号用于匹配位置。在7.1.1节中提到过 “match”方法和“search”方法之间的区别,“match” 方法是试图从整个字符串的开头进行匹配,而“search” 方法则可从一个字符串的任意位置开始匹配,因此为了体现对字符串边界位置的匹配,以下的例子主要使用了“search” 方法。 从字符串开头成功匹配: >>> | import re | >>> | pattern = re.compile(r'^abc') | >>> | res = pattern.search('abcxyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | abc |
正则表达式‘^abc’的含义是匹配任何以abc开头的的字符串,任何不是以abc开头的字符串则不能匹配:
>>> | import re | >>> | pattern = re.compile(r'^abc') | >>> | res = pattern. search('qqabcyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search | 另外,使用符号“^”时,一般只用于正则表达式的开头,用在中间位置是没有效果的: >>> | import re | >>> | pattern = re.compile(r'bb^abc') | >>> | res = pattern.search('bbabxyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search |
从字符串结尾开始匹配,与从字符串开头匹配相似:
>>> | import re | >>> | pattern = re.compile(r'a.c$') | >>> | res = pattern.search('aXcZZZaYc') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | aYc |
正则表达式“a.c$”的含义是匹配任何以最后一个字符是“c”,倒数第二个字符为任意字符,倒数第三个字符为“a”的任意字符串,在上面的例子中,可以明显看出,仅仅匹配了aYc,而没有匹配aXc,说明符号'$'的含义是以字符串结尾开始进行匹配,同时,任何不以“a.c”结尾的字符串不能匹配:
>>> | import re | >>> | pattern = re.compile(r'a.c$') | >>> | res = pattern.search('ccccc') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search | 与符号“^”类似,符号“$”只用于正则表达式的结尾,用在中间位置是没有效果的: >>> | import re | >>> | pattern = re.compile(r'a.c$xyz') | >>> | res = pattern.search('abcxyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search | 如需要在正则表达式用需要匹配符号“^”或符号“$”,则使用转义字符“\”进行转义: >>> | import re | >>> | pattern = re.compile(r'^abc\^xyz\$$') | >>> | res = pattern.search('abc^xyz$') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | abc^xyz$ |
符号“\A”与“^”意义相同,符号“\Z”与“$”意义相同,这两个符号的出现仅仅是为了兼容有些没有符号“^”或符号“$”的键盘。
(2) 单词边界进行匹配
符号“\b”或符号“\B”用来匹配单词边界。这里的单词是指被空格,换行符等字符分割的字符串,单词边界则是单词的左边界位置或者右边界位置。“\b”不匹配任何字符,仅仅匹配的是一个单词的边界位置:
>>> | import re | >>> | pattern = re.compile(r'xyz\b') | >>> | res = pattern.search(r'xyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | xyz |
由示例可以看出,正则表达式r“xyz\b”匹配的是字符串“xyz”右边界为单词边界的字符串,注意整个字符串的结尾或者开头也是单词边界,若待匹配字符串中的“xyz”右边界不是单词边界则不能匹配:
>>> | import re | >>> | pattern = re.compile(r'xyz\b') | >>> | res = pattern.search(r'xyzabc') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search | 在匹配单词时,一般需要使用两个符号“\b”: >>> | import re | >>> | pattern = re.compile(r'\bapple\b') | >>> | res = pattern.search(r'i like apple very much') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | apple | 若待匹配字符串中的“apple”不是单词则不能匹配: >>> | import re | >>> | pattern = re.compile(r'\bapple\b') | >>> | res = pattern.search(r'i_like_apple_very_much') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search | 符号“\B”与符号“\b”的意义相反,符号“\B”匹配的是非单词边界位置: >>> | import re | >>> | pattern = re.compile(r'\Bapp.e\B') | >>> | res = pattern.search(r'an apple or_appreciation') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | appre |
正则表达式“\Bapp.e\B”匹配的是在待匹配字符串中不是单词的位置的字符串,因此示例匹配输出为“appre”,而不是“apple”。
6.2.7 分组匹配(())
前面介绍的内容,可以匹配一个字符串和丢弃那些不匹配的字符串,但有时对匹配的数据本身更有兴趣。有时不仅想知道是否整个字符串匹配给定的正则表达式,还希望在匹配成功时取出某个特定的字符串或子字符串。要达到这个目的,只需要给正则表达式的两边加上一对圆括号,每一个被圆括号括起来的表达式称为子组,每一个子组有一个编号,从正则表达式左边开始,遇到第1个子组的左括号时,此子组的编号为1,以后每遇到一个子组的左括号,其编号为前一个子组的编号加1。
分组匹配的一个好处就是匹配的子串会被保存到一个子组中,便于以后使用。这些子组可以在同一次匹配或搜索中被重复调用,或被提取出来做进一步处理。在Python的re模块中,group方法不仅能返回被正则表达式匹配的整个字符串,也可以返回每个被子组匹配的子字符串,方式就是给group方法传入一个参数n,表示得到编号为n的子组。一个简单的分组匹配示例如下: >>> | import re | >>> | pattern = re.compile(r'(aa)(bbb)') | >>> | res = pattern.match(r'aabbbxxx') | >>> | if res: | >>> | print res.group() | >>> | print res.group(1) | >>> | print res.group(2) | >>> | else: | >>> | print 'no search' | aabbb | aa | bbb |
正则表达式“(aa)(bbb)”匹配的是包含字符串“aabbb”的字符串,同时把字符串“aa”作为一个分组,字符串“bbb”作为一个分组。子组也可以嵌套:
>>> | import re | >>> | pattern = re.compile(r'(aa(bbb))') | >>> | res = pattern.match(r'aabbbxxx') | >>> | if res: | >>> | print res.group() | >>> | print res.group(1) | >>> | print res.group(2) | >>> | else: | >>> | print 'no search' | aabbb | aabbb | bbb | 正则表达式“(aa(bbb))”匹配的是包含字符串“aabbb”的字符串,同时把字符串“aabbb”作 为一个分组,字符串“bbb”作为一个分组。有时也需要引用子组进行重复匹配,引用子组可以使得正则表达式变得更加简洁,避免了重复字符串在正则表达式中的多次出现。引用子组使用符号“\<number>”,即使用符号“\”加上子组的编号即可: >>> | import re | >>> | pattern = re.compile(r'(a.)\1') | >>> | res = pattern.match(r'aXaXzzz') | >>> | if res: | >>> | print res.group() | >>> | print res.group(1) | >>> | else: | >>> | print 'no search' | aXaX | aX | aabbb |
正则表达式“(a.)\1”匹配的是包含以字符“a”开始,之后是任意字符,之后又是字符“a”,最后是与之前任意字符相同的字符的字符串,注意与引用子组的相匹配的字符串必须与被引用子组相匹配的字符串完全一致,若不一致则不能匹配:
>>> | import re | >>> | pattern = re.compile(r'(a.)\1') | >>> | res = pattern.match(r'aXaYzzz') | >>> | if res: | >>> | print res.group() | >>> | print res.group(1) | >>> | else: | >>> | print 'no search' | no search |
为了方便子组的使用,还可以为子组起别名。起别名的方式为:“(?P<name>…)”,引用别名的方式为“(?P=name)”,并且,通过别名引用子组和通过编号引用子组的方式是可以混合使用的。在re模块中,匹配对象中的gruopdict()返回所有以子组别名为键值,以匹配的字符串为值的字典。子组别名与别名引用示例:
>>> | import re | >>> | pattern = re.compile(r'(?P<id>ab)xy(?P=id)\1') | >>> | res = pattern.match(r'abxyababmn') | >>> | if res: | >>> | print res.groupdict() | >>> | print res.groupdict()["id"] | >>> | print res.group() | >>> | print res.group(1) | >>> | else: | >>> | print 'no search' | {'id': 'ab'} | ab | abxyabab | ab |
正则表达式“(?P<id>ab)xy(?P=id)\1”匹配的是包含字符串“abxyabab”的字符串,并且正则表达式“(?P<id>ab)xy(?P=id)\1”,定义了子组“(ab)”的别名为“id”,其编号为1,之后通过别名引用的方式引用了子组“(ab)”,然后又通过编号引用的方式引用了子组“(ab)”。
当需要匹配括号时,使用符号“\”进行转义: >>> | import re | >>> | pattern = re.compile(r'\(ab\)') | >>> | res = pattern.match(r'(ab)zz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | (ab) |
正则表达式“\(ab\)”匹配的是包含字符串“(ab)”的字符串。
另外,括号匹配也可以限制符号“|”的作用范围,因此如果不想使符号“|”的作用在整个字符串中,可以将符号“|”包含在一个分组内: >>> | import re | >>> | pattern = re.compile(r"(ab|cd)y*") | >>> | res = pattern.match("abyyyyy") | >>> | if res: | >>> | print res.group() | >>> | else : | >>> | print "no match" | abyyyyy |
正则表达式“(ab|cd)y*”匹配的是以 “ab”或者 “cd”开始的,之后为任意个“y”的字符串,因此abyyyyy能够匹配,若不使用括号分组,则正则表达式“ab|cdy*”或者匹配字符串“ab”,或者匹配以“cd”开始,之后任意个“y”的字符串,这与原意不符。
6.2.8 重复匹配(*, +, ? ,{})
本小节介绍常用的正则表达式符号,即符号“*”,“+”,“?”和“{}”。这些符号针对符号前一个字符或者前一个子组进行可以进行指定次数的重复匹配。下面将依次介绍每个符号。
符号“*”用于匹配字符串模式出现零次或零次以上的情况: >>> | import re | >>> | pattern = re.compile(r'a*b*(xy)*') | >>> | res = pattern.match(r'aaaxyxy') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | aaaxyxy | 正则表达式“a*b*(xy)*”匹配的是包含任意多个“a”,之后是任意多个“b”,最后是任意个“xy”的字符串。因此“aaaxyxy”符合条件,可以匹配。
符号“+”用于匹配字符串模式出现零次以上的情况:
>>> | import re | >>> | pattern = re.compile(r'a+mn(xy)+') | >>> | res = pattern.match(r'aaamnxyxy') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | aaamnxyxy |
正则表达式“a+mn(xy)+”匹配的是包含至少一个“a”,之后是一个“mn”,最后是至少一个“xy”的字符串。因此“aaamnxyxy”符合条件,可以匹配。
符号“?”用于匹配字符串模式出现零次的情况: >>> | import re | >>> | pattern = re.compile(r'a.c$') | >>> | res = pattern.search('ccccc') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | no search |
正则表达式“abc?d?(xyz)?”匹配的是包含开始是“ab”,之后是零个或一个“c”, 之后是零个或一个“d”,最后是零个或一个“xyz”的字符串。因此“abdxyz”符合条件,可以匹配。
符号“{}”用于匹配字符串模式指定次数范围的情况,符号“{}”有两种情况,第一种情况恰好匹配n次,其形式为:“{n}”;第二种情况,匹配m到n次,其形式:“{m, n}”,其中m和n可以省略:若m省略,则匹配0至n次;若n省略,则匹配m至无限次。使用符号“{}”进行指定重复匹配: >>> | import re | >>> | pattern = re.compile(r'a{4}b{,3}c{2,}(xyz){2,4}') | >>> | res = pattern.match(r'aaaabccccxyzxyzxyz') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | aaaabccccxyzxyzxyz |
正则表达式“a{4}b{,3}c{2,}(xyz){2,4}”匹配的是包含开始恰好是四个“a”,之后是零个到三个“b”,之后是两个到无限个“c”,最后是两个到四个“xyz”的字符串。因此“aaaabccccxyzxyzxyz”符合条件,可以匹配。
以上介绍的用于重复匹配的符号,都处于贪婪模式。所谓贪婪模式是指若能匹配则尽量匹配,即使已经满足条件,若能匹配仍继续匹配。与贪婪模式相对,非贪婪模式是指当已经满足匹配条件,则不再继续匹配。对用于重复匹配的符号“*”,“+”,“?”,“{}”,其匹配的方式都是贪婪的,其非贪婪的版本为:“*?”,“+?”,“??”,“{}?”。 使用符号“*?”进行非贪婪匹配: >>> | import re | >>> | pattern = re.compile(r'abx*?') | >>> | res = pattern.match(r'abxxxx') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | ab | 正则表达式“abx*?”匹配的是包含字符串“ab”的字符串。对待匹配字符串“abxxxx”,由于 符号“*?”是非贪婪匹配符号,因此只需匹配“x”零次即满足匹配条件。 使用符号“+?”进行非贪婪匹配: >>> | import re | >>> | pattern = re.compile(r'abx+?') | >>> | res = pattern.match(r'abxxxx') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | abx | 正则表达式“abx+?”匹配的是包含字符串“abx”的字符串。对于待匹配字符串“abxxxx”,由于符号“+?”是非贪婪匹配符号,因此只需匹配“x”一次即满足匹配条件。 使用符号“??”进行非贪婪匹配: >>> | import re | >>> | pattern = re.compile(r'abx??') | >>> | res = pattern.match(r'abxxxx') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | ab | 正则表达式“abx??”匹配的是包含字符串“ab”的字符串。对于待匹配字符串“abxxxx”,由于符号“??”是非贪婪匹配符号,因此只需匹配“x”零次即满足匹配条件。 使用符号“{}?”进行非贪婪匹配: >>> | import re | >>> | pattern = re.compile(r'abx{2,4}?') | >>> | res = pattern.match(r'abxxxx') | >>> | if res: | >>> | print res.group() | >>> | else: | >>> | print 'no search' | abxx | 正则表达式“abx{2,4}?”匹配的是包含字符串“abxx”的字符串。对于待匹配字符串“abxxxx”,由于符号“{2,4}?”是非贪婪匹配符号,因此只需匹配“x”两次即满足匹配条件。
6.2.9 替换与分割
本节介绍使用正则表达式进行替换和分割操作。
(1)替换
替换是指对正则表达式匹配的部分全部替换成给定的字符串。Python中的sub()和subn()方法用于完成搜索和替换功能。用来替换的部分通常是一个字符串,但也可能是一个函数,该函数需要返回一个用来替换的字符串。sub()方法返回替换后的整体字符串,subn则返回一个二元元组,第一个表示替换后的整体字符串,第二个表示替换部分的数目。
使用sub()方法与subn()方法进行替换: >>> | import re | >>> | pattern = re.compile(r':') | >>> | res_sub = pattern.sub(",", "abc:xyz:mnq") | >>> | res_subn = pattern.subn(",", "abc:xyz:mnq") | >>> | | >>> | print res_sub | >>> | print res_subn | abc,xyz,mnq | ('abc,xyz,mnq', 2) |
本例中,使用符号“,”替换待匹配字符串中的所有的符号“:”,sub()方法输出替换后的字符串“abc,xyz,mnq”,subn()方法输出替换后的字符串“abc,xyz,mnq”与替换部分的数目为2。另外如果正则表达式没有匹配待匹配字符串中的任何部分,则sub方法返回原来的待匹配字符串。
(2) 分割
分割是指将待分割字符串被正则表达式匹配的部分进行分割。re模块中的split()方法用于分割,返回分割后的各个字符串部分的列表。
使用split()方法进行分割: >>> | import re | >>> | pattern = re.compile(r'AND') | >>> | res = pattern.split(r"ANDIANDyouANDtheyAND") | >>> | print res | ['', 'I', 'you', 'they', ''] |
本例中字符串“abc:xyz:mnq”被字符串“:”分割以后,返回列表['abc', 'xyz', 'mnq']。另外,如果正则表达式匹配了待分割字符串的起始位置或者结束位置,则将返回空字符串。
6.2.10 正则表达式举例
前几节对正则表达式的各种特殊符号和Python语言中re模块提供的使用正则表达式进行了系统的介绍,下面介绍几个综合的例子来巩固消化。
【例6-3】验证一个字符串是否是中国身份证号码,为简化问题一个身份证号码定义为15位的数字或者是18位的数字。
问题分析:构造身份证号码的正则表达式:“^[0-9]{15}|[0-9]{18}$”
1 | import re | 2 | pattern = re.compile(r"^[0-9]{15}|[0-9]{18}$") #构造身份证号码的正则表达式 | 3 | s = raw_input("input a ID card:") #输入一个身份证号码 | 4 | res = pattern.match(s) #进行匹配 | 5 | if res: | 6 | print "it is an ID card" | 7 | else: | 8 | print "it is not an ID card" |
运行结果:
input a ID card:233289199901017723 | it is an ID card |
程序分析:例6-3首先构造了匹配身份证的正则表达式,输入一个字符串,然后使用正则表达式去判断。
【例6-4】输出一个字符串文本,找出这个文本中所有的以大写字母开头的其他字母是小写字母并且长度不大于6的单词。
问题分析:构造要求单词的正则表达式:“\b[A-Z][a-z]{,5}\b”
1 | import re | 2 | pattern = re.compile(r"\b[A-Z][a-z]{,5}\b") #构造要求单词的正则表达式 | 3 | s = raw_input("input a text:") #输入一个文本字符串 | 4 | res = pattern.findall(s) #查找全部满足要求字符串 | 5 | print res |
运行结果:
input a text: I like You. However, I Do not Follow you. | ['I', 'You', 'I', 'Do', 'Follow'] |
程序分析:例6-4首先构造了匹配题意要求的字符串的正则表达式,输入一个字符串,然后使用正则表达式去找到所有满足要求的字符串。
【例6-5】输出一个字符串文本,把这个文本中的所有大写字母转换为小写字母,并输出转换之后的文本。
问题分析:本题目可以使用字符串方法lower,很方便的完成题目问题,但为了练习,可以使用正则表达式匹配大写字母,然后使用sub(),进行替换,之前介绍过sub()函数的第一个参数可以是一个字符串,也可以是返回字符串的函数。
1 | import re | 2 | pattern = re.compile(r"[A-Z]") #构造匹配大写字母的正则表达式 | 3 | def tran(mobj): #构造转换函数 | 4 | a = mobj.group() | 5 | return a.lower() | 6 | s = raw_input("input a text:") #输入一个文本字符串 | 7 | res = pattern.sub(tran, s) #使用转换函数进行转换 | 8 | print res |
运行结果:
input a text: I like You. However, I Do not Follow you. | i like you. however, i do not follow you. |
程序分析:例6-5首先构造了匹配大写字母的正则表达式,然后定义了一个转换函数,之后期待输入一个字符串,然后使用正则表达式去找到所有大写字母并将其替换为小写字母。
本章小结
本章第一部分学习了Python字符串的一些重要操作,掌握了Python语言中Python字符串的切片操作,使得能够简单快捷的提取字符串中的一段内容。掌握格式化操作方便的输出被格式化过的字符串,进一步地,理解了字符串模板,更加简化了格式化操作,更容易进行产生相同格式的字符串。了解原始字符串操作符,提供了所见即所得的字符,更加方便地得到非转义字符串。了解Unicode字符串操作符,则使得Python字符串国际化,更好的兼容其他字符集。
本章第二部分学习了Python语言中的使用正则表达式。掌握了Python语言中re模块一些与正则表达式相关的重要的方法:compile(),match(),search(),findall(),group()等,读者应该熟练的使用本章介绍的几个方法。读者应该熟悉正则表达式的特殊字符的各种用法,同时应该掌握各种字符的组合使用,这能使正则表达式的功能更加强大。
习题
* 填空题 1. Python中一个字符串s="abcxyz",则切片操作s[-2:]的值为____________。 2. 字符串格式化操作"I have %d %s apples"%(3,"red")的值为____________。 3. 请写出至少两种匹配只包含字符"a"且其字符数目至少是一个的字符串的正则表达式____________。 4. 正则表达式"(x(cc)y)(zz)"的子组数目为____________。 5. re模块中用于正则表达式字符串分割的方法为:____________。
二、选择题
1. 下面程序的运行后,a的值是( )。 s = "abcdefg" a = s[len(s) - 1: 2 : -2]
A. "abcdefg" B. "ge" C. "eg" D. "abc" 2. 下列正则表达式中,匹配的是仅仅包含字符串"xy"且其数目为至少一个的是()。
A. "xy(xy)*" B. "xy*" C. "(xy)*" D. "xy+" 3. 正则表达式中符号"\b"的作用是( )。
A. 匹配空格
B. 匹配单词边界位置
C. 匹配字符串边界位置
D. 匹配字符串换行符 4. 下面字符串的书写中错误的是( )。
A. ru"abab"
B. u"abab"
C. r"abab"
D. ur"abab" 5. 这段程序运行后,输出的结果是( )。 import re pattern = re.compile(r"\blike \w{10,}\b") a = pattern.findall("I like vegetables and not like junkfood") print a
A. ['like junkfood']
B. ['']
C. 'like junkfood'
D. ['like vegetables']
三、编程题
1. 输入一个字符串,输出其中的所有小写字母。 2. 输入两个字符串a,字符串b,输出字符串a在字符串b中出现的次数。 3. 输入一个字符串,判断其是不是非正整数。 4. 输入一个密码,判断其是否是安全密码,安全密码定义:
1:密码长度大于等于8, 且不超过16;
2:密码中的字符应该包括至少一个大写英文字母,至少一个小写英文字母,至少一个数字,至少一个特殊符号,特殊字符包括(~,!,@,#,$,%,^)。
5. 输入一个字符串,输出这个字符串的最大回文子串的长度。回文串是指这个字符串与这个字符串逆序以后的字符串相同,例如:字符串“abcdcba”就是回文串。 6. 输入一个字符串,判断其是否是一个IP地址。IP地址由4个大于等于0小于等于255的数字构成,并且数字之间由符号“.”分割,例如:“12.21.33.1”, “0.0.0.0”, “255.255.255.255”, “1.1.1.1”等都是IP地址。

第7章 函数
通过前面章节中读者已经对python语言的基本语句及语法有了深入的了解。在python语言程序开发过程中,除了要考虑程序的可执行性外,还需要考虑代码的重用性、简洁性和易用性。因此,在设计python语言程序时需要将程序按照功能进行划分,而不是全部写在一个主函数中。函数式编程思想就是将程序中完成某种功能的语句块从主函数中抽离出来,使用其他函数进行保存,通过调用函数的方式完成执行该语句块的目的。这样就使得程序代码简洁,提高了代码的可重用性,易于后期维护与理解。本章对函数定义、变量的作用域、递归等内容进行全面讲解,希望读者能够从中体会到利用函数进行编程的好处。
【体系结构】

【本章重点】
(1)了解函数的概念,对函数有宏观的认识。
(2)掌握函数的声明和定义的一般形式。
(3)掌握函数的参数和返回值。
(4)掌握函数的嵌套调用和递归调用和修饰器。
(5)理解变量的作用域和存储方式。
(6)掌握使用函数的程序编程。
【案例引入】五子棋游戏——封装及重构
封装就是将分散抽象的代码有机的结合在一起,形成一个整体,便于程序调用。重构就是通过改善代码的逻辑及实现,使代码性能和可读性更强。五子棋游戏中,每个功能如果都是独立的语句来完成,显得代码特别冗余。通过本章的学习,读者将学会使用函数结构定义解决画五子棋棋盘的问题。此外,五子棋游戏中在一方玩家率先完成五颗棋子相连时,游戏提示胜利信息并结束。通过本章的学习,读者也将学会如果调用函数来判断游戏玩家是否胜利。这样通过函数的模块的编程,将使程序的调用更加方便。
7.1 抽象与函数
抽象(abstraction)是一种简化复杂的实际问题的方法,它为实际问题找到准确的定义,并且可以恰当的解释问题。其中可能会忽略一些无关的细节,以便更充分地注意与当前问题有关的方面。简单的说,就是将一种复杂的活动、繁琐的流程简化为一个具体的计算机能够理解的操作。例如苹果、香蕉、生梨、葡萄、桃子等,它们共同的特性就是水果。得出水果概念的过程,就是一个抽象的过程。
模块化设计是将一个复杂的大型程序分解为多个功能明确、相互独立的模块,模块各自完成相应的功能,主程序规定各个模块与其它模块的接口。程序执行时,主程序通过调用相关模块完成特定的子功能,从而使主程序变成只负责调度和协调的程序。另外,模块还可以进一步划分成若干子模块,各级模块可以按照需求进行更低层次的模块划分。当然,模块划分的过程中,需要制定好各级模块和其上级模块之间的接口,这样编写出来的程序就是模块化设计的程序,如图7.1所示。

图7.1 模块图
所谓函数是可重用的代码段,其被允许起名字,然后通过调用该名字的方式,调用对应代码段(就是调用函数)。类似于其它语言,Python语言中采用函数实现各个功能模块,且每个函数具有特定的功能。具体来说,函数具有以下特征:
(1)Python语言函数是按照一定规则书写的能完成一定功能的代码段。
(2)一个Python语言函数应该能够完成一个简单、明确和单一的功能,函数名应该能够明确表达函数的功能。这样有助于提高代码的可读性和可重用性。如果不能够给函数起一个明确、简练的函数名,那说明该函数的功能不是单一的,函数实现了多种不同的功能,这个函数就可以被拆分为若干个小函数。
(3)Python语言程序由若干个文件组成,一个文件作为一个功能模块,完成一定功能。这样不仅便于程序的开发和管理,还能提高程序的重用性和开发效率。
(4)Python语言函数之间是平行的,相互独立。函数是独立的个体。函数虽然是平行的,但是函数之间可以相互调用。
(5)如果函数内没有return语句,就会自动返回None对象。
(6)Python函数是引用调用,因此在函数内,对参数的改变会影响到原始对象。不过事实上只有可变对象会有影响,对不可变对象来说,它的行为类似按值调用。
7.2 创建函数
定义函数的方法:使用def关键字,然后接函数名,在其后的小括号中填上需要的参数,函数参数是可选的(7-1中的arguments),该行要以冒号结尾。接下来是一块代码,它们就是函数体。
【例7-1】如何定义函数。 1 | ''' | 2 | 例 7-1 如何定义函数 | 3 | 函数定义的样式 | 4 | ''' | 5 | def Function_name([arguments]): | 6 | '''optional documentation string''' | 7 | Function_suite | 读者可以看到上面函数定义中,第5行是函数头,使用def关键字定义Function_name的函数,小括号中就是要传入的参数。以冒号结尾说明函数体已经开始。函数内容需要进行缩进处理。然后开始编写函数体如第6、7行。
那么如何调用函数呢?Python中的调用函数方法与其它高级语言一致,为函数名加上一对小括号(括号内是函数参数)。
【例7-2】库函数和用户自定义函数举例。
问题分析:编写Helloworld函数,其功能是打印字符串“Hello world”。 1 | ''' | 2 | 例 7-2 库函数和用户自定函数示例 | 3 | 使用用户自定义函数输出Hello world! | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Helloworld.py | 7 | | 8 | #HelloWorld:用户自定义函数,功能是输出Hello world!字符串 | 9 | def sayHello (): | 10 | #调用内建函数print,输出Hello world | 11 | print 'Hello World!' | 12 | | 13 | sayHello() # 调用函数 |
运行结果:
Hello World! | 程序分析:程序中的第9~11行是用户自定义函数,函数名为HelloWorld。通过调用用户自定义函数sayHello,从而实现调用内建函数print输出字符串“Hello World!”的目的,这个函数不使用任何参数,因此在圆括号中没有声明任何变量。参数对于函数来说,就是给函数输入变量,来方便传递不同的值给函数,等到用户需要的结果。因此也可以看出Print函数是具有输出功能的函数。
7.3 函数参数
7.3.1 函数参数类型
所谓函数参数,就是在函数声明后的小括号中的变量。当使用函数时,在小括号中提供的值是实际参数,或者称为参数。使用函数形参的简单例子如下。
【例7-3】函数形参举例。
问题分析:定义PrintMin函数,输出两个输入参数para_a和para_b的较小值。
1 | ''' | 2 | 例 7-3 函数形参示例 | 3 | 使用用户自定义函数输出两个数的较小值! | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: PrintMin.py | 7 | | 8 | # PrintMin:用户自定义函数,功能是输出两个数的较小值! | 9 | def PrintMin (para_a,para_b): | 10 | #判断参数大小 | 11 | if para_a < para_b: | 12 | print para_a,’ is minimux’ | 13 | else: | 14 | print para_b, ' is minimux’ | 15 | #调用内建函数print,输出较小值 | 16 | PrintMin(2,7)#直接参数调用 | 17 | a=6 | 18 | b=8 | 19 | PrintMin(a,b) # 以变量的形式提供参数 |
运行结果:
2 is minimux | 6 is minimux | 程序分析:首先定义了一个PrintMin的函数,它需要两个形参,分别是para_a和para_b。使用if_else语句找出两个数之间的较小值,并将其打印出来。在函数第16行,直接将数,即实参传递给函数。在函数第19行使用变量调用函数。PrintMin(a,b)使实参a的值赋给形参para_a,实参b的值赋给形参para_b。完成两次函数调用。
在python中,对于形参还有很多不同的参数类型:
* 必备参数 * 命名参数 * 缺省参数 * 不定长参数
(1)必备参数
必备参数需要按照参数声明的顺序如实传入函数中,如果未传入参数,或者参数数量不对应,就会出现语法错误。主要针对那些函数设计时就已经清楚了函数的各个参数及功能已经确定了。这样就可以直接定义函数体。在实际开发中,可能使用的很少,因为很少有功能参数完全确定的情况。
【例7-4】函数必备参数举例。
问题分析:定义PrintMin函数,输出两个输入参数para_a和para_b的较小值。 1 | ''' | 2 | 例 7-4 函数必备参数示例 | 3 | 使用用户自定义函数说明函数的必备参数 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: NecPara.py | 7 | | 8 | # PrintMin:用户自定义函数,功能是输出两个数的较小值! | 9 | def PrintMin (para_a,para_b): | 10 | #判断参数大小 | 11 | if para_a < para_b: | 12 | print para_a,’ is minimux’ | 13 | else: | 14 | print para_b, ' is minimux’ | 15 | #调用内建函数print,输出较小值 | 16 | PrintMin(2)#传入参数数量不对应 | 17 | PrintMin() #未传入参数 |
运行结果:
Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: PrintMin() takes exactly 2 arguments (1 given) | Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: PrintMin() takes exactly 2 arguments (0 given) |
程序分析:参数传入不同或者未传入参数,会直接报错。
(2)命名参数 基本用法和必备参数类似,如果参数较多,无法具体对应的写出参数的顺序,可以使用命名参数的形式,按名称指定参数。避免了参数先后顺序出错,影响函数正确性的情况。
【例7-5】函数命名参数举例。
问题分析:定义PrintMin函数,输出两个输入参数para_a和para_b的较小值。 1 | ''' | 2 | 例 7-5 函数命名参数示例 | 3 | 使用用户自定义函数说明函数的命名参数 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: NamePara.py | 7 | | 8 | # PrintMin:用户自定义函数,功能是输出两个数的较小值! | 9 | def PrintMin (para_a,para_b): | 10 | #判断参数大小 | 11 | if para_a < para_b: | 12 | print para_a,’ is minimux’ | 13 | else: | 14 | print para_b, ' is minimux’ | 15 | #调用内建函数print,输出较小值 | 16 | PrintMin(para_b=2,para_a=3)#使用名称指定参数 |
运行结果:
2 is minimux |
程序分析:可以通过命名参数的形式,进行参数传递。
(3)缺省参数 调用函数时,缺省参数的值如果没有传入,就会使用默认值。这种参数适用于那种在函数声明中某些可以确定值的参数,如果不添加该参数直接使用,会调用缺省参数,也能正确的使用函数。并且这种缺省参数,在某些函数具有多个参数的使用时,可以通过不输入缺省参数来达到控制函数功能的效果,比如定义参数缺省值为None,通过判断参数缺省值达到控制程序效果。
【例7-6】函数缺省参数举例。
问题分析:定义PrintMin函数,输出两个输入参数para_a和para_b的较小值。 1 | ''' | 2 | 例 7-6 函数缺省参数示例 | 3 | 使用用户自定义函数说明函数的缺省参数 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: NamePara.py | 7 | | 8 | # PrintMin:用户自定义函数,功能是输出两个数的较小值! | 9 | def PrintMin (para_a,para_b=2): | 10 | #判断参数大小 | 11 | if para_a < para_b: | 12 | print para_a,’ is minimux’ | 13 | else: | 14 | print para_b, ' is minimux’ | 15 | #调用内建函数print,输出较小值 | 16 | PrintMin(3,1)#未使用缺省参数 | 17 | PrintMin(3)#使用缺省参数 |
运行结果:
1 is minimux 2 is minimux |
程序分析:由于有缺省参数,在未对para_b赋值时,将使用初始缺省值2,所以输出结果如上。
(4)不定长参数 有时一个函数可能需要比声明时更多的参数。所以这时要使用不定长参数。使用方法是在变量名前加上星号(*),这样它就会存放所有未命名的变量参数。当然也可以不多传参数。这种参数主要适用于无法确定函数参数个数情况。此时使用列表的形式,进行参数的附加,达到不必修改函数的参数定义,就可以传入多个参数的目的。
【例7-7】函数不定长参数举例。
问题分析:定义PrintPara函数,打印输出所有传入的参数。 1 | ''' | 2 | 例 7-7 函数不定长参数示例 | 3 | 使用用户自定义函数说明函数的不定长数 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: UnPara.py | 7 | | 8 | # UnPara:用户自定义函数,打印传入参数 | 9 | def PrintPara (para_a,*para_b): | 10 | #打印传入参数 | 11 | print para_a | 12 | for temp in para_b:#遍历参数para_b获取各个数值 | 13 | print temp | 14 | | 15 | #调用内建函数PrintPara | 16 | PrintPara (2);#使用单个参数 | 17 | PrintPara (4,5,6);#使用多个参数 |
运行结果:
2 4 5 6 |
程序分析:在函数参数声明时使用*para_b将接受之后的多个参数,来实现不定长参数调用。
7.3.2 修改参数
经过上面的讲解,读者对参数有了一定的了解,那么这些参数值能修改吗?首先需要明确参数也是一种变量。在函数内对参数进行赋值,由于有参数作用域问题(之后章节会详细介绍)将不会改变外部变量的值,请看下面的例子。
【例7-8】函数改变参数举例。 1 | ''' | 2 | 例 7-8 函数改变参数示例 | 3 | 使用用户自定义函数说明函数改变参数的影响 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: ChangeNumber.py | 7 | | 8 | # ChangeNumber:用户自定义函数,尝试改变参数 | 9 | def ChangeNumber (number): | 10 | #修改参数值 | 11 | number=1 | 12 | print “number:”,number | 13 | #调用函数 | 14 | number=2 | 15 | ChangeNumber(number) | 16 | print “input_number:”number |
运行结果:
number: 1 input_number: 2 |
程序分析:在函数ChangeNumber内修改了参数number,但是它没有影响到test_number变量。这是因为参数存储在局部作用域内,函数内与函数外具有相同名称的变量没有任何关系,是两个不同的变量。
在python中数字、字符串、元组是不可变的,就是只能进行赋值,不会受到参数值修改操作的影响。但是如果是可变的数据结构(如:列表、字典)就可以参数修改操作,修改对象的原始值。请看下面的例子。
【例7-9】函数改变参数举例。
1 | ''' | 2 | 例 7-9 函数改变列表参数示例 | 3 | 使用用户自定义函数说明函数改变列表参数的影响 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: ChangeNumberList.py | 7 | | 8 | # ChangeNumberList:用户自定义函数,尝试改变列表参数 | 9 | def ChangeNumberList (number): | 10 | #修改参数值 | 11 | number[0]=1 | 12 | print “number:”,number | 13 | #调用函数 | 14 | number=[2,2] | 15 | ChangeNumberList (number) | 16 | print “input_number:”number |
运行结果:
number: [1,2] input_number:[1,2] |
程序分析:本例中变量number所绑定的列表的确改变了。其实当两个变量同时引用一个列表的时候,它们的确引用的是同一个列表,而不是副本。
这种现象其实就是参数的值传递和引用传递。所谓值传递就是当方法被调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参数值的改变不影响实际参数的值。
所谓引用传递,即方法调用时,实际参数是对象(或数组),这时实际参数与形式参数指向同一个地址,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,这个结果在方法结束后被保留了下来,所以方法执行中形式参数的改变将会影响实际参数。
如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值(相当于通过“传引用”来传递对象)。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象(相当于通过“传值”来传递对象),如图7.2所示。

图7.2 值传递与引用传递对比
7.4 变量的作用域
一个程序的所有的变量并不是在哪个位置都可以访问,访问权限决定于这个变量是在哪里赋值。变量的作用域决定了在哪一部分程序可以访问哪些特定变量。两种最基本的变量作用域如下:
* 全局变量:定义在函数外的拥有全局作用域,全局变量可以在整个程序范围内,被自由访问。 * 局部变量:定义在函数内部变量,拥有局部作用域(上一节提到的作用域问题),因此局部变量只能在其被声明的函数内部调用。
【例7-10】函数作用域举例。
问题分析:定义Add函数,完成参数number_a和number_b的求和运算。 1 | ''' | 2 | 例 7-10 函数作用域示例 | 3 | 使用用户自定义函数说明局部变量和全局变量的影响 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Variable.py | 7 | number=0; #全局变量 | 8 | # Add:用户自定义函数--求和 | 9 | def Add (number_a,number_b): | 10 | #计算两个数的和 | 11 | number =number_a+number_b; # number这里是局部变量 | 12 | print “In the function number:”,number | 13 | #调用函数 | 14 | Add(1,2) | 15 | print “Out the function number:”,number |
运行结果:
In the function number:3 Out the function number:0 |
程序分析:本例中首先定义了全局变量number赋值为0,然后在Add函数中定义了另一个number变量,是局部变量,这样在函数中对number的修改并未影响到全局变量的number。这就是因为它们作用域不同。
如果想要为一个定义在函数外的变量赋值,那么就得告诉Python该变量名不是局部的,而是全局的。通常使用global语句完成这一功能。
【例7-11】函数global语句使用举例。
问题分析:定义Add函数,完成参数number_a和number_b的求和运算,并使用global关键字更新全局变量值。 1 | ''' | 2 | 例 7-11 函数global语句使用示例 | 3 | 使用用户自定义函数说明global的使用方法 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: GlobalVariable.py | 7 | number=0; #全局变量 | 8 | # Add:用户自定义函数--求和 | 9 | def Add (number_a,number_b): | 10 | #计算两个数的和 | 11 | global number | 12 | number =number_a+number_b; # number这里是局部变量 | 13 | print “In the function number:”,number | 14 | #调用函数 | 15 | Add(1,2) | 16 | print “Out the function number:”,number |
运行结果:
In the function number:3 Out the function number:3 |
程序分析:本例中首先定义了全局变量number赋值为0,然后在Add函数中定义了另一个number变量,但是此处使用了global语句。这个number是全局的。当函数内number发生变化时,函数外的也跟着变了。
7.5 递归
程序调用自身的编程技巧称为递归(recursion)。递归做为一种算法在程序设计语言中广泛应用,它通常把一个大型复杂的问题,转化为一个与原问题相似的,且规模较小的问题来求解。递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
通常的递归函数包含: * 当函数直接结束时有基本的返回值。 * 递归调用函数自身:包括一个或多个对自身函数的调用。
下面举一个阶乘的例子,将分别使用循环和递归。
【例7-12】函数阶乘循环实现使用举例。
问题分析:设计函数Factorial计算i的阶乘,用局部静态变量result记录i的阶乘,当调用Factorial (10)时,result *=i,采用for循环,循环调用从而计算出1、2、3、4、…、n的阶乘。 1 | ''' | 2 | 例 7-12 函数阶乘循环实现使用示例 | 3 | 使用用户自定义循环函数实现阶乘的方法 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Factorial.py | 7 | #n阶乘的定义:n*(n-1)*(n-2)*……*1 | 8 | # Factorial:用户自定义函数—阶乘计算 | 9 | def Factorial (number_a): | 10 | #计算数number_a的阶乘 | 11 | result=number_a | 12 | for i in range(1,number_a): | 13 | result *=i | 14 | return result | 15 | #调用函数 | 16 | print Factorial (10) |
运行结果:
3628800 |
程序分析:本例中首先将n的值赋给result,然后通过for循环将result依次与1~n-1的数相乘,最后返回结果。
【例7-13】函数递归使用举例。
问题分析:设计函数Recursion计算i的阶乘,用局部静态变量result记录i的阶乘,当调用Recursion (10)时,实际返回10*Recursion(9),采用递归调用,从而计算出1、2、3、4、…、n的阶乘。 1 | ''' | 2 | 例 7-13 函数递归使用示例 | 3 | 使用用户自定义阶乘函数说明递归的使用方法 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Recursion.py | 7 | #n阶乘的定义:n*(n-1)*(n-2)*……*1 | 8 | # Recursion:用户自定义函数—阶乘计算 | 9 | def Recursion (number_a): | 10 | #计算数number_a的阶乘 | 11 | if number_a==1: | 12 | return 1 | 13 | else: | 14 | return number_a*Recursion(number_a-1) | 15 | #调用函数 | 16 | print Recursion(10) |
运行结果:
3628800 |
程序分析:本例中读者需要明确的是:(1)在数学上,自然数1的阶乘还是1;(2)大于1的数n的阶乘是n乘以n-1的阶乘。根据这两个规则所设计函数如上例所示,其中(1)函数中调用自身函数的那部分句子,即return n * recursion(n-1),把recursion(n-1)当做另一个独立的函数,该函数的功能返回n-1的值,如果n的值是1,则返回1,函数运行结束;(2)直观的看,可以把return n * recursion(n-1)看成return n*(n-1)*(n-2)...1。而递归函数无非是在指定的条件下做普通的循环而已。因此,阶乘计算过程可以使用递归方式完成计算。
7.6 函数修饰器
本节将介绍一种Python为函数设计的高级语法——修饰器。使用修饰器的目的是,对已有的函数添加一些小功能,却又不希望对函数内容有太多刚性的修改。函数修饰器以透明,动态为原则,可以灵活的为已有的函数添加一些额外的功能。
Python的函数修饰器充分借鉴了函数式编程中的技巧:将需要添加功能的函数像普通对象一样作为参数传入修饰器中;将函数作为修饰器的返回值返回。
下面将给出一个简单的使用函数修饰器的例子:
【例7-14】函数修饰器使用举例。
问题分析:现在已经实现了若干函数,这些函数的功能都是按照相应规则返回一段字符串。现在想为这些函数的所有输出字符串前添加一个类似“水印”的“Hello World_”前缀。 1 | ''' | 2 | 例 7-14 函数修饰器使用示例 | 3 | 使用用户自定义函数说明修饰器的使用方法 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Deco.py | 7 | def deco(func): | 8 | def wrappedFunc(): | 9 | return "Hello World_" + func() | 10 | return wrappedFunc | 11 | | 12 | @deco | 13 | def f(): | 14 | return "I am f" | 15 | def g(): | 16 | return "I am g" | 17 | | 18 | print f() | 19 | print g() |
运行结果:
Hello World_I am f | I am g |
程序分析:在本例中,修饰器即为函数deco。函数deco的传入参数为函数对象。在函数deco内定义了一个wrappedFunc函数(在Python函数体中创建另外一个函数是合法的,这个函数被称为内嵌函数),所有对于传入函数的修饰逻辑都将在这个内嵌函数中实现。在本例中即为在传入函数的返回值前加上前缀后再返回。而这个deco函数的返回值为wrappedFunc函数对象。在程序中如果有函数需要修饰器修饰,只需在函数定义前使用@加修饰器名的语法即可使用这个修饰器。在本例中,f函数调用了修饰器,而g函数未调用。它们的区别在运行结果中得到了体现。
在较老版本的Python中,使用修饰器的语法更接近修饰器的本质。对于下列修饰器的使用: 1 | @deco | 2 | def f(): | 3 | pass |
它等同于
1 | f = deco(f) |
所以说,修饰器的本质是Python解释器在发现函数调用修饰器后,将其传入修饰器中,然后用返回的函数对象将自身完全替换。
一个函数被可以被多个修饰器嵌套修饰,以完成更加复杂的功能。 1 | @deco3 | 2 | @deco2 | 3 | @deco1 | 4 | def f(): | 5 | pass |
它等同于
1 | f = deco3(deco2(deco1(f))) |
有时候,为了使得函数修饰器更加灵活,还可以为函数修饰器添加参数。带参的函数修饰器,需要在修饰器函数内再添加一层内嵌函数,作为对修饰器传入参数的处理。
下面以例7-14为例,但本例中前缀将由传入修饰器的参数决定。
【例7-15】带参函数修饰器使用举例。
问题分析:现在已经实现了若干函数,这些函数的功能都是按照相应规则返回一段字符串。现在想为这些函数的所有输出字符串前添加一个类似“水印”的前缀,具体内容由参数决定。 1 | ''' | 2 | 例 7-15 函数带参数的修饰器使用示例 | 3 | 使用用户自定义函数说明带参数修饰器的使用方法 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Deco_Para.py | 7 | def deco(prefix): | 8 | def _deco(func): | 9 | def wrappedFunc(): | 10 | return prefix + func() | 11 | return wrappedFunc | 12 | return _deco | 13 | | 14 | @deco("second_") | 15 | @deco("first_") | 16 | def f(): | 17 | return "I am f" | 18 | | 19 | print f() |
运行结果:
second_first_I am f |
程序分析:在本例中,deco函数和wrappedFunc函数之间增加了一个_deco函数,这是为了处理deco函数传入的prefix参数的。对于修饰器的传入参数prefix,修饰器不会立马将待修饰的函数作为参数传入完成修饰,而是先做了一个预处理,返回了一个_deco函数,而这个_deco函数才是真正被f函数调用的修饰器。对于例7-15中的修饰器调用部分,它等同于
1 | f = deco("second_")(deco("first_")(f)) |
7.7 完成五子棋(封装及重构)
之前的章节已经通过前面几章所学的知识实现了五子棋,下面需要用函数的知识来封装之前的五子棋游戏。
记录状态函数
根据前几章的代码可以将记录状态封装为一个函数:
1 | def Record(qipan): | 2 | who = True | 3 | while True: | 4 | t = raw_input('请输入棋子位置(x,y), 现在由' + ('〇' if who else '乂') + '方下子:') | 5 | t = t.split(',') | 6 | if len(t) == 2: | 7 | x = int(t[0]) | 8 | y = int(t[1]) | 9 | if qipan[x][y] == 0: | 10 | qipan[x][y] = 1 if who else 2 | 11 | who = not who | 12 | else: | 13 | print "当前位置已有棋子,请重新下子" | 14 | else: | 15 | print "输入位置有误,请输入要下的位置,如 1, 1" |
显示棋盘函数
对该功能进行封装,提供3个参数分别是(棋盘,最大行,最大列),详细代码如下。 1 | #coding:utf-8 | 2 | maxX = 10 | 3 | maxY = 10 | 4 | Qipan = [[0, 0, 0, 0, 1, 0, 0, 2, 0, 0], | 5 | [0, 1, 2, 1, 1, 0, 2, 0, 0, 0], | 6 | [0, 0, 0, 0, 1, 1, 0, 2, 0, 0], | 7 | [0, 0, 0, 0, 2, 0, 0, 1, 0, 0], | 8 | [0, 0, 0, 1, 1, 1, 2, 0, 0, 0], | 9 | [0, 0, 0, 2, 0, 0, 0, 2, 0, 0], | 10 | [0, 0, 1, 2, 0, 2, 2, 0, 1, 0], | 11 | [0, 0, 0, 2, 0, 0, 0, 1, 0, 0], | 12 | [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], | 13 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],] | 14 | def Printqp(qipan,maxx,maxy): | 15 | print(' 〇 一 二 三 四 五 六 七 八 九 ') | 16 | for i in range(maxx): | 17 | print i, | 18 | for j in range(maxy): | 19 | if qipan[i][j]==0: | 20 | print '十', | 21 | elif qipan[i][j]==1: | 22 | print '〇', | 23 | elif qipan[i][j]==2: | 24 | print '乂', | 25 | print '\n' | >>>Printqp(Qipan,maxX,maxY) |

棋局判胜函数
由此功能可以设计一个函数isWin参数分别为(棋盘的二维数组,棋子横坐标,纵坐标),返回值为是否赢了。是为true,否为false。
1 | def isWin(qipan,xPoint,yPoint):#判赢 | 2 | global maxx,maxy | 3 | flag=False | 4 | t=qipan[xPoint][yPoint] | 5 | x=xPoint | 6 | y=yPoint | 7 | #横向 | 8 | count=0 | 9 | x=xPoint | 10 | y=yPoint | 11 | while (x>=0 and t==qipan[x][y]): | 12 | count+=1 | 13 | x-=1 | 14 | x=xPoint | 15 | y=yPoint | 16 | while (x<maxx and t==qipan[x][y]): | 17 | count+=1 | 18 | x+=1 | 19 | if (count>5):return True | 20 | #纵向 | 21 | count=0 | 22 | x=xPoint | 23 | y=yPoint | 24 | while (y>=0 and t==qipan[x][y]): | 25 | count+=1 | 26 | y-=1 | 27 | y=yPoint | 28 | while (y<maxy and t==qipan[x][y]): | 29 | count+=1 | 30 | y+=1 | 31 | if (count>5): return True | 32 | #/ | 33 | count=0 | 34 | x=xPoint | 35 | y=yPoint | 36 | while (x<maxx and y>=0 and t==qipan[x][y]): | 37 | count+=1 | 38 | x+=1 | 39 | y-=1 | 40 | x=xPoint | 41 | y=yPoint | 42 | while (x>=0 and y<maxy and t==qipan[x][y]): | 43 | count+=1 | 44 | x-=1 | 45 | y+=1 | 46 | if (count>5):return True | 47 | #\ | 48 | count=0 | 49 | x=xPoint | 50 | y=yPoint | 51 | while (x>=0 and y>=0 and t==qipan[x][y]): | 52 | count+=1 | 53 | x-=1 | 54 | y-=1 | 55 | x=xPoint | 56 | y=yPoint | 57 | while (x<maxx and y<maxy and t==qipan[x][y]): | 58 | count+=1 | 59 | x+=1 | 60 | y+=1 | 61 | if (count>5): return True | 62 | return False | >>> isWin (Qipan,6,7) #如果在棋盘横6纵7的位置下子 | 对于新下的子,分别判断横向,竖向和斜向的连续相同状态的棋子个数,如果数量大于5,则满足胜利条件函数返回True,否则棋局未结束函数返回False,需要继续进行。这样就能通过这三个函数调用实现代码的封装。代码运行效果: 图7.3 代码普通运行效果 那么这样就是完美的封装了吗?不,读者可能发现isWin函数代码非常冗余,其实还可以对isWin函数进行重构使代码更加简洁。所谓重构就是通过改善代码的逻辑及实现,使代码性能和可读性更强。其实重构就是下一章面向对象的重要思想,为什么要重构?因为在实际系统开发中需求是经常改变,必然会不断的修改原有功能、追加新功能,导致系统的缺陷,影响最初的系统设计结构。而使用重构的方式,在不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的结构,使系统对于需求的变更时钟具有较强的适应能力。具体的面向对象的思想会在下一章进行详细讲解。
上面代码中对于不同方向的判断代码比较冗余,可以重构为如下代码:
1 | def inRange(qipan, xPoint, yPoint): | 2 | global maxx,maxy | 3 | #判断坐标是否还在棋盘内 | 4 | return xPoint < maxx and \ | 5 | yPoint < maxy and \ | 6 | xPoint >= 0 and \ | 7 | yPoint >= 0 | 8 | | 9 | def checkFiveInRow(qipan, xPoint, yPoint, xDir, yDir): | 10 | #判断以(xPoint, yPoint)点(xDir, yDir)方向是否五子连珠 | 11 | count = 0 | 12 | t = qipan[xPoint][yPoint] | 13 | x, y = xPoint, yPoint | 14 | while inRange(x, y) and t == qipan[x][y]: | 15 | count += 1 | 16 | x += xDir | 17 | y += yDir | 18 | x, y = xPoint, yPoint | 19 | while inRange(x, y) and t == qipan[x][y]: | 20 | count += 1 | 21 | x -= xDir | 22 | y -= yDir | 23 | return count > 5 | 24 | | 25 | def isWin(qipan, xPoint, yPoint): | 26 | # 以(xPoint, yPoint)点为中心在四个方向分别判断五子连珠 | 27 | return self.checkFiveInRow(qipan,xPoint, yPoint, 1, 0) or \ | 28 | self.checkFiveInRow(qipan,xPoint, yPoint, 0, 1) or \ | 29 | self.checkFiveInRow(qipan,xPoint, yPoint, 1, 1) or \ | 30 | self.checkFiveInRow(qipan,xPoint, yPoint, 1, -1) | 代码分析:如上面代码所示,本书将isWin函数拆分为inRange函数和checkFiveInRow函数以及isWin函数。其中inRange(qipan, xPoint, yPoint)函数参数是整个棋盘和x、y坐标,用来判断坐标是否在棋盘内。checkFiveInRow(qipan, xPoint, yPoint, xDir, yDir)函数参数是整个棋盘,x、y坐标以及移动方向坐标组成用来判断以x、y为坐标的xDir、yDir方向判断是否五子连珠。这样就实现了isWin的功能,而且代码更加精简。代码运行结果:

7.4 代码优化运行效果
本章小结
本章主要讲解函数相关内容。由于函数是Python语言的基本内容,甚至可以说,一个完整的Python程序就是由若干函数组成的,因此本章重点讲解函数模块化的设计思想,掌握函数的定义、调用声明以及返回值等有关函数的基本知识。在此基础上,本章还介绍了Python中几种有特点的参数使用用法,其中包括必备参数、命名参数、缺省参数、不定长参数作为函数的参数,其中不定长函数参数和普通变量做函数参数是有很大不同,不定长参数在之后的章节也会具体使用。本章利用具体实例对函数的嵌套调用和递归调用进行讲解。在深入介绍函数内容后,进一步介绍局部变量和全局变量的区别与联系,以及变量的存储方式。最后介绍了五子棋的封装和重构,为今后学习提供一个参考。

习题
一、填空题
1. 函数是____________。 2. 函数能提高代码的____________和____________。 3. 定义函数的样式____________。 4. 函数的参数从宏观上可以分为____________和____________。 5. 函数的形参可以分为____________和____________和____________和____________。 6. 使用____________关键字可以使局部变量成为全局变量。 7. 递归是____________。
二、选择题
1. 以下正确的函数声明语局是( )。C
A.void fun(int x) B.def fun(double x)
C.def fun(x) D.double(int x) 2. 下面代码实现什么功能( )。D def a(b,c,d): pass
A.定义一个元组
B.定义一个列表
C.定义一个空函数
D.定义一个有3个参数的空函数 3. 下面代码输出什么( )。A print type(1/2)
A.<type ‘int’>
B.<type ‘number’>
C.<type ‘float’>
D.<type ‘double’> 4. 下面代码输出什么( )。B def f(): pass print type(f())
A.<type ‘function’>
B.<type ‘NoneType’>
C.<type ‘tuple’>
D.<type ‘str’> 5. 下面代码输出什么( )。C counter=1 def do(): Global counter For i in (1,2,3): Counter+=1 do() print counter
A.1
B.3
C.4
D.7 6. 下面代码输出什么( )。B values=[1,2,1,3] nums=set(values) def checkit(num): if num in nums: return True else: return False for i in filter(checkit,values): print i
A.1 2 3
B.1 2 1 3
C.1 2 1 3 1 2 1 3
D.Syntax Error 7. 下面代码输出什么( )。B import math print math.floor(5.5)
A.5
B.5.0
C.5.5
D.6
三、编程题
1.用选择排序算法实现一个sort函数实现数组中的数据按从小到大排序。
答案:
def selection_sort(list2): for i in range(0, len (list2)): min = i for j in range(i + 1, len(list2)): if list2[j] < list2[min]: min = j list2[i], list2[min] = list2[min], list2[i] # swap
2.编写函数检测给定的两维数组中是否有重复数据(已知该数组中保存了正整数):check_data(mat),如果没有重复,返回真。
答案: def expand_list(list,num): if len(list) > num: return for i in range(len(list),num): list.append(0) def check_data(list): count = [] expand_list(count,10) for line in list: for var in line: if var > len(count): expand_list(count,var+1) if count[var] == 0: count[var] += 1 else: return False return True
3.设有一个背包能承受重量s(s>0), 又有n(n≥1)件物品, 其重量列表为w=[w1,w2,...,wn]. 要从物品中挑选若干件放入背包, 使得放入的物品重量之和正好是s. 试设计递归函数f(w,s,n): 当有解时返回True,并显示所选物品是哪些; 无解时返回False。[提示:递归之处在于f(w,s,n)的真假等于f(w,s−wn,n−1) or f(w,s,n−1)]
答案:
def f(w,s,n): if s==0: return True elif (s<0) or (s>0 and n<1): return False elif f(w,s-w[n-1],n-1): print w[n-1], return True else: return f(w,s,n-1)
4.完成一个斐波那契数列,给定n,返回n以内的。
答案: def fib(n): a,b=0,1 while b<n: print b, a,b=b,a+b
5.写程序打印9*9乘法表。
格式为:
1*1=1
1*2=2 2*2=4
……

答案: def gen(line_cnt): for i in range(1,line_cnt+1): for j in range(1,i+1): m=i*j print '%s*%s=%s\t' % (i,j,m),
#这边的逗号很重要,有了逗号,才能不换行
print''
6.取任意小于1元的金额,假设硬币有1分、5分、10分、25分钱4种,计算可换成最少多少枚硬皮。如0.51元可换成2两个25分和1个1分钱。
答案: def getmoney(money): coins = [25,10,5,1] for obj in coins: if money>=obj: print money/obj money %= obj
7.写一个简单的计算器程序。操作符为+、-、 *、 /、 %、 **。不适用系统函数如eval()。
答案:
expr = raw_input('input:') opera = ['+','-','*','/','%','**'] def go(n1,op,n2): """通过参数op判断操作符类型,实现两个数字(n1,n2)的算术运算""" if op == '+':return (n1+n2) if op == '-':return (n1-n2) if op == '*':return (n1*n2) if op == '/':return (n1/n2) if op == '%':return (n1%n2) if op == '**':return (n1**n2)
#遍历循环操作符,在对应的操作符下进行相应转换操作
for obj in opera: if expr.find(obj)>-1 and expr.count(obj)<2: #如果输入的字符串包含该操作符(注意这个条件区分*与**,如果没有最后面的条件则结果不一样) number = expr.split(obj) #通过该操作符进行分割 if number: newLis = [] #创建空列表用于存储要转换的值 for i in number: #循环遍历已分割生成的列表 if i.find('.')>-1: #如果存在该字符串(浮点型数字) i = float(i) #类型转换 newLis.append(i)#存入新列表 else: #该字符串(整型) i = int(i) newLis.append(i) print go(newLis[0],obj,newLis[1]) #调用函数实现相应功能

第8章 I/O操作与文件
在实际项目中,有时候我们希望程序运行结束后,再次时运行程序能够还原为结束前的状态,或者将程序运行的结果保存到硬盘上,而不是保存在内存中。因此,针对此问题,本章将介绍Python中用来读写文件以及访问目录内容的函数和类型。这些函数很重要,因为几乎所有比较大的程序都用文件来读取输入和存储输出的。
Python提供了丰富的输入/输出函数。首先介绍文件对象,它是Python中实现输入输出的基本方法。之后将介绍用于操作路径、获取文件信息和访问目录内容的函数。
【体系结构】

【本章重点】
(1)掌握字符串格式化操作;
(2)了解I/O操作原理;
(3)掌握文件的操作方法;
(4)掌握文件系统模块os与os.path中。
8.1 字符串格式化
字符串格式化操作是I/O的预操作,也只有提前将不规范的字符串按照某种规则格式化后,才能够保证I/O的结果是有效的,因此在介绍Python语言I/O相关操作之前,首先介绍一下Python语言字符串格式化的相关内容。
Python字符串格式化使用字符串格式化操作符(%),只适用于字符串类型,非常类似于C语言的printf()函数的字符串格式化。在%的左侧放置一个字符串(格式化字符串),在右侧放置希望格式化的值。左边的格式化字符串里面通常和在printf()函数的第一个参数一样,包含%的格式化字符串。格式化操作符的右操作数可以是任何参数,Python支持两种格式的输入参数。第一种是元组,这基本上是一种C的printf()风格的转换参数集;Python支持的第二种形式是字典形式,在这种形式里面,键是作为格式化字符串出现的,相对应的值作为参数在进行转化时提供格式字符串。格式字符串既可以跟print语句一起来向终端用户输出数据,又可以用来合并字符串形成新字符串,而且还可以直接显示到GUI界面上去。 在Python中格式化输出字符串通用的形式为: 格式化字符串 % 要输出的值组 | 以元组形式格式化输出字符串通用格式使用如下: >>> | format = "hello %s, %s enough!" | >>> | values = ('world' , 'Cool') | >>> | print format % values #字符串格式化格式 | hello world, Cool enough! |
以字典形式格式化输出字符串通用格式使用如下:
>>> | student = {"name":"LiMing","age":18,"gender":"male"} | >>> | "My name is %(name)s, age is %(age)d , gender is %(gender)s" % student #以字典形式 | 'My name is LiMing, age is 18 , gender is male' | 字典形式除了增加字符串键之外,转换说明符还是像以前一样工作。当以这种的方式使用字典的时候,只要所有给出的键都能在字典中找到,就可以获得任意数量得转换说明符。这类字符串格式化在模板系统中非常有用。
如果使用列表或者其他序列代替元组,那么序列就会被解释为一个值,只有元组和字典可以格式化一个以上的值。
>>> | print '%s %s %s' % (1, 2.3, ['one', 'two', 'three']) #元组中包含列表 | 1 2.3 ['one', 'two', 'three'] #列表整体输出成一个字符串 | 小技巧:如何分别输出列表中的元素? 格式化字符串的%s部分称为转换说明符(conversion specifier),它们标记了需要插入转换值的位置。s表示值会被格式化为字符串。其他转换说明符请参见表8.1。如果要在格式化字符串里面包括百分号,那么必须使用%%,这样Python就不会将%号误认为是转换符标志。
表8.1 字符串格式化符号 格式化字符 | 名 称 | %c | 转换成字符(ASCII码值,或者长度为一的字符串) | %r | 优先用repr()函数进行字符串转换 | %s | 优先用str()函数进行字符串转换 | %d | 转成有符号十进制 | %u | 转成无符号十进制 | %o | 转换成无符号八进制 | %x/%X | 转成无符号十六进制数 | %e/%E | 转成科学记数法 | %f/%F | 转成浮点型 | %% | 输出% | 简单的转换只需要写出转换类型,使用起来很简单,例如
十六进制输出:
>>> | "%x" % 100 | '64' | >>> | "%X" % 110 | '6E' | 浮点型和科学记数法形式输出: >>> | "%f" % 1000005 | '1000005.000000' | >>> | "%e" % 1000005 | '1.000005e+06' | 整型和字符串输出: >>> | "We are at %d%%" % 100 | 'We are at 100%' | >>> | s = "%s is %d years old" % ('Li',20) | >>> | print s | Li is 20 years old | 转换说明符还可以包括字段宽度和精度以及对齐等功能,请参见表8.2。
表8.2 格式化操作符辅助指令 符号 | 作用 | * | 定义宽度或者小数点精度 | - | 用做左对齐 | + | 在正数前面显示加号(+) | 0 | 显示的数字前面填充‘0’而不是默认的空格 | m.n | m是显示的最小总宽度,n是小数点后的位数 | 字段宽度是转换后的值所保留的最小字符的个数,精度则是结果中应该包含的小数位数,或者对于字符串转换来说是转换后的值所能包含的最大字符个数。 >>> | "%.3f" % 123.12345 | '123.123' | >>> | "%.5s" % "hello world" | 'hello' | >>> | "%+d" % 4 | '+4' | >>> | "%+d" % -4 | '-4' | >>> | from math import pi | >>> | '%-10.2f' % pi | '3.14 ' | >>> | '%10.2f' % pi | ' 3.14' |
8.2 I/O操作
在计算机领域中,英文I/O表示的是Input/Output,即输入和输出。通常程序在运行过程中,程序的中间结果和参数往往被保存于内存中。可以大大提高运算速度,缓解由于CPU和读写速度的不平衡,造成的系统效率低下的问题。举个例子来说,例如某个客户端实时的打开一个URL来读取Web页面,浏览器程序就需要通过网络获取访问的页面数据。浏览器首先发送请求给目标服务器,告诉它请求的资源(如网站的首页),这个动作是往外发送数据,叫做Output,随后服务器把请求的网页资源发送给客户端,浏览器就会接受服务器发送的数据,叫做Input。所以,通常程序完成I/O操作会有Input和Output两个数据流。所以一个网站的浏览,从整体上看,有上述两个过程。
此外,在对I/O进行编程时,Stream(流)是一个很重要的概念,同学们可以把流想象成一个水管,数据就是水管里的水。InputStream就是数据从外面(磁盘、网络)流进内存,OutputStrem就是数据从内存流到外面去。对于浏览网页来说,浏览器会在另一个独立的进程中执行一个命令,在浏览器与目标服务器进行通讯,可以理解为建立两根水管,一个是发送数据一个是接收数据,只是现实中只有一个管道进行双向通信。 Python中的标准输入是sys.stdin,标准输出是sys.stdout。stdin和stdout变量分别包含于标准I/O流对应的流对象。如果需要更好地控制输出,而print不能满足你的要求,他们就是你所需要的。你也可以替换它们,这时候你可以重定向输出和输入到其他设备,或者以非标准的方式处理它们。 stdin被除了脚本之外的所有解释器用作输入,包括调用input()和raw_input()。stdout用于输出print和表达式语句以及函数input()和raw_input()的提示内容。 1 | #testing stdout | 2 | #hello.py | 3 | | 4 | print "hello world!" | 运行结果: hello world! | 运行hello.py就会在标准输出的屏幕上打印 hello world!,再编写一个简单的标准输入的小程序stdin.py: 1 | #testing stdin | 2 | #stdin.py | 3 | print "hello world!" | 4 | user = raw_input("Please enter your name:") | 5 | print 'Hi,%s!' % user | 当你用键盘输入Mr zhang。
运行结果:
Hi, Mr zhang! | |
这就是从标准输入:键盘获取信息,再输出到标准输出:屏幕的例子。
那么上面的例子中print和raw_input是如何与标准输入/输出流建立关系的呢? 其实Python程序的标准输入/输出流定义在sys模块中,分别为:sys.stdin, sys.stdout。上面的程序分别与下列的程序是一样的: 1 | import sys | 2 | sys.stdout.write(' hello world!') | 3 | | 4 | import sys | 5 | print 'Please enter your name:' | 6 | user=sys.stdin.readline()[:-1] | 7 | print 'Hi, %s!' % user |
当你用键盘输入Mr zhang。
运行结果:
Hi, Mr zhang! |
8.3 文件打开与关闭 文件一般是指以文件名标识的,存储在外部介质上的一组相关数据的有序集合。文件是操作系统和用户进行数据管理的单位。文件一般存储在外部介质上,在使用时将文件调入内存。在程序中使用文件可以实现数据的输入和输出,长久保存程序的中间数据和结果数据,实现程序与数据的分离。根据文件的编码方式,可以将文件分为文本文件和二进制文件。文本文件由字符组成,因此也便于显示,可读性强。二进制文件将数据以在内存中的存储形式存放到磁盘文件中。二进制文件节省空间,速度快,但是其内容无法阅读。Python语言库提供了操作文件流的函数,通过这些库函数可以实现将数据写入文件和从文件中读取数据等功能。 内建函数open()返回一个文件对象,对该文件进行后继相关的操作都要用到它。还有大量的函数也会返回文件对象或类文件( file-like )对象。进行这种抽象处理的主要原因是许多的输入/输出数据结构更趋向于使用通用的接口。这样就可以在程序行为和实现上保持一致性。请记住,文件只是连续的字节序列。数据的传输经常会用到字节流,无论字节流是由单个字节还是大块数据组成。下面介绍一下Python语言中对文件进行打开和关闭的具体方法。
8.3.1 打开文件 open函数用来打开文件,提供了初始化输入/输出(I/O)操作的通用接口。open()内建函数成功打开文件后会返回一个文件对象,否则引发一个错误。当操作失败,Python会产生一个IOError异常。 Open函数打开文件语法如下: 1 | file_object = open(file_name, access_mode='r', buffering=-1) | open函数使用一个文件名作为唯一的强制参,然后返回一个文件对象。file_name是包含要打开的文件的字符串,它可以是相对路径或者绝对路径。可选变量access_mode 也是一个字符串,代表文件打开的模式,上例中的参数’r’表示以只读方式打开文件。另一个可选参数buffering用于指示访问文件所采用的缓冲方式,-1代表使用默认的缓冲区大小。 因此,假设有一个名为hellofile.txt的文本文件(可能是用文本编辑器创建的),其存储路径是d:\study(或者在UNIX下的~/study),那么可以像下面这样打开文件。 >>> | f = open(r'd:\study\hellofile.txt') | 注意默认打开模式时只读的,只有文件存在才能打开,如果文件不存在或是路径不对,则会看到一个类似下面这样的异常回溯: Traceback (most recent call last): | File "<pyshell#41>", line 1, in <module> | f = open(r'd:\study\hellofile.txt') | IOError: [Errno 2] No such file or directory: 'd:\\study\\hellofile.txt' | 在上面的异常中,打开文件时未指定打开方式即采用默认的只读方式打开,由于在d:\ study\路径下不存在hellofile.txt文件,所以不能打开,Python抛出IOError异常。如果采用w或者a模式打开,当文件不存在时Python会自动创建文件。如果没有向新文件输入内容,它将为空。
8.3.2 文件关闭 close()方法方法关闭打开的文件。关闭的文件无法读取或写入更多东西。文件已被关闭之后任何操作会引发ValueError。但是调用close()多次是可以的。当一个文件的引用对象被重新分配给另外一个文件时,Python语言会自动关闭文件,这是一种很好的机制。 close()方法使用见下例: 1 | #!/usr/bin/python | 2 | | 3 | # Open a file | 4 | fo = open("foo.txt", "wb") #打开文件 | 5 | print "Name of the file: ", fo.name | 6 | | 7 | # Close opend file | 8 | fo.close() | 运行结果: Name of the file: foo.txt | 在写入了一些文件内容之后,通常的想法是希望这些改变内容立即体现在文件中,这样一来其他读取这个文件的程序也能知道改变。但是实际情况数据可能会被缓存了(在内存中临时性地存储),直到关闭文件才会被写入文件。
8.3.3 文件模式 如果open函数只带一个文件名参数,那么可以获得能读取文件内容的文件对象。如果要向文件内写入内容,则必须提供一个模式参数来显式的声明。open函数中的模式参数只能有几个值,如表8.3所示。 明确指定读模式和什么模式参数都不用的效果是一样的。使用写模式可以向文件写入内容。'+'参数可以用到其他任何模式中,指明读和写都是允许的。比如'r+'能在打开一个文本文件用来读写时使用。
表8.3 open函数中模式参数的常用值 符号 | 描述 | 'r' | 读模式 | 'w' | 写模式 | 'a' | 追加模式 | 'b' | 二进制模式(可添加到其他模式中使用) | '+' | 读/写模式(可添加到其他模式中使用) | 使用r模式打开的文件必须是已经存在的。使用w模式打开的文件如存在则首先清空,然后(重新)创建。以a模式打开的文件是为追加数据作准备的,所有写入的数据都将追加到文件的末尾。如果文件不存在,将自动创建与w模式打开文件类似。b代表二进制访问,对于所有POSIX兼容的Unix系统(包括Linux)来说,b是可有可无的,因为它们把所有的文件当作二进制文件, 包括文本文件。一般来说,Python假定处理的是文本文件(包含字符)。通常这样做不会有任何问题。但如果处理的是一些其他类型的文件(二进制文件),比如声音剪辑或者图像,那么应该在模式参数中增加b,参数rb可以用来读取一个二进制文件。
下面的add_some_text()实现向文本追加文字的功能:
>>> | def add_some_text(): | | f = open(r'd:\study\hellofile1.txt','a') | | f.write("Here is some text!") | | f.close() | >>> | add_some_text() |

通过add_some_text()函数使用a模式打开文件,往d:\study\hellofile1.txt写入文本Here is some text!,再通过'r'模式打开文件读取文本内容。 >>> | f = open(r'd:\study\hellofile1.txt','r') | >>> | f.readline() | 'Here is some text!' |
8.3.4 缓冲 open函数的第3个参数控制着文件的缓冲。如果参数是0(或者False),I/O(输入/输出)就是无缓冲的(所有的读写操作都直接针对硬盘);如果是1(或者是True),I/O就是缓冲的,表示只缓冲一行数据,意味着Python使用内存来代替硬盘,让程序更快,只有使用flush或者close时才会更新硬盘上的数据。大于1的数字代表缓冲区的大小(单位是字节),-1(或者是任何负数)代表使用默认的缓冲区大小。即对任何类电报机( tty )设备使用行缓冲,其它设备使用正常缓冲。
8.4 文件读和写 文件(或流)最重要的能力是提供或者接受数据。open()成功执行并返回一个文件对象之后, 所有对该文件的后续操作都将通过这个句柄进行。文件方法可以分为四类:输入、输出、文件内移动、以及杂项操作。现在来讨论每个类的方法。
(1)输入
在输入过程中个,read()方法用来直接读取字节到字符串中,最多读取给定数目个字节。如果没有给定 size 参数(默认值为 -1)或者size值为负,文件将被读取直至末尾。未来的某个版本可能会删除此方法。 readline() 方法读取打开文件的一行(读取下个行结束符之前的所有字节)。然后整行,包括行结束符,作为字符串返回。和 read() 相同,它也有一个可选的 size 参数,默认为 -1,代表读至行结束符。如果提供了该参数,那么在超过size个字节后会返回不完整的行。 readlines() 方法并不像其它两个输入方法一样返回一个字符串。它会读取所有(剩余的)行然后把它们作为一个字符串列表返回。它的可选参数 sizhint 代表返回的最大字节大小. 如果它大于0,那么返回的所有行应该大约有sizhint字节(可能稍微大于这个数字,因为需要凑齐缓冲区大小)。 >>> | f = open(r'd:\study\hellofile1.txt','r') | >>> | f.read(4) #读取四个字节 | 'Here' | >>> | f.read() #读取至文件末尾 | ' is some text!' | >>> | f = open(r'd:\study\hellofile1.txt','r') | >>> | f.readline() #读取一行 | 'Here is some text!' |

当使用输入方法如 read() 或者 readlines() 从文件中读取行时,Python 并不会删除行结束这个操作。例如这样的代码在 Python 程序中很常见: 1 | f = open('myFile', 'r') | 2 | data = [line.strip() for line in f.readlines()] #line.strip()用于删除换行符 | 3 | f.close() #关闭文件 | 类似地,输出方法 write() 或 writelines() 也不会自动加入行结束符。应该在向文件写入数据前自己完成。
(2)输出
在输出过程中write() 内建方法功能与 read() 和 readline() 相反。它把含有文本数据或二进制数据块的字符串写入到文件中。和 readlines() 一样,writelines() 方法是针对列表的操作,它接受一个字符串列表作为参数,将它们写入文件。行结束符并不会被自动加入,所以如果需要的话,你必须在调用writelines()前给每行结尾加上行结束符。 注意这里并没有 "writeline()" 方法,因为它等价于使用以行结束符结尾的单行字符串调用 write() 方法。 >>> | f = open(r'd:\study\hellofile1.txt','w') | >>> | f.write('hello,') | >>> | f.write('world!') | >>> | f.close() |
表8.4 操作文件对象方法 符号 | 描述 | f.close() | 关闭文件 | f.fileno() | 获得文件描述符,是一个数字 | file.flush() | 刷新文件的内部缓冲区 | file.isatty() | 判断file是否是一个类tty设备 | f.read([count]) | 读出文件,如果有count,则读出count个字节 | f.readline() | 读出一行信息 | f.readlines() | 读出所有行,也就是读出整个文件的信息 | f.seek(offset[,where]) | 把文件指针移动到相对于where的offset位置。 where为0表示文件开始处,这是默认值;1表示当前位置;2表示文件结尾 | f.truncate([size]) | 截取文件,使文件的大小为size | f.write(string) | 把string字符串写入文件 | f.writelines(list) | 把list中的字符串一行一行地写入文件,是连续写入文件,没有换行 |
8.5 处理二进制文件 计算机文件基本上分为二种:二进制文件和ASCII(也称纯文本文件),图形文件及文字处理程序等计算机程序都属于二进制文件。这些文件含有特殊的格式及计算机代码。ASCII 则是可以用任何文字处理程序阅读的简单文本文件。文本文件的编码基于字符定长,译码相对要容易一些;二进制文件编码是变长的,灵活利用率要高,而译码要难一些,不同的二进制文件译码方式也是不同的。 前面讲解open函数时使用处理二进制文件时,需要用如下方法binfile =open(filepath,'rb')读二进制文件或binfile=open(filepath,'wb')写二进制文件那么和binfile =open(filepath,'r')的结果到底有何不同呢? (1)使用'r'的时候如果碰到'0x1A',就会视为文件结束,这就是EOF。使用'rb'则不存在这个问题。即,如果你用二进制写入再用文本读出的话,如果其中存在'0X1A',就只会读出文件的一部分。使用'rb'的时候会一直读到文件末尾。 (2)对于字符串x='abc/ndef',可用len(x)得到它的长度为7,/n称为换行符,实际上是 '0X0A'。当用'w' 即文本方式写的时候,在windows平台上会自动将'0X0A'变成两个字符'0X0D','0X0A',即文件长度实际上变成8。当用'r'文本方式读取时,又自动的转换成原来的换行符。如果换成'wb'二进制方式来写的话,则会保持一个字符不变,读取时也是原样读取。所以如果用文本方式写入,用二进制方式读取的话,就要考虑这多出的一个字节了。'0X0D'又称回车符。linux下不会变。因为linux只使用'0X0A'来表示换行。 Python使用struct模块来处理二进制数据。struct模块中最重要的三个函数式pack(), unpack(),calcsize()。这三个函数的功能如下: * pack(fmt, v1, v2, ...)按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流) * unpack(fmt, string)按照给定的格式(fmt)解析字节流string,返回解析出来的tuple * calcsize(fmt)计算给定的格式(fmt)占用多少字节的内存 【例8-1】用pack函数对数据进行二进制转化。 问题分析:为了显示二进制数据需要对数据进行相应的格式转化,因为在输出时是按照文本格式显示的。 >>> | a='hello' | | b='world!' | | c=2 | | d=45.123 | | ba=struct.pack('5s6sif',a,b,c,d) | >>> | ba | 运行结果: 'helloworld!\x00\x02\x00\x00\x00\xf4}4B' | 程序分析:'5s6sif'这个叫做fmt,就是格式化字符串,由数字加字符构成,5s表示占5个字符的字符串,2i,表示2个整数等等。表8.5是可用的字符及类型,ctype表示可以与Python中的类型一一对应。 Format | C Type | Python | 字节数 | x | pad byte | no value | 1 | c | char | string of length 1 | 1 | b | signed char | integer | 1 | B | unsigned char | integer | 1 | ? | _Bool | bool | 1 | h | short | integer | 2 | H | unsigned short | integer | 2 | i | int | integer | 4 | I | unsigned int | integer or long | 4 | l | long | integer | 4 | L | unsigned long | long | 4 | q | long long | long | 8 | Q | unsigned long long | long | 8 | f | float | float | 4 | d | double | float | 8 | s | char[] | string | 1 | p | char[] | string | 1 |
表8.5 Struct支持的格式
注1.q和Q只在机器支持64位操作时有意思
注2.每个格式前可以有一个数字,表示个数
注3.s格式表示一定长度的字符串,4s表示长度为4的字符串,但是p表示的是pascal字符串
注4.P用来转换一个指针,其长度和机器字长相关
注5.最后一个可以用来表示指针类型的,占4个字节
为了同c中的结构体交换数据,还要考虑有的c或c++编译器使用了字节对齐,通常是以4个字节为单位的32位系统,故而struct根据本地机器字节顺序转换.可以用格式中的第一个字符来改变对齐方式.定义如表8.6:

表8.6 Struct对齐格式符说明 Character | Byte order | Size and alignment | @ | native | native 凑够4个字节 | = | native | standard 按原字节数 | < | little-endian | standard 按原字节数 | > | big-endian | standard 按原字节数 | ! | network (= big-endian) | standard 按原字节数 |

【例8-2】熟悉struct中pack与unpack函数的作用原理与fmt格式的应用。 问题分析:在写入时如果对数据进行了编码,那么在读出时也需要进行相应的解码,才能还原出原始数据。在struct模块中pack与unpack函数是相对应的编解码函数。 1 | import struct | 2 | | 3 | # native byteorder | 4 | buffer = struct.pack("ihb", 1, 2, 3) #将二进制数据分装成字符串 | 5 | print repr(buffer) | 6 | print struct.unpack("ihb", buffer) #将字符串反解析成数据 | 7 | | 8 | # data from a sequence, network byteorder | 9 | data = [1, 2, 3] | 10 | buffer = struct.pack("!ihb", *data) #将数据按照大端格式转换 | 11 | print repr(buffer) | 12 | print struct.unpack("!ihb", buffer) | 运行结果: '\x01\x00\x00\x00\x02\x00\x03' | (1, 2, 3) | '\x00\x00\x00\x01\x00\x02\x03' | (1, 2, 3) | 程序分析:首先将参数1、2、3打包,打包前1、2、3属于python数据类型中的integer,pack后就变成了C结构的二进制串,转成python的string类型来显示就是\x01\x00\ x00\ x00\x02\x00 \x03。由于本机是小端(little- endian,故而高位放在低地址段。i代表C struct中的int类型,故而本机占4位,1则表示为01000000;h 代表C struct中的short类型,占2位,故表示为0200;同理b代表C struct中的signed char类型,占1位,故而表示为03。
8.6 访问文件系统 对文件系统的访问大多通过 Python 的 os 模块实现。该模块是 Python 访问操作系统功能的主要接口。os模块实际上只是真正加载的模块的前端,而真正的那个“模”明显要依赖与具体的操作系统,这个“真正”的模块可能是以下几种之一:posix ( 适用于 Unix 操作系统),nt (Win32),mac(旧版本的 MacOS),dos (DOS),os2 (OS/2)等。你不需要直接导入这些模块。只要导入 os 模块,Python 会为你选择正确的模块,你不需要考虑底层的工作。根据你系统支持的特性,你可能无法访问到一些在其它系统上可用的属性。 除了对进程和运行环境进行管理外,os 模块还负责处理大部分的文件系统操作,应用程序开发人员可能要经常用到这些。这些功能包括删除/重命名文件,遍历目录树,以及管理文件访问权限等。表 8.7列出 os 模块提供的一些常见文件或目录操作函数。另一个模块 os.path可以完成一些针对路径名的操作。它提供的函数可以完成管理和操作文件路径名中的各个部分,获取文件或子目录信息,文件路径查询等操作。表 8.8 列出了os.path中的几个比较常用的函数.
表 8.7 os 模块的文件/目录访问函数 函数 | 描述 | 文件处理 | | mkfifo()/mknod() | 创建命名管道/创建文件系统节点 | remove()/unlink() | Delete file 删除文件 | rename()/renames() | 重命名 | stat() | 返回文件信息 | tmpfile() | 创建并打开('w+b')一个新的临时文件 | walk() | 生成一个目录树下的所有文件名 | 目录/文件夹 | | chdir()/fchdir() | 改变当前工作目录/通过一个文件描述符改变当前工作目录 | chroot() | 改变当前进程的根目录 | listdir() | 列出指定目录的文件 | getcwd()/getcwdu() | 返回当前工作目录/功能相同, 但返回一个 Unicode 对象 | mkdir()/makedirs() | 创建目录/创建多层目录 | rmdir()/removedirs() | 删除目录/删除多层目录 | 访问/权限 | | access() | 检验权限模式 | chmod() | 改变权限模式 | chown()/lchown() | 改变 owner 和 group ID/功能相同, 但不会跟踪链接 | umask() | 设置默认权限模式 | 文件描述符操作 | | open() | 底层的操作系统 open (对于文件, 使用标准的内建 open() 函数) | read()/write() | 根据文件描述符读取/写入数据 |

表 8.8 os.path 模块中的路径名访问函数 函数 | 描述 | 分隔 | | basename() | 去掉目录路径, 返回文件名 | dirname() | 去掉文件名, 返回目录路径 | join() | 将分离的各部分组合成一个路径名 | split() | 返回 (dirname(), basename()) 元组 | splitdrive() | 返回 (drivename, pathname) 元组 | splitext() | 返回 (filename, extension) 元组 | 信息 | | getatime() | 返回最近访问时间 | getctime() | 返回文件创建时间 | getmtime() | 返回最近文件修改时间 | getsize() | 返回文件大小(以字节为单位) | 查询 | | exists() | 指定路径(文件或目录)是否存在 | isabs() | 指定路径是否为绝对路径 | isdir() | 指定路径是否存在且为一个目录 | isfile() | 指定路径是否存在且为一个文件 | samefile() | 两个路径名是否指向同个文件 | 【例8-3】遍历文件夹和文件。练习walk函数和join函数熟悉目录中文件及文件夹的遍历与路径的生成。 问题分析:在操作文件系统时,有时需要对文件夹内的文件进行批量处理,这时就需要遍历文件,并将文件和子文件夹分开处理。 1 | #!/usr/bin/env python | 2 | #traverse.py | 3 | import os | 4 | import os.path | 5 | rootdir = "d:\study" | 6 | print rootdir | 7 | for parent, dirnames, filenames in os.walk(rootdir): #用walk函数遍历根目录 | 8 | #case1 | 9 | for dirname in dirnames: #显示rootdir下所有子目录 | 10 | print "parent is: " + parent | 11 | print "dirname is: " + dirname | 12 | #case2 | 13 | for filename in filenames: #显示rootdir下所有文件 | 14 | print "parent is: " + parent | 15 | print "filename with full path : " + os.path.join(parent, filename) | 运行结果: d:\study | parent is: d:\study | dirname is: dir1 | parent is: d:\study | dirname is: dir2 | parent is: d:\study | filename with full path : d:\study\hellofile.txt | parent is: d:\study | filename with full path : d:\study\hellofile1.dat | parent is: d:\study | filename with full path : d:\study\hellofile1.txt | 程序分析:os.walk返回一个三元组。其中dirnames是所有文件夹名字(不包含路径),filenames是所有文件的名字(不包含路径)。parent表示父目录。case1 演示了如何遍历所有目录访问数据库,case2 演示了如何遍历所有文件。os.path.join(dirname,filename) : 将形如"/a/b/c"和"d.java"变成/a/b/c/d.java"。

【例8-4】分割路径和文件名。练习path中三种常用的分割路径函数:split,splitdrive和splitext。 问题分析:在操作文件系统需要进行获取盘符、当前目录和文件后缀名等操作时可以综合使用path模块中split,splitdrive和splitext等函数。 1 | #!/usr/bin/env python | 2 | #split.py | 3 | import os.path | 4 | | 5 | spath = "D:\study\hellofile.txt" | 6 | | 7 | # case 1: | 8 | p,f = os.path.split(spath) #分割路径 | 9 | print "dir is: " + p | 10 | print "file is: " + f | 11 | | 12 | # case 2: | 13 | drv,left = os.path.splitdrive(spath) #分割得到盘符 | 14 | print "driver is: " + drv | 15 | print "left is: " + left | 16 | # case 3: | 17 | f,ext = os.path.splitext(spath) #获得文件后缀名 | 18 | print "f is: " + f | 19 | print "ext is: " + ext | 运行结果: dir is: D:\study | file is: hellofile.txt | driver is: D: | left is: \study\hellofile.txt | f is: D:\study\hellofile | ext is: .txt | 程序分析:这三个函数都是返回二元组。case1是分割目录和文件名,case2是分割盘符和文件名,case3是分割文件和扩展名。

【例8-5】创建一个文本文件写入少量数据,然后重命名,输出文件内容。同时还进行一些辅助性的文件操作,比如遍历目录树和文件路径处理。对本章的知识进行综合应用。 问题分析:在创建一个文件前首先要判断路径是否存在,如果文件所在的文件夹不存在,则需要创建文件夹。写入文件时需要用'a'或是'w'模式打开文件。写入内容时需要为每一行添加换行符。 1 | #!/usr/bin/env python | 2 | #filelib.py | 3 | import os | 4 | import os.path | 5 | rootdir = r"D:/study" #根路径 | 6 | | 7 | if os.path.exists(rootdir): #判断根路径是否存在 | 8 | if not os.path.isdir(rootdir): #是否为文件夹 | 9 | print "Error: the same name file is existed,Please check" #如果不是文件夹报错 | 10 | else: | 11 | os.makedirs(rootdir) #如果不存在创建 | 12 | | 13 | os.chdir(rootdir) #切换到根目录 | 14 | cwd = os.getcwd() #获取当前路径 | 15 | | 16 | print '***current temporary directory***' | 17 | print cwd | 18 | | 19 | print '***original directory listing***' | 20 | print os.listdir(cwd) #输出当前目录下文件 | 21 | | 22 | print '***creating test file***' | 23 | fobj = open('person.txt','a') #以追加方式打开文件 | 24 | person = {} | 25 | person['name'] = raw_input("Your name:") | 26 | person['age'] = raw_input("Your age:") | 27 | person['notes'] = raw_input("Your words:") | 28 | | 29 | addlist = [key+":"+val+'\n' for key,val in person.items()] #将内容保存为列表 | 30 | | 31 | fobj.writelines(addlist) #写入数据 | 32 | fobj.close() | 33 | | 34 | print '***updated directory listing***' | 35 | print os.listdir(cwd) | 36 | | 37 | | 38 | | 39 | print '***full file pathname***' | 40 | path = os.path.join(cwd,os.listdir(cwd)[0]) | 41 | print path | 42 | print '***pathname,basename***' | 43 | print os.path.split(path) | 44 | print '*** (filename, extension) ==' | 45 | print os.path.splitext(os.path.basename(path)) | 46 | | 47 | print '*** displaying file contents:' | 48 | fobj = open(path) #输出文件内容 | 49 | for eachLine in fobj: | 50 | print eachLine | 51 | fobj.close() | 52 | while True: | 53 | delete = raw_input("Do you want delete(Y/N):") | 54 | if delete in ['Y','N']: #判断输入时候合法 | 55 | break | 56 | else: | 57 | print "Your input is illegal,try again" | 58 | if delete == 'Y': | 59 | print '*** deleting test file***' | 60 | os.remove(path) #删除文件 | 61 | print '*** updated directory listing***' | 62 | print '*** updated directory listing***' | 63 | os.chdir(os.pardir) #切换到上层目录 | 64 | print '*** deleting test directory***' | 65 | os.rmdir('study') #删除study文件夹 | 66 | print '*** DONE***' | 运行结果: ***current temporary directory*** | D:\study | ***original directory listing*** | [] | ***creating test file*** | Your name:Your age:Your words:***updated directory listing*** | ['person.txt'] | ***full file pathname*** | D:\study\person.txt | ***pathname,basename*** | ('D:\\study', 'person.txt') | *** (filename, extension) == | ('person', '.txt') | *** displaying file contents: | notes:I'm a boy! | | age:16 | | name:green | | Do you want delete(Y/N): | *** deleting test file*** | *** updated directory listing*** | [] | *** deleting test directory*** | *** DONE*** | | | 程序分析:在创建一个文件时一定要注意文件路径所在的父目录是否存在,因为open函数只会创建文件,路径不对的话是不能自动创建文件夹的。writelines函数只能对列表进行操作。在第29行中将存储学生信息字典进行遍历,拼接成字符串,并加上换行符。
8.7 文本处理举例----词频统计 在很多情况下会遇到这样的问题:对于一篇给定文章,希望统计其中多次出现的词语,进而概要分析文章内容,这就是“词频统计”问题。这个问题的解决有搜索引擎或其他类似工具对网络上的信息进行自动检索和归档。在信息爆炸时代,这种归档和分类十分必要。 首先分析词频统计的思路,即简单的统计文章中出现的单词,每出现一次,则该单词的次数加一。怎么存储这个次数呢,可以用数组存储,对单词进行字符串hash,以哈希值作为数组下标,这样可能需要很大的数组。当然,也可以运用以前学到的Python中的数据结构,字典就是类似的结构,可以把词语当作键,计数次数当作值,构成<词语>:<计数次数>的键值对。充分运用字典的优势。 下面采用字典来解决“词频统计”问题。该问题的IPO描述如下: 输入:从文件中读取一篇英文文章 处理:采用字典数据结构统计词语出现次数 输出:文章中最常出现的20个词语及出现次数,并输出文章总词语数量 由于英文文章以空格或标点符号来分割词语,而中文文章需要根据语义分割词语。为了避免引入“汉语分词”问题和相关技术,本例子仅处理英语文章。这里,以莎士比亚的《罗密欧与朱丽叶》作为输入,从网络上找到全文,并保存为RomeoandJuliet.txt,由于文中部分词汇及语法涉及到莎士比亚时代,与现代英语有区别,并没有对其进行排版。 对于从上述文件中获得每一个词语,如果该词语出现在字典结构中,则可以进行计数处理: 1 | counts[word] = counts[word] + 1 | 如果该词语没有出现在字典结构中(遇到一个新词语),则需要在字典中新建键值对: 1 | counts[new_word] = 1 | 这个处理逻辑可以用if/else结构进行处理,逻辑代码如下: 1 | if w in counts: | 2 | counts[w] = counts[w] + 1 | 3 | else: | 4 | counts[w] = 1 |

或者,这个处理逻辑可以调用字典的内建函数get(),代码如下: 1 | counts[w] = counts.get(w,0) + 1 | 这个语句表示,如果w在counts,则返回其值加1;如果w在counts中,则返回0并加1。 分割出英文文章中的词语是需要考虑的第一个问题,例如:同一个单词会存在大小写,但计数却不能区分大小写;输入的文件中会有一些特殊符号或标点符号,需要去掉。下面是实现这些功能的代码: 1 | f = raw_input("Input the file to analyze:") | 2 | txt = open(f,"r").read() | 3 | txt = txt.lower() | 4 | for ch in '!"#$%&()*+,-./:;<=>?@[\\]^_\'{|}]~': | 5 | txt = txt.replace(ch," ") | 6 | words = txt.split() | 7 | counts = {} | 8 | for w in words: | 9 | counts[w] = counts.get(w,0) + 1 | 执行上述代码,读入文件内容,其中特殊字符或标点符号被替换成空格;进而,将所有以空格分隔的单词保存在列表words中。通过字典counts实现对列表words中单词频率的统计。 如果需要输出前20个高频词语,需要对字典counts中的值信息进行排序。而字典中元素不存在顺序,更无法按照值大小进行排序。所以,排序操作可以考虑采用列表实现。实现过程如下: 1 | items =counts.items() | 2 | items.sort(key = lambda x:x[1],reverse = True) #以元组的第2列排序 | 3 | for i in range(20): | 4 | word,count = items[i] | 5 | print "{0:<15}{1:>5}".format(word,count) #输出前20高频词 | 其中range(20)返回的是列表,元素从0~19,用来表示列表前20个索引。print语句中“{0:<15}{1:>5}”含义为:第一个变量格式为左对齐,共15个位置;第二个变量格式对齐,共5个位置。该程序的完整代码如下: 【例8-6】对英语文章《罗密欧与朱丽叶》进行词频统计,输出使用最多的前20个单词。 问题分析:对英语文章进行词频统计时首先要考虑分词问题,然后是统计保存的格式,对统计数据进行排序输出。 1 | #!/usr/bin/env python | 2 | #cipin.py | 3 | f = raw_input("Input the file to analyze:") #输入文件路径 | 4 | txt = open(f,"r").read() #以只读方式打开文件 | 5 | txt = txt.lower() #将文本转换为小写 | 6 | for ch in '!"#$%&()*+,-./:;<=>?@[\\]^_{|}]~': | 7 | txt = txt.replace(ch," ") #将文本中的特殊符号替换为空格 | 8 | words = txt.split() #将文本字符串以空格为标志分隔成列表 | 9 | counts = {} | 10 | for w in words: | 11 | counts[w] = counts.get(w,0) + 1 #以字典存储词频 | 12 | | 13 | items =counts.items() #将字典转换为列表,元素为元组 | 14 | items.sort(key = lambda x:x[1],reverse = True) #以元组的第2列排序 | 15 | for i in range(20): | 16 | word,count = items[i] | 17 | print "{0:<15}{1:>5}".format(word,count) #输出前20高频词 | 运行程序后,输入文件路径如F:\book\RomeoandJuliet.txt。 运行结果: total words count is: 26800 | and 720 | the 681 | i 658 | to 577 | a 470 | of 401 | my 361 | that 355 | is 349 | in 320 | you 295 | s 293 | thou 278 | me 265 | not 260 | with 255 | d 236 | it 228 | this 226 | for 225 |
8.8 五子棋游戏保存读取功能 在运行五子棋程序时,由于某些原因在游戏未出现胜负时需要退出程序,这时候就需要保存游戏的状态。在下次运行五子棋程序时,可以从上次保存的文本文件中读取保存的状态,从而恢复上次的游戏状态。 在保存五子棋时首先要确定需要保存的状态,这里需要保存棋盘的长度maxx,棋盘的宽度maxy,棋盘每个位置的状态数组qipan,当前下棋者who。这些参数都是GoBang类中的属性,因此保存GoBang类就可以了。如果能将参数以序列化的形式保存在文本中,在读取时就能方便操作。但是在写入文件时,文本是依次输入依次读出输出的。那如何才能避免这种顺序,如同字典一样只要知道关键字就能得到对应的值呢? Python提供了pickle模块,通过pickle模块中的dump和load函数,能实现对数据的序列化存储与读取。首先定义保存的对象,这里定义了GoBang类,如下: 1 | class GoBang(object): | 2 | def __init__(self, maxx, maxy): #构造方法 | 3 | self.maxx, self.maxy = maxx, maxy #设置棋盘尺寸 | 4 | self.qipan = [[0] * maxy for i in xrange(maxx)] #初始化棋盘 | 5 | self.who = False #游戏角色变量 | 当输入是’S’时会调用save函数,保存当前游戏状态。 1 | if t == 'S': | 2 | self.save() | 3 | continue | 在save函数中通过dump函数将GoBang类按照特定格式保存到文件中。 1 | def save(self): | 2 | fpath = raw_input('请输入保存文件路径:') | 3 | file = open(fpath, 'w') #以写的方式打开文件 | 4 | pickle.dump(self, file) #存储游戏状态 | 5 | file.close() | 当输入是’L’时会调用load函数,读取保存的游戏状态。 1 | if t == ' L': | 2 | status = self.load() | 通过load函数读取保存的文件,函数返回一个GoBang类。 1 | def load(self): | 2 | fpath = raw_input('请输入读取文件路径:') | 3 | file = open(fpath,'r') #以读的方式打开文件 | 4 | status = pickle.load(file) #获取保存的游戏状态 | 5 | return status | 将返回类变量的值赋给游戏中的变量就能恢复游戏状态。 1 | status = self.load() | 2 | self.maxx = status.maxx | 3 | self.maxy = status.maxy | 4 | self.qipan = status.qipan | 5 | self.who = status.who |
本章小结
本章中介绍了如何通过文件对象和类文件对象与环境互动,I/O也是Python中最重要的技术之一。使用file对象可以向文件中写入字符串,并且一次性地或逐行读取文件中的内容。可以使用这些技术读取一个程序的输入,生成输出文件,或者存储中间结果。下面是本章的关键知识:(1)文本文件只能存储常规字符串,通常以换行符'\n'作为行的结束。文本文件可用文本编辑器进行编辑。(2)二进制文件可以直接读写字符串,读写其他对象时必须进行转换。(3)读、写内容都会自动移动文件指针。(4)当结束读文件时,确保要显示地关闭该文件。
习题
* 填空题 1. Python语言中系统的标准输出文件是指____________。 2. 若要用open函数打开一个新的二进制文件,该文件要既能读又能写,则打开方式是____________。 3. Python语言可以处理的文件类型是____________和____________。 4. writelines函数能操作的对象是____________。 5. walk函数返回的三个值分别是____________、____________和____________。

* 选择题 1. 下述关于Python语言文件操作的结论中,( )是正确的。 A. 对文件的操作必须先关闭文件 B. 对文件的操作必须先打开文件 C. 对文件操作无顺序要求 D. 对文件操作前必须先测试文件是否存在,然后再打开文件 2. 如果需要打开一个已经存在的非空文件“FILE”并进行修改,正确的打开语句是( )。 A. f=open(“FILE”,“r”); B. f=open(“FILE”,“ab+”); C. f=open(“FILE”,“w+”); D. f=open(“FILE”,“r+”); 3. 在高级语言中,对文件的操作一般步骤是( )。
A.打开文件—操作文件—关闭文件
B.操作文件—修改文件—关闭文件
C.读写文件—打开文件—关闭文件
D.读文件—写文件—关闭文件 4. 若要以“a+”方式打开一个已存在的文件,则以下叙述正确的是( )。
A.文件打开时,原有文件内容不被删除,位置移动到文件末尾,可做添加和读操作
B.文件打开时,原有文件内容不被删除,位置移动到文件开头,可做重写和读操作
C.文件打开时,原有文件内容被删除,只可做写操作
D.以上各种说法都不正确 5. 以下叙述中错误的是( )。
A.二进制文件打开以后可以先读文件的末尾,而顺序文件不可以
B.在程序结束时,应当用函数close()关闭已打开的文件
C.向文本追加内容时可以以'w'模式打开
D.格式化字符串时若对象是列表则每个元素都会单独输出成字符串

* 上机题 1. 将10个整数写入数据文件test.txt中,再读出test.txt中的数据并求和。 2. 一个以%5d格式存放20个整数的文件test.txt,顺序号定为0~19。输入某一顺序号之后,读出相应的数据并显示在屏幕上。 3. 将10名职工的数据从键盘输入, 然后送入磁盘文件worker1. txt 中保存。设职工数据包括:职工名、年龄、工资,再从磁盘调入这些数据,依次打印出来(用 read和write函数)。 4. 将存放在worker1.txt中的职工数据按工资高低排序,将排好序的各记录存放在worker2.txt中(用 read和write函数)。 5. 用scanf函数从键盘读入 5个学生数据(包括:学生名、学号、三门课程的分数),然后求出平均分数。用 write函数输出所有信息到磁盘文件stud.txt中,再用 read函数从 stud.txt中读入这些数据并在显示屏上显示出来。

第9章 面向对象编程
在前面的章节中,读者已经学习了Python提供的一些内置类型,诸如元组,列表,字典以及字符串。使用这些类型,再辅以三种流程控制语句,程序设计者就能编写出复杂而又高效的程序。但是在实际的程序开发中,仅使用这些内置类型去描述现实中的事物是非常乏力的。面向对象编程为程序设计者提供了一种新的编程范式,让程序设计者可以自己设计类型,并使用这些类型来描述业务中的具体事物。本章将着眼于面向对象编程中三个主要的特征——类与封装,继承以及多态,进行叙述。由于Python是一门解释性语言,所以Python的面向对象编程与C++,Java等编译性语言的面向对象编程有很大不同,若读者曾经接触过编译性语言的面向对象编程,请在阅读本章的过程中注意区别。
【体系结构】

【本章重点】
(1)掌握Python中类的定义方式;
(2)掌握Python中类的实例化方式;
(3)掌握类中实例成员和类成员的用法和区别;
(4)掌握类的初始化以及构造方法使用的技巧;
(5)了解类中property修饰器的使用方法;
(6)掌握类的继承以及使用的目的;
(7)了解类的多重继承;
(8)了解Python多态的定义以及使用场合。
【案例引入】重构五子棋——封装成类
第7章已经基本完成了五子棋游戏的所有逻辑,并且实现了函数的封装。本章介绍的面向对象编程是一种更加高级的抽象,是对函数抽象的扩充。通过本章的学习,读者将了解如何将面向对象编程思想融入到平时的程序设计中,并且感受面向对象编程在处理这类现实问题时所表现的简练和高效。
9.1 类
类(class)的出现是区别面向对象语言和面向过程语言的一个重要的体现,虽然它们的区别远远不止于此。
在之前的章节中,一个重要的概念就是抽象。把一段经常会用到的代码段,抽离出来当成一个包含特定功能的完整函数。当需要用到这个功能时,不需要再重复的粘贴代码段,而是调用这个函数,达到代码逻辑清晰且代码高效复用的目的。
事实上,对于一个具体事物的描述,通常包含两个方面:特征与行为。当描述一个人的时候,除了需要记录这个人的身高体重这些静态特征之外,通常还要描述他的行走跳跃这些动态行为。显然,函数只能做到对于行为的抽象,而无法持有单独的特征数据。
所以更加高级的抽象应该是一组数据和一组行为的有机结合,行为参考数据,数据影响行为。
在Python中,提供了class关键字让用户可以创建自己的类。Python中声明类的格式如下:
【例9.1】如何声明类。 1 | class ClassName(SuperClass1, SuperClass2): | 2 | """ | 3 | optional documentation string | 4 | """ | 5 | Class_suite |

声明类的方法如例9.1:使用class关键字,然后接类名,在其后的小括号是一个元组,元组内是该类需要继承的超类列表(关于继承将在9.3提到),该行要以冒号结尾。接下来是一块代码,它们就是类的主体部分,包含类的成员。
如果定义的类没有超类,超类列表为空时,可以用object关键字作为补充。object类是Python中所有类的默认超类。当然还有一种写法即是在没有超类时,直接省略括号以及括号中的超类列表,但是这种格式的类被称为旧式类。新式类与旧式类是Python版本更迭的产物,在使用时的区别主要表现在对超类命名空间检索时使用的算法不同。本书所有类的定义全部使用新式类。如果读者对这部分内容有兴趣,可以查阅相关资料。
9.2 类的创建
本章主要介绍面向对象编程,而至今还未提到何为对象。在详细说明类的创建之前,先解释对象的定义是很有必要的。
在Python中,一切可操作的实体皆对象:列表是对象,函数是对象,包括类本身也是对象。每一个对象总是与一个类或类型关联,对象由类或类型定义,而每一个对象都是其类或类型的一个实例。
如果在程序中有一个名为Bird的类,则在程序中每一个Bird类的实体都是一个实例对象。创建对象的方式如例9.2所示:
【例9.2】如何创建一个类的实例对象。
问题分析:创建Bird类的三个实例对象,然后全部装入一个list中。
1 | b1 = Bird() | 2 | b2 = Bird() | 3 | b3 = Bird() | 4 | a = [b1, b2, b3] |
程序分析:例9.2展示了对象的创建方式。在Python中,创建一个类的实例对象,只需调用其构造方法即可。Python中在外部调用类的构造方法名与类名相同,在无特别声明与定义的情况下,默认的构造方法是无传入参数的,如Bird()。所以上述程序中的b1,b2,b3都是绑定了Bird类实例对象的引用。相应的对于列表a,它是由三个Bird对象组成的列表。在Python中,类的每一个实例对象都维护了属于自己的命名空间以及数据集,同一个类的不同对象之间是相互独立的存在。
在Python中,类与类型是有区别的,虽然这种区别正在慢慢的被淡化。一般称内建的对象是基于类型的,而自定义的对象一般基于类。或者说类型来自于Python内建,而类由用户自己定义。本节接下来的内容将主要围绕自定义类进行介绍。
9.2.1 创建类
在Python中,创建一个类是极为简单的,并不像别的面向对象语言中那样被太多的规则约束。Simple is better than complex在Python类的设计中得到完美的诠释。
假如现在需要对Bird这个类进行少许的扩充,即对其补充一个位置坐标的属性,然后利用这个位置属性完成一个移动的动作,则Bird类可以设计如下:
【例9.3】创建一个包含三个实例方法的Bird类。
问题分析:实现一个Bird类,分别包含setCoordinate,setCoordinate以及move三个实例成员方法。
1 | class Bird(object): | 2 | def setCoordinate(self, x, y): | 3 | self.x, self.y = x, y | 4 | def getCoordinate(self): | 5 | print (self.x, self.y) | 6 | def move(self, dx, dy): | 7 | self.x += dx | 8 | self.y += dy |
程序分析:在这个Bird类中有三个长得很像前文函数的东西存在,它们作为类的成员之一,被称为绑定在这个类上的实例方法。之所以称之为实例方法,完全是因为调用这些方法需要传入一个名叫self的参数。在实际使用中,如果一个Bird的实例对象调用了move方法,则self会绑定在了这个实例对象后作为第一个参数传入方法中。Python对于实例方法的第一个参数名没有严格约束,但是一般情况下,习惯使用self作为第一个参数名。
在程序中定义了Bird类后,就可以创建Bird类的实例对象,并且完成一些简单的操作。 >>> | bird = Bird() | >>> | bird.setCoordinate(1, 1) | >>> | bird.getCoordinate() | (1, 1) | >>> | bird.move(1, 2) | >>> | bird.getCoordinate() | (2, 3) |

上述操作向我们展示了如何利用Bird类的实例对象调用类的实例方法。对于绑定了Bird类实例对象的引用bird,调用实例方法的格式是:实例对象的引用名连接一个“.”,然后再连接上具体的实例方法名。在调用实例方法时,self参数并不需要显式传入,所以参数列表只需传入self之后参数的实参即可,而self参数即为bird本身。 self参数的存在使得实例方法可以访问该实例对象本身:不仅可以调用该实例对象的属性变量值,还能调用该实例对象的其它实例方法。 在Bird类中,除了有三个实例方法外,还有若干属性变量,如x坐标以及y坐标,它们被称为实例成员变量。这些属性变量也如实例方法一样,在每一个Bird类的实例对象中是相互独立,互不相同的。
在类的外部,也可以用类似调用对象实例方法的方式来访问对象的实例成员变量:
>>> | bird = Bird() | >>> | bird.x, bird.y = 1, 1 | >>> | print (bird.x, bird.y) | (1, 1) | >>> | bird.x += 1 | >>> | bird.y += 2 | >>> | bird.x, bird.y | (2, 3) |
访问实例成员变量的格式是:实例对象的引用名连接一个“.”,然后再连接上具体的成员变量名称。
显然上述两种访问方式(通过方法访问成员变量和直接访问变量)是具有完全相同功能的。在面向对象编程中,一个重要的原则是封装,即对类外部隐藏内部具体的实现。类的成员变量不像成员方法那样一般是只读的,类的设计者以及创建者也并不希望成员变量在不加监测和验证的情况下就被人直接访问和修改。所以对成员变量的访问应当尽量避免在类外直接的访问。
当然上述说法只是情理上的约束,Python在语法上也有对类中重要成员变量的保护。如果类的设计者不希望类的使用者在外部直接访问成员变量,可以将该变量设置为类的私有变量,格式为在变量名前加上双下划线。具体示例如下:

1 | class Bird(object): | 2 | def setCoordinate(self, x, y): | 3 | self.__x, self.__y = x, y | 4 | def getCoordinate(self): | 5 | print (self.__x, self.__y) |

对于Bird的私有成员变量,只能由实例方法访问和修改,而无法在类外直接访问。对于类的外部来说,私有成员是不可见的。除了类的成员变量可以设置为私有外,类的方法也可以设置为私有。私有成员方法也只能在类内被调用。

>>> | bird = Bird() | >>> | bird.setCoordinate(1, 1) | >>> | bird.getCoordinate() | (1, 1) | >>> | bird.__x, bird.__y | Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> bird.__x, bird.__yAttributeError: 'Bird' object has no attribute '__x’ | 通过上述调用过程可以发现,如果在类外直接访问私有变量,Python解释器会抛出一个运行时错误,显示Bird实例对象中没有相应属性。
如果深究Python对于私有成员的所谓“私有”的实现,读者会发现,这种“隐藏”不是真正的隐藏。Python解释器对类中所有加了双下划线的成员名会做一些名字上的替换,如果存在一个成员的名字为__fooName,则Python解释器会在检测到它存在后将其替换为_ClassName__fooName,即在成员名前加上单下划线开头的类名。
所以类外部的用户直接访问bird._Bird__x,那么Bird类的设计者对此也将毫无办法。所以Python的“私有”变量并不是绝对私有的。
9.2.2 类变量和类方法 Python除了能在类中定义实例成员变量和实例方法外,还可以定义类的类变量和类方法。顾名思义,这二者与类直接有关。类变量和类方法将在类被定义后在内存中占用一部分空间,而与类的实例化次数无关。类变量和类方法与C++中类的静态变量和静态函数很类似。 对例9.3中Bird类进行扩充如下: 【例9.4】添加了类变量和类方法后的Bird类。
问题分析:实现一个Bird类,包含name_zh,name_en两个类成员变量,以及getName,setName两个类成员方法。
1 | class Bird(object): | 2 | name_zh = u"鸟类" | 3 | name_en = u"bird" | 4 | | 5 | @classmethod | 6 | def getName(cls, type): | 7 | if type == "zh": | 8 | return cls.name_zh | 9 | else: | 10 | return cls.name_en | 11 | @classmethod | 12 | def setName(cls, type, new_name): | 13 | if type == "zh": | 14 | cls.name_zh = new_name | 15 | else: | 16 | cls.name_en = new_name |
程序分析:在例9.4中,name_zh和name_en都是类变量,它们在每个类中唯一存在。类变量需要在定义类的时候就被声明和定义。getName函数为类方法,它与实例方法的区别是,需要在为方法添加名为classmethod的修饰器。与实例方法类似,类方法也有一个默认传入的参数——cls。cls是class的简写,也预示着这个方法传入的第一个参数是类对象而非类的实例对象。

下面将展示在类外如何调用和方法这些类方法和类变量。 >>> | Bird.getName("zh") | u'鸟类' | >>> | Bird.getName("en") | u'bird' | >>> | Bird.name_zh | u'鸟类' | >>> | Bird.name_en | u'bird' | >>> | Bird.setName("en", "birdbird") | >>> | Bird.getName("en") | 'birdbird' | >>> | Bird.name_zh = "新鸟类" | >>> | Bird.name_zh | '新鸟类' |
在上述程序中,Bird即为Bird类的类对象。Python为一切实体都创建了对象,类本身也是一个对象,Python中类对象名与类名相同。
与实例对象访问实例成员相同,类对象访问类变量或类方法的格式是:类对象名连接一个“.”,然后再连接上具体的属性变量名称。类变量和类方法也可以通过加双下划线前缀的方式转变为对类外隐藏的私有成员。
在Python中,类变量和类方法也能被实例成员方法访问,但是只具有读的权限,而无写的权限。
下列程序将展示如何使用实例对象访问类变量和类方法。 >>> | class Bird(object): | | name = u"bird" | | | | @classmethod | | def getName(cls): | | return cls.name | | | | def getName2(self): | | print "directly:", self.name | | print "by call classmethod: ", self.getName() | | | | | >>> | bird = Bird() | >>> | bird.getName2() | directly: bird | by call classmethod: bird | >>> | bird.name | u'bird' | >>> | bird.getName() | u'bird' |
上述程序中,bird作为Bird类的一个实例对象,可以通过bird的实例方法getName2去访问Bird类的类变量name以及类方法getName。也可以直接使用bird对象的引用去访问类变量和类方法。
在定义一个类的时候,Python是允许类中出现相同名字的类变量以及实例变量的。这时,不同对象在访问这些变量时就会出现不同的优先级。类对象或类方法只能访问类变量而不能访问实例变量;实例对象或实例方法在访问变量的时候,会优先访问实例变量的值,只有当实例变量命名空间中不存在这个名称的变量时,才会尝试去类变量的命名空间中去查找这个名称的变量是否存在。
由于这个优先级的存在,以及实例对象和实例方法对类变量只具有读权限的限制,在Python程序中往往会出现让人困惑的“bug”。
>>> | class Bird(object): | | name = u"bird" | | def setName(self, new_name): | | self.name = new_name | >>> | bird = Bird() | >>> | bird.name | u'bird' | >>> | Bird.name | u'bird' | >>> | bird.setName("birdbird") | >>> | bird.name | 'birdbird' | >>> | Bird.name | u'bird' |
在上述程序中,在未调用bird.setName()方法之前,实例对象和类对象在访问变量name时,显示的值都是类变量name的值。当调用了bird.setName()后,二者再次访问变量name出现了分歧。原因就在于调用bird.setName()方法时,对self.name进行了修改操作,实例对象bird在发现本次并非读操作,且实例对象命名空间中并没有name这个变量时,则创建了一个名叫name的实例变量。所以在此之后,由实例对象bird访问name,显示的都是实例变量的值;而由类对象Bird访问name依然是类变量的值。
9.2.3 静态方法
在Python的类中,还有一种不同于实例方法和类方法的成员函数,名为静态方法。静态方法是位于类命名空间中的一种特殊函数,它不像类方法和实例方法那样会将调用它的对象当成第一个参数传入,所以静态方法无法对任何实例成员进行操作。
在使用上静态函数与一般的模块全局函数差不多,唯一的区别只是所处命名空间的不同,所以在调用静态函数的时候,需要由类对象或者实例对象来调用。
由于静态方法并不需要像类方法那样去频繁访问类变量,所以拥有大量静态方法的类,往往更像是一个静态方法的打包。比如要实现一个类作为数学函数的集合,那么集合中的那些相互独立的数学函数就能分别实现为类中的静态方法。这个类的实现方法如下:
【例9.5】使用静态方法封装了数学公式的Math类。
问题分析:实现一个Math类,分别包含pow,sqrt以及log三个静态方法。
1 | class Math(object): | 2 | @staticmethod | 3 | def pow(x, y): # 求实数的幂 | 4 | pass | 5 | @staticmethod | 6 | def sqrt(x): # 求实数的开根号 | 7 | pass | 8 | @staticmethod | 9 | def log(x): # 求实数的对数 | 10 | pass |
程序分析:例9.5中的Math类集成了数学函数中的三个经典运算作为它的静态方法成员,这三个方法也因此被集成到了同一个命名空间中。和类方法类似,当一个函数需要被声明为静态方法时,只需要为这个函数添加名为staticmethod的修饰器即可。当这个类被实现后,用户需要调用相应数学运算如开根号时,只需要调用Math.sqrt即可。
在Python中,类的实例对象也是可以调用类中的静态方法的。
虽然静态方法无法操作实例变量和实例方法,但是对于类变量和类方法,还是有办法可以去访问的,那么就是像类外访问那样直接使用类对象去访问这些类变量和类方法。比如使用静态方法实现例9.4中Bird类的setName类方法:
【例9.6】添加了静态方法的Bird类。
问题分析:实现一个Bird类,包含一个名叫setName的静态方法。
1 | class Bird(object): | 2 | name = u"bird" | 3 | @staticmethod | 4 | def setName(new_name): | 5 | Bird.name = new_name |

对于例9.6中静态方法的调用过程如下所示。 >>> | Bird.name | u'bird' | >>> | Bird.setName('birdbird') | >>> | Bird.name | 'birdbird' |
例9.6中的静态方法setName做的了同例9.4中同名类方法相同的功能,而且在类外,调用方法完全相同,这就会给人一种类方法和静态方法功能完全相同的错觉。虽然在一定程度上类方法和静态方法确实可以相互取代,但它们的相同终究只是一种语法上的巧合。Python的设计者在设计这二者的时候一定考虑过它们使用时的场合,读者们可以在平时的练习和使用中仔细的体会区别。
9.2.4 property修饰器
本小节将回到私有变量访问这个话题上。如前文所说,类的设计者通过将变量设置为私有来防止类外的直接访问。对于一个私有变量X的读写,可以通过两个实例方法setX和getX来间接完成。这种方式确实在对外开放读写权限与安全性之间找到了一个解决方案,但却给类的使用者造成了困扰。毕竟对于访问类内的一个成员变量,类外的用户还是更习惯于直接访问class.X而非调用class.getX实例方法。
为了让类的使用者可以有更友好的访问方式,Python提供了property修饰器来修饰实例方法,以改变这些方法对外的访问方式,使得它们在被外部访问时更像是在访问一个变量,而非函数。
【例9.7】添加了property对象的Bird类。
问题分析:实现一个Bird类,包含一个名叫name的property对象,并且支持读,写,删三个功能。 1 | class Bird(object): | 2 | def __init__(self, name): | 3 | self.__name = name | 4 | @property | 5 | def name(self): | 6 | return self.__name | 7 | @name.setter | 8 | def name(self, name): | 9 | self.__name = name | 10 | @name.deleter | 11 | def name(self): | 12 | del self.__name |
程序分析:例9.7中,使用property修饰器修饰了一个名叫name的实例方法,这个name方法类似于之前的getName方法,是一个为私有变量__name提供了读权限的实例方法。在有了name这个property成员后,类的外部便可以将name当成一个假想的成员变量一样访问。当然仅限于property修饰器修饰过的方法,且该私有变量是只读的。若需要为这个私有变量增加额外的修改和删除权限,只需要为这个property在添加相应的setter和deleter以及被它们修饰方法即可,格式如例9.7中后两个name实例方法。
有了以上实现,那么在类外对私有变量__name就可以如下列程序展示的一样访问和操作了。 >>> | bird = Bird("bird") | >>> | bird.name | 'bird' | >>> | bird.name = "birdbird" | >>> | bird.name | 'birdbird' | >>> | del bird.name | >>> | bird.name | Traceback (most recent call last): File "<pyshell#97>", line 1, in <module> bird.name File "<pyshell#91>", line 6, in name return self.__name |

从上述程序中可以看出,Bird类中名叫name的property对象完全成为了__name的替身,所以对__name的操作都可以通过对name的间接操作来完成。有了name作为访问的中间层,一些验证的逻辑就可以在name的读写方法中实现了。
由于property修饰器是在Python2.6及更高版本中出现的,所以在旧式类中使用property修饰器会出现一些不明的错误。所以对于使用property修饰器的类,请确保它是一个新式类,以规避一些因为版本迭代而出现的问题。
9.2.5 类的初始化
在介绍本节对象的部分中,曾简要的用到了类的构造方法。但是当时的程序只是对它进行了调用,却没有写出它的实现。
在Python中,为一个类创建一个实例对象时,会立刻调用这个类的构造方法。Python为类提供了一个名叫__init__的关键字来定义类的构造方法。换句话说,当对象被创建时,Python解释器会去类中查找名叫__init__的函数。
其实,在之前章节实现的类中,并没有显式的去实现构造方法,一直在调用Python为每个类默认设置的超类的构造方法。这个构造方法只是单纯的为这个实例对象在内存中构建一个对象所有最基本的元数据,而没有在对象创建的初始就添加一些特征属性。
如果在创建一个对象的初始就希望为这个对象添加一些实例变量,那么这个构造方法需要类的设计者重新实现。比如对于前文中的Bird类,如果希望在创建对象的时候就立即完成对实例变量name的创建和初始化,则需要为Bird类的构造方法赋予一些初始化的功能。 >>> | class Bird(object): | | def __init__(self, name): | | self.name = name | | | | | >>> | bird_1 = Bird("bird_1") | >>> | bird_2 = Bird("bird_2") | >>> | bird_1.name | 'bird_1' | >>> | bird_2.name | 'bird_2' |
如上述程序中Bird类的构造方法,__init__函数的第一个参数也需要是绑定了实例对象引用self(当然self同样也是一种习惯命名,并不是强制规定的关键字)。而在创建对象的时候,只需要将除了self以外的实参传入即可完成一次对构造方法的调用。
由于Python规定一个类只允许有一个构造方法存在,无法像C++语言那样提供函数重载的机制,所以构造方法设计的不够灵活,也会给类的实例化造成极大的不便。比如前文Bird类的构造方法强制类的实例化过程必须传入一个参数,那么对于那些创建对象过程中并不想做过多初始化来说,这里显然做了很多冗余的工作。
当然上述问题可以通过一些特有的编程技巧来解决。
1 | def __init__(self, name = None): | 2 | if name: | 3 | self.name = name |
上述程序提供了一种解决方案,即使用可选参数的方式,这样就相当于为类设计了两个版本的构造方法,一个带参,一个无参。
但是上述问题只是众多类实例化问题中的冰山一角。更普遍的问题是,连类的设计者都不清楚实例化的过程中,会向构造方法内传入参数的个数。所以更普适的做法是如下列程序一般将构造方法的参数表设计为可变长参数表。 >>> | class Bird(object): | | def __init__(self, *args, **kwargs): | | if kwargs.get('name', None): | | self.name = kwargs['name'] | | if kwargs.get('weight', None): | | self.weight = kwargs['weight'] | | | | | >>> | bird_1 = Bird(name = "bird_1", weight = 1) | >>> | bird_1.name | 'bird_1' | >>> | bird_1.weight | 1 |
9.3 继承 面向对象编程的主要内容都集中在了类的设计上,一个高度抽象的类,可以让这个类的使用者不需要再去关心代码的具体实现,只关注于与功能有关的方法以及传参规则。
但是对类的设计仅限于此,未免也过于简单。在遇到真正的工程设计时,不同的逻辑实体之间是存在联系的。这种联系一般基于下面两个事实:
1. 两种逻辑实体间存在包含与被包含的关系,如鸟类之于禽类,如建筑之于大桥;
2. 两种逻辑实体间存在明显的共性,如自行车和汽车都靠轮子移动,如猫类和犬类都有尾巴。
如果忽略上述联系,而对每个逻辑实体独立的设计类,这种设计显然做了大量重复工作,不符合程序设计的基本原则。
面向对象编程针对上述问题,为类添加了一个名叫继承的功能。对于包含与被包含:如果已经设计了鸟类,那么在设计禽类的时候,只需要考虑禽类之于其它鸟类比较特殊的特征和行为,剩下来的可以直接从鸟类那里继承过来;对于存在共性:那么可以将这些共性都抽象出来,作为一个类似接口的存在,再设计完这些接口后,再分别设计两个类的不同部分,而共性部分只需从接口中继承过来。
在Python中,对于存在继承关系的两个类,一般称被继承者为超类(或者基类、或者父类),继承者为子类。
9.3.1 继承与重写 假设当前业务需要在已有的Animal类基础上扩展出Bird类和Dog类,按照生物学的关系来说,显然后两者是Animal类的子类。对于Animal类的设计,显然只需要把动物的一些基本特征和行为定义即可,比如进食,繁衍。而对于鸟类独有的飞行以及犬类独有的奔跑,则可以分别放到Bird类和Dog类中取实现。这三个类的简单实现如下: 【例9.8】类的继承实例。
问题分析:实现三个类:Animal,Bird,Dog,并且令后两者继承Animal类。
1 | class Animal(object): | 2 | def __init__(self, *args, **kwargs): | 3 | pass | 4 | def eat(self): | 5 | print "Animal is eating" | 6 | def reproduce(self): | 7 | print "Animal is reproducing" | 8 | | 9 | class Bird(Animal): | 10 | def __init__(self, *args, **kwargs): | 11 | Animal.__init__(self, *args, **kwargs) | 12 | def fly(self): | 13 | print "Bird is flying" | 14 | def reproduce(self): | 15 | print "Bird is reproducing" | 16 | | 17 | class Dog(Animal): | 18 | def __init__(self, *args, **kwargs): | 19 | Animal.__init__(self, *args, **kwargs) | 20 | def run(self): | 21 | print "Dog is running" | 22 | def eat(self): | 23 | print "Dog is eating" |
程序分析:在例9.8中,由于Animal类为Dog类和Bird类的超类,所以在定义Bird类和Dog类时,需要在类名后括号中添加Animal类。在Python中,子类将继承超类中的所有成员属性。比如Bird类中虽然没有实现eat方法,但是它却从超类Animal类中继承了eat方法,所以Bird类的实例对象同样可以调用eat方法。Python允许子类出现和超类相同名称的方法名和变量名,当然这种共存的现象是在一方妥协的前提下完成的,即子类中的同名方法会对超类中的同名方法进行覆盖。这种在继承中的同名覆盖机制被称为重写。这项机制对于所有方法都有效,包括构造方法。但是子类的构造方法需要对超类构造方法进行显式调用,并且将当前对象当做第一个绑定参数传入。
例9.8中,三个类的成员方法构成如图9.1所示。 图9.1 类继承中类成员方法归属图示

9.3.2 多重继承
当一个实例对象需要访问命名空间中的一个属性时,首先会在子类的命名空间中查找,若存在,则直接返回该属性;若不存在,则将查找过程递归到该子类的最近超类中进行。在单一继承即一个子类只有一个超类的情况下,整个类图是一个树状的结构,所以自底向上的查找路径也是唯一的;但是如果一个子类存在多个超类,则类图结构会变得非常复杂,则查找超类的过程就存在一个优先级问题以及防止重复遍历的问题。鉴于多重继承中的诸多问题,对于设计者和使用者都具有很大的挑战性,所以一般情况下,是不推荐在程序中大量使用多重继承的。
当然作为一个工具,多重继承在某些场合还是具有其独特的优势的。如果现在要对鸟类和汽车分别设计Bird类和Car类,显然二者无法像Bird和Dog那样简单的在生物种属上找到一个共同的超类,所以对二者的超类设计无法在现实中直接找到参照物。如果在对Bird类和Car类的设计中需要分别实现二者移动以及发声这两个行为,那么完全可以把移动和发声作为两个超类抽象出来,然后让Bird类和Car类去分别继承这两个超类。
【例9.9】多重继承示例。
问题分析:实现四个类:Sound,Movement,Bird和Car。将Sound和Movement作为超类进行实现后,令Bird和Car分别继承Sound和Movement类。 1 | class Sound(object): | 2 | def sound(self, voice): | 3 | print voice | 4 | | 5 | class Movement(object): | 6 | def move(self, target_x, target_y): | 7 | self.x, self.y = target_x, target_y | 8 | | 9 | class Bird(Sound, Movement): | 10 | pass | 11 | | 12 | class Car(Sound, Movement): | 13 | pass |
程序分析:在例9.9中,Bird类和Car类通过对Sound和Movement两个类的继承完成了发声和移动两个特性的获取。在面向对象编程中,这种将某个特性抽象出来作为一个超类供子类继承的技巧使用的很普遍。由于这些特性本身没有实际意义,更多的是功能性的,所以它们一般都不会被实例化,所以在C++语言中,对这种类称为抽象类,在Java中,更是把它设计成了一种叫做接口的东西配合多重继承来使用。
例9.9中的继承关系可由图9.2表示。

图9.2 多重继承关系图示

在定义多重继承的时候,引入超类的顺序对于实例对象查找属性名称是有影响的。以程序9.9为例,若发现Car类的命名空间中不存在需要的属性名,则会优先从超类sound类中取查找。
9.4 多态 多态是面向对象编程中最实用的一个特性,前面做的很多工作其实都是为了让对象的使用者可以体验到多态的便利。 在Python中,一个很重要的特性就是动态类型:一个引用可以绑定任意类型的对象。换个方面说这样做的影响就是对于一个引用,在运行时往往不能确定它的类型。 1 | def readFile(file): | 2 | return file.read() |

对于上述程序中readFile函数,从函数名以及传入参数名,可以很轻易的了解到这个函数的功能时得到一个文件对象的内容。但是在这个函数内是没有任何类型检查的:如果传入函数的file引用绑定的并不是文件对象,而是一个其它类型的对象,那么直到参数被传入函数后,程序都不会有任何异常。如果恰好传入的对象存在名为read的绑定方法,那么这个函数还会很神奇的运行成功,并且有相应的返回值。 这在其它静态类型语言看来,是灾难性的,但是对于动态类型语言来说,它却是一个非常有用的特性。 如果有一个盒子,盒子里有很多物品,有收玩具,有足球,有书,等等。现在想知道这个盒子里所有东西的总重量。如何在程序中对这个过程进行实现呢? 假设每个物品都是对应类型的一个实例,而盒子就是保存了这些对象的一个列表。那么首先可以实现一个函数来计算一个物品的重量: 1 | def getWeight(obj): | 2 | return obj.weight | 对于上述程序,得有一个提前的约定,即对于所有涉及到的类中,都必须存在weight属性。有了这个函数后,计算总重量的代码就能一行解决了。 1 | box = [toy, football, book] | 2 | print sum(getWeight(obj) for obj in box) |

在这个装满实例对象的列表中,所有元素再也没有类型之分,只有是否存在weight属性之分,这就是多态的一种表现。
Python的另一类多态可以表现在运算符上,对于一个加法运算式x + y,如果x和y分别是数字对象,那么这个语句会返回两个数字之和;如果x和y分别是字符串对象,那么这个语句会返回两个字符串拼接后的新字符串。一切还是源于没有类型检查,所以直到运行到加法这个语句,Python解释器都不能确定符号两端的对象分别是什么类型的,唯一的做的就是去两个对象的命名空间中取检查是否有和“+”运算相关的实现。
如果在程序9.22中,在列表中存在一个物品,没有weight这个属性;或者在x + y中,x为数字对象,而y为字符串对象,那么程序在运行到相关语句的时候,都会抛出运行时错误。所以Python这种动态类型设计所带来的多态特性,也并不是完美的,它的高效和稳定依赖于详细的解释文档,良好的编程习惯以及充分的代码测试。
9.5 重构五子棋
相信读者们都已经在本书之前的章节引导下完成了属于自己的五子棋游戏。本章介绍的面向对象编程是一种更高级的抽象,用本章学习的知识来重构之前的五子棋代码将会是一种全新的体验。
通过对游戏逻辑的分析,可以发现,在游戏中主要有四个逻辑实体:分别是玩家、棋盘、文件存读档以及游戏本身。那么重构的过程可以分别围绕抽象这四者的Player类(玩家类)、Board类(棋盘类)、FileStatus类(文件存读档类)以及GoBang类(游戏类)的设计进行。
Player类:在游戏中,应当有Player类的两个实例对象分别作为游戏中的两个玩家。在五子棋游戏中,Player的功能相对单一,仅有的行为即在控制台输入字符,控制落子,以及存档读档等。所以对于Player类,只需实现一个play方法——完成输入的读取和返回。另外,为了区别两个玩家,还需要在Player类中添加一个name的实例变量来标识对象。
Board类:作为棋盘实体的抽象,所有涉及棋盘的属性都应该在这个类中进行封装。所以之前代码中作为普通变量存在的棋盘尺寸以及棋盘矩阵等都应该被移入Board类中作为类的实例变量。此外,五子棋游戏中的落子,判断胜利等函数也应该移入该类中。
FileStatus类:这个类主要提供文件存读档的接口给GoBang类继承。在这个类中需要声明save和load两个方法。
GoBang类:这是游戏的主体,游戏中的玩家对象以及棋盘对象都将作为该类的实例成员出现。该类将主要管理游戏的主要控制功能,如运行、退出、存档、读档以及最为重要的刷新屏幕显示等。
重构的过程没有太多新的代码逻辑添加,只需将之前的代码全部封装到相应的类中即可。读者重构的过程可以参考例9.10。
【例9.10】 重构后五子棋代码。
问题分析:利用面向对象编程思想分别构建五子棋游戏中四个类:Player类,Board类,FileStatus类以及GoBang类。实现对五子棋代码的重构。
1 | #coding:utf-8 | 2 | import os | 3 | import pdb | 4 | import pickle | 5 | | 6 | # 玩家类 | 7 | class Player(object): | 8 | number = 0 | 9 | def __init__(self, name = ''): | 10 | """ | 11 | 玩家类构造方法 | 12 | """ | 13 | if not name: | 14 | Player.number += 1 | 15 | name = 'Player%d' % Player.number | 16 | self.name = name | 17 | | 18 | def play(self): | 19 | """ | 20 | 玩家输入下一步落子坐标 | 21 | """ | 22 | t = raw_input('Please input(x,y),now is ' + \ | 23 | self.name + ':') | 24 | return t | 25 | | 26 | # 棋盘类 | 27 | class Board(object): | 28 | class Status(object): | 29 | """ | 30 | 状态类,提供状态常量 | 31 | """ | 32 | NONE = 0 | 33 | WHITE = 1 | 34 | BLACK = 2 | 35 | def __init__(self, maxx = 10, maxy = 10): | 36 | """ | 37 | 棋盘类构造方法 | 38 | 确定尺寸,以及创建棋盘成员对象 | 39 | """ | 40 | self.maxx, self.maxy = maxx, maxy | 41 | self.qipan = [[0] * maxy for i in xrange(maxx)] | 42 | | 43 | def hasChessman(self, xPoint, yPoint): | 44 | """ | 45 | 判断是否有棋子存在 | 46 | """ | 47 | return self.qipan[xPoint][yPoint] != Board.Status.NONE | 48 | | 49 | def downPawn(self, xPoint, yPoint, who): | 50 | """ | 51 | 玩家在某个位置落子 | 52 | """ | 53 | if self.hasChessman(xPoint, yPoint): | 54 | return False | 55 | else: | 56 | self.qipan[xPoint][yPoint] = Board.Status.WHITE \ | 57 | if who else Board.Status.BLACK | 58 | return True | 59 | | 60 | def inRange(self, xPoint, yPoint): | 61 | """ | 62 | 判断坐标是否还在棋盘内 | 63 | """ | 64 | return xPoint < self.maxx and \ | 65 | yPoint < self.maxy and \ | 66 | xPoint >= 0 and \ | 67 | yPoint >= 0 | 68 | | 69 | def checkFiveInRow(self, xPoint, yPoint, xDir, yDir): | 70 | """ | 71 | 判断以(xPoint, yPoint)点(xDir, yDir)方向是否五子连珠 | 72 | """ | 73 | count = 0 | 74 | t = self.qipan[xPoint][yPoint] | 75 | x, y = xPoint, yPoint | 76 | while self.inRange(x, y) and t == self.qipan[x][y]: | 77 | count += 1 | 78 | x += xDir | 79 | y += yDir | 80 | x, y = xPoint, yPoint | 81 | while self.inRange(x, y) and t == self.qipan[x][y]: | 82 | count += 1 | 83 | x -= xDir | 84 | y -= yDir | 85 | return count > 5 | 86 | | 87 | def isWin(self, xPoint, yPoint): | 88 | """ | 89 | 以(xPoint, yPoint)点为中心在四个方向分别判断五子连珠 | 90 | """ | 91 | pdb.set_trace | 92 | return self.checkFiveInRow(xPoint, yPoint, 1, 0) or \ | 93 | self.checkFiveInRow(xPoint, yPoint, 0, 1) or \ | 94 | self.checkFiveInRow(xPoint, yPoint, 1, 1) or \ | 95 | self.checkFiveInRow(xPoint, yPoint, 1, -1) | 96 | | 97 | def printQp(self): | 98 | """ | 99 | 打印棋盘 | 100 | """ | 101 | qiType = ["十", "〇", "乂"] | 102 | print(' 〇 一 二 三 四 五 六 七 八 九 ') | 103 | for i in range(self.maxx): | 104 | print i, | 105 | print ' '.join(qiType[x] for x in self.qipan[i]) | 106 | | 107 | # 文件存读档类 | 108 | class FileStatus(object): | 109 | def save(self): | 110 | """ | 111 | 存档方法 | 112 | """ | 113 | fpath = raw_input('请输入保存文件路径:') | 114 | file = open(fpath, 'w') | 115 | pickle.dump(self, file) | 116 | file.close() | 117 | | 118 | def load(self): | 119 | """ | 120 | 读档方法 | 121 | """ | 122 | pass | 123 | | 124 | # 游戏类 | 125 | class GoBang(FileStatus): | 126 | def __init__(self, qipan, white, black): | 127 | """ | 128 | 游戏类构造方法 | 129 | 创建成员变量 | 130 | """ | 131 | self.qipan = qipan | 132 | self.white = white #白方 | 133 | self.black = black #黑方 | 134 | self.who = True #游戏角色变量 | 135 | | 136 | def start(self): | 137 | """ | 138 | 游戏主流程方法 | 139 | """ | 140 | os.system('cls') | 141 | self.printQp() | 142 | while True: | 143 | t = (self.white if self.who else self.black).play() | 144 | if t == 'S': #存档 | 145 | self.save() | 146 | continue | 147 | if t == 'L': #读档 | 148 | self.load() | 149 | continue | 150 | t = t.split(',') | 151 | if len(t) == 2: | 152 | x, y = int(t[0]), int(t[1]) | 153 | if self.qipan.downPawn(x, y, self.who): | 154 | os.system('cls') | 155 | self.printQp() | 156 | if self.qipan.isWin(x, y): #判断游戏是否结束 | 157 | print (self.white.name if \ | 158 | self.who else self.black.name) + ' Win' | 159 | break | 160 | self.who = not self.who #切换游戏角色 | 161 | os.system('pause') | 162 | | 163 | | 164 | def load(self): | 165 | """ | 166 | 重写读档方法 | 167 | """ | 168 | fpath = raw_input('请输入读取文件路径:') | 169 | file = open(fpath, 'r') | 170 | status = pickle.load(file) | 171 | file.close() | 172 | # 读档、拷贝 | 173 | self.qipan = status.qipan | 174 | self.white = status.white | 175 | self.black = status.black | 176 | self.who = status.who | 177 | os.system('cls') | 178 | self.printQp() | 179 | | 180 | def printQp(self): | 181 | """ | 182 | 打印棋盘 | 183 | """ | 184 | self.qipan.printQp() | 185 | print('按L读取游戏,按S保存游戏') | 186 | | 187 | if __name__ == '__main__': | 188 | t = GoBang(Board(), Player(), Player()) # 创建一个游戏的实例对象 | 189 | t.start() # 启动这个游戏 |

最终的游戏效果如下图所示:

图9.3 五子棋游戏运行界面
本章小结
本章介绍了面向对象编程的基础知识,以及Python中如何设计自定义的类;列举了Python类中的几类成员变量、成员方法以及它们的用法和联系。还介绍了类的继承以及多态的应用。学习本章以掌握编程思想为主,熟练掌握本章的几个重要知识点,可以为后续深入学习面向对象编程打好基础。
习题
一、填空题 1. 面向对象编程的三个特征是____________,____________和____________。 2. 在创建类的实例对象时,首先会调用类中的____________。 3. 自定义类的构造方法名一般为____________。 4. 静态方法和类方法的最大区别是____________。
二、选择题
1. 通过内置函数修饰器____,可以指定一个方法为静态方法( )。
A.staticmethod B.classmethod C.static D.staticfunction 2. 下列说法中不正确的是( )。
A.实例对象可以访问实例方法
B.类对象可以访问实例方法
C.实例对象可以访问类方法
D.静态方法可以访问类方法 3. 下面程序运行后,会有什么输出( )。 class Base(object): pass a = Base() b = Base()
a.obj = 2 print b.obj
A.2
B.0
C.-999999999
D. AttributeError: 'Base' object has no attribute 'obj' 4. 下面程序运行后,会有什么输出( )。 class Base(object): def A(self): return "Base.function_A" class Sub(Base): def A(self): return "Sub.function_A" def C(self): return "Sub.function_C" obj = Sub() print obj.A(), print obj.C(),
A. Base.function_A Base.function_C
B. Sub.function_A Sub.function_C
C. Base.function_A Sub.function_C
D. Sub.function_A Base.function_C
三、编程题
1. 在Python中的内置类型中有整型和浮点型两种数字类型,现在请设计一种自定义的复数Complex类,要求支持复数对象的四则运算以及打印功能。 2. 先建立一个Point(点)类,包含成员变量x,y(坐标点);以它为超类派生出一个Circle(圆)类,增加成员变量r(半径)和成员方法area(求面积);再以Circle类为超类派生出一个Cylinder(圆柱)类,增加成员变量h(高)和成员方法volume(求体积)。 3. 现在要设计一个仓库管理原材料库存的Goods类,要求能够处理进库、出库、查询库存。 4. 现在请实现单例模式:设计一个singleton类,保证这个类仅有一个实例对象。在类中提供一个给外界获取实例的类方法getInstance。

第10章 异常处理
在编写程序中,经常会遇到某个事件运行异常而造成程序运行结果的错误,甚至会造成程序的崩溃的现象。这时程序员需要提前预判会造成程序异常的情况,并做出相对应的处理,将这种错误消灭于萌芽之中。一种简单的方法是程序员使用条件语句判断出所有可能发生的异常情况,然而这种方法需要将所有可能发生的异常及原因全部清晰的罗列出来。但现实编程中很难达到如此完美,而且这种方法也会造成代码难以阅读。Python为使用者提供了非常强大的异常处理方法。本章中,将学习一些抛出异常和捕获异常的方法。
【体系结构】

【本章重点】
(1)了解Python语言中异常概念。
(2)掌握抛出异常的方法和自定义并使用异常类。
(3)掌握捕获异常的不同语句,以及它们的不同使用方法。
(4)掌握finally语句用法和异常处理完整形式。
【案例引入】五子棋游戏——让五子棋更健壮
前面章节介绍的五子棋游戏中,当用户输入棋子坐标或者读取保存文件时,有可能因为操作错误而使程序终止运行。显然,这样的程序健壮性还差一些,需要进一步提高五子棋的容错能力。这一章,通过学习异常的相关知识,一起来修改五子棋游戏的程序,使它更加健壮。
10.1 异常
所谓异常,即程序运行时遇到非正常的情况,例如符号错误、逻辑错误、语法错误等等。Python语言使用异常对象(exception object)来表示异常情况,由于它是一种面向对象语言,因此程序中抛出的异常也是一种类,所有的异常都是从基类Exception继承而来,而且是在exceptions模块中被定义。Python将所有异常名称放在内建命名空间中,使用者在使用异常时不需要导入任何模块。如果异常出现时程序没有进行捕捉或者处理,在程序中会使用回溯(Traceback)来终止执行。例如当程序发生除0异常时: >>> 1/0Traceback (most recent call last): File "<stdin>", line 1, in <module>ZeroDivisionError: integer division or modulo by zero |
上面程序中当遇到0为除数时,程序会终止运行,并报出“ZeroDivisionError”的错误。常见的Python异常类如表10.1所示。
表10.1 Python中常见的内建异常类 异常类名 | 描述 | Exception | 所有异常基类 | SyntaxError | 语法错误 | NameError | 访问变量没有被声明 | ZeroDivisionError | 除数为0 | IndexError | 索引超出序列范围 | KeyError | 请求一个不存在的字典关键字 | IOError | 输入输出错误 | AttributeError | 访问未知的对象属性 | TypeError | 对象类型错误 | ValueError | 传入无效的参数 | EOFError | 发现一个不期望的文件或输入结束 |
10.2 抛出异常 异常有两种激活方式,一种是在程序运行出错时自动引发,另一种是程序员自己引发,即抛出异常。抛出异常往往是程序自动根据错误特征进行自动识别和抛出,是有时候程序员希望抛出的异常更为有效,在Python中可以使用raise语句强制抛出异常。
10.2.1 raise语句 raise语句是Python提供给程序员一种自发引发异常的方法。当程序员想抛出一个异常时,直接在raise语句中指明错误或异常的名称即可。 >>> raise ExceptionTraceback (most recent call last): File "<stdin>", line 1, in <module>Exception |
上面程序中使用raise语句引发了一个普通的异常,raise语句还可以为抛出的异常添加一些异常信息。raise语句的一般用法为:
raise [SomeException [, args [,traceback]]
第一个参数SomeExceptions,是引发异常的名字,它必须是一个异常类,或者异常类的实例。
第二个参数agrs,是一个可选参数,它是传递给SomeExceptions的参数。它可以是一个元组,也可以是一个单独的对象。因为异常参数只能为元组,所以当agrs是一个单独的对象时,传入时会生成一个只有一个元素的元组。
第三个参数traceback,同样是一个可选参数,它是当异常触发时新生成一个用于异常-正常化的跟踪记录对象。这个参数其实很少用到。
下面介绍一下raise语句的不同使用方式: >>> raise NameErrorTraceback (most recent call last): File "<stdin>", line 1, in <module>NameError |
第一条语句会触发一个NameError类型的异常,会生成一个不带任何参数的NameError的实例。
>>> raise NameError()Traceback (most recent call last): File "<stdin>", line 1, in <module>NameError |
第二条语句作用和第一条语句一样,但这里的“NameError()”不是类,而是通过函数调用操作符作用于类名,生成一个NameError的实例。
>>> raise NameError,"This is NameError Exception"Traceback (most recent call last): File "<stdin>", line 1, in <module>NameError: This is NameError Exception |
第三条语句是触发NameError异常的同时提供个一个字符串作为参数,这里会生成一个只有一个元素的元组后再传给异常。
>>> raise NameError("This is NameError Exception")Traceback (most recent call last): File "<stdin>", line 1, in <module>NameError: This is NameError Exception |
第四条语句的作用和第三条语句是一样的,但它是通过创建一个带有参数的实例来触发异常,它等价于“raise NameError,NameError("This is NameError Exception")”。 >>> raise Exception,NameError("This is NameError Exception")Traceback (most recent call last): File "<stdin>", line 1, in <module>NameError: This is NameError Exception |
第五条语句同样是通过实例来触发异常,但这时实例类型(NameError)是异常类型(Exception)的子类,这时触发的新异常的类型会是子类的类型(NameError)。
>>> raise TypeError,NameError("This is NameError Exception")Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: This is NameError Exception |
第六条语句和第五条语句属于同一种用法,但这里实例类型不是异常类型,也不是异常的子类类型,这时程序会复制实例为异常参数去生成一个新的异常类型的实例。
10.2.2 自定义异常类 Python已经为使用者提供了丰富的内建异常类,它们可以满足大部分的需求。但有些时候,程序员需要使用自己定义的异常,这时也可以创建自己的异常类。和内建异常类一样,用户自定义的异常类应该典型的继承自Exception类,可以通过直接或者间接的方式。自定义的异常类常使用raise语句引发,而且只能通过人工方式触发。编写一个自定义异常类的基本方式为: >>> class CustomError(Exception):... pass... |
定义了这个异常,程序中就可以使用raise来触发这个异常了,如下面代码:
>>> raise CustomErrorTraceback (most recent call last): File "<stdin>", line 1, in <module>__main__. CustomError | 在上面的例子中,自定义一个CustomError的异常,它继承自Exception类,但类中没有写任何东西。这时,CustomError和Exception是一样的。当然也可以在自定义类中写些自己的方法。
【例10-1】 自定义传参异常 1 | class MyError(Exception): | 2 | def __init__(self,value): | 3 | super(MyError,self).__init__ (value) | 4 | self.value=value | 5 | | 6 | raise MyError,"Error Information" |
运行结果:
Traceback (most recent call last): File "10-2.py", line 6, in <module> raise MyError,"Error Information"__main__.MyError: Error Information |
程序分析:例10-1中自定义了一个MyError异常类,它继承自Exception类。第2行重写了Exception类的__init__方法,它只能接收一个参数value,即如果raise引起异常时传入多余一个参数,程序会抛出一个TypeError异常,并提示参数个数不正确。程序第3行使用super方法调用了其超类的构造方法,如果不写这行代码,触发MyError后,将无法打印错误信息(Error Information)。第4行将参数赋值给本身的成员变量,这样在以后捕捉这个异常时就可以使用这个变量来获取错误信息了,异常的捕捉将在下一节中讲解。
10.3 捕获异常 在python编程中,为了使程序能够不崩溃并且正确运行,需要对异常进行捕获和处理。这一节中将讲解捕获异常的几种方法。
10.3.1 try-except语句
(1)try-except语句
最常见的捕获异常的语句为try-except语句。用try-except语句捕获处理异常时,将可能出现异常的语句放到try子句的代码块中,将处理异常的语句放入except子句的代码块中。try-except语句的一般形式为: try: try blockexcept Exception[, reason]: exception block |
上面代码的工作方式为:首先执行try子句中的代码块。如果没有发生任何异常,except子句中的代码会在try语句执行完之后被忽略掉。如果执行try子句中的代码时引发了异常,那么try代码块中引发这个异常的后面的代码就会被忽略掉。如果引发的异常与except关键字后面的指定异常类型相匹配,那么就跳转到这个except子句中执行except代码块的语句。如果try语句中引发的异常在except子句中没有与它匹配的分支,它会传递到上一级的try语句中。如果最终仍找不到与之匹配的except语句,它就会成为一个未处理异常,这时程序会终止运行,并提示异常信息。
注意:except子句可以没有接任何异常和异常参数,这时try语句捕获的任何异常都会交给except子句中的代码块来处理。
对于本章开始介绍的除0异常,使用try-except捕捉这个异常,并提示错误的代码为:
>>> try:... 1/0... except ZeroDivisionError:... print "The divisor cannot be zero" |
上面的代码使用except 捕捉除零错误,所以当程序运行到“1/0”时,程序不会报错,而是跳转到except语句的代码块中,并打印“The divisor cannot be zero”。
(2)try-except-else结构
与条件语句和循环语句类似,else语句也可以应用到异常捕获中。它可以和try-except语句一起组成try-except-else的结构。这个语句结构的工作方式为:如果程序在try范围内捕获到了except语句指定的异常,就跳转到except语句中继续执行;如果try范围内没有捕获到任何异常,就执行else语句中的代码块。如下面代码: try: myfile=open('myfile.txt','w')except: print 'myfile open failed.'else: print 'myfile open successfully.' myfile.close() | 上面代码try语句中试图打开一个“myfile.txt”的文件,如果打开文件时出现异常,使用except语句捕捉,并打印文件打开失败(“myfile open failed.”);如果打开文件时没有出现任何异常,程序会运行else中的语句,即打印文件成功打开(“myfile open successfully.”),并关闭文件。
(3)异常参数
10.2中raise语句引发一个异常时,可以为这个异常提供异常的参数。而Python为用户提供的内建异常类也自带一些异常参数。在捕捉异常时,要想访问提供的异常原因,用户必须保留一个变量来保存这个参数。在try-except的一般形式中可以看到except语句中指定异常类型的后面有一个可选参数reason,这个reason就是用来保存异常参数的变量。还以除0错误为例,如下面代码所示: >>> try:... 1/0... except ZeroDivisionError,e:... print e |
输出结果:
integer division or modulo by zero |
上面程序中的e即为接收异常参数的变量,在except代码块中直接输出e,可以看到输出的结果即为引发异常的原因。
其实这个可选参数reason是一个包含来自导致异常的代码的诊断信息的类的实例。前面已经讲过,异常参数自身会组成是一个元组,并存储异常类实例的属性。对于大多内建异常,这个元组只包含一个指示错误原因的字符串(如上例中的“integer division or modulo by zero”)。而一些操作系统或其它环境类型的错误,元组会在错误字符串的前面放置操作系统的错误编号。
【例10-2】 计算输入的两个整数相除的结果(案例1)
问题分析:依次提示输入两个整数x和y,如果y的值为零,提示重新输入两个数,否则计算并打印x除以y的值。
1 | while True: | 2 | try: | 3 | x=input('Enter x:') | 4 | y=input('Enter y:') | 5 | x/y | 6 | except ZeroDivisionError, e: | 7 | print e, " Please input again." | 8 | else: | 9 | print "x/y=%f"%(x*1.0/y) | 10 | break |
输入与运行结果:
Enter x:4Enter y:0integer division or modulo by zero Please input again.Enter x:5Enter y:3x/y=1.666667 |
程序分析:例10-2中,第1行使用while循环语句,后面的逻辑判断语句为True,即当程序抛出除零异常时一直循环下去。第3~5行为try语句的代码块,分别提示输入两个整数x和y,并使用x除以y。第7行为except语句的代码块,即当程序捕捉到除零异常时,打印通过异常参数传递来的异常原因,并提示重新输入。第9~10行为else语句的代码块,当没有捕捉到除0异常时,打印x除以y的计算结果,并使用break语句跳出while循环。
10.3.2 捕获多种异常
在编写程序时,往往一段代码所引发的异常不只一种,如例10-3中,当做如下输出时,对应的运行结果为:
Enter x:4Enter y:rTraceback (most recent call last): File "10-3.py", line 4, in <module> y=input('Enter y:') File "<string>", line 1, in <module>NameError: name 'r' is not defined |
当输入y的值时,错误得输入了一个字母“r”时,程序报出了一个NameError错误(input接收一个python表达式,当它接收到r时,会把它当做一个变量,但前面没有定义过变量r,这时会报出NameError)。虽然这段代码在try语句中,但因为except语句中没有指定捕捉NameError这种异常,造成程序报错并中断运行。这时就需要捕捉多种异常的语句。
(1)带有多个except的try语句 一种捕捉多种异常的方法是在同一个try-except语句后面加上另一个except子句。当然,也可以将多个except语句连接在一起,来处理一个try语句块中可能引发的多种异常。 带多个except的try语句的工作方式为:首先尝试执行try子句,如果没有错误,则忽略所有except语句。如果引发某种异常,程序会依次在except语句中寻找与引发异常类型相匹配的异常。如果找到,会跳转到该except语句中,否则继续向下寻找。如果都没有找到,则会将异常传递给上一层try语句。
【例10-3】 计算输入的两个数相除的结果(案例2) 1 | while True: | 2 | try: | 3 | x=input('Enter x:') | 4 | y=input('Enter y:') | 5 | x/y | 6 | except ZeroDivisionError, e: | 7 | print e, "Please input again." | 8 | except NameError, e: | 9 | print e, "Please input again." | 10 | except TypeError, e: | 11 | print e, "Please input again." | 12 | else: | 13 | print "x/y=%f"%(x*1.0/y) | 14 | break | 运行结果: Enter x:4Enter y:rname 'r' is not defined Please input again.Enter x:4Enter y:"e"unsupported operand type(s) for /: 'int' and 'str' Please input again.Enter x:4Enter y:5x/y=0.800000 | 程序分析:例10-3中除了捕捉ZeroDivisionError,还使用了另外两个except语句分别捕捉了NameError和TypeError。这样,当try语句中的代码引发异常时,会依次判断三个except语句中所指定的异常是否与引发的异常相匹配,如果匹配,则执行该except语句中的代码。例如第一次输入y时,输入r会引发NameError,这时会跳转到第9行输出NameError对应的异常信息;第二次输入y时输入了一个字符串,这时使用x除以y时会引发TypeError,这时会跳转到第11行输出异常信息。
注意:就是多个except子句截获异常时,如果各个异常类之间具有继承关系,则子类应该写在前面,否则父类将会直接截获子类异常。放在后面的子类异常也就不会执行到了。
(2)处理多个异常的except语句 可以看出,例10-4中第7、9和11行代码完全相同。其实完全可以将相同的3行代码只写一遍,因为Python支持一个except子句里处理多个异常。使用一个except语句处理多个异常时需要将异常信息放在一个元组里,如 except (Exception1,Exception2,…)[,reason]:exception block |
需要注意的是except后的多个异常信息一定要放在括号中,即写成一个元组中。另外虽然一个except语句后可以接多个异常,但后面可选的参数的异常信息(reason)只需要写一遍即可。例10-3的代码也可以修改为:
【例10-4】 计算输入的两个数相除的结果(案例3) 1 | while True: | 2 | try: | 3 | x=input('Enter x:') | 4 | y=input('Enter y:') | 5 | x/y | 6 | except (ZeroDivisionError, NameError, TypeError), e: | 7 | print e, "Please input again." | 8 | else: | 9 | print "x/y=%f"%(x*1.0/y) | 10 | break |
例10-4和例10-3的运行结果完全相同。
10.3.3 捕获所有异常
(1)捕捉全部异常
例10-4中的程序既可以捕捉除零错误,访问量未声明的错误,还可以捕捉类型错误,似乎看起来很完美了。请仔细观察下例的输入对应的结果: Enter x:4Enter y:=Traceback (most recent call last): File "10-4.py", line 5, in <module> y=input('Enter y:') File "<string>", line 1 = ^SyntaxError: unexpected EOF while parsing |
当输入y时输入一个“=”,程序会引发一个SyntaxError,这个异常并不在except语句指定的异常中,这时程序仍会不报错并停止运行。在实际编程中,同样会有许多程序员无法预判的异常引发,使程序崩溃并造成重大损失。这时,也许会需要一些方法可以捕捉所有可能引发的异常。
在10.3.1小节中介绍try-except语句时,曾经提醒过读者,当except后没有接任何异常和异常参数时,会捕捉所有的异常。这当然是一种解决方法。但是像这样捕捉所有异常其实非常危险,因为它会隐藏所有程序员难以预料错误。它虽然捕捉到了所有异常,但可能会使程序员忽略掉重要的错误。这些错误应该让调用者知道并做一定处理。而且,这样做也无法保存异常发生的原因。所以,这里不推荐使用空except语句捕捉所有异常。考虑到Exception类是所有异常的基类,所以另一个的方法是: except Exception, e
这样的确可以捕捉所有的错误异常,并且可以通过e进行检查,来做出相应的处理。
(2)真正全捕捉 其实,有些异常不是由于程序中的错误引起的,例如SystemExit(当前应用程序需要退出)和KeyboardInterupt(用户按下CTRL+C)。在Python2.7中,这两个异常类与Exception类平级。它们都有一个共同的父类,叫做BaseException。当用户需要捕捉全部异常时,可以使用这个BaseException类,即: try: ......except BaseException,e: ...... |
10.4 finally语句
程序设计时有时候希望有些代码无论是否引发异常都会执行,比如文件的关闭操作或者socket的关闭等。这时就需要使用finally语句。
finally子句是指无论异常是否引发,是否被捕获都一定会执行的代码块。finally语句可以和try语句一起使用,也可以和try-except语句一起使用,当然更可以和try-except-else一起使用。
(1)try-finally语句
当finally单独和try语句连用时,即: try: try blockfinally: finally block |
这个组合其实无法捕捉异常,它只会保证无论try子句中是否引发异常,finally语句中的代码块一定会被执行。它的工作方式为:如果没有发生异常,python运行try子句,然后finally子句;如果在try子句发生了异常,python就会回来执行finally子句,然后把异常递交给上层try,控制流不会通过整个try语句。
【例10-5】 将文件中数值求和并关闭文件(案例1) 1 | try: | 2 | f=open("in.txt","r") #打开in.txt文件 | 3 | sum=0 | 4 | line = f.readline() #读取文件第一行 | 5 | while line: | 6 | sum=sum+int(line) #将这行转换为整型并累加 | 7 | line = f.readline() #读取文件中的下一行 | 8 | finally: | 9 | f.close() | 10 | print sum |
文件in.txt中的内容为:
123445678 | 运行结果: 1246Traceback (most recent call last): File "10-5.py", line 6, in <module> sum=sum+int(line)ValueError: invalid literal for int() with base 10: '' | 程序分析:例10-5中的try子句中,第2行使用只读的方式打开文件in.txt。第3行定义变量sum来统计文件中数值的和。第5~7行使用while循环依次读取文件中的每行,第5行将读取的字符串转换为整型,并加到sum上。在finally语句中,第9行保证文件安全关闭。第10行打印计算的sum值。运行结果中,首先输出的“1246”为文件中前3行数字的和。而之后程序报出一个ValueError,引起这个异常的原因是在文件的最后一行是一个空行,它是一个空字符串,但并不是文件的结尾,所以while语句继续运行。这时将这个空字符串转化为整形时,就报出了ValueError。由于try-finally语句无法捕捉异常,只能将这个异常传递给上一层,所以控制台中报出了这个异常。
(2)try-except-finally语句
例10-5中虽然计算出了结果,也安全关闭了文件,但最终以报错的方式结束程序显然不是一个希望的结果。将finally放到try-ecxept的后面可以解决这个问题。 try-except-finally语句的工作方式为:当try语句中没有引发异常,运行完try子句后继续运行finally子句中的代码;当try子句中引发异常时,如果except指定的异常类型有与之相匹配的,进入except语句中进行异常处理,处理结束后仍然运行finally子句中的代码。所以例10-5中的代码可以修改为: 【例10-6】 将文件中数值求和并关闭文件(案例2) 1 | try: | | 2 | f=open("in.txt","r") | #打开in.txt文件 | 3 | sum=0 | | 4 | line = f.readline() | #读取文件第一行 | 5 | while line: | | 6 | sum=sum+int(line) | #将这行转换为整型并累加 | 7 | line = f.readline() | #读取文件下一行 | 8 | except ValueError,e: | | 9 | print e | #打印捕捉到的异常 | 10 | finally: | | 11 | f.close() | #关闭文件 | 12 | print sum | #打印累加结果 | 运行结果: invalid literal for int() with base 10: ''1246 | 程序分析:例10-6(改)比例10-5添加了第8~9行,使用except语句来捕捉ValueError,并打印异常信息。这时运行程序,程序会打印异常信息和运行结果后,正常退出。
(3)try-except-else-finally语句
try-except-else-finally语句是异常处理语句的完全体,它的完整格式如下: try: try_blockexcept Exception1: block _for_Exception1except (Exception2, Exception3, Exception4): block _for_Exceptions_2_3_and_4except Exception5, Argument5: block _for_Exception5_plus_argumentexcept (Exception6, Exception7), Argument67: block _for_Exceptions6_and_7_plus_argumentexcept: block _for_all_other_exceptionselse: no_exceptions_detected_ blockfinally: always_execute_ block | 这里重新总结梳理一下这个完整形式的工作方式: 1) 正常执行的程序在try下面的try_block执行块中执行,在执行过程中如果发生了异常,则中断当前在try_block中的执行跳转到对应的异常处理块中开始执行; 2) Python从第一个except X处开始查找,如果找到了对应的exception类型,则进入block _for_Exception中进行处理;如果没有找到则直接进入except块处进行处理。except块是可选项,如果没有提供,该exception将会被提交给 python进行默认处理,处理方式则是终止应用程序并打印提示信息; 3) 如果在try_block执行块中执行过程中没有发生任何异常,则在执行完try_block后会进入else执行块中(如果存在的话)执行; 4) 无论是否发生了异常,只要提供了finally语句,以上try-except-else-finally代码块,执行的最后一步总是执行finally所对应的代码块。
注意:
1) 使用这套完整形式时,所有except必须出现在else和finally之前。else必须出现在finally之前。指定异常类型的except语句必须出现在没有指定异常类型的except语句之前。 2) 完整形式中except语句,else语句和finally语句都是可选的,一旦使用,必须按照try-except-else-finally的顺序使用。使用else语句时一定要有except语句。 3) 不推荐使用不指定异常类型的except语句。
(4)finally语句特性
在程序中使用finally语句时,不得不了解一下finally语句的两点特性: 1) 当在try语句块中含有return语句时,执行到return并不会直接返回,而是由Python再去执行finally语句块之后再执行return。 2) 有时候在处理了finally中的资源释放之后就不再需要继续处理抛出的异常了,在这种情况下可以考虑在finally语句块中使用return语句。
10.5 处理异常的特殊方法 处理异常时除了前几个节介绍的常规方法之外,还有一些特殊的方法。这里简单介绍一下常用的两种特殊方法:assert语句和with语句。
(1)assert语句
assert语句,前面章节已经介绍过,是断言语句,是一句必须等于布尔真的判定。如果断言成功不采取任何措施,否则会触发AssertionError的异常。AssertionError异常和其它的异常一样,可以使用try-except语句捕捉,如果没有捕捉,它也会终止程序并报出错误。一个assert语句报错如下: >> assert 1==2,"one does not equal two!"Traceback (most recent call last): File "<stdin>", line 1, in <module>AssertionError: one does not equal two! | 上面代码中,当assert后面的表达式的布尔值为假时,会抛出AssertionError异常,并将后面的字符串当做异常信息传递给这个异常。
(2)with语句
with语句即上下文管理语句。with语句的目的在于从流程图中把try、except和finally关键字和资源分配释放相关代码统统去掉,而不是像try-except-finally那样仅仅简化代码使之易用。with语法的基本用法如下: with context_expr[as var]: with block |
使用with语句操作文件对象的代码为:
with open(r'somefileName') as somefile: for line in somefile: print line # ...more code |
这里使用了with语句,不管在处理文件过程中是否发生异常,都能保证with语句执行完毕后已经关闭了打开的文件句柄。
with是一个控制流语句,它可以用来简化try-finally的代码。它引入了一个上下文管理协议,实现方法是为一个类定义__enter__和__exit__两个函数。每次使用with语句,会首先执行__enter__函数,它的返回值赋值给var,当with block执行完成后,会执行__exit__函数。所以with语句等价于: try: 执行__enter__内容 执行with blockfinally: 执行__exit__内容 |
10.6 让五子棋更健壮 前面章节编写的五子棋程序已经很完善,但仍会有一些操作会导致程序崩溃,比如输入坐标时输入非法字符、输入数字超出棋盘范围、读取文件时输入的文件不存在或者该文件中存储的不是棋盘信息等。这些情况,程序会抛出异常,导致程序终止。这时,可以利用本章学习的内容,对这些异常进行捕捉。
(1)定义自己的异常类
在五子棋程序中,可能会引发异常的原因有两种,即用户输入坐标时引发的错误和读取文件时引发的错误。另外用户输入坐标时如果坐标位置已经有棋子,那么该输入为无效。可以对这三种情况分别定义一个异常类。因为这三个异常类都是针对五子棋游戏的程序,这里可以定义一个五子棋的异常基类,然后让这三个异常类都继承自这个基类。自定义异常类的代码如下: 1 | class GobangError(Exception): #定义五子棋异常基类 | 2 | Pass | 3 | | 4 | class InputError(GobangError): #定义输入格式错误异常类 | 5 | ErrorMessage="输入格式不合法!请重新输入。" | 6 | | 7 | class CollisionError(GobangError): #定义棋子冲撞异常类 | 8 | def __init__(self, x, y, chess): | 9 | self.ErrorMessage="(%d,%d)位置已经有棋子%s!请重新输入。"%(x,y,'〇' if chess==1 else '乂') #生成冲撞异常信息 | 10 | | 11 | class GobangIOError(GobangError): #定义文件读写异常类 | 12 | def __init__(self, message): | 13 | self.ErrorMessage=message |
程序分析:程序第1~2行定义一个五子棋的异常基类,这个基类继承自Exception类,类中没有内容,直接写pass。第4~5行定义一个输入错误的异常类,它会接受所有因为用户输入所引发的异常。第7~9行定义一个棋子碰撞的异常类,它用来接受下子该位置被占据时的错误。这里重写了该类的构造方法,构造方法接收棋子碰撞的位置和该位置的棋子信息。第11到第13行定义一个文件的异常类,用于接收读取写入文件时可能引发的异常。
(2)处理文件异常 在保存和读取文件时,可能会发生一些异常,对于这里引发的所有异常,这里都抛出一个GobangIOError的异常。这里修改FileStatus类中的save方法为: 1 | def save(self): | 2 | """ | 3 | 存档方法 | 4 | """ | 5 | fpath = raw_input('请输入保存文件路径:') | 6 | try: #捕捉写入文件时IO异常 | 7 | file = open(fpath, 'w') | 8 | try: #捕捉腌制信息时的异常 | 9 | pickle.dump(self, file) | 10 | except Exception: | 11 | raise GobangIOError("存储游戏信息出错!") #抛出相应GobangIOError | 12 | finally: #保证安全关闭文件 | 13 | file.close() | 14 | except IOError: | 15 | raise GobangIOError("保存文件错误!") #抛出相应GobangIOError | 修改GoBang类中的load方法为: 1 | def load(self): | 2 | """ | 3 | 重写读档方法 | 4 | """ | 5 | fpath = raw_input('请输入读取文件路径:') | 6 | try: #捕捉读取文件时IO异常 | 7 | file = open(fpath, 'r') | 8 | try: #捕获文件内容中的异常 | 9 | status = pickle.load(file) | 10 | os.system('cls') | 11 | status.printQp() #打印棋盘信息 | 12 | except Exception: | 13 | self.printQp() | 14 | raise GobangIOError("文件内容不是游戏信息!") | 15 | else: | 16 | # 读档、拷贝 | 17 | self.qipan = status.qipan | 18 | self.white = status.white | 19 | self.black = status.black | 20 | self.who = status.who | 21 | finally: #保证安全关闭文件 | 22 | file.close() | 23 | except IOError: | 24 | raise GobangIOError("不存在这个文件!") |
程序分析:save方法和load方法中修改的代码类似,这里以load方法中的代码为例。第6~22行为外层try语句,如果这里抛出IOError,在第23行捕捉这个异常,并抛出信息为“不存在这个文件!”的GobangIOError。第8~11行为内层try语句,当pickle.load出现错误时,或者由于版本问题当打印棋盘内容发生错误时,使用第12~14行进行捕捉并抛出错误信息为“文件内容不是游戏信息!”的GobangIOError。如果没有引发异常,将读取的内容拷贝给当前GoBang类。这里无论是否引发异常,都在finally中关闭文件。

(3)游戏主流程中异常的捕捉
玩家下棋时,当输入的坐标合法时,有可能该坐标下已经被棋子占据。这时可以抛出一个自定义的棋子碰撞异常。Board类中的downPawn方法修改如下:
1 | def downPawn(self, xPoint, yPoint, who): | 2 | """ | 3 | 玩家在某个位置落子 | 4 | """ | 5 | if self.hasChessman(xPoint, yPoint): #如果坐标已有棋子,抛出含有坐标信息的异常 | 6 | raise CollisionError(xPoint,yPoint,self.qipan[xPoint][yPoint]) | 7 | else: | 8 | self.qipan[xPoint][yPoint] = Board.Status.WHITE \ | 9 | if who else Board.Status.BLACK |
在游戏主流程方法中,捕捉本节开始提到的所有异常,并做出相应的错误提示。游戏主流程方法的代码修改为:
1 | def start(self): | 2 | """ | 3 | 游戏主流程方法 | 4 | """ | 5 | os.system('cls') | 6 | self.printQp() | 7 | while True: | 8 | try: #捕获所有GoBang异常 | 9 | t = (self.white if self.who else self.black).play() | 10 | if t == 'S': #存档 | 11 | self.save() | 12 | Continue | 13 | if t == 'L': #读档 | 14 | self.load() | 15 | Continue | 16 | try: #捕获输入信息异常 | 17 | t=t.split(',') | 18 | assert(len(t)==2) #断言必有2个信息 | 19 | x, y = int(t[0]), int(t[1]) | 20 | assert(self.qipan.inRange(x,y)) #断言输入坐标必在棋盘内 | 21 | except Exception: | 22 | raise InputError #抛出输入异常 | 23 | else: #没有异常,继续执行 | 24 | self.qipan.downPawn(x, y, self.who) | 25 | os.system('cls') | 26 | self.printQp() | 27 | if self.qipan.isWin(x, y): #判断游戏是否结束 | 28 | print (self.white.name if \ | 29 | self.who else self.black.name) + ' Win' | 30 | break | 31 | self.who = not self.who #切换游戏角色 | 32 | except GobangError, e: | 33 | print e.ErrorMessage #打印捕获的异常信息 |
程序分析:程序第8行的try语句和第32行的except语句用来捕捉所有可能引发的GobangError,并在第33行打印错误信息。这里的 GobangError可能是第11行或第14行抛出的GobangIOError、第22行引发的InputError或第24行抛出的CollisionError。第16行的try语句和第第21行的except语句用来捕捉用户输入坐标信息时格式无效的情况,并在第27行引发InputError。在else语句中调用downPawn方法下棋子,如果在downPawn方法中发现有效输入的位置被占据,downPawn方法会抛出 CollisionError。
本章小结
本章介绍了Python中异常和异常的处理机制。读者应该了解常见的异常类型。重点讲解了异常的抛出和捕获的方法,包括raise语句、try-except语句和else语句的使用方法和拓展方法。然后,详细讲解了finally语句的使用,并帮助读者列出异常处理语句的完整格式。最后简单介绍了assert和with两种处理异常的特殊方法。
习题
一、填空题 1. Python中使用__________语句强制抛出异常。 2. Python中所有内建异常的基类为_____________。 3. 捕获异常的统一出口是通过___________语句实现。 4. 异常处理的完整格式为__________________________________________________。 5. try-except与try-finally语句的不同点为:_____________________________________________________________________。
二、选择题
1. 对于except子句的排列,下列正确的是( )。 A. 父类在先,子类在后。 B. 子类在先,父类在后。 C. 有继承关系的异常不能在同一个try程序段内。 D. 先有子类,其它如何排列无关。 2. 一个异常将终止( )。 A. 整个程序 B. 只终止抛出异常的方法 C. 产生异常的try块 D. 以上说法都不对 3. 在异常处理中,如释放资源、关闭文件、关闭数据库等由( )来完成。 A. try子句 B. except子句
C.finally子句 D. else子句 4. 代码段: def test(): try: n=1/0 except Exception: return n print test()
输出结果为:
A. 0 B. -1
C.1 D. 报出异常 5. 代码段: def test(): n=3 try: n=n/0 except Exception: return n finally: print 1, print test()
输出结果为:
A. 1 B. 3
C.1 3 D. 报出异常
三、上机题
1. 从命令行得到5个整数,放入一整型数组,然后打印输出,要求:如果输入数据不为整数,要捕获产生的异常,显示“请输入整数”,如果输入数据不为5个,捕获该异常,显示“请输入至少5个整数”。 2. 读取in文件,将文件中内容打印out文件中,捕获所有可能产生的异常,并做出异常提示。 3. 对于一个字典dic,如果存在a属性,将其输出,否则什么都不输出,尝试使用try-except语句实现。 4. 捕捉所有异常,如果是KeyboardInterupt或SystemExit,直接传递给上一层。 5. 使用with语句,修改下面处理多线程加锁的代码。 try:lock.Lock() ......except: prcess_except()finally: lock.Unlock |

第11章 模块
本章介绍Python语言中常见模块和如何把数据从模块中导入到编程环境中。同时也会涉及包的相关概念。从宏观角度,模块是用来组织Python代码的方法,包则是用来组织模块的。模块让你能够有逻辑地组织你的Python代码段。把相关的代码分配到一个模块里能让代码更好用,更易懂。
【体系结构】

【本章重点】
(1)掌握模块使用;
(2)理解名称空间;
(3)掌握包的使用;
11.1 Python模块
Python模块实际上是从逻辑上对Python代码的一种组织方式。当代码量变得很大时,利用模块能把这些代码分散地组织起来,使得代码更有层次,易于阅读。这些代码片段相互间有一定的联系,可以互相调用,可能是一个类,也可能是一组相关但彼此独立的函数。由于这些代码段是共享的,所以Python允许调入一个模块,这一调入操作叫做导入(import)。那些自我包含并且有组织的代码片断就是模块(module )在实际存储时,一个文件就是一个模块。因此,一个文件被看作是一个独立模块,一个模块也可以被看作是一个文件。模块的文件名就是模块的名字加上扩展名.py。任何Python程序都可以作为模块导入。下面的函数就是一个简单的模块。
【例11-1】模块函数举例,用户自定义模块。
问题分析:定义printName函数,打印输出所有传入的参数。 1 | #!/usr/bin/python | 2 | # Filename: hello.py | 3 | # printName:打印参数 | 4 | def printName(name): | 5 | print "hello:", name |
本例定义了文件hello.py,同时也定义了模块,其模块名字为hello。
如果要引用已建立的模块或系统的模块,需要使用import关键字。它可以从外部模块获取函数并且为自己的程序使用。
使用 import 语句导入模块,它的语法如下所示: import module1 import module2

import moduleN
也可以在一行内导入多个模块:
import module1[, module2[,... moduleN]]
例如,引用系统的模块math方法如下,
>>> | import math | >>> | math.cos(0) | 1.0 |
引用模块hello方法如下,
>>> | import hello | >>> | hello.printName("Mr.Liu") | hello: Mr.Liu |
如果待引用模块文件位置与引用模块的文件不一致时,需要指定路径。比如上述模块hello在文件夹\home\python下时,
>>> | import sys | >>> | sys.path.append(‘\home\username\python’) | >>> | import hello | >>> | hello.printName("Mr.Liu") | hello: Mr.Liu |
一行内导入多个模块的代码可读性不如多行的导入语句,在性能上和生成Python字节代码时这两种做法没有什么不同。所以多数情况下,使用多行导入较好。解释器执行到这条语句,如果在搜索路径中找到了指定的模块,就会加载它。该过程遵循作用域原则,如果在一个模块的顶层导入,那么它的作用域就是全局的;如果在函数中导入,那么它的作用域是局部的。如果模块是被第一次导入,它将被加载并执行。
import语句将一个模块的所有属性全部导入,在Python中也可以仅仅导入一部分属性,这时需要使用from-import语句,其语法为: from module import name1[, name2[,... nameN]]
例如当只需要模块math中的acos()函数和sqrt()函数时:
>>> | from math import acos, sqrt | >>> | print acos(-1) | 3.14159265359 | >>> | print sqrt(2) | 1.41421356237 |
示例中利用from-import 语句实现了只导入acos()函数和sqrt()函数的目的。当然,from-import语句也可以进行多行导入。有时候导入的模块或是模块属性名称已经在自己的程序中使用了,或者不想使用导入的名字。一个比较常用的解决方案是把模块赋值给一个变量: >>> | import math | >>> | like_name = math | >>> | print like_name.acos(-1) | 3.14159265359 |
在示例中,使用like_name代替了math,这样就实现了使用自己想要的名字替换模块的原始名称。除了这种方法外,还可以利用Python语言里的语句来实现:as语句。
as语句的语法: import old_module as new_ module
或者:
from module import old_name as new_name
语法import old_module as new_ module表示将模块old_module导入到当前文件中,同时为模块old_module起了一个新名称:new_module,在当前文件中只能使用新名称new_module不能使用旧名称old_module,示例: >>> | import math as like_name | >>> | print like_name.acos(-1) | 3.14159265359 |
语法from module import old_name as new_name表示将模块module中的名称old_name导入到当前文件中,同时为名称old_name起了一个新名称:new_name,在当前文件中只能使用新名称new_name不能使用旧名称old_name,示例: >>> | from math import acos as like_name | >>> | print like_name(-1) | 3.14159265359 |
11.2 名称空间
名称空间是名称(变量标识符)到对象的映射。向名称空间添加名称的操作过程,涉及到绑定标识符到指定对象的操作,和给该对象的引用计数加一。
(1) 程序执行期的名称空间
在程序执行期间有两个或三个活动的名称空间。这三个名称空间分别是局部名称空间,全局名称空间和内建名称空间,但局部名称空间在执行期间是不断变化的(时有时无)。在名称空间中访问这些名字时,依赖于系统加载这些名称空间的顺序。在程序运行时,Python解释器首先加载内建名称空间,它由__builtins__模块中的名字构成。之后加载执行模块的全局名称空间,它会在模块开始执行后变为活动名称空间。这样就有了两个活动的名称空间。如果在执行期间调用了一个函数或者是类的方法,那么将创建出第三个名称空间,即局部名称空间。使用globals()和locals()内建函数可以输出全局名称空间中的名称,和局部名称空间的名称,两个函数的详细说明在12.4节。
(2) 变量作用域
标识符的作用域是其声明在程序里的可应用范围,即是变量可见性。变量可以是局部域或者全局域。定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域。如果变量定义在函数中,它的出现即为函数的局部变量,全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数,全局变量的值都是可以被访问的,然而局部变量,就像它们存放的栈,暂时地存在,仅仅只依赖于定义它们的函数现阶段是否处于活动。当一个函数调用出现时,其局部变量就进入声明它们的作用域。在那一刻,一个新的局部变量名为那个对象创建了,一旦函数完成,变量将会离开作用域:
>>> | global_var = 'global' #定义一个全局变量 | >>> | def f(): #定义一个函数 | >>> | local_var = 'local' | >>> | return global_var + local_var | >>> | f() #调用函数 | globallocal | >>> | print global_var #使用一个全局变量 | global | >>> | print local_var #使用一个局部变量 | Traceback (most recent call last): | File "<stdin>", line 1, in <module> | NameError: name 'local_var' is not defined |
程序分析:在上例中,global_var 是全局变量,可以在函数f()内部访问,也可以在全局作用域访问;而local_var是局部变量,只能在函数f()内部访问,在函数f()外访问将导致错误。 (3) 名称空间与变量作用域比较
名称空间是纯粹意义上的名字和对象间的映射关系,而作用域还指出了从用户代码的哪些物理位置可以访问到这些名字。从作用域的观点来看,所有局部名称空间的名称都在局部作用范围内。局部作用范围以外的所有名称都在全局作用范围内。还要记得在程序执行过程中,局部名称空间和作用域会随函数调用而不断变化,而全局名称空间是不变的。
(4) 名称查找,确定作用域,覆盖
确定作用域的规则是如何联系到名称空间的呢? 它所要做的就是名称查询。访问一个属性时,解释器必须在三个名称空间中的一个找到它。首先从局部名称空间开始,如果没有找到,解释器将继续查找全局名称空间。如果这也失败了,它将在内建名称空间里查找。如果最后的尝试也失败了,将会得到这样的错误: >>> | gogo | Traceback (most recent call last): | File "<stdin>", line 1, in <module> | NameError: name ' gogo' is not defined |
在查找名称时,先查找的名称空间将会“遮蔽”其它后搜索的名称空间。例如,局部名称空间中找到的名字会隐藏全局或内建名称空间的对应对象。这就相当于覆盖了那个全局变量,例如:
>>> | def g(): | >>> | var = 10 | >>> | print "in function g(), var =" , var | >>> | var = 20 | >>> | g() | in function g(), var = 10 |
函数g()的局部名称空间里的变量var覆盖了全局的var变量。即使变量var存在于全局名称空间里,但程序首先找到的是局部名称空间里的那个var,所以覆盖了全局的变量var。
11.3 模块导入特性
一个模块只被加载一次,无论它被导入多少次。这可以阻止多重导入时代码被多次执行。例如,模块导入了math模块,而导入的其它3个模块也导入了math模块,那么每次都加载math模块将会造成不必要的浪费。所以,加载只在第一次导入时发生。
导入到当前名称空间的名称调用from-import可以把名字导入当前的名称空间里去,这意味着不需要使用句点属性标识来访问模块的标识符。例如,当需要访问模块math中的acos名字时是这样被导入的:from math import acos,这样使用单个的acos就可以访问它自身。把acos导入到名称空间后就再没必要引用模块了。当然,也可以把指定模块的所有名称导入到当前名称空间里: from module import *
但在实践中,“from module import *”不是良好的编程习惯,因为这样可能会污染当前名称空间,可能会覆盖当前名称空间中现有的名称。但如果某个模块有很多要经常访问的变量或者模块的名字很长,这也是一个好的方法。
从模块中只导入名字的另一个副作用是,那些名字会成为局部名称空间的一部分。这可能导致覆盖一个已经存在的具有相同名字的对象。而且对这些变量的改变只影响它的局部拷贝而不是所导入模块的原始名称空间。也就是说,绑定只是局部的而不是整个名称空间。下面介绍了一个只导入名称产生的副作用。
首先,提供了一个模块的代码hello.py: 1 | string = "abc" | 2 | def print_string(): | 3 | print string |
然后,当只导入hello模块内属性的名字时:
>>> | from hello import string,print_string | >>> | string = "xyz" | >>> | print_string() | abc |
在示例中可以看出,当使用from import语句导入模块内属性的名字时,导入的是属性的拷贝,因此对导入的名字改变时,不会改变被导入模块内的属性。若要想改变被导入模块的属性,应该使用import语句导入整个模块: >>> | import hello | >>> | hello.string = "xyz" | >>> | hello.print_string() | xyz |
使用import语句导入整个模块,对模块属性的引用将不再是原有模块属性的拷贝,而是原来模块中的属性,这样就可以改变被导入模块中的属性值。
11.4 模块内建函数 Python为模块提供了一些功能上的支持,即内建函数。它们在__builtin__模块中定义。这个模块一般不用手动导入。
11.4.1 __import__() __import__()函数是实际上导入模块的函数,import语句也是通过调用__import__函数完成工作的。用户可以覆盖__import__()函数,来实现自定义的导入算法,也可以使用该函数实现延迟化的模块导入。__import__()函数的一般形式为:
__import__(module_name[,globals[,locals[,fromlist]]])
其中,modlue_name是要导入模块的名称,而后面的globals,locals和fromlist都是可选参数。globals和locals分别是包含当前全局符号表的名字和包含局部符号表名字的字典;fromlist是一个使用from-import语句所导入符号的列表。
__import__()语句的具体使用方法为:
>>>__import__('sys')>>> __import__('os',globals(),locals(),['path','pip']) |
上面两个语句分别等价于
>>>import sys>>>from os import path, pip |
11.4.2 globals()和locals() globals()和locals()是分别返回调用者全局和局部名称空间的字典。在一个函数的内部,局部名称空间代表在函数执行时候定义的所有名字,locals()函数返回的就是包含这些名字的字典,而globals()会返回函数可访问的全局名字。另外在全局名称空间下,globals()和locals()会返回相同的字典。这两个函数的使用方法为: >>> | print globals() | {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', '__d | oc__': None, '__package__': None} | >>> | def f(a, b): | >>> | print locals() | >>> | f(1,2) | {'a': 1, 'b': 2} | >>> | locals() | {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'f': | <function f at 0x00000000025F0908>, '__doc__': None, '__package__': None} | >>> | globals() | {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'f': | <function f at 0x00000000025F0908>, '__doc__': None, '__package__': None} |
上面的代码中,可以看到第一个globals()函数输出了当前所有的全局名称,接下来定义了一个函数f(),并在函数中f()中调用了locals()函数,接下来在调用函数f()时,输出了函数f()这个局部空间的名称与其值的字典。接下来在全局作用域分别调用了globals()函数和locals()函数,但是其输出是相同了,这说明locals()函数输出的是以其被调用的位置为参考所在名字空间的名字。在函数f()中,对locals()来说,locals()所指的局部空间为整个函数f(),在全局空间调用locals()时,locals()所指的局部空间就是全局空间了。
11.4.3 dir() dir()函数会列出模块定义的标识符。标识符有函数、类和变量。它的一般形式为: dir([object]) 当给dir()提供一个模块名字时,它返回在那个模块中定义的名字的列表。当没有为其提供参数时,它返回当前模块中定义的名字的列表。
【例11-2】dir()函数应用举例
1 | class A: | 2 | def a(self): | 3 | pass | 4 | class A1(A): | 5 | def a1(self): | 6 | pass | 7 | if __name__ == '__main__': | 8 | print "dir without arguments:", dir() | 9 | print "dir class A:", dir(A) | 10 | print "dir class A1:", dir(A1) | 11 | a = A1() | 12 | print "dir object a(A1):", dir(a) | 13 | print "dir function a.a:", dir(a.a1) | 运行结果: dir without arguments: ['A', 'A1', '__builtins__', '__doc__', '__file__', '__name__', '__package__']dir class A: ['__doc__', '__module__', 'a']dir class A1: ['__doc__', '__module__', 'a', 'a1']dir object a(A1): ['__doc__', '__module__', 'a', 'a1']dir function a.a: ['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__func__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'im_class', 'im_func', 'im_self'] | 程序分析:程序中首先定义一个类A,并定义一个类A1继承自A类,并在A类和A1类中分别定义函数a和a1。第8行打印全局的属性列表。第9行打印类A中的属性列表。第10行打印类A1中的属性列表,这里包括从A继承来的函数‘a’。第11行创建一个A1的示例a,第12行打印这个实例中的属性列表,它是和类A1一样的。第13行打印a中函数a的属性列表,这里打印了Python中函数的内建属性。
11.4.4 reload() reload()内建函数可以重新导入一个已导入模块,它一般用于原模块有变化等特殊情况,其一般形式为: reload(module) 注意reload前该模块必须已经导入过,而且必须被成功导入。这里的module必须是模块自身,并不是字符串。另外,模块中的代码在导入时被执行,但只执行一次。以后执行import语句不会再次执行这些代码,只是绑定模块代码。
11.5 包 包是一个有层次的文件目录结构,它将有联系的模块组织在一起,有效地避免了模块名称的冲突问题。包不仅为平坦的名称空间加入有层次的组织结构,而且允许分发者使用目录结构,而不是一堆混乱的文件。与类和模块相同,包也使用句点属性来访问它们的元素,也使用标准的import和from-import语句倒入包中的模块。 假设一个包的例子有如下目录结构: Education/ __init__.py common_util.py Teacher/ __init__.py Model.py View.py Student/ __init__.py Url.py Ajax.py College/ __init__.py Admin.py |
上面例子中,Education为最顶层的包,Teacher,Student和College都是它的子包。注意到几乎每层目录下都有一个__init__.py文件,这些是初始化模块,只有包含这个文件,多层目录结构的包才能让import语句找到。当需要导入子包时,
import Education.Teacher.View |
这时使用View.py中的getInfo函数时,
Education.Teacher.View.getInfo() |
当然也可以使用from-import来实现不同需求的导入,例如当只导入顶层子包时,
from Education import Teacher |
使用文件中函数时,
Teacher.View.getInfo() |
另一种方法是引入更深层的子包,如,
from Education.Teacher import View |
这时使用函数时为,
View.getInfo() |
还可以沿着树形结构更深层次的导入,
from Education.Teacher.View import getInfo |
这时函数可以直接使用,
getInfo() |
(1)全部导入
包同样支持from-import all语句: from package.module import * |
这样写时会导入哪些文件取决于操作系统的文件系统。当然,用户也可以在__init__.py中自定义需要导入的模块,即将想要导入的模块名字组成一个字符串列表赋值给__all__变量。
(2)相对导入 因为导入子包肯能会和真正的标准模块发生冲突,这时包模块会把名字相同的标准库模块隐藏掉。所以,所有的导入现在都被认为是绝对的,即通过sys.path访问,如import Education.Teacher.View。除了绝对导入,Python也允许通过模块或包名称前置句点实现相对导入。 相对导入的特性稍微地改变了import语法,让程序员告诉导入者在子包的哪里查找某个模块。因为import语句总是绝对导入,所以相对导入只应用于from-import语句。语法的第一部分是一个句点,指示一个相对的导入操作。之后的其它附加句点代表当前from起始查找位置后的一个级别。 假设现在是在Education.Teacher.View的模块下,则下面语句 from . import Modelfrom .. import Studentfrom ..College import Admin |
分别等价于:
import Education.Teacher.Modelimport Education.Studentimport Education.College.Admin |
本章小结
本章介绍了Python模块的相关知识,和包的相关知识。学习了如何创建Python模块,如何导入模块。模块的使用能使程序更加具有层次性,使得程序更加易于管理。掌握名称空间,有助于对程序运行时名称引用的理解。之后介绍了几种模块的内建函数,包括__import__()、globals()、locals()、dir()和 reload()等。最后介绍了包的概念,使读者了解了树形的文件目录结构,同时讲解了全部导入和相对导入的相关概念。
习题
一、填空题 1. 一个Python语言的代码文件名称为hello.py,则其所代表的模块名称是____________。 2. 有一个Python模块名称为hello,则导入这个模块的语句为____________。 3. 有一个Python模块名称为hello,则导入这个模块时,希望使用新名称world来代替旧名称hello,则导入语句为____________。 4. 模块中常见的内建函数有_________、__________、__________、__________和__________。 5. 包使用__________和__________导入包中的模块。
二、选择题
1. 下面语句中导入模块hello正确的是( )。
A. import hello
B. as hello
C. from hello
D.from hello import hello
2. 对于下面的程序中的变量string说法正确的是()。 1 | def f(): | 2 | string = "hello world" | 3 | return string | 4 | def g(): | 5 | return "say " + string | 6 | print string |
A.string变量是全局变量
B.string变量是局部变量
C.在函数g()中可以引用变量string
D.在第六行引用变量string不会报错
3. Python中提供返回全局名称空间的字典的函数为( )
A. __import__() B. globlas()
C. locals() D. reload()
4. 对于11.5节中的文件目录结构,要访问Url.py文件中的getUrl函数,当from
Education.Student import Url导入时,以下使用正确的是( ) A. Education.Student.Url.getUrl() B. Student.Url.getUrl() C. Url.getUrl() D. getUrl()
5. 对于11.5节中的文件目录结构,假设现在在Education.Student.Url模块下,要使用 common_util.py文件,正确的导入方式为( ) A. from . import common_util B. from .. import common_util C. from ..Education import common_util D. import common_util

第12章 Python开发游戏
Python语言除了能够开发应用程序外,在游戏开发领域也被经常被用到。使用Python开发游戏中,由于游戏包含大量图片、交互等,因此需要对游戏开发库重新设计,目前较为常用的开发库是Pygame,其深受开发者的喜爱。本章将会围绕Pygame进行2D游戏设计,并介绍Pygame的几个常用模块以及游戏设计理念。通过本章的学习,读者可以对游戏设计有初步认识。
12.1 Pygame介绍

图12.1 Pygame图标

Pygame是一个跨平台专业开发游戏的Python模块包,基于SDL库,帮助用户脱离底层语言的束缚,完全使用高级语言Python来开发游戏。
Pygame是完全不受图形库限制的。很多游戏开发框架是完全依托OpenGL,但是鉴于OpenGL在很多平台上的糟糕表现,这样的设计会让开发者非常苦恼。而Pygame并不受图形库的约束,可以使用OpenGL,也可以使用directx,windib,X11,甚至是ASCII ART这样的冷门图形库。
Pygame在设计时还考虑到了Python做为一门高级语言在性能上的不足会对游戏运行带来的影响,所以Pygame在很多核心代码部分使用了更高效的C语言和汇编语言。而对于提高性能的另一个方面——多线程,为了跳出Python GIL的约束,Pygame对多线程利用C语言进行了重新实现,使得游戏可以在多核计算机上运行的更加出色。
当然Pygame的最大优点还是继承自Python语言本身的简洁和易用。使用Pygame可以大大降低游戏开发的门槛,开发人员只需很少的知识储备,就可以直接上手开发游戏了。如果读者希望在学完本章后对Pygame有更多的了解,可以访问Pygame官方网站http://www.pygame.org 查看Pygame的开发文档。如果希望能在学习完本章后可以进行实践的,也可以在Pygame官方网站上根据开发平台下载安装包。当然如果开发平台支持apt-get,emerge,pkg_add或yast,也可以使用它们直接安装。如果读者已经下载pygame且完成安装,可以使用下列方法确认是否安装成功以及当前安装的版本。 >>> | import pygame | >>> | print pygame.ver | 1.9.1release |
12.2 常用模块介绍 Pygame的发布版包括多个模块,涉及图形,声音,外设等。表12.1是对模块的一个列举。
表12.1 Pygame模块列表 模块名 | 功能 | pygame.cdrom | 访问光驱 | pygame.cursors | 加载光标 | pygame.display | 访问显示设备 | pygame.draw | 绘制形状、线和点 | pygame.event | 管理事件 | pygame.font | 使用字体 | pygame.image | 加载和存储图片 | pygame.joystick | 使用游戏手柄或者类似的东西 | pygame.key | 读取键盘按键 | pygame.mixer | 声音 | pygame.mouse | 鼠标 | pygame.movie | 播放视频 | pygame.music | 播放音频 | pygame.overlay | 访问高级视频叠加 | pygame.rect | 管理矩形区域 | pygame.sndarray | 操作声音数据 | pygame.sprite | 操作移动图像 | pygame.surface | 管理图像和屏幕 | pygame.surfarray | 管理点阵图像数据 | pygame.time | 管理时间和帧信息 | pygame.transform | 缩放和移动图像 |
由于本章内容有限,并不会对每一个模块都进行介绍和使用,所以接下来只对本章将会涉及的模块进行简要的介绍。
12.2.1 pygame及pygame.locals模块 导入pygame模块后会自动导入其它Pygame模块。通常使用import pygame就能在程序中使用Pygame的所有功能了。pygame.init函数需要写在整个游戏逻辑的开头,因为只有执行了这个函数,才会随之初始化其它所有的Pygame模块。 pygame.locals模块是Pygame的本地名字库,保存了事件类型、键和视频模式等的名字。
12.2.2 pygame.surface及pygame.font模块 pygame.surface模块可以返回一个Surface对象。Surface对象是一个具有确定尺寸的图像,可以利用Surface对象blit方法使得Surface对象中的元素在屏幕上完成绘制和移动。pygame.font模块主要处理游戏中的文本,设置文字文本的位置以及样式。font.render函数可以将文本生成为可以在屏幕上显示的图像。
12.2.3 pygame.display模块 pygame.display模块控制Pygame中所有与显示和窗口有关的操作,例如屏幕对象的获取与设置以及刷新。下面列举该模块中几个常用的方法:
(1)pygame.display.flip:对于屏幕中的所有元素进行刷新操作;
(2)pygame.display.update:对于屏幕中部分有修改的元素进行刷新操作;
(3)pygame.display.set_caption:设置当前窗口的标题;
(4)pygame.display.get_caption:获取当前窗口的标题;
(5)pygame.display.set_mode:设置显示窗口的尺寸大小;
(6)pygame.display.get_surface:获取一个当前显示对象的引用。
12.2.4 pygame.sprite模块 pygame.sprite.Sprite类是所有可视游戏对象的基类。对于游戏中需要创建的对象,都可以继承自这个类,只需重写构造方法以及几个涉及游戏逻辑的方法诸如update以及kill方法等。 pygame.sprite中另一个重要的成员是Group类,它可以作为Sprite类的容器并且容器中的对象进行管理。当需要同时对批量的对象进行刷新时,可以将这些对象全部装入一个Group,然后调用Group的update方法,这时Group内的所有对象都会被调用update方法,实现批量操作。 python.sprite. groupcollide可以批量处理两个group中元素之间的碰撞测试。groupcollid可以控制第三个和第四个参数来设置是否在碰撞测试结束后立刻调用碰撞元素的kill方法。
12.2.5 pygame.mouse模块
鼠标作为玩家控制游戏的重要外设,鼠标可以使得游戏玩法更加丰富。pygame.mouse封装了一些控制鼠标的方法。下面列举该模块中几个常用的方法:
(1)pygame.mouse.get_pressed:返回鼠标按键的状态;
(2)pygame.mouse.get_pos:得到当前鼠标的位置;
(3)pygame.mouse.set_visible:显示或者隐藏鼠标图标;
(4)pygame.mouse.set_pos:设置鼠标的位置。
12.2.6 pygame.event模块 pygame.event是一个保存了所有外设执行事件的队列。诸如鼠标移动,键盘按下和释放都会被作为一个事件被保存。Pygame最重要的一个环节就是从队列中取出事件,然后进行交互,这是玩家通过外设控制游戏元素的一个桥梁。下面列举该模块中几个常用的方法:
(1)pygame.event.get:从队列中拿出所有事件;
(2)pygame.event.poll:从队列头部拿出一个独立事件;
(3)pygame.event.wait:队列为空格时等待直到队列有事件再返回这个独立事件;
(4)pygame.event.peek:测试队列中是存在特定类型的事件。
12.2.7 pygame.Rect模块 pygame.rect是提供给开发者使用的标准矩形类。一般的游戏对象除了有一个代表图像的image属性,还有一个包络在image外的rect属性。通常对于一个image对象,可以直接调用它的get_rect方法得到包络它的rect对象。通过这个pygame.Rect模块内的方法可以轻松完成游戏对象的移动以及碰撞检测等操作。下面列举该模块中几个常用的方法:
(1)pygame.Rect.move:传入位移向量后移动游戏对象,返回移动后的rect对象;
(2)pygame.Rvent.move_ip:传入位移向量后移动游戏对象,原地操作无返回值;
(3)pygame.Rect.clamp:将一个矩形移动到另一个矩形内,返回移动后的rect对象;
(4)pygame.Rect.clamp_ip:将一个矩形移动到另一个矩形内,原地操作无返回值;
(5)pygame.Rect.colliderect:检测两个矩形是否有重合;
(6)pygame.Rect.union:将两个矩形合并后,返回合并后的rect对象;
(7)pygame.Rect.union_ip:将两个矩形合并后,原地操作无返回值。
12.3 游戏初步设计 通过上面两个小节的内容,读者已经对Pygame有了初步的了解,接下来将利用一个实际的游戏项目来向读者展示如何用Pygame完成一个简单的2D小游戏。 这个游戏的灵感来自大富翁游戏中的接金币小游戏以及经典游戏吃豆人(Pac-Man)。这个游戏将对这两个游戏进行一些融合:Pac-Man一直生活在管道中吃豆子,现如今他来到了一个全新的世界,接受全新的挑战。在这个世界里,天上会飘下无数的金元宝和炸弹。对于吃了一辈子豆子的Pac-Man来说,能吃到金元宝实在是太幸福了。但是在移动的时候,他也需要防备身边无数碰之即炸的炸弹。Pac-Man可以在游戏世界中沿着四个方向自由移动,只要碰到金元宝,就能把它吃掉,然后增加一点得分;而一旦碰到一个炸弹,就会将其引爆,并且使得自己的生命值减少一点。 游戏中所有的素材都可以通过互联网得到,对于这个游戏,只需图12.2中的三个图片即可。 图12.2 游戏中的图片素材

对于初试游戏开发的新手来说,在设计这个游戏时,可以先不必考虑整个游戏的全局逻辑,而只需要先设计一个游戏中的简单模型即可。 不妨就以炸弹作为此次游戏设计的第一步。炸弹在游戏中的唯一行为就是移动。所以只要完成了炸弹的移动操作,就相当于为游戏中所有对象的移动做了一个参考和示范,也能为未来的继续开发提供便利。 虽然这个任务很简单,但是适当的设计计划还是很必要的。如果要让这个程序成功运行,那么下面几点缺一不可:
(1)初始化过程:Pygame的模块初始化(pygame.init),窗口对象的创建,背景、炸弹图像对象的创建;
(2)炸弹类的设计:声明炸弹类(继承自pygame.sprite.Sprite),重写构造方法和update方法,按照需求添加需要的其它方法;
(3)炸弹对象的创建:按照需要创建若干个炸弹类的实例对象,并且全部装入Group中进行统一管理;
(4)事件监听:在游戏主流程中,应当有一个循环语句监听事件队列。对于本程序,会发生的事件只有退出游戏的请求,所以只需监听两类事件(退出键的点击、键盘Esc的按下)即可;
(5)屏幕的刷新:在游戏的主流程中,通过screen.blit、Group.update、Group.draw以及pygame.display.flip()完成对屏幕中所有游戏对象的刷新。
上面5点要求按照Pygame的语法全部实现出来,就能得到预期的游戏效果了。完整的炸弹移动代码如例12.1。 【例12.1】炸弹的移动效果设计(bomb.py) 1 | #coding:utf-8 | 2 | import os, sys, random | 3 | import pygame | 4 | from pygame.locals import * | 5 | | 6 | # 初始化 | 7 | pygame.init() | 8 | pygame.display.set_caption("Pac-Man challenge!") # 设置窗口标题 | 9 | screen = pygame.display.set_mode((800, 600)) # 获取固定尺寸的窗口对象 | 10 | pygame.mouse.set_visible(0) # 隐藏鼠标图标 | 11 | | 12 | # 获取背景对象并进行填充 | 13 | background = pygame.Surface(screen.get_size()) | 14 | background = background.convert() | 15 | background.fill((100, 100, 255)) | 16 | | 17 | # 载入炸弹的图片,并且转化成Pygame的image对象 | 18 | bomb_image = pygame.image.load("bomb.png").convert_alpha() | 19 | | 20 | # 炸弹类 | 21 | class Bomb(pygame.sprite.Sprite): | 22 | def __init__(self, centerx): | 23 | pygame.sprite.Sprite.__init__(self) | 24 | self.image = bomb_image | 25 | self.rect = self.image.get_rect() | 26 | self.reset() | 27 | def update(self): | 28 | """ | 29 | 重写超类update方法,将当前炸弹按照位移向量进行移动 | 30 | 若到达底部,则将其从顶部随机位置重新释放 | 31 | """ | 32 | self.rect.move_ip((self.dx, self.dy)) | 33 | if self.rect.top > screen.get_height(): | 34 | self.reset() | 35 | def reset(self): | 36 | """ | 37 | 重置方法,将炸弹放回顶部,并且重新初始化坐标和位移向量 | 38 | """ | 39 | self.counter = random.randint(0, 60) | 40 | self.rect.bottom = 0 | 41 | self.rect.centerx = random.randrange(0, 600) | 42 | self.dy = random.randrange(1, 4) | 43 | self.dx = random.randrange(-2, 2) | 44 | | 45 | # 游戏主过程 | 46 | def main(): | 47 | #创建三个炸弹对象,并且装入Group中 | 48 | global BombSprites | 49 | BombSprites = pygame.sprite.Group(()) | 50 | BombSprites.add(Bomb(200)) | 51 | BombSprites.add(Bomb(300)) | 52 | BombSprites.add(Bomb(400)) | 53 | | 54 | # 创建一个计时器对象 | 55 | clock = pygame.time.Clock() | 56 | | 57 | while True: | 58 | # 计时器延迟等待 | 59 | clock.tick(30) | 60 | | 61 | # 处理事件队列中的事件 | 62 | for event in pygame.event.get(): | 63 | if event.type == pygame.QUIT: | 64 | sys.exit() | 65 | if event.type == pygame.KEYDOWN and \ | 66 | event.key == K_ESCAPE: | 67 | sys.exit() | 68 | | 69 | # 将背景绘制入screen对象中 | 70 | screen.blit(background, (0, 0)) | 71 | | 72 | # 完成所有炸弹的状态刷新 | 73 | BombSprites.update() | 74 | # 将Group中所有炸弹全部绘制入screen对象中 | 75 | BombSprites.draw(screen) | 76 | # 更新全屏所有显示部分 | 77 | pygame.display.flip() | 78 | | 79 | if __name__ == "__main__": | 80 | main() |
对于例12.1中bomb.py使用下列命令即可成功运行。
$ python bomb.py |
最后的炸弹移动效果如图12.3所示:

图12.3 炸弹移动效果图
12.4 进一步完善游戏 在12.3节的介绍中,炸弹降落的逻辑已经实现了,但是与其说它是游戏,更不如说它是个动画。本节将会把游戏剩下来的逻辑全部实现了,使得这个程序真正的成为一个可以人机交互的游戏。 通过对本游戏中三类游戏对象的分析,可以发现炸弹的很多属性行为,如移动、位置重置,三者都有。所以可以把12.3节中的Bomb类的部分逻辑抽象出来作为一个超类,然后让游戏中的其余自定义游戏对象类来继承它。该类的实现如下: 1 | class BaseObject(pygame.sprite.Sprite): | 2 | def __init__(self): | 3 | """ | 4 | 构造方法,显式调用超类pygame.sprite.Sprite的构造方法 | 5 | """ | 6 | pygame.sprite.Sprite.__init__(self) | 7 | def update(self): | 8 | """ | 9 | 重写超类update方法,将当前对象按照位移向量进行移动 | 10 | 若到达底部,则将其从顶部随机位置重新释放 | 11 | """ | 12 | self.rect.move_ip((self.dx, self.dy)) | 13 | if self.rect.top > screen.get_height(): | 14 | self.reset() | 15 | def reset(self): | 16 | """ | 17 | 重置方法,将当前对象放回顶部,并且重新初始化坐标和位移向量 | 18 | """ | 19 | self.counter = random.randint(0, 60) | 20 | self.rect.bottom = 0 | 21 | self.rect.centerx = random.randrange(0, screen.get_width()) | 22 | self.dy = random.randrange(1, 4) | 23 | self.dx = random.randrange(-2, 2) |
程序分析:这个超类虽然不能涵盖所有游戏对象的行为特点,但是在继承遇到与超类不同的时候可以使用方法的重写来重新实现超类中的方法。游戏中的主角Pac-man是本游戏设计中的重点,因为它是由玩家控制的,而且存在和炸弹,金元宝之间的碰撞检验逻辑。假设Pac-man在游戏中的载体名为Player类,那么这个Player类只从BaseObject类中继承是显然不够的,所以还需重新实现类中的update方法。
在Player类中,除了要完成移动外,还需要实现下述三点:
(1)根据当前键盘按键信息设置当前图片的朝向方向以及对象的位移向量;
(2)四个边界的边界判断以及处理;
(3)判断Pac-man和炸弹以及金元宝的碰撞,并且结算得分和生命剩余量。当生命非正数时,调用当前Player的kill方法。
在完成了所有游戏对象的设计后,为了增加游戏的体验,应当还有游戏的结束菜单提供给玩家,让他们选择是退出游戏还是再玩一盘。如果完成了上述这些功能,那么这个游戏基本就实现了。在例12.2中,将给出这个游戏的完整实现,读者可以通过程序重新体会这个游戏的设计思想。 【例12.2】Pac-Man challenge!游戏完整程序 (game.py) 1 | #coding:utf-8 | 2 | import os, sys, random | 3 | import pygame | 4 | from pygame.locals import * | 5 | | 6 | # 初始化 | 7 | pygame.init() | 8 | pygame.display.set_caption("Pac-Man challenge!") # 设置窗口标题 | 9 | screen = pygame.display.set_mode((800, 600)) # 获取固定尺寸的窗口对象 | 10 | pygame.mouse.set_visible(0) # 隐藏鼠标图标 | 11 | | 12 | # 获取背景对象并进行填充 | 13 | background = pygame.Surface(screen.get_size()) | 14 | background = background.convert() | 15 | background.fill((100, 100, 255)) | 16 | | 17 | | 18 | # 载入所有素材图片,并且转化成Pygame的image对象 | 19 | gold_image = pygame.image.load("gold.png").convert_alpha() | 20 | bomb_image = pygame.image.load("bomb.png").convert_alpha() | 21 | pacman_image_right = pygame.image.load("pac-man-right.png").convert_alpha() | 22 | pacman_image_left = pygame.image.load("pac-man-left.png").convert_alpha() | 23 | pacman_image_down = pygame.image.load("pac-man-down.png").convert_alpha() | 24 | pacman_image_up = pygame.image.load("pac-man-up.png").convert_alpha() | 25 | | 26 | # 游戏对象超类 | 27 | class BaseObject(pygame.sprite.Sprite): | 28 | def __init__(self): | 29 | """ | 30 | 重写构造方法,显式调用超类pygame.sprite.Sprite的构造方法 | 31 | """ | 32 | pygame.sprite.Sprite.__init__(self) | 33 | def update(self): | 34 | """ | 35 | 重写超类update方法,将当前对象按照位移向量进行移动 | 36 | 若到达底部,则将其从顶部随机位置重新释放 | 37 | """ | 38 | self.rect.move_ip((self.dx, self.dy)) | 39 | if self.rect.top > screen.get_height(): | 40 | self.reset() | 41 | def reset(self): | 42 | """ | 43 | 重置方法,将当前对象放回顶部,并且重新初始化坐标和位移向量 | 44 | """ | 45 | self.counter = random.randint(0, 60) | 46 | self.rect.bottom = 0 | 47 | self.rect.centerx = random.randrange(0, screen.get_width()) | 48 | self.dy = random.randrange(1, 4) | 49 | self.dx = random.randrange(-2, 2) | 50 | | 51 | # Pac-man | 52 | class Player(BaseObject): | 53 | def __init__(self): | 54 | """ | 55 | 重写构造方法,显式调用超类构造方法 | 56 | 初始化成员变量 | 57 | """ | 58 | BaseObject.__init__(self) | 59 | self.image = pacman_image_right | 60 | self.rect = self.image.get_rect() | 61 | self.rect.center = (400, 500) | 62 | self.dx = 0 | 63 | self.dy = 0 | 64 | self.life = 3 | 65 | self.score = 0 | 66 | | 67 | def update(self): | 68 | """ | 69 | 重写超类update方法 | 70 | """ | 71 | | 72 | # 移动当前游戏对象 | 73 | self.rect.move_ip((self.dx, self.dy)) | 74 | | 75 | # 根据方向按键设置当前对象的图片朝向 | 76 | key = pygame.key.get_pressed() | 77 | if key[pygame.K_UP]: | 78 | self.image = pacman_image_up | 79 | elif key[pygame.K_DOWN]: | 80 | self.image = pacman_image_down | 81 | elif key[pygame.K_LEFT]: | 82 | self.image = pacman_image_left | 83 | elif key[pygame.K_RIGHT]: | 84 | self.image = pacman_image_right | 85 | | 86 | # 边界判断 | 87 | if self.rect.left < 0: | 88 | self.rect.left = 0 | 89 | elif self.rect.right > 800: | 90 | self.rect.right = 800 | 91 | | 92 | if self.rect.top < 0: | 93 | self.rect.top = 0 | 94 | elif self.rect.bottom > 600: | 95 | self.rect.bottom = 600 | 96 | | 97 | # 碰撞检验以及相应的数据维护 | 98 | | 99 | # 与炸弹Group的碰撞检验,对于所有判断出碰撞的炸弹调用kill方法 | 100 | if pygame.sprite.groupcollide(playerSprite, BombSprites, 0, 1): | 101 | self.life -= 1 | 102 | if self.life <= 0: | 103 | self.kill() | 104 | | 105 | # 与金元宝Group的碰撞检验,对于所有判断出碰撞的金元宝调用kill方法 | 106 | if pygame.sprite.groupcollide(playerSprite, GoldSprites, 0, 1): | 107 | self.score += 1 | 108 | | 109 | def set_direction(self, dx, dy): | 110 | """ | 111 | 设置对象的位移向量 | 112 | """ | 113 | self.dx, self.dy = dx, dy | 114 | | 115 | # 炸弹类 | 116 | class Bomb(BaseObject): | 117 | def __init__(self, centerx): | 118 | """ | 119 | 重写构造方法,显式调用超类构造方法 | 120 | 初始化成员变量 | 121 | """ | 122 | BaseObject.__init__(self) | 123 | self.image = bomb_image | 124 | self.rect = self.image.get_rect() | 125 | self.reset() | 126 | | 127 | # 金元宝类 | 128 | class Gold(BaseObject): | 129 | def __init__(self, centerx): | 130 | """ | 131 | 重写构造方法,显式调用超类构造方法 | 132 | 初始化成员变量 | 133 | """ | 134 | BaseObject.__init__(self) | 135 | self.image = gold_image | 136 | self.rect = self.image.get_rect() | 137 | self.reset() | 138 | | 139 | # 菜单类 | 140 | class SpaceMenu(object): | 141 | def __init__(self, *options): | 142 | """ | 143 | 构造方法 | 144 | 传入选项列表,完成成员变量的初始化 | 145 | """ | 146 | self.options = options # 选项列表 | 147 | self.x = 0 | 148 | self.y = 0 | 149 | self.font = pygame.font.Font(None, 32) | 150 | self.option = 0 # 焦点选项编号 | 151 | self.width = 1 | 152 | self.color = [0, 0, 0] | 153 | self.hcolor = [0, 0, 0] | 154 | self.height = len(options) * self.font.get_height() # 菜单高度 | 155 | | 156 | # 根据选项最大宽度确定菜单宽度 | 157 | for o in options: | 158 | text = o[0] | 159 | ren = self.font.render(text, 1, (0, 0, 0)) | 160 | if ren.get_width() > self.width: | 161 | self.width = ren.get_width() | 162 | | 163 | def draw(self, surface): | 164 | """ | 165 | 实现draw方法 | 166 | """ | 167 | i = 0 | 168 | for o in self.options: | 169 | # 判断当前选项是否为焦点选项,并且为之设置字体颜色 | 170 | if i == self.option: | 171 | clr = self.hcolor | 172 | else: | 173 | clr = self.color | 174 | | 175 | # 将选项的文字绘制到屏幕中 | 176 | text = o[0] | 177 | ren = self.font.render(text, 1, clr) | 178 | if ren.get_width() > self.width: | 179 | self.width = ren.get_width() | 180 | surface.blit(ren, (self.x, self.y + i * self.font.get_height())) | 181 | i += 1 | 182 | | 183 | def update(self, events): | 184 | """ | 185 | 实现update方法 | 186 | """ | 187 | | 188 | # 根据上下按键,更新焦点选项编号 | 189 | # 若按下回车键,则调用当前焦点选项绑定函数 | 190 | for e in events: | 191 | if e.type == pygame.KEYDOWN: | 192 | if e.key == pygame.K_UP: | 193 | self.option -= 1 | 194 | elif e.key == pygame.K_DOWN: | 195 | self.option += 1 | 196 | elif e.key == pygame.K_RETURN: | 197 | self.options[self.option][1]() | 198 | if self.option > len(self.options) - 1: | 199 | self.option = 0 | 200 | elif self.option < 0: | 201 | self.option = len(self.options) - 1 | 202 | | 203 | def set_pos(self, x, y): | 204 | """ | 205 | 设置当前菜单左上角位置 | 206 | """ | 207 | self.x = x | 208 | self.y = y | 209 | | 210 | def set_font(self, font): | 211 | """ | 212 | 设置当前菜单的字体 | 213 | """ | 214 | self.font = font | 215 | for o in self.options: | 216 | text = o[0] | 217 | ren = self.font.render(text, 1, (0, 0, 0)) | 218 | if ren.get_width() > self.width: | 219 | self.width = ren.get_width() | 220 | | 221 | def set_highlight_color(self, color): | 222 | """ | 223 | 设置焦点选项的高亮字体颜色 | 224 | """ | 225 | self.hcolor = color | 226 | | 227 | def set_normal_color(self, color): | 228 | """ | 229 | 设置非焦点选项的普通字体颜色 | 230 | """ | 231 | self.color = color | 232 | | 233 | def center_at(self, x, y): | 234 | """ | 235 | 通过设定菜单中心点设置菜单左上角位置 | 236 | """ | 237 | self.x = x - (self.width / 2) | 238 | self.y = y - (self.height / 2) | 239 | | 240 | | 241 | # 游戏主过程 | 242 | def main(): | 243 | | 244 | # 创建字体对象 | 245 | font = pygame.font.Font(None, 32) | 246 | | 247 | #游戏对象的创建 | 248 | #创建一个Pac-Man对象,并且装入Group中 | 249 | global playerSprite | 250 | playerSprite = pygame.sprite.RenderPlain((Player())) | 251 | | 252 | #创建三个炸弹对象,并且装入Group中 | 253 | global BombSprites | 254 | BombSprites = pygame.sprite.Group(()) | 255 | BombSprites.add(Bomb(200)) | 256 | BombSprites.add(Bomb(300)) | 257 | BombSprites.add(Bomb(400)) | 258 | | 259 | #创建三个金元宝对象,并且装入Group中 | 260 | global GoldSprites | 261 | GoldSprites = pygame.sprite.Group(()) | 262 | GoldSprites.add(Gold(250)) | 263 | GoldSprites.add(Gold(350)) | 264 | GoldSprites.add(Gold(450)) | 265 | | 266 | # 创建一个计时器对象 | 267 | clock = pygame.time.Clock() | 268 | | 269 | counter = 0 | 270 | | 271 | while True: | 272 | # 计时器延迟等待 | 273 | clock.tick(30) | 274 | | 275 | # 处理事件列表,根绝按键判断退出,或设置Pac-Man的位移向量 | 276 | for event in pygame.event.get(): | 277 | if event.type == pygame.QUIT: | 278 | sys.exit() | 279 | elif event.type == pygame.KEYDOWN: | 280 | if event.key == pygame.K_ESCAPE: | 281 | sys.exit() | 282 | elif event.key == pygame.K_LEFT: | 283 | player.set_direction(-10, 0) | 284 | elif event.key == pygame.K_RIGHT: | 285 | player.set_direction(10, 0) | 286 | elif event.key == pygame.K_UP: | 287 | player.set_direction(0, -10) | 288 | elif event.key == pygame.K_DOWN: | 289 | player.set_direction(0, 10) | 290 | | 291 | | 292 | elif event.type == pygame.KEYUP: | 293 | | 294 | | 295 | # 更新所有游戏对象 | 296 | screen.blit(background, (0, 0)) | 297 | playerSprite.update() | 298 | BombSprites.update() | 299 | GoldSprites.update() | 300 | text = "LIFE:" + str(player.life) | 301 | life = font.render(text, True, (255, 255, 255)) | 302 | text = "SCORE:" + str(player.score) | 303 | score = font.render(text, True, (255, 255, 255)) | 304 | | 305 | # 绘制所有游戏对象并且刷新屏幕 | 306 | playerSprite.draw(screen) | 307 | BombSprites.draw(screen) | 308 | GoldSprites.draw(screen) | 309 | screen.blit(life, (0, 0)) | 310 | screen.blit(score, (0, 22)) | 311 | pygame.display.flip() | 312 | | 313 | # 创建新的炸弹对象和金元宝对象 | 314 | counter += 1 | 315 | if counter >= 20 and len(BombSprites) < 20: | 316 | BombSprites.add(Bomb(300)) | 317 | counter = 0 | 318 | if counter >= 20 and len(GoldSprites) < 20: | 319 | GoldSprites.add(Gold(300)) | 320 | counter = 0 | 321 | | 322 | # 判断游戏是否结束 | 323 | if len(playerSprite) == 0: | 324 | gameOver() | 325 | break | 326 | | 327 | # 游戏结束菜单过程 | 328 | def gameOver(): | 329 | | 330 | # 创建一个菜单标题对象 | 331 | menuTitle = SpaceMenu( | 332 | ["GAME OVER"]) | 333 | menuTitle.set_font(pygame.font.Font(None, 80)) | 334 | menuTitle.center_at(400, 230) | 335 | menuTitle.set_highlight_color((255, 255, 255)) | 336 | | 337 | # 创建一个菜单对象 | 338 | info = SpaceMenu( | 339 | ["exit", main], ["continue", sys.exit]) | 340 | info.set_font(pygame.font.Font(None, 40)) | 341 | info.center_at(400, 350) | 342 | info.set_highlight_color((255, 255, 255)) | 343 | | 344 | # 处理事件列表 | 345 | while True: | 346 | events = pygame.event.get() | 347 | for event in events: | 348 | if event.type == pygame.KEYDOWN: | 349 | if event.key == pygame.K_ESCAPE: | 350 | sys.exit() | 351 | elif event.type == pygame.QUIT: | 352 | sys.exit() | 353 | | 354 | # 更新菜单对象 | 355 | info.update(events) | 356 | | 357 | # 绘制菜单标题对象及菜单对象,刷新屏幕 | 358 | menuTitle.draw(screen) | 359 | info.draw(screen) | 360 | pygame.display.flip() | 361 | | 362 | | 363 | if __name__ == "__main__": | 364 | main() # 进入游戏 |
最后的游戏效果如图12.4和图12.5。
本章小结
本章介绍了使用Python进行游戏开发的知识。简要介绍了Python游戏开发第三方库Pygame的特点。针对Pygame介绍了几个常用的模块,例如Pygame.local,Pygame.surface,Pygame.sprite等。最后利用前面介绍的模块设计了一个2D小游戏,希望读者通过本章学习,能够对Pygame库有一个大概的了解,并且在今后学习中能够利用Pygame设计出自己的游戏。

图12.4 游戏运行效果图

图12.5 游戏结束菜单

第13章 TCP/UDP网络编程
本章将通过具体案例给读者介绍Python网络编程的方法和技巧。Python语言是一种强大的网络编程工具,这是因为Python有很多成型的针对网络协议的库,库中的函数均已完整的实现,这样可以集中精力在程序的逻辑处理上,而不用特别在意网络协议实现的细节和格式。同时Python对于字节流的处理能力也十分突出。本章从网络编程的背景、相关设计模块开始介绍,再结合相关的TCP/UDP编程实例,让读者能更好的理解和使用Python网络编程。
【体系结构】

【本章重点】
(1)了解网络编程的概念及背景;
(2)掌握套接字的概念及使用;
(3)掌握主要的网络设计模块;
(4)掌握UDP编程;
(5)掌握TCP编程;
13.1 问题引入
目前互联网已经被广泛使用,并已日渐成熟。随着互联网技术的飞速发展,网络编程已经成为各大公司使用的核心技术。无论是日常的访问网站,还是收发电子邮件,无不使用网络编程的知识。而Python语言由于有着丰富的网络协议库,更是各个开发技术中的佼佼者。
那么,如何通过计算机网络实现用户之间的通信?如何开发基于网络的应用系统(如协议分析、网络计费、网络监控、防火墙、网络入侵检测等)?如何有效地管理网络?如何减少因网络使用带来的不良影响?解决上述问题的关键是网络编程和网络协议分析。通过网络编程可以实现数据包的接收与发送,通过协议分析可以解释接收到的数据包,进而根据不同的应用需求实现相应的应用程序编制工作。
网络编程应用程序和现实生活中的电话聊天有很多相似之处。一个完整的电话聊天过程,需要打电话和接电话方的互相配合。拨出电话时,需要对方的电话号码。当接收方通过来电显示确认对方的号码后,接通电话。整个通话连接就建立了,再将信息通过电话传输过去。信息传达完,双方挂断电话,象征这整个通信过程的结束。
13.1.1 客户端服务器网络简介
在计算机的世界里,凡是提供服务的一方称为服务器(Server),而接受服务的一方称为客户端(Client)。比如,局域网络里的文件服务器所提供的文件存储服务,提供文件存储的计算机就是服务器,而使用访问服务器的另一方则称作客户端。但是谁是客户端谁是服务器也不是绝对的,如果提供服务的服务器要使用其他机器所提供的服务,则这个服务器便转变为客户端。
生活中有很多具体的应用,比如,打印(机)服务是一个硬件服务器。它们处理打印任务,并把任务发给相连的打印机(或其它打印设备)。这样的电脑一般是可以通过网络访问并且客户机器可以远程给它发送打印请求。再比如,网上银行服务,用户访问银行服务,从他们的电脑使用Web浏览器客户端发送请求到在银行Web服务器。也可以通过本地的银行客户端查询在另一家银行的数据库服务器检索帐户信息。再将余额返回到本地银行的数据库客户端。

图13.1 网络图
13.1.2 客户端服务器网络编程
在实际网络编程中,设计到一系列的处理工作,来完成上面提到的服务。对于服务器端,首先要创建一个通讯端口,用来监听客户端的请求,即一直等待客户发来的请求。然后,服务器准备好后,会通知所有的客户。接着,在客户端方面,也需要建立一个通讯端口,就像你和别人通话一样,要有对方的电话号码,这样才能建立到服务器的连接。然后客户端会向服务器发出请求,请求接收后,会进行数据交互,就像在电话中进行交流。当请求处理完成,客户端收到结果,这次通话就结束了。
13.2 套接字
套接字(Socket)是一种机器或者进程间交互数据的协议,是支持TCP/IP网络通信的基本操作单元,类似于上文中提到的通讯端口,它起源于BSD UNIX类的操作系统中,那时主要提供进程间通信。套接字一般包括两个:客户端套接字和服务器套接字。创建一个服务器套接字后,让它等待连接。这样就可以在某个网络地址处(IP地址和一个端口号的组合)监听,它需要随时待命准备处理客户端的多个连接。而客户端套接字只需要建立连接,处理事务,并断开连接。
TCP/IP协议是Transmission Control Protocol/Internet Protocol的简写,中文名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。TCP/IP 定义了电子设备怎样连入因特网,以及数据之间传输的标准。协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。一般来说:TCP负责发现传输过程中的问题,一旦出现问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。常用的3种套接字类型如下:
(1)流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于它使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
(2)数据报套接字(SOCK_DGRAM)
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
(3)原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
13.3 网络设计模块
前几节已经对客户端/服务器、套接字、网络方面的知识有了初步的了解。接下来内容中,本节对Python中几种常用的网络模块进行讲解。主要讨论socket模块、urllib和urllib2模块。
13.3.1 Socket模块
一个套接字就是一个socket模块中的socket类的实例。通常使用socket.socket()函数来创建套接字。它的实例化需要3个参数:第一个参数是地址族(默认是socket.AF_INET,地址族是地址划分的标准集合,AF_INET是基于网络的地址家族),第二个参数是流(默认是socket.SOCK_STREAM,就是上一节中介绍的套接字类型),第三个参数是使用的协议(默认值是0,使用默认值即可)。语法如下:
socket(socket_family,socket_type,protocol=0) | 创建一个TCP/IP的套接字: socket.socket(socket.AF_INET,socket.SOCK_STREAM) | 创建一个UDP/IP的套接字: socket.socket(socket.AF_INET,socket.SOCK_DGRAM) | 表13.1和表13.2是套接字对象的常用方法和属性。
表13.1 套接字对象常用函数表 函数 | 描述 | bind() | 绑定地址(主机,端口号)到套接字 | listen() | TCP监听函数 | accept() | 被动接受TCP客户的连接,阻塞式(accept会阻塞程序,运行到这里会挂起) | connect() | 主动初始化TCP服务器连接,出错时抛出异常 | connect_ex() | 在connect函数基础上增加了出错返回错误码 | recv() | 接受TCP数据 | send() | 发送TCP数据 | sendall() | 完整发送TCP数据 | recvfrom() | 接受UDP数据 | sendto() | 发送UDP数据 | getpeername() | 连接到当前套接字的远端地址 | getsockname() | 当前套接字的地址 | getsockopt() | 返回指定套接字的参数 | setsockopt() | 设置指定套接字的参数 | close() | 关闭套接字 | setblocking() | 设置套接字的阻塞和非阻塞模式 | settimeout() | 设置阻塞套接字的超时时间 | gettimeout() | 得到阻塞套接字操作的超时时间 | fileno() | 套接字的文件描述符 | makefile() | 创建一个与该套接字关联的文件 | ssl() | 初始化一个安全套接字层(SSL) | getaddrinfo() | 得到地址信息 | getfqdn() | 返回完整的域的名字 | gethostname() | 获取当前主机名 | gethostbyname() | 通过主机名获取ip地址 | gethostbyname_ex() | 获取主机所有别名和IP地址列表 | gethostbyaddr() | 由IP地址得到DNS信息 | getprotobyname() | 由协议名得到对应的号码 | getservbyname() | 由服务器名获取对应的端口号 | getservbyport() | 由端口号获取服务器名 | getdefaulttimeout() | 得到默认的套接字超时时间 | setdefaulttimeout() | 设置默认的套接字超时时间 |

表13.2 套接字对象常用属性表 属性 | 描述 | AF_UNIX,AF_INET,AF_INET6 | Python支持的套接字地址族 | SO_STREAM,SO_DGRAM | 套接字类型(TCP-流,UDP-数据报) | has_ipv6 | 表示是否支持IPv6的标志变量 | error | 套接字相关错误 | herror | 主机和地址相关的错误 | gaierror | 地址相关的错误 | timeout | 超时 |
以上函数使用过程如下:
* 服务器端套接字要先使用bind方法设置主机和端口号,再使用listen方法(只有一个参数,即服务器端允许排队等待的连接数目)监听这个主机和端口。 * 客户端套接字使用connect方法(地址要和服务器端相同)连接到服务器,使用gethostname方法可以在服务器端获取当前主机名。 * 服务器端使用accept方法来接受客户端的连接,这个方法会阻塞本地进程直到客户端有连接出现。该方法会返回一个元组(client,address),client是客户端socket,address是地址(必须是一个双元素元组(host,port),主机名或者ip地址+端口号)。当服务器处理完客户端请求后,会调用下一个accept等待下一个连接。 * 客户端与服务器发送和接收数据一般使用send和recv方法,send的参数是字符串,而recv的参数是接收数据的字节数大小(不确定时可以使用1024为参数)。 那么如何使用这些socket方法实现一个简单的客户端及服务器通信程序呢?请看下面的例子。
问题分析:为了实现通信,此处以TCP通信为例。发送端和接收端都必须创建TCP的Socket对象。接着在服务器端需要绑定具体的IP和端口(比如:localhost即127.0.0.1本机ip,端口为13014),然后设置监听这个端口,等待客户端连接。然后在客户端同样设置通信的ip和端口后,接着发送数据到服务器,服务器设置接受超时和缓存大小后,接受并处理信息,在发回给客户端。客户端接收数据并打印,最后完成整个通信流程。
【例13-1】socket套接字服务器端使用方法。 1 | ''' | 2 | 例 13-1 socket套接字服务器端使用方法 | 3 | 分成客户端client和服务器server两个程序 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Server.py | 7 | #socket套接字的服务器端写法 | 8 | import socket | 9 | sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #定义TCP的socket | 10 | sock.bind(('localhost',13014)) #绑定端口 | 11 | sock.listen(5) #设置监听 | 12 | while True: | 13 | connection,address = sock.accept() #获取地址 | 14 | print "client ip is " | 15 | print address #打印地址 | 16 | try: | 17 | connection.settimeout(5) #设置超时时间 | 18 | buf = connection.recv(1024) #设置接受缓存 | 19 | if buf == '1': | 20 | connection.send('welcome to python server!') #发送数据 | 21 | else: | 22 | connection.send('please go out!') #发送数据 | 23 | except socket.timeout: | 24 | print 'time out' #如果异常超时,打印超时信息 | 25 | connection.close() |
【例13-2】socket套接字客户端使用方法。
1 | ''' | 2 | 例 13-2 socket套接字客户端使用方法 | 3 | 分成客户端client和服务器server两个程序 | 4 | ''' | 5 | #!/usr/bin/python | 6 | # Filename: Client.py | 7 | #socket套接字的客户端端写法 | 8 | import socket | 9 | import time | 10 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #定义TCP套接字 | 11 | sock.connect(('localhost', 13014)) #设置连接地址端口 | 12 | time.sleep(2) #休眠 | 13 | sock.send('1') #发送数据 | 14 | print sock.recv(1024) #打印缓存信息 | 15 | sock.close() |
运行结果:分别开启两个终端并运行
>>>python Server.py | client ip is | (‘127.0.0.1’, 13014) | >>>python Client.py | welcome to python server! | 程序分析: 这两段代码主要实现了一个简单客户端服务器socket,首先在服务器端设置端口和ip并开始监听,而客户端则是向服务器发送一个字符1,当服务器接收到客户端发来的1时,使用send方法发送那段字符,客户端接收并打印出来。
13.3.2 urllib模块 urllib是网络设计模块中功能最强大的。它们能通过网络远程访问文件,通过极其简单的函数调用,可以把所有URL(Uniform Resource Locators,统一资源定址器)指向的网页和文件作为程序输入。
核心函数:
def urlopen(url, data=None, proxies=None)
参数说明:url:符合URL规范的字符串(包括http,ftp,gopher,local-file标准);data:向指定的URL发送的数据字符串,GET和POST都可,但必须符合标准格式格式为key=value&key1=value1....;proxies代理服务器地址字典,如果未指定,在WINDOWS平台上则依据IE的设置,不支持需要验证的代理服务器,例如:proxies = {'http': 'http://www.anyproxy.com:10750'}该例子表示一个http代理服务器http://www.anyproxy.com:10750
返回值:返回一个类似文件对象的对象(file_like) object
该对象拥有的方法为:
* read([bytes]) ----从文件对象中读出所有或bytes个字节 * readline()----以字节字符串形式读取单行文本 * readlines()----读取所有输入行并返回列表 * fileno()----返回整数文件描述符 * close()----关闭连接 * info()----返回从服务器传回的MIME标签头 * geturl()----返回真实的URL,之所以称为真实,是因为对于某些重定向的URL,将返回被重定后的。 * getcode()----返回整数形式的HTTP响应代码
下面是一个具体例子:
>>>from urllib import urlopen >>>try: | … web=urlopen('http://www.python.org') … resp=web.read() … except HTTPError as e: … resp=e.read() >>>resp |
运行结果:
>>>web | <addinfourl at 56323768 whose fp = <socket._fileobject object at 0x035B92F0>> | >>>resp | <!doctype html> <!--[if lt IE 7]> <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9"> <![endif]--> <!--[if IE 7]> <html class="no-js ie7 lt-ie8 lt-ie9"> <![endif]--> <!--[if IE 8]> <html class="no-js ie8 lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr"> <!--<![endif]--> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <link rel="prefetch" href="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"> <meta name="application-name" content="Python.org"> <meta name="msapplication-tooltip" content="The official home of the Python Programming Language"> <meta name="apple-mobile-web-app-title" content="Python.org"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="HandheldFriendly" content="True"> <meta name="format-detection" content="telephone=no"> <meta http-equiv="cleartype" content="on"> <meta http-equiv="imagetoolbar" content="false"> <script src="/static/js/libs/modernizr.js"></script> <link href="/static/stylesheets/style.css" rel="stylesheet" type="text/css" title="default" /> |
程序分析:本例中主要是使用urlopen函数获取网页信息。web包含一个链接到http://www.python.org网页的类文件对象,resp变量中保存的就是整个网页的html信息。如果下载期间出现错误,会引发URLError异常。这包括了与HTTP协议本身有关的错误,如禁止访问或请求验证。对于此类错误,服务器返回的内容通常会提供额外的描述信息。获取该内容可以将异常实例本身作为读取的类文件对象操作。
表13.3 urllib模块核心函数表 函数 | 描述 | urlopen(urlstr) | 打开URL地址urlstr | urlretrieve(urlstr,local-file=None,downloadStatusHook=None) | 将URL urlstr定位的文件下载到localfile或临时文件中(localfile没有给定时);如果文件已经存在downloadStatusHook将会获得下载的统计信息 | quote(urldata,safe=’/’) | 将urldata的无效的URL字符编码;在safe列的则不编码 | quote_plus(urldata,safe=’/’) | 将空格编译成加号,其他功能同上 | unquote(urldata) | 将urldata中编码后的字母解码 | unquote_plus(urldata) | 将加号转化为空格,其余同上 | urlencode(dict) | 将字典键-值对编译成有效的CGI请求字符串 |

13.3.3 urllib2模块 urllib2模块相当于对urllib模块进行了扩增,而且变得更加灵活。urllib2的新特性包括: * 将对url的处理单独成一个request类 * URLopener和FancyURLopener都下架,取而代之的是 OpenerDirector * 另添加了很多 handlers,这些handlers主要对HTTP连接,HTTP request或者HTTP response的处理,譬如往HTTP request中添加几个特定的cookies或者状态码处理,所有你能想到的HTTP request的预处理或者HTTP response的善后处理。
简单地说,如果是使用简单的下载功能,urllib就能完成。但是如果需要使用HTTP request或者cookie或者要为其他协议写扩展程序时,需要使用urllib2。
由于基本适用方法和urllib类似,这里就不赘述了。下面举几个urllib2适用的情况。
(1)cookie处理 cookie就是浏览器缓存,由于HTTP是无状态协议,所以用户的身份和协议信息要在本地文件cookie中缓存。cookielib模块的主要作用是提供可存储cookie的对象,以便于与urllib2模块配合使用来访问Internet资源。例如可以利用本模块的CookieJar类的对象来捕获cookie并在后续连接请求时重新发送。如下面代码: >>>import urllib2 >>> import cookielib #本地的cookies.txt文件,可以使用浏览器来导出 >>>cookiejar=cookielib.MozillaCookieJar("cookies.txt") #获取cookiejar类型的对象,处理HTTPcookie的类 >>>cookie=HTTPCookieProcessor(cookiejar) #通过cookie构建自定义的opener对象 >>>opener=urllib2.build_opener(cookie) #打开指定的url >>>open=opener.open("http://www.python.org ") #读取网页信息 >>>html=open.read() |
运行结果:
>>>html | <!doctype html> <!--[if lt IE 7]> <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9"> <![endif]--> <!--[if IE 7]> <html class="no-js ie7 lt-ie8 lt-ie9"> <![endif]--> <!--[if IE 8]> <html class="no-js ie8 lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr"> <!--<![endif]--> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <link rel="prefetch" href="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"> <meta name="application-name" content="Python.org"> <meta name="msapplication-tooltip" content="The official home of the Python Programming Language"> <meta name="apple-mobile-web-app-title" content="Python.org"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="HandheldFriendly" content="True"> <meta name="format-detection" content="telephone=no"> <meta http-equiv="cleartype" content="on"> <meta http-equiv="imagetoolbar" content="false"> <script src="/static/js/libs/modernizr.js"></script> <link href="/static/stylesheets/style.css" rel="stylesheet" type="text/css" title="default" /> |
程序分析:上面的例子就是使用本地已有的cookie文件来访问网站,然后获取网站信息。运行结果仍然是网页的html信息。
(2)代理 urllib2会自动检测代理设置,默认使用环境变量http_proxy来设置 HTTP Proxy通常情况下,这是很有帮助的,因为也可能会出现问题(因为通过代理获取本地URL资源时会被阻止,因此如果你正在通过代理访问Internet,那么使用脚本测试本地服务器时必须阻止urllib2模块使用代理)。因此,如果想在程序中明确Proxy的使用而不受环境变量的影响,可以通过创建ProxyHandler实例,并将实例作为build_opener()的参数来实现。如下面代码: 1 | import urllib2 | 2 | enable_proxy = True | 3 | #定义指定和非指定的代理ProxyHandler对象 | 4 | proxy_handler = urllib2.ProxyHandler({"http" : 'http://代理:8080'}) | 5 | null_proxy_handler = urllib2.ProxyHandler({}) | 6 | #通过代理对象定义opener对象,if语句判断是否打开代理 | 7 | if enable_proxy: | 8 | opener = urllib2.build_opener(proxy_handler) | 9 | else: | 10 | opener = urllib2.build_opener(null_proxy_handler) | 11 | #安装opener对象作为urlopen的全局opener | 12 | urllib2.install_opener(opener) |
程序分析:使用urllib2.install_opener() 会设置 urllib2 的全局 opener。这样后面的使用会很方便,但不能做更细粒度的控制,比如想在程序中使用两个不同的 Proxy 设置等。比较好的做法是不使用install_opener去更改全局的设置,而只是直接调用opener的open方法代替全局的urlopen方法。
13.3.4 其他常见的模块
(1)Internet应用程序编程
* ftplib,ftplib模块实现了ftp的client端协议。此模块很少使用,因为urllib提供了更高级的接口。 * http包,包含了http client和server的实现和cookies管理的模块。 * smtplib,smtplib包含了smtp client的底层接口,用来使用smtp协议发送邮件。 * xmlrpc,xmlrpc模块被用类实现XML-RPC client。
(2)Internet 数据处理和编码 * base64,base64模块提供了base64,base32,base16编码方式,用来实现二进制与文本间的编码和解码。base64通常用来对编码二进制数据,从而嵌入到邮件或http协议中。 * binascii,binascii模块提供了低级的接口来实现二进制和各种ASCII编码的转化。 * csv,csv模块用来读写comma-separated values(CSV)文件。 * email,email包提供了大量的函数和对象来使用MIME标准来表示,解析和维护email消息。 * hashlib,hashlib模块实现了各种secure hash和message digest algorithms,例如MD5和SHA1。 * htmlparser(html.parser),此模块定义了HTMLParser来解析HTML和XHTML文档。使用此类,需要定义自己的类且继承于HTMLParser。 * json,json模块被用类序列化或饭序列化Javascript object notation(JSON)对象。 * xml,xml包提供了各种处理xml的方法。
(3)Web 编程 * cgi,cgi模块用来实现cgi脚本,cgi程序一般地被webserver执行,用来处理用户在form中的输入,或生成一些动态的内容。当与cgi脚本有关的request被提交,webserver将cgi作为子进程执行,cgi程序通过sys.stdin或环境变量来获得输入,通过sys.stdout来输出。 * webbrowser,webbrowser模块提供了平台独立的工具函数来使用web browser打开文档。 * 其他:wsgiref/WSGI (Python Web Server Gateway Interface).
13.4 UDP编程
(1)UDP概述
UDP,用户数据报传输协议,它位于TCP/IP协议的传输层,是一种无连接的协议,它发送的报文不能确定是否完整地到达了另外一端。UDP广泛应用于需要相互传输数据的网络应用中,如QQ使用的就是UDP协议。在网络质量不好的情况下,使用UDP协议时丢包现象十分严重,但UDP占用资源少,处理速度快,UDP依然是传输数据时常用的协议。如图13.2所示。

图13.2 UDP编程模型图
UDP编程的服务器端一般步骤是:
* 创建一个socket,用函数socket(); * 设置socket属性(socket.AF_INET,socket.SOCK_DGRAM); * 绑定IP地址、端口等(此处使用localhost本机和10750端口)信息到socket上,用函数bind(); * 循环接收数据,用函数recvfrom(); * 关闭网络连接;
下面是用python实现UDP服务器的代码:
1 | #!/usr/bin/env python | 2 | import socket | 3 | address=('localhost',10750) | 4 | #建立UDP套接字 | 5 | sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) | 6 | sock.bind(address) | 7 | while 1: | 8 | #接收数据和地址信息,并设置缓存为2048 | 9 | data,addr=sock.recvfrom(2048) | 10 | if not data: #没接收到数据就退出 | 11 | break | 12 | print "got data from",addr #打印地址 | 13 | print data #打印数据 | 14 | sock.close() |
UDP编程的客户端一般步骤是:
* 创建一个socket,用函数socket(); * 设置socket属性(socket.AF_INET,socket.SOCK_DGRAM); * 绑定IP地址、端口等(此处使用localhost本机和10750端口)信息到socket上,用函数bind(); * 设置对方的IP地址和端口等属性; * 发送数据,用函数sendto(); * 关闭网络连接;
UDP客户端的代码:
1 | #!/usr/bin/env python | 2 | import socket | 3 | #添加地址和端口 | 4 | addr=('localhost',10750) | 5 | #建立UDP套接字 | 6 | sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) | 7 | while 1: | 8 | data=raw_input() #接受命令行输入 | 9 | if not data: #没接收到数据就退出 | 10 | break | 11 | #发送数据 | 12 | sock.sendto(data,addr) | 13 | sock.close() |
运行这两个程序,会显示以下结果:
服务器端: got data from (‘127.0.0.1',56480) | hello world |
客户端:
hello world |
程序分析:本例使用socket的UDP套接字。按照上文中介绍的代码流程,实现了基于UDP的客户端和服务器的通讯程序。运行时先启动服务器,再启动客户端,在客户端界面输入要发送的消息,就能在服务器端看到输出结果。
(2)UDP的应用
在局域网中,如果要想局域网内所有计算机发送数据,可以使用广播,广播不能用TCP实现,可以用UDP实现,接受方收到广播数据后,如果有进程在侦听这个端口,就会接收数据,如果没有进程侦听,数据包会被丢弃。
广播的发送方: 1 | #!usr/bin/env python | 2 | import socket | 3 | host='' | 4 | port=10750 | 5 | sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #建立套接字 | 6 | # 标示套接口的描述符信息为广播 | 7 | sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) | 8 | sock.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) | 9 | sock.bind((host,port)) #绑定地址端口 | 10 | while 1: | 11 | try: #异常判断 | 12 | data,addr=sock.recvfrom(1024) #接收数据 | 13 | print "got data from",addr #打印数据 | 14 | sock.sendto("broadcasting",addr) #发送数据 | 15 | print data | 16 | except KeyboardInterrupt: | 17 | raise |
广播的接收方:
1 | #!/usr/bin/env python | 2 | import socket,sys | 3 | addr=('<broadcast>',10750) #设置地址 | 4 | sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #建立套接字 | 5 | #标示套接口描述信息为广播 | 6 | sock.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) | 7 | sock.sendto("hello from client",addr) #发送数据 | 8 | while 1: | 9 | data=sock.recvfrom(1024) #接收数据 | 10 | if not data: #没接收到数据就退出 | 11 | break | 12 | print data |
运行广播程序,发送端会显示以下结果:
got data from ('192.168.2.26',50861) | hello from client |
接收端会显示以下结果:
(‘broadcasting',( '192.168.2.26',10750)) |
程序分析:本例使用socket套接字的广播属性按照上文中介绍的代码流程,实现了基于UDP的广播的通讯程序。运行时先启动服务器,再启动客户端,这样就能看到上面的输出结果。表示服务器端接受到来自客户端的广播信息,192.168.2.26为测试机的ip地址。
13.5 TCP编程
TCP是一种面向连接的可靠地协议。适用于传输大批量的文件,能检查是否正常传输。它不能发送广播和组播,只能单播。只有先建立连接才能通话,如图13.3所示。
TCP编程的服务器端一般步骤是: * 创建一个socket,用函数socket() * 设置socket属性(AF_INET, SOCK_STREAM) * 绑定IP地址、端口等(此处使用localhost本机和10750端口)信息到socket上,用函数bind() * 使用listen()函数设置同时接收的连接数 * 循环接收数据,用函数recvfrom() * 关闭网络连接

图13.3 TCP编程模型图
服务器端代码:
1 | from socket import * | 2 | from time import ctime | 3 | | 4 | #HOST变量为空,表示bind()函数可以绑定在所有有效的地址上 | 5 | HOST = '' | 6 | PORT = 10750 | 7 | #设置缓冲大小为128,可以根据网络情况和需求来进行修改 | 8 | BUFSIZ = 128 | 9 | ADDR = (HOST, PORT) | 10 | | 11 | #创建TCP套接字 | 12 | tcpSerSock = socket(AF_INET, SOCK_STREAM) | 13 | tcpSerSock.bind(ADDR) | 14 | #listen()函数的参数表示最多允许同时接收的连接数,超过这个数目的连接会被拒绝掉 | 15 | tcpSerSock.listen(5) | 16 | | 17 | while True: | 18 | print 'waiting for connection...' #打印等待信息 | 19 | tcpCliSock, addr = tcpSerSock.accept() #获取客户端套接字和地址 | 20 | print '...connected from:', addr #打印地址 | 21 | | 22 | while True: #循环发送数据 | 23 | #接收数据 | 24 | data = tcpCliSock.recv(BUFSIZ) | 25 | if not data: #数据为空跳出 | 26 | break | 27 | #发送数据 | 28 | tcpCliSock.send('From server response:[%s] %s' % (ctime(), data)) | 29 | | 30 | tcpCliSock.close() #关闭客户端套接字 | 31 | tcpSerSock.close() #关闭服务器套接字 |
TCP编程的客户端一般步骤是:
* 创建一个socket,用函数socket() * 设置socket属性(AF_INET, SOCK_STREAM) * 绑定IP地址、端口等(此处使用localhost本机和10750端口)信息到socket上,用函数bind() * 设置对方的IP地址和端口等属性 * 调用connect()函数连接服务器 * 发送数据,用函数send() * 关闭网络连接
客户端代码
1 | HOST = 'localhost' | 2 | PORT = 10750 | 3 | BUFSIZ = 256 | 4 | ADDR = (HOST, PORT) | 5 | | 6 | #创建客户端TCP套接字 | 7 | tcpCliSock = socket(AF_INET, SOCK_STREAM) | 8 | #连接服务器 | 9 | tcpCliSock.connect(ADDR) | 10 | | 11 | while True: #循环判断发送数据 | 12 | data = raw_input('>') #命令行输入信息 | 13 | if not data: #判断数据是否为空,为空退出 | 14 | break | 15 | #发送数据 | 16 | tcpCliSock.send(data) | 17 | #接收数据 | 18 | data = tcpCliSock.recv(BUFSIZ) | 19 | if not data: #判断接受数据是否为空 | 20 | break | 21 | print data #打印数据 | 22 | | 23 | tcpCliSock.close() #关闭连接 |
服务器运行结果:
waiting for connection… | …connected from(‘127.0.0.1’,’37944’) |
客户端运行结果:
>hello world | From server response:[Fri Jan 23 15:33:12 2015] hello world |
程序分析:本例使用socket的TCP套接字。按照上文中介绍的代码流程,实现了基于TCP的客户端和服务器的通讯程序。运行时先启动服务器,再启动客户端,在客户端界面输入要发送的消息,服务器端会显示谁连接到它,并将接受的数据发回给客户端,然后由客户端输出从服务器返回的信息。它与UDP的主要区别是,TCP连接过程中需要在服务器添加listen监听函数设置服务器最大同时连接数,然后客户端需要调用connect函数进行连接,最后服务器端要使用accept函数确认连接。这样客户端和服务器就可以进行消息的交互了。
本章小结
本章学习了python语言的TCP/UDP网络编程的知识,介绍了网络套接字tcp/ip相关的知识,以及python中的网络设计模块,各个模块的使用方法和举例。这些模块功能都略有不同,同样适用范围也略有区别。本章最后为大家讲解了TCP和UDP的编程的具体例子和应用,可以帮助读者更好的理解。希望读者好好掌握本章内容,之后的章节中会继续使用本章的相关知识。

第14章 Python爬虫程序 前一章学习了Python网络编程的一些应用,而网络编程中还一个非常有意思的应用,那便是网络爬虫。在茫茫的互联网中,信息爆炸,互联网上的资源以超乎想象的速度增长,如何记录并快速索引到这些资源是一个非常重要的问题,于是网络爬虫和搜索引擎应运而生。本章将讲解爬虫程序中所涉及的一些基本概念、Python中网络库urllib2的应用和如何编写一个简单的爬虫程序。
14.1 搜索引擎和网络爬虫
在互联网发展初期,网站相对较少,信息查找比较容易。然而伴随互联网爆炸性的发展,普通网络用户想找到所需的资料简直如同大海捞针,这时为满足大众信息检索需求的专业搜索网站便应运而生了。
1990年以前,没有任何人能搜索互联网。所有搜索引擎的祖先,是1990年由Alan Emtage,Peter Deutsch,BillWheelan发明的Archie(Archie FAQ),一个可以用文件名查找文件的系统。Archie是第一个自动索引互联网上匿名FTP网站文件的程序,但它还不是真正的搜索引擎。 当时,“机器人”一词在编程者中十分流行。电脑“机器人”(Computer Robot)是指某个能以人类无法达到的速度不间断地执行某项任务的软件程序。由于专门用于检索信息的“机器人”程序像爬虫一样在网络间爬来爬去,因此,搜索引擎的“机器人”程序就被称为爬虫程序。世界上第一个用于监测互联网发展规模的爬虫程序是Matthew Gray开发的World wide Web Wanderer。刚开始它只用来统计互联网上的服务器数量,后来则发展为能够检索网站域名。 随着互联网的迅速发展,检索所有新出现的网页变得越来越困难,这需要爬虫程序及时并全面地搜寻到的最新的网页,以便搜索引擎将新网页加入到索引。因此,在Matthew Gray的爬虫程序的基础上,一些编程者将传统的爬虫程序工作原理作了些改进。其设想是,既然所有网页都可能有连向其他网站的链接,那么从跟踪一个网站的链接开始,就有可能检索整个互联网。到1993年底,基于此原理改进的爬虫程序不断被开发改良,相应的搜索引擎开始纷纷涌现,其中以JumpStation、The World Wide Web Worm(Goto的前身,也就是今天Overture),和Repository-Based Software Engineering (RBSE) spider最负盛名。 现代意义上的搜索引擎出现于1994年7月。当时Michael Mauldin将John Leavitt的爬虫程序接入到其索引程序中,创建了大家现在熟知的Lycos。同年4月,斯坦福大学的两名博士生,David Filo和美籍华人杨致远(Gerry Yang)共同创办了超级目录索引Yahoo,并成功地使搜索引擎的概念深入人心。从此搜索引擎进入了高速发展时期。目前,互联网上有名的搜索引擎已达数百家,其检索的信息量也与从前不可同日而语。最近风头正劲的Google,其数据库中存放的网页已达30亿之巨。
14.2 一些基本概念
网络爬虫的基本功能是抓取网页,而抓取网页的过程其实和读者使用浏览器浏览网页的过程十分相似。当在浏览器的地址栏中输入一个URL并按下回车键后,打开网页的过程其实就是浏览器作为一个浏览网页的客户端,向服务器端发送了一次请求,并把服务器端的HTML网页文件传输到本地,再进行解释、展现。
而网络爬虫所做的,则是通过URL网址向一些站点发送请求,获取服务器端的HTML网页文件内容,并储存到本地存储器或数据库中。
14.2.1 URI和URL
当讨论到互联网相关的问题时,总会提到URI和URL这两个概念。
(1)什么是URI
互联网中每种可用的资源,如HTML文档、图像、视频片段、程序等都是由一个通用资源标志符(Universal Resource Identifier,URI)进行定位的。
URI通常由三部分组成:
* 访问资源的命名机制; * 存放资源的主机名; * 资源自身的名称,由路径表示。
例如对于下面的这个URI:
“http://www.untitled.com/01/16/entity”
可以这样解释它为,
* 这是一个可以通过HTTP协议访问的资源; * 位于主机 www. untitled.com上; * 通过路径“/01/16/entity”访问。 (2)什么是URL
URL是URI的一个子集。它是Uniform Resource Locator的缩写,中文意思是“统一资源定位符”。通俗地说,URL是Internet上描述信息资源的字符串,主要用在各种万维网客户端程序和服务器程序上。URL可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。URL的一般格式为(带方括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment |
可以把URL的格式分成以下几个部分进行理解:
* potocol:协议部分,表示使用的传输协议,最常用的是HTTP协议,它也是目前万维网中应用最广的协议。 * hostname:是指存放资源的服务器的域名系统(DNS)主机名或IP地址。有时,在主机名前也可以包含连接到服务器所需的用户名和密码(格式:username@password)。。 * port:端口号,可选,省略时使用方案的默认端口,各种传输协议都有默认的端口号,如http的默认端口为80。有时候出于安全或其他考虑,可以在服务器上对端口进行重定义,即采用非标准端口号,此时,URL中就不能省略端口号这一项。 * path:路径,由零或多个“/”符号隔开的字符串,一般用来表示主机上的一个目录或文件地址。 * parameters:参数,这是用于指定特殊参数的可选项。 * query:查询,可选,用于给动态网页(如使用CGI、ISAPI、PHP/JSP/ASP/ASP。NET等技术制作的网页)传递参数,可有多个参数,用“&”符号隔开,每个参数的名和值用“=”符号隔开。 * fragment:信息片断,用于指定网络资源中的片断。例如一个网页中有多个名词解释,可使用fragment直接定位到某一名词解释。 (3)URL和URI简单比较
URI是属于URL更低层次的抽象,一种字符串文本标准。换句话说,URI属于父类,而URL属于URI的子类。URI表示请求服务器的路径,定义了一个资源;而URL同时说明要如何访问这个资源,以一种具体的方式定位了这个资源。
爬虫最主要的处理对象就是URL,它根据URL地址取得所需要的文件内容,然后对它进行进一步的处理。因此,准确地理解URL对理解网络爬虫至关重要。
14.2.2 HTTP协议 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。它定义了客户端(如浏览器)怎样向服务器请求万维网文档,以及服务器怎样把文档传送给浏览器。 客户端指的是终端用户(如浏览器或网络应用程序),服务器端指的是网站。服务器端存储着资源,比如HTML文件和图像。通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如“HTTP/1.1 200 OK”,和响应的内容,相应的内容可能是被请求的文件、错误消息、或者其它一些信息。 HTTP使用TCP协议在客户端和服务器端建立连接。使用TCP而不是UDP的原因在于打开一个网页必须传送很多数据,而TCP协议提供传输控制,按顺序组织数据,和错误纠正。 通过HTTP或者HTTPS协议请求的资源由统一资源标示符(Uniform Resource Identifiers)(或者,更准确一些,URLs)来标识。
14.3 准备工作 学习使用Python编写爬虫程序前,需要先学习编写爬虫程序所涉及到的一些工具。
14.3.1 初探urllib2网络库
在Python中提供了很多网络库,urllib2便是其中之一,urllib2几乎包含了开发一个爬虫程序所需要的所用工具。下面先介绍如何使用urllib2网络库通过URL下载网页资源。
(1)urlopen函数 urlopen函数是urllib2中的一个函数,它接受一个URL字符串,并返回一个Request对象。下面的例子简单的展示了urlopen函数的使用方法: 1 | import urllib2 | 2 | response = urllib2.urlopen('http://www.baidu.com/') #打开百度首页,获取响应资源 | 3 | html = response.read() #读取内容 | 4 | print html |
运行结果:
<!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="content-type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta content="always" name="referrer"><link rel="dns-prefetch" href="//s1.bdstatic.com"/><link rel="dns-prefetch" href="//t1.baidu.com"/><link rel="dns-prefetch" href="//t2.baidu.com"/><link rel="dns-prefetch" href="//t3.baidu.com"/><link rel="dns-prefetch" href="//t10.baidu.com"/><link rel="dns-prefetch" href="//t11.baidu.com"/><link rel="dns-prefetch" href="//t12.baidu.com"/><link rel="dns-prefetch" href="//b1.bdstatic.com"/><title>百度一下,你就知道</title>… |
使用浏览器打开百度主页,查看源代码,将会发现它的html源代码和使用urlopen获取到的html代码完全相同。
(2)Request类 urllib2还可以用一个Request对象来映射你提出的HTTP请求。Request类也在urllib2网络库中定义,因此引入urllib2库后可以直接使用Request类。Request类提供了一个接受一个URL参数的构造函数,通过这个构造函数得到一个Request对象后,再通过调用urlopen并传入这个Request对象,将返回一个相关请求Response对象,这个应答对象如同一个文件对象,所以可以调用这个Response对象中的read函数读取内容。 1 | import urllib2 | 2 | req = urllib2.Request('http://www.baidu.com') | 3 | response = urllib2.urlopen(req) | 4 | the_page = response.read() | 5 | print the_page |
运行结果:
<!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="content-type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta content="always" name="referrer"><link rel="dns-prefetch" href="//s1.bdstatic.com"/><link rel="dns-prefetch" href="//t1.baidu.com"/><link rel="dns-prefetch" href="//t2.baidu.com"/><link rel="dns-prefetch" href="//t3.baidu.com"/><link rel="dns-prefetch" href="//t10.baidu.com"/><link rel="dns-prefetch" href="//t11.baidu.com"/><link rel="dns-prefetch" href="//t12.baidu.com"/><link rel="dns-prefetch" href="//b1.bdstatic.com"/><title>百度一下,你就知道</title>… |
(3)urlopen返回值
urlopen的返回值是一个addinfourl对象,addinfourl对象包含两个十分重要的函数,分别是geturl函数和info函数。 geturl函数: 有些网站在打开时会有重定向,因此实际打开的网站的URL可能和urloepn请求的URL不同,geturl函数能够返回urlopen打开网站的真实URL: 1 | import urllib2 | 2 | req = urllib2.Request('http://z.cn') | 3 | response = urllib2.urlopen(req) | 4 | print response.geturl() |
运行结果:
http://www.amazon.cn/ | info函数 info函数返回请求内容的页面情况,通常是服务器发送的特定headers,一般包括“Content-length”,“Content-type”,“Transfer-Encoding”等内容。 1 | import urllib2 | 2 | req = urllib2.Request('http://z.cn') | 3 | response = urllib2.urlopen(req) | 4 | print response.info() |
运行结果:
Date: Tue, 27 Jan 2015 12:15:04 GMT | Server: Server | Set-Cookie: session-id=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: session-id-time=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: session-token=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: ubid-acbcn=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: at-main=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: x-acbcn=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | Set-Cookie: UserPref=-; path=/; domain=.www.amazon.cn; expires=Mon, 27-Jan-2003 12:15:04 GMT | pragma: no-cache | x-amz-id-1: 15BES4TAJEWYQG9N33A8 | cache-control: no-cache | x-frame-options: SAMEORIGIN | expires: -1 | x-amz-id-2: C9fTnNPFJKFbaa8U5EibRXFS+xapVREw0SokHkGXQ2SSsbJrpLF1CQ== | Vary: Accept-Encoding,User-Agent | Content-Type: text/html; charset=UTF-8 | Transfer-Encoding: chunked |
前面的例子都是基于HTTP协议的URL的应用,事实上,urllib2使用相同的接口处理所有类型协议的URL,比如ftp协议或file协议。例如可以这样创建一个ftp请求:
1 | req = urllib2.Request('ftp://example.com/') |
(4)HTTP协议应用
由于HTTP协议的特殊性,在使用urllib2网络库处理HTTP请求时,允许做额外的两件事:
发送data表单数据
在浏览网页时,经常需要填写一些信息并点击提交。这些填写的内容就是data表单数据,而提交并发送data表单数据的动作,叫做POST请求。一般的HTML表单,数据需要编码成标准形式,然后做为参数传到Request对象。其中编码需要用到urllib库的urlencode函数。下例展示了如何使用Python执行POST请求: 1 | import urllib | 2 | import urllib2 | 3 | | 4 | url = 'http://www.someserver.com/register.cgi' | 5 | | 6 | values = {'name' : 'WHY', | 7 | 'location' : 'SDU', | 8 | 'language' : 'Python' } | 9 | | 10 | data = urllib.urlencode(values) # 编码工作 | 11 | req = urllib2.Request(url, data) # 发送请求并传输data表单 | 12 | response = urllib2.urlopen(req) # 接受响应 | 13 | the_page = response.read() # 读取响应内容 |
与POST请求相对的,是GET请求。数据同样可以通过GET请求进行传输:
1 | import urllib2 | 2 | import urllib | 3 | | 4 | data = {} | 5 | | 6 | data['name'] = 'WHY' | 7 | data['location'] = 'SDU' | 8 | data['language'] = 'Python' | 9 | | 10 | url_values = urllib.urlencode(data) # 编码 | 11 | print url_values # 打印编码后的数据 | 12 | | 13 | url = 'http://www.example.com/example.cgi' | 14 | full_url = url + '?' + url_values | 15 | | 16 | data = urllib2.open(full_url) |
设置Headers到http请求
User-Agent是附着在HTTP请求中的一个属性,例如通过火狐浏览器发送的请求中的User-Agent是“Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)”。如果使用浏览器发送请求,因为请求中包含了表明自己身份的User-Agent,服务器端会认为这是一个由人为操作所产生的请求,并正常的返回相应内容。 urllib2默认使用 “Python-urllib/x.y”(x和y是Python主版本和次版本号,例如Python-urllib/2.7)作为User-Agent,这类奇怪的User-Agent会使服务器“疑惑”,并被认为是一个由非人为所产生的请求;一些服务器可能不希望被非人为访问,因此服务器可能不响应这类请求。
为了避免上述情况的发生,可以通过伪造User-Agent信息,把自身模拟成一个浏览器。下例展示了如何通过修改User-Agent,把请求伪造成一个由火狐浏览器发送的请求。
1 | import urllib | 2 | import urllib2 | 3 | | 4 | url = 'http://www.someserver.com/ ' | 5 | | 6 | user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' # User-Agent | 7 | values = {'name' : 'WHY', | 8 | 'location' : 'SDU', | 9 | 'language' : 'Python' } | 10 | | 11 | headers = { 'User-Agent' : user_agent } # 将User-Agent加入到Header | 12 | data = urllib.urlencode(values) # 编码 | 13 | req = urllib2.Request(url, data, headers) | 14 | response = urllib2.urlopen(req) | 15 | the_page = response.read() |
(5)Opener和Handle
Opener:
访问一个URL时实际上是使用一个opener(一个urllib2.OpenerDirector的实例)。通常情况情况下,通过urlopen函数使用默认的opener。如果想创建更多元化的opener,需要通过handler实现。
Handler: opener通过handler实现更丰富的功能。不同的handler有不同的处理URL的方式,如:使用怎样的协议,如何处理重定向和cookies。在这种情况下,可以首先创建一个opener,即实例化一个OpenerDirector,然后调用add_handler(some_handler_instance)的实例方法。 install_opener函数用来创建全局默认opener。这个表示调用urlopen将使用你“安装”的opener。Opener对象有一个open方法。该方法可以像urlopen函数那样直接用来获取urls。
14.3.2 urllib2异常处理
当调用urlopen发生异常时,一般抛出URLError异常。当然一般的Python APIs异常如ValueError,TypeError等也可能产生。HTTPError是URLError的子类,通常在请求HTTP协议的URL时产生。
(1)URLError
通常URLError异常在网络连接失败,或者服务器不存在时产生。这种情况下,被抛出的异常对象会带有“reason”属性,包含了一个错误号和一个错误信息。 1 | import urllib2 | 2 | | 3 | req = urllib2.Request('http://www.pretend_server.org') | 4 | | 5 | try: urllib2.urlopen(req) | 6 | | 7 | except urllib2.URLError, e: | 8 | print e.reason |
运行结果:
| [Errno 11001] getaddrinfo failed |
从返回结果可以看出,错误号是11001,内容是getaddrinfo failed
(2)HTTPError
服务器返回的每一个HTTP 应答对象都包含一个数字“状态码”。HTTP状态码表示HTTP协议所返回的响应的状态。比如客户端向服务器发送请求,如果成功地获得请求的资源,则返回的状态码为200,表示响应成功;如果请求的资源不存在,则通常返回404错误。
有些状态码表明服务器无法完成请求,而urllib2默认会处理一部分这种应答。例如:如果服务器响应“重定向”,urllib2会自动重定向到别的地址发送请求。对于其他不能处理的响应,urlopen会产生一个HTTPError。典型的错误包含“404”(页面无法找到),“403”(请求禁止,表明没有权限访问),和“401”(带验证请求)。
(3)Error Code urllib2能够处理状态码在300以内的情况,而且100到299的状态码表明请求成功,所以HTTPError异常中的错误码都是400以上的数字。 BaseHTTPServer.BaseHTTPRequestHandler.responses是一个非常有用的字典,包含了所有HTTP状态码和对应的相应信息: 1 | from BaseHTTPServer import BaseHTTPRequestHandler | 2 | import pprint | 3 | | 4 | pprint.pprint(BaseHTTPRequestHandler.responses) |
运行结果:
{100: ('Continue', 'Request received, please continue'), | 101: ('Switching Protocols', | 'Switching to new protocol; obey Upgrade header'), | 200: ('OK', 'Request fulfilled, document follows'), | 201: ('Created', 'Document created, URL follows'), | 202: ('Accepted', 'Request accepted, processing continues off-line'), | 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), | 204: ('No Content', 'Request fulfilled, nothing follows'), | 205: ('Reset Content', 'Clear input form for further input.'), | 206: ('Partial Content', 'Partial content follows.'), | 300: ('Multiple Choices', 'Object has several resources -- see URI list'), | 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), | 302: ('Found', 'Object moved temporarily -- see URI list'), | 303: ('See Other', 'Object moved -- see Method and URL list'), | 304: ('Not Modified', 'Document has not changed since given time'), | 305: ('Use Proxy', | 'You must use proxy specified in Location to access this resource.'), | 307: ('Temporary Redirect', 'Object moved temporarily -- see URI list'), | 400: ('Bad Request', 'Bad request syntax or unsupported method'), | 401: ('Unauthorized', 'No permission -- see authorization schemes'), | 402: ('Payment Required', 'No payment -- see charging schemes'), | 403: ('Forbidden', 'Request forbidden -- authorization will not help'), | 404: ('Not Found', 'Nothing matches the given URI'), | 405: ('Method Not Allowed', 'Specified method is invalid for this resource.'), | 406: ('Not Acceptable', 'URI not available in preferred format.'), | 407: ('Proxy Authentication Required', | 'You must authenticate with this proxy before proceeding.'), | 408: ('Request Timeout', 'Request timed out; try again later.'), | 409: ('Conflict', 'Request conflict.'), | 410: ('Gone', 'URI no longer exists and has been permanently removed.'), | 411: ('Length Required', 'Client must specify Content-Length.'), | 412: ('Precondition Failed', 'Precondition in headers is false.'), | 413: ('Request Entity Too Large', 'Entity is too large.'), | 414: ('Request-URI Too Long', 'URI is too long.'), | 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), | 416: ('Requested Range Not Satisfiable', 'Cannot satisfy request range.'), | 417: ('Expectation Failed', 'Expect condition could not be satisfied.'), | 500: ('Internal Server Error', 'Server got itself in trouble'), | 501: ('Not Implemented', 'Server does not support this operation'), | 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), | 503: ('Service Unavailable', | 'The server cannot process the request due to a high load'), | 504: ('Gateway Timeout', | 'The gateway server did not receive a timely response'), | 505: ('HTTP Version Not Supported', 'Cannot fulfill request.')} |
当一个错误产生后,服务器返回一个HTTP错误号,和一个“错误页面”。捕获到的HTTPError实例,同样包含了read、geturl和info方法,所以可以像正常的相应对象一样查看它的页面源码。
1 | import urllib2 | 2 | req = urllib2.Request('http://www.python.org/fish.html') | 3 | try: | 4 | urllib2.urlopen(req) | 5 | except urllib2.URLError, e: | 6 | print e.code | 7 | print e.read() |
运行结果:
404<!doctype html><!--[if lt IE 7]> <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9"> <![endif]--><!--[if IE 7]> <html class="no-js ie7 lt-ie8 lt-ie9"> <![endif]--><!--[if IE 8]> <html class="no-js ie8 lt-ie9"> <![endif]--><!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr"> <!--<![endif]--><head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <link rel="prefetch" href="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"> <meta name="application-name" content="Python.org"> <meta name="msapplication-tooltip" content="The official home of the Python Programming Language"> <meta name="apple-mobile-web-app-title" content="Python.org"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="HandheldFriendly" content="True"> <meta name="format-detection" content="telephone=no"> <meta http-equiv="cleartype" content="on"> <meta http-equiv="imagetoolbar" content="false"> <script src="/static/js/libs/modernizr.js"></script>… |
运行的输出结果中错误码是404,表明没有找到这个页面。而通过read函数读取到的页面信息则是服务器在接收到的请求会引发404错误时返回的“错误页面”,而非实际要请求的URL页面(在上例中指的是'http://www.python.org/fish.html')。
(4)捕获异常
捕获并处理HTTPError或URLError异常的方式一般有两种。
第一种: 1 | from urllib2 import Request, urlopen, URLError, HTTPError | 2 | | 3 | req = Request('http://a.cannotreach.url') | 4 | | 5 | try: | 6 | response = urlopen(req) | 7 | | 8 | except HTTPError, e: | 9 | print 'The server couldn\'t fulfill the request.' | 10 | print 'Error code: ', e.code | 11 | | 12 | except URLError, e: | 13 | print 'We failed to reach a server.' | 14 | print 'Reason: ', e.reason | 15 | | 16 | else: | 17 | print 'No exception was raised.' | 18 | # everything is fine |
这里要注意的一点,except HTTPError必须在第一个,否则except URLError将同样会捕获到HTTPError异常 。因为HTTPError是URLError的子类,如果URLError在前面它会捕获到所有的URLError(包括HTTPError)。 第二种方法是: 1 | from urllib2 import Request, urlopen, URLError, HTTPError | 2 | | 3 | req = Request('http://bbs.csdn.net/callmewhy') | 4 | | 5 | try: | 6 | response = urlopen(req) | 7 | | 8 | except URLError, e: | 9 | if hasattr(e, 'code'): | 10 | print 'The server couldn\'t fulfill the request.' | 11 | print 'Error code: ', e.code | 12 | | 13 | elif hasattr(e, 'reason'): | 14 | print 'We failed to reach a server.' | 15 | print 'Reason: ', e.reason | 16 | | 17 | else: | 18 | print 'No exception was raised.' | 19 | # everything is fine |
14.3.3 urllib2使用细节 同一个爬虫程序,在不同的网络环境下可能会产生不同的行为,甚至无法正常工作或引发错误。为了使程序在不同的网络环境下,都能够正常地运行,需要在程序修改或添加一些网络设置,下面介绍一些在使用urllib2网络库编写爬虫时需要注意的一些细节,并介绍通用的设置方法。
(1)Proxy的设置
urllib2默认使用环境变量http_proxy设置HTTP Proxy。urllib2提供了在程序中显式设置Proxy的方法,从而不受环境变量的影响: 1 | import urllib2 | 2 | enable_proxy = True | 3 | proxy_handler = urllib2.ProxyHandler({"http" : 'http://some-proxy.com:8087'}) #设置Proxy | 4 | null_proxy_handler = urllib2.ProxyHandler({}) #默认不设置Proxy | 5 | if enable_proxy: # 需要设置Proxy的情况 | 6 | opener = urllib2.build_opener(proxy_handler) | 7 | else: # 不需要设置Proxy的情况 | 8 | opener = urllib2.build_opener(null_proxy_handler) | 9 | urllib2.install_opener(opener) |
这里需要注意一个细节,使用urllib2.install_opener()会设置urllib2的全局opener。但不能做更细致的控制,即不能在程序中使用两个不同的Proxy设置。比较好的做法是不使用install_opener去更改全局的设置,而只是直接调用opener的open 方法代替全局的urlopen方法。
(2)Timeout 设置 当尝试发送一个请求却长时间未收到相应时,需要终止这个请求。Timeout属性用于设置最长的连接等待时间,当尝试连接时间超过timeout时,终止这个连接: 1 | import urllib2 | 2 | response = urllib2.urlopen('http://www.baidu.com', timeout=10) |
(3)在 HTTP Request 中添加特定的 Header
通过伪造请求的Header,从而绕过一些服务器的检查。具体做法是首先实例化一个Request对象,然后使用Request对象的add_header方法添加Header。
1 | import urllib2 | 2 | request = urllib2.Request('http://www.baidu.com/') | 3 | request.add_header('User-Agent', 'fake-client') | 4 | response = urllib2.urlopen(request) | 5 | print response.read() | 运行结果: <!DOCTYPE html><!--STATUS OK--><html><head><meta http-equiv="content-type" content="text/html;charset=utf-8"><meta http-equiv="X-UA-Compatible" content="IE=Edge"><meta content="always" name="referrer"><link rel="dns-prefetch" href="//s1.bdstatic.com"/><link rel="dns-prefetch" href="//t1.baidu.com"/><link rel="dns-prefetch" href="//t2.baidu.com"/><link rel="dns-prefetch" href="//t3.baidu.com"/><link rel="dns-prefetch" href="//t10.baidu.com"/><link rel="dns-prefetch" href="//t11.baidu.com"/><link rel="dns-prefetch" href="//t12.baidu.com"/><link rel="dns-prefetch" href="//b1.bdstatic.com"/><title>百度一下,你就知道</title><style index="index" id="css_index">html,body{height:100%}html{overflow-y:auto}#wrapper{position:relative;_position:;min-height:100%}#head{padding-bottom:100px;text-align:center;*z-index:1}#ftCon{height:100px;position:absolute;bottom:44px;text-align:center;width:100%;margin:0 auto;z-index:0;overflow:hidden}#ftConw{width:720px;margin:0 auto}body{font:12px arial;text-align:;background:#fff}body,p,form,ul,li{margin:0;padding:0;list-style:none}body,form,#fm{position:relative}td{text-align:left}img{border:0}a{color:#00c}a:active{color:#f60}.bg{background-image:url(http://s1.bdstatic.com/r/www/cache/static/global/img/icons_3bfb8e45.png);background-repeat:no-repeat;_background-image:url(http://s1.bdstatic.com/r/www/cache/static/global/img/icons_f72fb1cc.gif)}.bg_tuiguang_browser{width:16px;height:16px;background-position:-600px 0;display:inline-block;vertical-align:text-bottom;font-style:normal;overflow:hidden;margin-right:5px}.bg_tuiguang_browser_big{width:56px;height:56px;position:absolute;left:10px;top:10px;background-position:-600px -24px} | Header中有两个比较重要的属性,服务器会针对这些属性做检查: * User-Agent:有些服务器或Proxy通过该值来判断是否是浏览器发出的请求。 * Content-Type:在使用REST接口时,服务器检查该值,用来确定HTTP Body中的内容该怎样解析。常见的取值有: * application/xml:在XML RPC(如RESTful/SOAP)调用时使用; * application/json:在JSON RPC调用时使用; * application/x-www-form-urlencoded:浏览器提交数据表单时使用。 在使用服务器提供的RESTful/SOAP服务时,Content-Type设置错误会导致服务器拒绝服务,因此需要格外注意Content-Type的设置。
(4)Cookie
使用urllib2网络库时,一般不需要对Cookie做额外设置;如果需要得到某个Cookie项的值,可以这么做: 1 | import urllib2 | 2 | import cookielib | 3 | cookie = cookielib.CookieJar() # 实例化一个Cookie对象 | 4 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) # 建立opener | 5 | response = opener.open('http://www.baidu.com') # 使用opener打开网页 | 6 | for item in cookie: # 连接成功后Cookie将存在opener绑定的cookie对象中 | 7 | print 'Name: ' + item.name | 8 | print 'Value: ' + item.value |
运行结果输出访问百度的Cookie值:
Name: BAIDUID | Value: 7630E6CD7319DB3E802092C017824008:FG=1 | Name: BAIDUPSID | Value: 7630E6CD7319DB3E802092C017824008 | Name: H_PS_PSSID | Value: 7886_4395_11214_1458_11101_11276_11241_11280_11150_11243_10617 | Name: BDSVRTM | Value: 0 | Name: BD_HOME | Value: 0 |
(5)使用HTTP的PUT和DELETE方法
urllib2只支持HTTP的GET和POST方法,如果要使用HTTP PUT和DELETE,只能使用比较低层的httplib库。虽然如此,我们还是能通过下面的方式,使urllib2能够发出PUT DELETE的请求: 1 | # coding: utf-8 | 2 | import urllib2 | 3 | import urllib | 4 | | 5 | url = 'http://gw.api.taobao.com/router/rest' | 6 | | 7 | values = {'a' : 'a', | 8 | 'b': 'b'} | 9 | | 10 | data = urllib.urlencode(values) # 编码工作 | 11 | req = urllib2.Request(url, data) # 发送请求并传输data表单 | 12 | | 13 | request = urllib2.Request(url, data = data) | 14 | request.get_method = lambda: 'PUT' # 或'DELETE' | 15 | response = urllib2.urlopen(request) | 16 | print response.read() | 运行结果: <?xml version="1.0" encoding="utf-8" ?><error_response><code>21</code><msg>Missing method</msg><request_id>13yk5mff0k28p</request_id></error_response><!--top010179031187.s.et2--> |
14.4 一个简单爬虫程序 通过本章的学习,读者已经了解了编写一个爬虫程序所需要的所有基本知识。本节将介绍如何动手编写一个简单的爬虫程序。 编写一个网络爬虫程序,首先需要知道它的基本工作流程: (1) 准备一些种子URL; (2) 将这些URL添加到待抓取URL队列; (3) 从待抓取URL队列中取出待抓取的URL,解析DNS,并且得到主机的IP地址,并将URL对应的网页下载下来,存储到本地存储器或数据库中。此外,还需将这些处理过的URL放进已抓取URL队列。 (4) 分析抓取下来的网页,将网页中包含的未被抓取过的URL,添加到待抓取URL队列。若待抓取URL队列不为空,重复第3步。 (5) 当待抓取URL队列为空时,结束爬虫程序。 可以看出,爬虫程序的核心步骤在于第三步的网页抓取和第四步的网页分析。
网页抓取,就是把URL地址中指定的网络资源从网络流中读取出来。对于第三步的网页抓取,可以通过urlopen函数轻松地获取到一个URL对应的网页内容。然而获取到的网页内容中有包含了许多新的URL,这些URL大多包含在一个“a标签”中,样子可能是:
<a href="https://a.new.url.com">新链接</a> | 提取到这些标签中的URL,便是爬虫程序第四步中的网页分析。由于这些标签都有类似的结构,而这正是正则表达式所擅长处理的。分析发现,这个URL都包含在这样的结构中: href=" potocol ://url" | 对于只抓取采用http协议的URL,可以使用下列正则表达式进行匹配: href_re = re.compile(r'href="(http://\S*)"') | 随着网络安全越来越被看重,http协议正广泛的被https协议所取代。所以需要同时匹配到http协议和https协议的URL: href_re = re.compile(r'href="(https?://\S*)"') | 由于一个网页内容中的URL数量可能十分多,可能需要一些限制条件对这些URL进行筛选,最简单的筛选条件就是只获取网页内容中特定数量的URL。而且网页内容中的URL也可能是之前爬虫程序已经爬过的URL,于是还需要一个全局的URL集合记录已经爬过的URL,当再次遇到已经爬过的URL时则跳过该URL: 1 | pages = set() #已经爬过的URL | 2 | | 3 | def fetch(…): | 4 | … | 5 | cnt = 0 | 6 | for new_url in href_re.findall(html): | 7 | if new_url in pages: continue #如果已经爬过则跳过 | 8 | pages.add(new_url) | 9 | cnt += 1 | 10 | nxt_que.append(new_url) #将新的seed放入nxt_que中 | 11 | if cnt >= count: #最多选count个url | 12 | Break | 13 | … |
然后需要对新获取到的URL队列,递归地调用爬虫函数。为了避免爬虫程序无限递归下去,我们需要设定一个递归停止条件,比如,设定一个最深可递归层数,当递归到这一层时,不再递归调用爬虫函数:
1 | depth = 3 #最深递归层 | 2 | | 3 | def fetch(…, dep): | 4 | … | 5 | if dep >= depth: # 当深度大于设定值时,退出 | 6 | return | 7 | else: | 8 | fetch(…, dep + 1) # 否则递归调用 | 根据这些爬虫程序的设计,整个程序将如下所示: 1 | import string, urllib2 | 2 | import re | 3 | import os | 4 | | 5 | seed = "http://www.baidu.com/" #最开始的种子URL | 6 | depth = 3 #最多递归depth层,避免递归栈过深 | 7 | count = 5 #每个网页只抓取count个URL作为新的seed | 8 | href_re = re.compile(r'href\s*=\s*"(https?://\S*)"') #通过正则匹配网页源码中的URL | 9 | http_re = re.compile(r'\w+') #通过正则匹配URL中的文字部分 | 10 | pages = set() #已经爬过的URL | 11 | | 12 | save_dir = "." #保存路径 | 13 | def get_path(url): | 14 | ''' | 15 | 通过url获取保存文件路径,使用'_'拼接url中的文字部分。 | 16 | 为了避免文件名过长,只取拼接后字符串的前30个字符 | 17 | ''' | 18 | name = '_'.join(http_re.findall(url))[:30] | 19 | return os.path.join(save_dir, '%s.txt' % name) | 20 | | 21 | def fetch(que = [seed,], dep = 0): | 22 | ''' | 23 | 深度优先搜寻爬取que列表中的url, | 24 | 并选取网页内容中的count个url作为新seed。 | 25 | fetch函数最多递归depth层。 | 26 | ''' | 27 | nxt_que = [] #下一层递归所用到的seed列表 | 28 | for url in que: | 29 | print 'depth: %d fetching %s ...' % (dep, url) | 30 | html = urllib2.urlopen(url).read() | 31 | with open(get_path(url), 'w+') as f: | 32 | f.write(html) #保存网页内容 | 33 | | 34 | cnt = 0 #新seed的计数 | 35 | for new_url in href_re.findall(html): | 36 | if new_url in pages: continue #如果已经爬过则跳过 | 37 | pages.add(new_url) | 38 | cnt += 1 | 39 | nxt_que.append(new_url) #将新的seed放入nxt_que中 | 40 | if cnt >= count: #最多选count个url | 41 | break | 42 | | 43 | if dep < depth: | 44 | fetch(nxt_que, dep + 1) | 45 | | 46 | if __name__ == "__main__": | 47 | fetch() | 运行结果: depth: 0 fetching http://www.baidu.com/ ... | depth: 1 fetching http://news.baidu.com ... | depth: 1 fetching http://www.hao123.com ... | depth: 1 fetching http://map.baidu.com ... | depth: 1 fetching http://v.baidu.com ... | depth: 2 fetching http://www.baidu.com/ ... | depth: 2 fetching http://news.baidu.com/view.html ... | depth: 2 fetching http://tieba.baidu.com/ ... | depth: 2 fetching http://zhidao.baidu.com/ ... | depth: 2 fetching http://s1.hao123img.com/v4/cx/Fs/Mi/cE/bD/cxFsMicEbD.css ... | depth: 2 fetching http://update.123juzi.net/dl.php?edition=hao123_juzi_canal&amp;cid=h2 ... | depth: 2 fetching http://www.hao123.com/sethome ... | depth: 2 fetching http://www.hao123.com/shouji ... | depth: 2 fetching http://nlbc.baidu.com/lbs-lbc/?from=map ... | depth: 2 fetching http://developer.baidu.com/map/ ... | depth: 2 fetching http://www.baidu.com ... | depth: 2 fetching https://passport.baidu.com/v2/?login&tpl=ma ... | depth: 2 fetching http://vs6.bdstatic.com/browse_static/v3/index/pkg/home_23cbff0.css ... | depth: 3 fetching http://tieba.baidu.com ... | depth: 3 fetching http://www.baidu.com/gaoji/preferences.html ... | … | 下图是爬虫程序运行结束后获取到的网页:

本章小结
本章学习了网络爬虫的基本知识,介绍了URI、URL和HTTP协议的概念。详细介绍了使用urllib2网络库与服务器联立连接,下载所请求资源的方法;针对HTTP协议,还学习了使用Python向服务器发送表单,修改HTTP请求中的Header。之后介绍了urllib2网络库使用过程中会引发的异常和相应的异常处理方法;为了使同一个爬虫程序适应不同的网络环境,介绍了使用urllib2修改或设置网络参数。最后,详细讲解了如何设计并实现一个网络爬虫。

第15章 访问数据库
使用简单的纯文本文件只能实现有限的数据存储功能。但有时需要序列化存储一些内容,这时可以选择使用pickle模块将内容以特定文本形式存储在文本文件中,例如XML文件,但是这种方式存储的数据一方面还是有限制的,另一方面如果程序需要更强大的特性,使用数据库是比较好的解决方案。几乎所有的大规模商业系统都使用数据库来存储数据。例如,在网上购物网站taobao.com就需要数据库来存储销售的每件产品的信息。Python对数据库有良好的支持,能够对多种数据库进行访问。使用Python能够高效地开发出管理信息系统,可以是网站程序,还可以是桌面应用程序。
【体系结构】

【本章重点】
(1)了解关系型数据库;
(2)熟悉Python的DB-API原理;
(3)掌握Python操作SQLite;
(4)掌握Python操作MySQL;
15.1 数据库基础知识
数据库(Database)是按照一定的数据模型来组织、存储和管理数据的仓库,它产生于距今六十多年前,随着信息技术和市场的发展,特别是二十世纪九十年代以后,数据管理不再仅仅是存储和管理数据,而转变成用户所需要的各种数据管理的方式。数据库有很多种类型,从最简单的存储各种数据的表格到能够进行海量数据存储的大型数据库系统都在各个方面得到了广泛的应用。
在信息化社会,充分有效地管理和利用各类信息资源,是进行科学研究和决策管理的前提条件。数据库技术是管理信息系统、办公自动化系统、决策支持系统等各类信息系统的核心部分,是进行科学研究和决策管理的重要技术手段。
15.1.1 关系型数据库
关系模型具有完备的数据基础,简单灵活,易学易用,已经成为数据库的标准。采用关系模型作为数据模型的数据库称为关系型数据库。关系型数据库已经产生几十年了,因此他们是一种非常成熟和知名的技术,是一种进行复杂数据存储时首选的技术。
关系模型把世界看作是由实体(Entity)和联系(Relationship)构成的。实体是指现实世界中具有一定特征或属性并与其他实体有联系的对象。在一个关系数据库中,数据存储在各个表中,表可以被当成是一个二维数据结构。每个列,或者说二维矩阵中垂直的部分,都具有相同的数据类型,比如字符串、数值、日期等。表的每个水平部分由一些行组成,这些行也被称作记录。每行又由多个属性组成。通常,每个记录保存于一个条目相关的信息,例如一个音频CD、一个人、一个订购单、一辆汽车等。例如,表15.1显示了同学录系统中的同学信息表。 id | name | sex | phone | address | 1 | 张天 | 男 | 022-5333432 | 天津 | 2 | 王林 | 女 | 0411-8655112 | 大连 | 3 | 杜明 | 男 | 0411-8865115 | 大连 | 4 | 陈曦 | 女 | 0414-8722673 | 本溪 |
表15.1 学生表 这个表包含5列: * id:保存同学的ID(编号)。关系数据库广泛使用ID号,也称为主键,由数据库管理唯一号码的分配,可以通过这些号码对每行进行引用,保证每行都是唯一的(即使它们具有相同的数据)。然后便可以通过ID号引用每一个同学。 * name:保存同学的姓名。 * sex:保存同学的性别。 * phone:保存同学联系电话号码。 * address:保存同学的家庭住址。
在此示例中,列id是学生的ID号,用作主键。一个主键是对一个表的一个唯一索引,其中每个元素必须是唯一的,因为数据库可能使用那个元素作为给定行的键以及作为引用该行中数据的方式,这是一种与Python字典中的键和值类似的方式。因此,每个学生都需要有一个唯一的ID号,而且一旦有了ID号,便可以查找任何学生。因此,id将作为此表的内容的键。
15.1.2 SQL语句 结构化查询语言(Structured Query Language,SQL)定义了用于查询和修改数据库的一种标准语言。SQL支持在表15.2中列出的一些基本操作。
表15.2 SQL基本操作 操作 | 用法 | Select | 执行一个查询,以在数据库中搜索指定的数据 | Update | 修改一行或若干行,通常是根据特定的条件进行修改 | Insert | 在数据库中创建新行 | Delete | 从数据库中删除行 | 创建数据库 CREATE DATABASE contacts; GRANT ALL ON contacts.* to user(s); 第一行创建一个名为“contacts”的数据库,第二行将该数据库的权限赋给具体的用户(或者全部户),以便它们可以执行下面的数据库操作。 选择要使用的数据库 USE contacts; 如果在登录数据库时没有指定要使用那个数据库,这条简单的语句就可以指定你打算访问的数据库. 删除数据库 DROP DATABASE contacts; 创建表
CREATE TABLE stuinfo ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(10),sex VARCHAR(6), phone VARCHAR(11) , address VARCHAR(11) ) ; 这个语句用于创建表stuinfo,它有五个属性,id是主键,类型是整型自增长;name存储姓名属性,类型是可变字符串;sex存储性别属性,类型是可变字符串;phone存储电话属性,类型是可变字符串;address存储地址属性,类型是可变字符串。 删除表 DROP TABLE stuinfo ; DROP TABLE语句删除数据库中的一个表和它的所有数据。 插入行
INSERT INTO stuinfo VALUES('1','张天','男','022-5333432','天津') INSERT 语句用来向数据库中添加新的数据行。语句中必须指定要插入的表及该表中各个字段的值。上例中,表名是stuinfo,字符串'1'对应着id字段,'张天'对应着name字段,'男'对应着sex字段,'022-5333432'对应着phone字段,'天津'对应着address字段。 更新行 UPDATE stuinfo SET address='大连' WHERE name='张天'; UPDATE语句用来改变数据库中的已有记录。使用SET关键字来指定你要修改的字段及新值,你可以指定条件来筛选出需要更新的记录。例子中,将name是张天的记录的地址设为大连。 查询行 select * from stuinfo ; select语句用于查询表中符合条件的记录,*表示返回所有列,此查询将会返回所有的列和所有的行(因为没有where子句)。
15.2 Python与数据库 上节已经介绍了目前广泛使用的关系型数据库,以及操作数据库的SQL语言。SQL语言目前已经成为数据库的一种标准。现在支持SQL标准的可用数据库有很多,其中多数在Python中都有对应的客户端模块。所有的数据库大多数基本功能是相同的,所以大部分功能模块是相同的,不同的是它们的接口(API)。为了解决各种数据库模块间的兼容问题,Python提供了一个标准的DB-API。 DB-API是一个规范。它定义了一系列必须的对象和数据库存取方式,以便为各种各样的底层数据库系统和多种多样的数据库接口程序提供一致的访问接口。 图15.1表示Python数据库应用程序的结构(包括使用和不使用ORM)。可以看到DB-API是数据库客户端C库的接口。数据库和应用程序之间有多层通讯。第一个盒子一般是C/C++程序,应用程序通过DB-API兼容接口程序访问数据库。ORM通过程序处理数据库细节来简化数据库开发。

图15.1 Python DB-API应用程序结构
15.3 SQLite介绍 SQLite是D.Richard Hipp用C语言编写的开源嵌入式数据库引擎,它是完全独立的,不需要数据库服务器。SQLite支持多数SQL92标准,可以在所有主要的操作系统上运行,并且支持大多数计算机语言。SQLite非常健壮,SQLite对SQL92标准的支持包括索引、限制、触发和查看。SQLite不支持外键限制,但支持原子的、一致的、独立和持久(ACID)的事务。这意味着事务是原子的,因为它们要么完全执行,要么根本不执行;事务也是一致的,因为在不一致的状态中,该数据库从未被保留;事务还是独立的,所以,如果同一时间在同一数据库上有两个执行操作的事务,那么这两个事务是互不干扰的;而且事务是持久性的,所以,该数据库能够在崩溃和断电时,不会丢失数据或损坏。SQLite通过数据库级上的独占性和共享锁性来实现独立事务处理。这意味着当多个进程和线程在同一时间从同一数据库读取数据时,只有一个可以写入数据。在某个进程或线程向数据库执行写入操作之前,必须获得独占锁定。在发出独占锁定后,其他的进程或线程的读写操作将不被允许。 SQLite 由以下几个部分组成:SQL编译器、内核、后端以及附件。SQLite通过利用虚拟机和虚拟数据库引擎(VDBE),使调试、修改和扩展SQLite的内核变得更加方便。所有SQL语句都被编译成易读的、可以在SQLite虚拟机中执行的程序集。 SQLite支持大小高达2TB的数据库,每个数据库完全存储在单个磁盘文件中。这些磁盘文件可以在不同字节顺序的计算机之间移动。这些数据以B+树(B+tree)数据结构的形式存储在磁盘上。SQLite根据该文件系统获得其访问数据库权限。 SQLite采用动态数据类型,当某个值插入到数据库时,SQLite将会检查它的类型,如果该类型与关联的列不匹配,SQLite则会尝试将该值转换成该列的类型,如果不能转换,则该值将作为本身的类型存储,SQLite称这为“弱类型”。但有一个特例,如果是INTEGER PRIMARY KEY,则其他类型不会被转换,会报一个“datatype mismatch”的错误。概括来讲,SQLite支持NULL、INTEGER、REAL、TEXT和BLOB数据类型,分别代表空值、整型值、浮点值、字符串文本、二进制对象。 SQLite有许多内置函数用于处理字符串或数字数据。表15.3列出了一些有用的SQLite 、内置函数,且所有函数都是大小写不敏感,这意味着可以使用这些函数的小写形式、大写形式或混合形式。
表15.3 SQLite常用函数 SQLite COUNT | 聚集函数是用来计算一个数据库表中的行数 | SQLite MAX | 聚合函数允许我们选择某列的最大值 | SQLite MIN | 聚合函数允许我们选择某列的最小值 | SQLite AVG | 聚合函数计算某列的平均值 | SQLite SUM | 聚合函数允许为一个数值列计算总和 | SQLite RANDOM | 返回一个介于 -9223372036854775808 和+9223372036854775807 之间的伪随机整数 | SQLite ABSSQLite UPPER | 返回数值参数的绝对值把字符串转换为大写字母 | SQLite LOWER | 把字符串转换为小写字母 | SQLite LENGTH | 返回字符串的长度 | SQLite sqlite_version | 返回 SQLite 库的版本 | 由于资源占用少、性能良好和零管理成本,嵌入式数据库有了它的用武之地,它将为那些以前无法提供用作持久数据的后端的数据库的应用程序提供了高效的性能。如今没有必要使用文本文件来实现持久存储。SQLite之类的嵌入式数据库的易于使用性可以加快应用程序的开发,并使得小型应用程序能够完全支持复杂的SQL。这一点对于对于小型设备空间的应用程序来说尤其重要。
15.4 Python使用SQLite 在Python中使用SQLite需要导入sqlite3模块。SQLite虽然轻小但是功能强大。开发基于数据库的管理信息系统,首先要掌握有关数据库的基本操作。这些操作包括建立一个数据库,建立一张表,新增(插入)记录,删除记录,更新记录和查询记录。下面依次介绍它们的功能及操作。
15.4.1 导入sqlite3模块
Python标准库中带有sqlite3模块,可以直接导入:
>>> import sqlite3 |
15.4.2 创建数据库
通过调用connect函数来创建数据库或打开数据库,一般格式为:
连接对象 = sqlite3.connect(数据库文件名)
该函数返回一个数据库的连接对象,程序员通过连接对象访问数据库。
【例15-1】使用sqlite创建一个名为contacts数据库。 问题分析:在使用数据库之前先要创建数据库。 >>> import sqlite3 #导入sqlite3模块 | >>>conn = sqlite3.connect(r'F:\book\contacts.db') #获取数据库连接对象 |
程序分析:要使用sqlite的connect函数,首先要引入sqlite对应的DB-API即sqlite3模块。在调用connect函数的时候,指定数据库的名称,通过提供一个文件名(可以是文件的绝对路劲或者相对路径),如果指定的数据库存在就直接打开这个数据库,如果不存在就新创建一个再打开。在调用connect函数后可以发现,在指定的路径下如例中F:\book\下有一个contacts.db文件,其实SQLite的一个数据库就是一个文件,可以通过复制进行备份。
在获得数据库的连接对象后,才能对数据库进行其他操作如下。 * execute(SQL语句):执行SQL语句。 * cursor():创建一个游标。 * commit():事务提交。 * rollback():事务回滚。 * close():关闭一个数据库连接。
15.4.3 创建游标 游标是通过调用连接对象的cursor()函数来创建的,创建游标的一般格式为:
游标对象 = 连接对象.cursor() 游标提供了一种对从表中检索数据进行操作的灵活手段,其实际上是一种能从包括多条数据记录的结果集中,每次提取一条记录的机制。游标总是与1条SQL选择语句相关联。游标由结果集(可以是0条、1条或相关选择语句检索出的多条记录)和结果集中指向特定记录的游标位置组成,当决定对结果集进行处理时,必须声明一个指向该结果集的游标。 游标可以进行的主要操作如下, * execute(SQL语句):执行一个SQL语句,通常执行查询语句。 * fetchall():返回一个列表,包含结果集中剩下的所有行。 * fetchone():以元组的方式返回查询结果的下一行,结果集用尽则返回None。 * Description:返回游标活动状态(一个包含七个元素 的元组):(name, type_code, display_size, internal_ size, precision, scale, null_ok);只有name和type_code是必须提供的。
在获得游标完成查询并且做出某些更改后确保已经进行了提交,这样才可以将这些修改真正地保存到文件中:
>>>conn.commit() 可以在每次修改数据库后都进行提交,而不是仅仅在准备关闭时才提交,准备关闭数据库时,使用close方法:
>>>conn.close()
从15.1节可知数据库连接conn也有execute()函数,它与游标curs的execute()有什么区别呢?其实conn的execute函数也是返回一个游标,如果不需要对执行sql语句结果处理可以直接使用conn的execute()函数。 >>> conn = sqlite3.connect(r'F:\book\contacts.db') #获取数据库连接 | >>> conn.execute("select * from stuinfo") #使用conn进行数据库查询 | <sqlite3.Cursor object at 0x0000000002706B20> #返回类型是游标 |
由上述代码的返回结果发现返回的结果类型是sqlite3.Cursor即游标类型。
【例15-2】在数据库contacts.db创建15.1节中学生信息表(暂时不考虑外键内容,对表中部分列内容进行了替换,见程序)。 问题分析:对数据的表进行查询、删除和插入等操作前,先要创建表。 1 | #coding=utf-8 | 2 | import sqlite3 | 3 | | 4 | conn = sqlite3.connect(r'F:\book\contacts.db') #获取数据库连接 | 5 | curs = conn.cursor() #获得游标 | 6 | | 7 | curs.execute(''' #创建表student的SQL语句 | 8 | CREATE TABLE IF NOT EXISTS stuinfo( #如果不存在studentinfo就创建 | 9 | sid INTEGER PRIMARY KEY AUTOINCREMENT, | 10 | name VARCHAR(10), | 11 | sex VARCHAR(6) , | 12 | phone VARCHAR(20), | 13 | address VARCHAR(30) | 14 | ) | 15 | ''') | 16 | conn.commit() #提交事务 | 17 | conn.close() | | |
程序分析: 第3-4行获取数据库连接和游标。第7-15行执行创建表stuinfo语句。第16行修改数据库后一定要提交事务。 【例15-3】在向stuinfo表中插入多条记录。 1 | #coding=utf-8 | 2 | import sqlite3 | 3 | conn = sqlite3.connect(r'F:\book\contacts.db') | 4 | conn.text_factory = str #设置python处理文本的类型 | 5 | curs = conn.cursor() | 6 | | 7 | curs.execute("INSERT INTO stuinfo VALUES \ #插入一条记录并指定主键值 | 8 | ('1','张天','男','022-5333432','天津')") | 9 | curs.execute(''' | 10 | INSERT INTO stuinfo (name,sex,phone,address) VALUES \ #插入记录主键自动分配 | 11 | ('王林','女','0411-8655112','大连') | 12 | ''') | 13 | stu_list = [('杜明','男','0411-8865115','大连') , ('陈曦','女','0414-8722673','本溪')] | 14 | curs.executemany("INSERT INTO stuinfo \ #以列表形式插入多条记录 | 15 | (name,sex,phone,address) VALUES(?,?,?,?)",stu_list) | 16 | conn.commit() | 17 | | 18 | cur = conn.execute("select * from stuinfo") | 19 | for row in cur.fetchall(): #显示插入的同学的信息 | 20 | for tmp in row: | 21 | print str(tmp) + '\t', #添加制表符对齐显示 | 22 | print | 23 | conn.close() |
运行结果:
1 张天 男 022-5333432 天津 | 2 王林 女 0411-8655112 大连 | 3 杜明 男 0411-8865115 大连 | 4 陈曦 女 0414-8722673 本溪 |
程序分析:第3-5行获取数据库连接和游标。第7-15行插入记录。第16行修改数据库后一定要事务提交。
【例15-4】在stuinfo表中查询性别是男的学生。 1 | #coding=utf-8 | 2 | import sqlite3 | 3 | conn = sqlite3.connect(r'F:\book\contacts.db') | 4 | conn.text_factory=str #设置python处理文本的类型 | 5 | curs = conn.cursor() | 6 | | 7 | query = "SELECT * FROM stuinfo WHERE sex = '男'" #查询条件性别为男 | 8 | curs.execute(query) | 9 | stuset = curs.fetchall() #获取所有学生记录 | 10 | for stu in stuset: | 11 | for item in stu: | 12 | print str(item) + '\t', | 13 | Print | 14 | conn.commit() #提交事务 | 15 | conn.close() |
运行结果:
1 张天 男 022-5333432 天津 | 3 杜明 男 0411-8865115 大连 |
程序分析: 第3-5行获取数据库连接和游标。第4行设置python处理文本的类型。第7行是查询性别是boy的学生。第9行通过fetchall函数获取所有查询记录,stuset是一个列表,记录以元组形式存在列表中。第10-13行输出所有学生信息。由于没有修改数据库,第14行可以省略。 【15-5】更改数据库中姓名为张天的同学信息,将其住址改为大连,并删除住址为本溪的同学。 1 | #coding=utf-8 | 2 | import sqlite3 | 3 | | 4 | def show_students(curs): #显示所有学生 | 5 | curs.execute("select * from stuinfo") | 6 | for stu in curs.fetchall(): | 7 | for item in curs.fetchall(): | 8 | print str(item) + '\t', #添加制表符对齐显示 | 9 | print | 10 | | 11 | conn = sqlite3.connect(r'F:\book\contacts.db') #获取数据库连接 | 12 | curs = conn.cursor() #获取游标 | 13 | print "---origin students---" | 14 | show_students(curs) #打印现有所有学生 | 15 | | 16 | print "---update student---" | 17 | sql_command = "update stuinfoset address = '大连' \ #更改地址为Dalian且性别为 | 18 | where address = 'Dalian' and sex ='girl'" #girl的学生的地址为WuHan | 19 | curs.execute(sql_command) | 20 | conn.commit() | 21 | show_students(curs) #显示更改后所有学生信息 | 22 | | 23 | print "delete student address = '本溪'" | 24 | sql_command = "delete from stuinfowhere address = '本溪'" #删除地址为Tokyo的学生 | 25 | curs.execute(sql_command) | 26 | conn.commit() | 27 | show_students(curs) #显示删除后所有学生信息 | 28 | conn.close() |
运行结果:
---origin students---1 张天 男 022-5333432 天津 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8865115 大连 4 陈曦 女 0414-8722673 本溪 ---update student---1 张天 男 022-5333432 天津 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8865115 大连 4 陈曦 女 0414-8722673 本溪 delete student address = '本溪'1 张天 男 022-5333432 天津 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8865115 大连 |
程序分析:第4-9行定义了函数show_students,用于打印所有学生信息,由于在本例中多处使用,所以将代码抽取成一个函数方便使用。第16-21行执行更新语句并打印学生信息。第23-28行执行删除学生语句并打印学生信息。
15.5 MySQL介绍 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。MySQL是最流行的关系型数据库管理系统,在WEB应用方面MySQL是最好的RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。MySQL所使用的SQL语言是用于访问数据库的最常用标准化语言。MySQL软件采用了双授权政策(本词条“授权政策”),它分为社区版和商业版,由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,一般中小型网站的开发都选择MySQL作为网站数据库。由于其社区版的性能卓越,搭配PHP和Apache可组成良好的开发环境。

图15.2 Mysql图标
MySQL的特性:
(1) 使用C和C++编写,并使用了多种编译器进行测试,保证源代码的可移植性。
(2) 支持AIX、FreeBSD、HP-UX、Linux、Mac OS、Novell Netware、OpenBSD、OS/2 Wrap、Solaris、Windows等多种操作系统。
(3) 为多种编程语言提供了API。这些编程语言包括C、C++、Python、Java、Perl、PHP、Eiffel、Ruby和Tcl等。
(4) 支持多线程,充分利用CPU资源。
(5) 优化的SQL查询算法,有效地提高查询速度。
(6) 既能够作为一个单独的应用程序应用在客户端服务器网络环境中,也能够作为一个库而嵌入到其他的软件中提供多语言支持,常见的编码如中文的GB 2312、BIG5,日文的Shift_JIS等都可以用作数据表名和数据列名。
(7) 提供TCP/IP、ODBC和JDBC等多种数据库连接途径。
(8) 提供用于管理、检查、优化数据库操作的管理工具。
(9) 可以处理拥有上千万条记录的大型数据库。 与其他的大型数据库例如Oracle、DB2、SQL Server等相比,MySQL自有它的不足之处,如规模小、功能有限(MySQL Cluster的功能和效率都相对比较差)等,但是这丝毫也没有减少它受欢迎的程度。对于一般的个人使用者和中小型企业来说,MySQL提供的功能已经绰绰有余,而且由于MySQL是开放源码软件,因此可以大大降低总体拥有成本。 目前Internet上流行的网站构架方式是LAMP(Linux+Apache+MySQL+PHP),即使用Linux作为操作系统,Apache作为Web服务器,MySQL作为数据库,PHP作为服务器端脚本解释器。由于这四个软件都是自由或开放源码软件(FLOSS),因此使用这种方式不用花一分钱就可以建立起一个稳定、免费的网站系统。 可以使用命令行工具管理MySQL数据库(命令mysql 和 mysqladmin),也可以从MySQL的网站下载图形管理工具MySQL Administrator和MySQL Query Browser。phpMyAdmin是由php写成的MySQL资料库系统管理程式,让管理者可用Web界面管理MySQL资料库。phpMyBackupPro也是由PHP写成的,可以透过Web界面创建和管理数据库。它可以创建伪cronjobs,可以用来自动在某个时间或周期备份MySQL 数据库。另外,还有其他的GUI管理工具,例如早先的mysql-front 以及ems mysql manager,navicat等等。
15.6 Python使用MySQL 与使用SQLite一样,要使用Mysql数据库也需要用到Python的DB-API。Python中使用MySQLdb作为Mysql的唯一接口程序。在连接SQLite的数据库时,操作某个数据库的权限是由对文件操作的权限决定的。而连接MySQL数据库则需要对应的用户名与密码(一般在安装Mysql数据库时会提示设置用户名与密码)。 从Python2.5开始已经内置了SQLite3,成为了内置模块,这给我们省了安装的时间,只需导入即可。在使用前先要安装Mysql和MySQLdb。可以通过http://dev.Mysql.com /downloads/mysql/下载Mysql的安装程序,通过http://dev.mysql.com/downloads/connector /python/下载MySQLdb的安装程序。通过下面代码来确定是否安装MySQLdb。 >>> import MySQLdb | 如果执行结果出现下面异常,请安装相应的MySQLdb。 Traceback (most recent call last): File "test.py", line 3, in <module> import MySQLdbImportError: No module named MySQLdb | 本书结合Python中的MySQLdb讲解Mysql的基本使用,关于Mysql的sql语句更多内容读者可以查阅相关书籍或是网上资料进一步学习。
15.6.1 创建数据库
通过调用connect函数来创建数据库或打开数据库,一般格式为:
连接对象 = MySQLdb.connect(host,user,passwd,db,port) host指连接的数据库主机,本机是localhost,其他机器填写对应的IP;user是进入数据库的用户名;passed指登录密码;db指要访问的数据库名称,一般一台数据库服务器上会存在多个数据库,所以连接某个数据库时要指定访问数据库的名称;port是Mysql服务的端口号默认是3306,连接时可以不填。该函数返回一个数据库的连接对象,程序员通过连接对象访问数据库。 【例15-6】使用mysql创建一个名为Contacts 数据库。 1 | #coding=utf-8 | 2 | import MySQLdb #导入MySQLdb模块 | 3 | | 4 | conn = MySQLdb.connect("localhost","root","root") #获取数据连接对象 | 5 | curs = conn.cursor() #获取游标 | 6 | sql_command = "CREATE DATABASE Contacts DEFAULT CHARACTER SET utf8" #拼写sql语句创建Contacts 数据库编码为utf8 | 7 | curs.execute(sql_command) #执行创建数据库的语句 | 8 | conn.commit() #提交事务 |
程序分析:在1-2行中注明代码编码为utf-8,引入MySQLdb模块。第4行调用connect的函数创建了一个数据库连接,在函数参数中只定义了三个localhost标明访问的是本机数据库,第一个root是用户名,第二个root是密码,作者为了简洁将用户名与密码设置一样,读者可以根据自己实际情况填写。由于数据库还未创建所以没有传入数据库名称参数。第5行通过cursor函数获得游标,与SQLite使用类似。第6行是sql语句表示创建一个名为“Contacts”的数据库,其默认编码格式为utf8。第7-8行执行sql语句并提交事务。在获得数据库的连接对象后,才能对数据库进行其他操作如下。
* execute(SQL语句):执行SQL语句。 * cursor():创建一个游标。 * commit():事务提交。 * rollback():事务回滚。 * close():关闭一个数据库连接。
15.6.2 创建游标 Mysql游标与SQLite一样是通过调用连接对象的cursor()函数来创建的,创建游标的一般格式为:
游标对象 = 连接对象.cursor() 游标对象的操作方法与SQLite一样,只是执行sql语句可能不同,使用时请参考官方文档API。 Mysql游标可以进行的主要操作如下。 execute(SQL语句):执行一个SQL语句,通常执行查询语句。 fetchall():返回一个列表,包含结果集中剩余的所有行。 fetchone():以元组的方式返回查询结果的下一行,结果集用尽则返回None。 Description:返回游标活动状态(一个包含七个元素 的元组):(name,type_code, display_size,internal_ size,precision,scale,null_ok);只有name和type_code是必须提供的。 在获得游标完成查询并且做出某些更改后,确保已经进行了提交,这样才可以将这些修改真正地保存到文件中:
>>>conn.commit()
可以在每次修改数据库后都进行提交,而不是仅仅在准备关闭时才提交,准备关闭数据库时,使用close方法:
>>>conn.close()
通过SQLite与Mysql使用过程,可以发现在Python中不论是什么数据库,它们的操作步骤和方法基本类似,Python将它们按照相同的结构进行了封装,方便用户使用。 下面总结一下在Python中使用不同数据库时相同的步骤。
(1)通过导入模块的connect函数获得连接对象
(2)通过连接对象的cursor函数获得游标对象
(3)执行SQL语句
(4)提交事务
(5)关闭数据库连接
【例15-7】在数据库Contacts创建一张表stuinfo。 1 | #coding=utf-8 | 2 | import MySQLdb #导入MySQLdb模块 | 3 | | 4 | conn = MySQLdb.connect("localhost","root","root","Contacts") #获取数据库连接 | 5 | curs = conn.cursor() #获取游标 | 6 | | 7 | sql_command = '''CREATE TABLE stuinfo( #创建stuinfo表 | 8 | id int NOT NULL AUTO_INCREMENT, | 9 | name VARCHAR(20), | 10 | sex VARCHAR(6), | 11 | phone VARCHAR(11) , | 12 | address VARCHAR(30) , | 13 | PRIMARY KEY ( id ) | 14 | )''' | 15 | | 16 | curs.execute(sql_command) #执行创建表操作 | 17 | conn.commit() #提交事务 | 18 | conn.close() #关闭数据库连接 |
程序分析:第4行是连接数据库指定数据库为Contacts;第7-14是创建表stuinfo的sql语句,其中第13行表示设置id为主键。第16-17行表示执行语句并提交事务。运行createtb.py后在数据库stuinfo中会发现多了一张表stuinfo。
【例15-8】在表stuinfo中插入数据,更改数据库中姓名为张天的同学信息,将其住址改为大连,并删除住址为本溪的同学。 1 | #coding=utf-8 | 2 | import MySQLdb #导入MySQLdb模块 | 3 | | 4 | def show_students(curs): #定义函数显示所有学生 | 5 | curs.execute("select * from stuinfo") | 6 | for stu in curs.fetchall(): | 7 | for tmp in stu: | 8 | print str(tmp) + '\t', | 9 | print | 10 | | 11 | conn = MySQLdb.connect("localhost","root","root","Contacts") #连接数据库 | 12 | curs = conn.cursor() #获取游标 | 13 | | 14 | print "***插入学生***" #执行插入操作 | 15 | curs.execute("INSERT INTO stuinfo (name,sex,phone,address) VALUES ('张天','男', '022-5333432','天津')") | 16 | curs.execute("INSERT INTO stuinfo (name,sex,phone,address) VALUES ('王林','女', '0411-8655112','大连')") | 17 | curs.execute("INSERT INTO stuinfo (name,sex,phone,address) VALUES ('杜明','男', '0411-8655112','大连')") | 18 | curs.execute("INSERT INTO stuinfo (name,sex,phone,address) VALUES ('陈曦','女', '0414-8722673','本溪')") | 19 | conn.commit() #事务提交 | 20 | show_students(curs) | 21 | | 22 | print "***更改学生信息***" #执行更改操作 | 23 | curs.execute("UPDATE stuinfo SET address = '大连' WHERE name = '张天' ") | 24 | conn.commit() | 25 | show_students(curs) | 26 | | 27 | print "***删除学生***" #执行删除操作 | 28 | curs.execute("DELETE FROM stuinfo WHERE address = '本溪'") | 29 | conn.commit() | 30 | show_students(curs) | 31 | | 32 | conn.close() |
输出结果:
***插入学生***1 张天 男 022-5333432 天津 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8655112 大连 4 陈曦 女 0414-8722673 本溪 ***更改学生信息***1 张天 男 022-5333432 大连 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8655112 大连 4 陈曦 女 0414-8722673 本溪 ***删除学生***1 张天 男 022-5333432 大连 2 王林 女 0411-8655112 大连 3 杜明 男 0411-8655112 大连 |
程序分析:第4-9行定义了一个函数show_students,传入参数curs表示一个游标,函数功能是显示stuinfo表中所有学生,第5行是执行"select * from stuinfo"语句表示从stuinfo表中查找所有记录并返回所有列。每条记录会以元组形式保存起来,可以通过fetchall函数获取。第6-9行是遍历输出记录。第11-12行是获取数据库连接和游标。第15-18行是插入4个学生记录,注意在执行的SQL语句修改了数据库后一定要进行事务提交,否则相应的修改不能保存到数据库中。
15.7 编写电子同学录
15.7.1 需求分析
随着科技的飞速发展,信息化的时代逐渐显现,纸质的同学录已经不能满足大数据时代的需求了。相比纸质同学录,电子同学录不但环保符合科学发展的要求,而且存储量更大,添加、删除和查找更方便。
一款合格的同学录管理系统必须具备以下特点: * 能够对同学录信息进行集中管理。 * 能够大大提高用户的工作效率。 * 能够对同学录信息实现增、删、改和查功能。
同学录管理系统最重要的功能包括以下几方面:实现新增功能,即添加新同学信息;实现删除功能,即删除同学的信息;实现修改功能,即变更同学的信息;实现查询功能,即输入姓名,显示所有该姓名的同学信息。
15.7.2 系统设计
同学录系统主要实现对同学信息的增、删、改和查的基本操作。用户可以通过的自己的需求点击相应的按钮,实现对同学信息的快速管理。同学录管理系统的功能结构如图15.3所示。
15.7.3 数据库的设计
同学录软件管理系统主要用来记录同学(学生)的信息.首先需要创建一个数据库和一张表。这里选择用sqlite数据库,因为它是集成在Python中的轻量级数据库。这里数据库名为Contacts.db路径采取相对路径。创建了一张学生信息表stuinfo,表有5个属性,id是主键,name是同学姓名,sex是性别,phone是电话,address是住址。

图15.3 同学录管理系统功能结构

1 | self.conn = sqlite3.connect(r'Contacts.db') #创建数据库 | 2 | self.conn.text_factory = str #定义Python读取数据格式 | 3 | self.curs =self.conn.cursor() #获取游标 | 4 | self.curs.execute(''' #创建stuinfo表 | 5 | CREATE TABLE IF NOT EXISTS stuinfo( | 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 7 | name VARCHAR(10), | 8 | sex VARCHAR(6) , | 9 | phone VARCHAR(20) , | 10 | address VARCHAR(30) , | 11 | ) | 12 | ''') | 13 | self.conn.commit() |
15.7.4 页面设计
同学录软件的页面编写使用的是wxPython。wxPython是Python编程语言的一个GUI工具箱。他使得Python程序员能够轻松的创建具有健壮、功能强大的图形用户界面的程序。它是Python语言对流行的wxWidgets跨平台GUI工具库的绑定。可以在根据系统环境在http://www.wxpython.org/download.php选择对应的wxPython版本下载安装。同学录管理软件界面如图15.4。
这里只对wxPython进行简要的介绍,更多的内容读者可以查阅官方的API文档http://www.wxpython.org/Phoenix/docs/html/main.html。
使用wxPython时,需要先导入wx模块。最简单的wxPython程序应该像下面这样:
1 | import wx #导入wx模块 | 2 | app = wx.App() #创建wx程序实例 | 3 | app.MainLoop() #运行程序 | app是wxPyhon程序的主体,要想显示按钮等组件,还需要先创建一个框架 图15.4 同学录管理软件界面
(Frame),它是wx.Frame类的实例。
1 | class MyFrame(wx.Frame): | 2 | def __init__(self): | 3 | wx.Frame.__init__(self,None, -1,title="同学录管理软件", size=(700, 450)) | 4 | self.Center() #框架位置居中 |
在创建框架后,还需要添加背景组件wx.Panel。
1 | panel = wx.Panel(self) |
为同学录添加输入框,wx.StaticText类用于显示静态文本,wx.TextCtrl类用于文本输入。
1 | wx.StaticText(panel,-1,"电话",pos = (x0+3*dx,y0)) #定义标签“电话”self.tel = wx.TextCtrl(panel,-1,"",pos=(x0+3*dx,y0+dy),size=(w1,30)) #定义输入框 | 2 | |
为同学录添加按钮,wx.Button类用于实现按钮,Bind函数将按钮的事件与函数绑定。
1 | self.insertButton = wx.Button(panel,-1,label="新增",\ | 2 | pos=(x1,y0+3*dy),size=(w2,25)) #定义“新增”按钮 | 3 | self.insertButton.Bind(wx.EVT_BUTTON,self.insertStu) #定义事件与函数绑定 |
上面的代码第3行将insertButton按钮得点击事件与insertStu函数绑定,当点击“新增”按钮时,会调用insertStu函数。函数首先从文本输入框中读取学生的姓名、性别、电话和地址信息,然后进行插入操作,最后提交事务。
1 | def insertStu(self,event): | 2 | stu_name = self.name.GetValue() #获取输入的姓名 | 3 | stu_sex = self.sex.GetStringSelection() #获取输入的性别 | 4 | stu_phone = self.tel.GetValue() #获取输入的电话 | 5 | stu_address = self.address.GetValue() #获取输入的地址 | 6 | new_stu = (stu_name,stu_sex,stu_phone,stu_address) | 7 | sql = "insert into stuinfo (name,sex,phone,address) \ #格式化插入sql语句 | 8 | values ('%s','%s','%s','%s')" % new_stu | 9 | self.curs.execute(sql) #执行插入操作 | 10 | self.conn.commit() #提交事务 | 11 | self.curs.execute("select * from stuinfo") | 12 | self.setGridLabel(self.curs) #刷新显示表格 |
15.7.5 模块实现
(1)插入功能
添加一个新的同学信息时需要填入姓名,性别,电话和地址信息。比如输入一个学生姓名是陈曦,性别是女,电话是0414-8722673,地址是本溪。点击“新增”按钮就能在下方表格中看见新添加的数据,如图15.5。

图15.5新增一个同学
在insertStu函数中首先获取四个文本框的内容(姓名、性别、电话和地址)。然后执行插入的sql语句,完成插入记录操作。最后是setGridLabel函数,它的作用是刷新网格grid,来显式新增的记录。
1 | def insertStu(self,event): | 2 | stu_name = self.name.GetValue() #获取输入的姓名 | 3 | stu_sex = self.sex.GetStringSelection() #获取输入的性别 | 4 | stu_phone = self.tel.GetValue() #获取输入的电话 | 5 | stu_address = self.address.GetValue() #获取输入的地址 | 6 | new_stu = (stu_name,stu_sex,stu_phone,stu_address) | 7 | sql = "insert into stuinfo (name,sex,phone,address) \ #格式化插入sql语句 | 8 | values ('%s','%s','%s','%s')" % new_stu | 9 | self.curs.execute(sql) #执行插入操作 | 10 | self.conn.commit() #提交事务 | 11 | self.curs.execute("select * from stuinfo") | 12 | self.setGridLabel(self.curs) #刷新显示表格 |
(2)删除功能
在网格中选择一条记录,它的内容就会在文本框中显示出来,点击删除按钮就会后执行删除操作。这里以删除姓名为陈曦的学生为例,如图15.6。点击网格中的第四行中任意一列,内容显示在四个文本框中,编号是4,姓名为程曦,性别为女,电话是0414-8722673,地址是本溪。

图15.6 选择一条记录
点击删除按钮,姓名为陈曦的同学记录会被删除,如图15.7。

图15.7 删除后成员信息
先将删除按钮绑定deleteStu函数。在deleteStu函数中根据主键id筛选出记录并删除。
1 | self.deleteButton = wx.Button(panel,-1,label="删除 \ | 2 | ",pos=(x1+dx1,y0+3*dy),size=(w2,25)) | 3 | self.deleteButton.Bind(wx.EVT_BUTTON,self.deleteStu) | 4 | def deleteStu(self,event): | 5 | id = self.num.GetValue() #获取主键值 | 6 | sql = "delete from stuinfo where id = '%s' " % id | 7 | query = self.curs.execute(sql) #执行删除操作 | 8 | self.conn.commit() | 9 | self.curs.execute("select * from stuinfo") | 10 | self.setGridLabel(self.curs) #刷新同学录列表 | 11 | self.inputClean() #清除文本框内容 |
(3)修改功能
修改同学信息时,先在列表中选择需要修改的人,选中的同学的信息会在文本框中显示出来如图15.8,选择姓名为张天的同学。

图15.8 选择名为张天的同学
将他的地址改为大连,点击修改按钮后,同学列表中的信息会被修改,地址由天津改为了大连,如图15.9。

图15.9 信息修改成功
修改功能首先也要将按钮得事件与函数绑定,这里如前面功能一样,绑定了updateStu函数。修改学生信息时要先获得需要修改的记录的主键,不指定主键会对数据库中所有记录进行修改。
1 | def updateStu(self,event): | 2 | id = self.num.GetValue() | 3 | update_stu = (self.name.GetValue(),self.sex.GetStringSelection(),\ | 4 | self.tel.GetValue(),self.address.GetValue(),id) #获得记录 | 5 | sql = "update stuinfo set name = '%s',sex = '%s',\ | 6 | phone = '%s',address = '%s' where id = '%s'" % update_stu | 7 | self.conn.execute(sql) | 8 | self.conn.commit() | 9 | self.curs.execute("select * from stuinfo") | 10 | elf.setGridLabel(self.curs) |
(4)查询功能
同学录管理软件提供了两种查询功能,第一种是输入姓名,点击查询时会筛选出姓名符合的同学,第二种查询是当不输入任何姓名时,查询会返回所有同学的信息。在图15.10中输入姓名为王林。

图15.10 检索姓名王林
点击查询按钮后,显示的结果如图15.11。

图15.11 姓名为王林的同学检索结果
先判断姓名文本框中值是否为空,若为空则选择所有记录,若不为空,则以输入的内容作为筛选条件,进行查询。并在学生列表中刷新查询结果。
1 | def Search(self,event): | 2 | if self.name.GetValue(): #判断姓名输入是否为空 | 3 | sql = "select * from stuinfo where name = '%s' " % self.name.GetValue() | 4 | else: | 5 | sql = "select * from stuinfo" | 6 | self.inputClean() | 7 | self.curs.execute(sql) | 8 | self.setGridLabel(self.curs) |
下面是程序完整代码:
1 | #-*- coding: UTF-8 -*- | 2 | import wx | 3 | import wx.grid | 4 | import sqlite3 | 5 | import types | 6 | | 7 | class MyFrame(wx.Frame): | 8 | def __init__(self): | 9 | x0 = 22 #定义长度常量 | 10 | y0 = 20 | 11 | x1 = 40 | 12 | w1 = 100 | 13 | w2 = 110 | 14 | dx = 120 | 15 | dx1 = 120 | 16 | dy = 30 | 17 | wx.Frame.__init__(self,None, -1,title="同学录管理软件", size=(700, 450)) | 18 | self.Center() | 19 | panel = wx.Panel(self) | 20 | wx.StaticText(panel,-1,"编号",pos = (x0,y0)) | 21 | self.num = wx.TextCtrl(panel,-1,"",pos=(x0,y0+dy),size=(w1,30)) | 22 | self.num.SetEditable(False) #“编号”输入框禁止编辑 | 23 | | 24 | wx.StaticText(panel,-1,"姓名",pos = (x0+dx,y0)) | 25 | self.name = wx.TextCtrl(panel,-1,"",pos=(x0+dx,y0+dy),size=(w1,30)) | 26 | | 27 | wx.StaticText(panel,-1,"性别",pos = (x0+2*dx,y0)) | 28 | self.sex = wx.Choice(panel,-1,\ | 29 | choices=["男","女"],pos=(x0+2*dx,y0+dy),size=(w1,30)) | 30 | | 31 | wx.StaticText(panel,-1,"电话",pos = (x0+3*dx,y0)) | 32 | self.tel = wx.TextCtrl(panel,-1,"",pos=(x0+3*dx,y0+dy),size=(w1,30)) | 33 | | 34 | wx.StaticText(panel,-1,"地址",pos = (x0+4*dx,y0)) | 35 | self.address = wx.TextCtrl(panel,-1,"",pos=(x0+4*dx,y0+dy),size=(w1,30)) | 36 | | 37 | self.insertButton = wx.Button(panel,-1,label="新增",\ | 38 | pos=(x1,y0+3*dy),size=(w2,25)) | 39 | self.insertButton.Bind(wx.EVT_BUTTON,self.insertStu) | 40 | | 41 | self.deleteButton = wx.Button(panel,-1,label="删除",\ | 42 | pos=(x1+dx1,y0+3*dy),size=(w2,25)) | 43 | self.deleteButton.Bind(wx.EVT_BUTTON,self.deleteStu) | 44 | | 45 | self.deleteButton = wx.Button(panel,-1,label="修改",\ | 46 | pos=(x1+2*dx1,y0+3*dy),size=(w2,25)) | 47 | self.deleteButton.Bind(wx.EVT_BUTTON,self.updateStu) | 48 | | 49 | self.searchButton = wx.Button(panel,-1,label="查询",\ | 50 | pos=(x1+3*dx1,y0+3*dy),size=(w2,25)) | 51 | self.searchButton.Bind(wx.EVT_BUTTON,self.Search) | 52 | self.searchButton.SetToolTip(wx.ToolTip("查询姓名")) | 53 | | 54 | | 55 | self.dataGrid = wx.grid.Grid(panel,pos=(10,150),size=(600,220)) | 56 | | 57 | self.conn = sqlite3.connect(r'Contacts.db') #连接数据库 | 58 | self.conn.text_factory = str #定义读取数据格式 | 59 | self.curs =self.conn.cursor() #获取游标 | 60 | self.curs.execute(''' | 61 | CREATE TABLE IF NOT EXISTS stuinfo( #若表不存在则创建 | 62 | id INTEGER PRIMARY KEY AUTOINCREMENT, | 63 | name VARCHAR(10), | 64 | sex VARCHAR(6), | 65 | phone VARCHAR(20) , | 66 | address VARCHAR(30) | 67 | ) | 68 | ''') | 69 | self.conn.commit() | 70 | self.curs.execute("select * from stuinfo") #初始显示所有同学信息 | 71 | self.setGridLabel(self.curs,True) #调用刷新表格函数 | 72 | | 73 | def setGridLabel(self,curs,flag = False): #定义刷新表格函数 | 74 | query = curs.fetchall() | 75 | col_name_list = [tuple[0] for tuple in curs.description] #获取表中列名 | 76 | colname_dic = {"id":"编号","name":"姓名",\ #建立中文对应字典 | 77 | "sex":"性别","phone":"电话","address":"地址"} | 78 | rowsNum = len(query) #获得查询结果行数 | 79 | colsNum = len(col_name_list) #获取查询结果的列数 | 80 | if flag: | 81 | self.dataGrid.CreateGrid(rowsNum,colsNum) #只在初始化时创建表格 | 82 | self.dataGrid.Bind(wx.grid.EVT_GRID_SELECT_CELL,\ | 83 | self.gridSELECT) #绑定表格单元选定事件 | 84 | self.dataGrid.SetDefaultColSize(100) | 85 | self.dataGrid.SetDefaultCellAlignment( \ | 86 | wx.ALIGN_CENTRE ,wx.ALIGN_CENTRE ) | 87 | if self.dataGrid.GetNumberRows(): #重新绘制表格 | 88 | self.dataGrid.DeleteRows(0,self.dataGrid.GetNumberRows()) | 89 | self.dataGrid.AppendRows(rowsNum) | 90 | | 91 | for col in range(colsNum): #对表中列头进行赋值 | 92 | self.dataGrid.SetColLabelValue(col,\ | 93 | colname_dic[col_name_list[col]]) | 94 | i = 0 | 95 | for line in query: #遍历记录对表格进行赋值 | 96 | j = 0 | 97 | for tmp in line: | 98 | tmp = self.changcode(tmp) | 99 | self.dataGrid.SetCellValue(i , j , tmp) | 100 | j = j + 1 | 101 | print | 102 | i = i + 1 | 103 | | 104 | def changcode(self,old_str): #定义中文转码函数 | 105 | if type(old_str) == types.IntType: | 106 | old_str = str(old_str) | 107 | return old_str.decode('utf-8').encode('gbk') #将utf-8编码转为gbk | 108 | | 109 | def gridSELECT(self,event): #获得表格中选择行并在文本框中显示信息 | 110 | row = event.GetRow() #得到选择的行的行数 | 111 | if not self.dataGrid.GetNumberRows(): #判断当前表格是否有数据 | 112 | return | 113 | id = self.dataGrid.GetCellValue(row,0) #获得该行第一列的值即id | 114 | sql = "select * from stuinfo where id = '%s' " % id | 115 | query = self.curs.execute(sql).fetchall() #对id进行查询获得筛选的结果 | 116 | if len(query): | 117 | self.num.SetValue(self.changcode(query[0][0])) | 118 | self.name.SetValue(self.changcode(query[0][1])) | 119 | self.sex.SetStringSelection(self.changcode(query[0][2])) | 120 | self.tel.SetValue(self.changcode(query[0][3])) | 121 | self.address.SetValue(self.changcode(query[0][4])) | 122 | | 123 | def inputClean(self): #定义函数清除文本框内容 | 124 | self.num.Clear() | 125 | self.name.Clear() | 126 | self.tel.Clear() | 127 | self.address.Clear() | 128 | | 129 | def deleteStu(self,event): #删除同学功能 | 130 | id = self.num.GetValue() #获得删除同学的id | 131 | sql = "delete from stuinfo where id = '%s' " % id | 132 | query = self.curs.execute(sql) | 133 | self.conn.commit() | 134 | self.curs.execute("select * from stuinfo") | 135 | self.setGridLabel(self.curs) #刷新表格 | 136 | self.inputClean() | 137 | | 138 | def updateStu(self,event): #修改同学功能 | 139 | id = self.num.GetValue() #获取修改同学的id | 140 | update_stu = (self.name.GetValue(),self.sex.GetStringSelection(),\ | 141 | self.tel.GetValue(),self.address.GetValue(),id) #获得修改信息 | 142 | sql = "update stuinfo set name = '%s',sex = '%s',\ | 143 | phone = '%s',address = '%s' where id = '%s'" % update_stu | 144 | self.conn.execute(sql) | 145 | self.conn.commit() | 146 | self.curs.execute("select * from stuinfo") | 147 | self.setGridLabel(self.curs) | 148 | | 149 | def insertStu(self,event): #新增同学功能 | 150 | stu_name = self.name.GetValue() #获得插入的信息 | 151 | stu_sex = self.sex.GetStringSelection() | 152 | stu_phone = self.tel.GetValue() | 153 | stu_address = self.address.GetValue() | 154 | new_stu = (stu_name,stu_sex,stu_phone,stu_address) | 155 | sql = "insert into stuinfo (name,sex,phone,address) \ | 156 | values ('%s','%s','%s','%s')" % new_stu | 157 | self.curs.execute(sql) | 158 | self.conn.commit() | 159 | self.curs.execute("select * from stuinfo") | 160 | self.setGridLabel(self.curs) | 161 | | 162 | def Search(self,event): #查询同学功能 | 163 | if self.name.GetValue(): #判断查询条件 | 164 | sql = "select * from stuinfo where name = '%s' " %\ | 165 | self.name.GetValue() | 166 | else: | 167 | sql = "select * from stuinfo" | 168 | self.inputClean() | 169 | self.curs.execute(sql) | 170 | self.setGridLabel(self.curs) | 171 | | 172 | if __name__ == '__main__': | 173 | app = wx.App() #创建wx程序实例 | 174 | frame = MyFrame() #创建窗体 | 175 | frame.Show(True) #初始显示窗体 | 176 | app.MainLoop() #运行程序 |
本章小结
通过本章学习了解Python的DB-API模块的作用,以及在使用不同数据库时的操作方法,例如(1)游标的获取与使用,熟悉cursor函数;(2)不同数据库中关于插入、更新、删除的SQL语句;(3)execute函数的使用;(4)fetchall函数的使用;(5)修改数据库后及时用commit函数提交事务。此外,介绍了两种比较常用的数据库,即SQLite和MySQL,并且Python针对不同数据库,所使用的连接方法已经数据库操作方法也有所区别,需要读者区别对待。随着python发展的不断完善,目前已经几乎支持所有类型和种类的数据库,可以使用Python对任何数据库进行全面开发。

第16章 CGI编程
上一章讲到Python在数据库中的应用,其实Python在万维网程序设计中同样有重要的应用。本章将介绍Python如何在Web应用和Web服务开发的具体流程。掌握使用CGI开发网站的全流程,学习网页语言HTML,表单提交的POST和GET方法。掌握网站开发流程,需要先了解网站需求、数据库设计和代码开发等流程。
【体系结构】

【本章重点】
(1)了解CGI;
(2)掌握HTML基本知识;
(3)掌握表单提交POST和GET方法;
(4)掌握Python操作Sqlite3;
16.1 CGI介绍
CGI是通用网关接口,它是一段程序,运行在服务器上如:HTTP服务器,提供同客户端HTML页面的接口。CGI是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与Web服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的规程。CGI规范允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器,CGI将Web的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。Web 开发的最初目的是在全球范围内对文档进行存储和归档。这些零碎的信息通常产生于静态的文本或者HTML。HTML是一个文本格式而算不上是一种语言,它包括改变字体的类型、大小、风格。HTML 的主要特性在于它对超文本的兼容性,文本以一种或者是高亮的形式指向另外一个相关文档。可以通过鼠标点击或者其他用户的选择机制来访问这类文档。这些静态的HTML 文档在Web 服务器上,当有请求时,将被送到客户端。随着因特网和Web 服务器的形成,产生了处理用户输入的需求。在线零售商需要能够单独订货,网上银行和搜索引擎需要为用户分别建立帐号。因此发明了这种执行模式,并成为了Web 站点可以从用户那里获得特殊信息的唯一形式(在Java applets出现之前)。反过来,在客户提交了特定数据后,就要求立即生成HTML页面。
Web 服务器获取用户对文件的请求,将HTML文件返回给客户端。将请求生成动态HTML页面的扩展应用程序中并返回给客户端,这些还没有成为Web 服务器的职责。Web服务器从客户端接到了请求(GET或者POST),并调用合适的程序。一旦生成HTML,会将生成的动态HTML页面返回到服务器端,然后服务器端再将这个最终结果返回给客户端。客户端填写表单,提交到服务端。服务端接收到表单请求,进行下一步处理外部应用程序交互,收到并返回新生成的HTML页面都发生在一个叫做Web服务器CGI(Common Gateway Interface)的接口上。与描述了CGI 的工作原理,逐步展示了一个用户从提交表单到返回最终结果Web 页面的整个执行过程和数据流。客户端输入给Web服务器端的表单可能包括处理过程和一些存储在后台数据库中的表单。需要记住的是,在任何时候都可能有一个用户去填写这个字段,或者点击提交按钮或者图片,这更像激活了某种CGI活动。创建HTML的CGI应用程序通常是用高级编程语言来实现的,可以接受、处理数据,向服务器端返回HTML页面。目前使用的编程语言有Perl,PHP,C/C++,或者Python。
16.2 网页与HTML
16.2.1 HTML语言简介 本章讲的CGI编写网站涉及到HTML语言,本节将简单介绍HTML,方便读者理解下一节网站的初步实现。HTML是一种标记语言,全称是超文本标记语言(Hyper Text Markup Language)。HTML就是一个文本,但是比文本更厉害一点。HTML是一种建立网页文件的语言,透过标记式的指令(Tag),将影像、声音、图片、文字、动画、影视等内容显示出来。因为它可以从一个文件跳转到另一个文件,与世界各地主机的文件连接。超文本传输协议规定了浏览器在运行HTML文档时所遵循的规则和进行的操作.HTTP协议的制定使浏览器在运行超文本时有了统一的规则和标准。
【例16-1】使用html语言创建第一个网页。
问题分析:在记事本中,粘贴下面代码,文本保存first.html,代码如下: 1 | <html> | 2 | <head> | 3 | <title>第一个网页</title> | 4 | </head> | 5 | <body> | 6 | <h1>这是你的第一个网页HTML</h1> | 7 | </body> | 8 | </html> | 程序分析:1-8行<html>标签定义文档的开始点和结束点,2-4行<head>标签为网页标题,5-7行<body>标签为页面显示的内容。
用浏览器打开可看到如图16.1所示的效果。读者可以看到<head>,<body>这些东西浏览器没有显示出来,只显示了“第一个网页”和“这是你的第一个网页HTML”。所以<head>,<body>这些东西叫做HTML的标签或者元素。

图16.1 HTML页面
16.2.2 HTML标签简介
HTML语言是由标签组成的,本小节将介绍几种常用的标签。仅介绍16.3和16.4使用的标签,其他标签介绍请读者自行学习其他HTML语言学习教程,本节只是简单介绍。
<html>标签此元素可告知浏览器其自身是一个 HTML 文档。<html>与</html>标签限定了文档的开始点和结束点,在它们之间是文档的头部和主体。正如您所了解的那样,文档的头部由<head>标签定义,而主体由<body>标签定义。
<head>标签用于定义文档的头部,它是所有头部元素的容器。<head>中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等等。
<body>标签定义文档的主体。包含文档的所有内容(比如文本、超链接、图像、表格和列表等等)。
<title>标签可定义文档的标题。浏览器会以特殊的方式来使用标题,并且通常把它放置在浏览器窗口的标题栏或状态栏上。同样,当把文档加入用户的链接列表或者收藏夹或书签列表时,标题将成为该文档链接的默认名称。
<form>标签用于为用户输入创建HTML表单。表单用于向服务器传输数据。表单能够包含input元素,比如文本字段、复选框、单选框、提交按钮等等。
<input>标签用于搜集用户信息。根据不同的type属性值,输入字段拥有很多种形式。输入字段可以是文本字段、复选框、掩码后的文本控件、单选按钮、按钮等等。
<a>标签定义超链接,用于从一张页面链接到另一张页面。最重要的属性是href属性,它指示链接的目标。
<p>标签定义段落。会自动在其前后创建一些空白。浏览器会自动添加这些空间,您也可以在样式表中规定。
<h1> ~ <h6> 标签可定义标题。<h1>定义最大的标题。<h6>定义最小的标题。由于h元素拥有确切的语义,因此请您慎重地选择恰当的标签层级来构建文档的结构。
16.3 一个网站的初步实现
16.3.1 下载和安装Apache Apache HTTP Server(简称Apache),中文名:阿帕奇,是Apache软件基金会的一个开放源码的网页服务器,可以在大多数计算机操作系统中运行,由于其多平台和安全性被广泛使用,是最流行的Web服务器端软件之一。
Apacheweb服务器软件拥有以下特性:1.支持最新的HTTP/1.1通信协议。2.拥有简单而强有力的基于文件的配置过程。3.支持通用网关接口。4.支持基于IP和基于域名的虚拟主机。5.支持多种方式的HTTP认证得到Apache相关软件最直接的方法就是去访问它的网站(http://httpd.apache.org)。本书中Apache版本为httpd-2.2.25。安装过程参照官网的提示。安装完成后,后看到如图16.2所示的根目录。

图16.2 apache目录 bin文件夹内为Apache的执行文件,点击该文件夹内ApacheMonitor,可以对Apache服务进行,启动、停止和重启动等操作。 cgi-bin为python CGI 脚本放在那里,将一些HTML文件放到该目录下,就可以在浏览器地址栏中输入网址访问Web站点。如http://localhost/cgi-bin/test.py (该文件内存在test.p脚本)。 conf文件夹内文件为Apache Web 站点配置文件,通过修改其中的文件修改Apache 服务器内容。 htdocs 文件夹内为Apache官方说明文档,通过它可以更加深入了解Apache和查看源码以及函数解释等。
修改Apache 配置文件,用记事本打开Apache程序根目录下的conf文件夹内的httpd.conf文件,找“AddHandler cgi-script .cgi”改为“AddHandler cgi-script .cgi .py”,如果没有,请添加为如图所示,粗体为添加文字。配置文件修改完成。 1 | <Directory "D:/SoftWare/Apache2.2/cgi-bin"> | 2 | AllowOverride None | 3 | Options None | 4 | Order allow,deny | 5 | Allow from all | 6 | </Directory> | 7 | AddHandler cgi-script .cgi .py |

16.3.2 第一个CGI程序
使用Python创建第一个CGI程序,文件名为hello.py,文件位于Apache安装目录cgi-bin文件夹中,内容如下
【例16-2】使用python语言创建第一个CGI程序。 问题分析:使用html语言。 1 | #coding=utf-8 | 2 | print "Content-type: text/html" | 3 | print "" | 4 | print "<html><head></head><body>" | 5 | print "Hello World" | 6 | print "</body></html>" | 程序分析:这段代码中有两个地方值得注意。第一点是代码中第1行#coding=utf-8,表示文件的编码方式为utf-8,支持中文。如果在Python中并没有声明别的编码方式,就是以ASCII编码作为标准编码方式的。第二点是代码中第2行,表示发送到浏览器并告知浏览器显示的内容类型为“text/html”。2-4行为html代码(html可以参照网上学习资料,进行学习)。在浏览器地址栏中输入http://localhost/cgi-bin/hello.py。看到图所16.3示“Hello World”页面。完成cgi第一个程序的编写。

图16.3 helloworld显示结果
16.3.3 GET和POST方法
HTTP 定义了与服务器交互的不同方法,最基本的方法是 GET 和 POST。GET方法向特定的资源发出请求。注意:GET方法不应当被用于产生“副作用”的操作中,例如在Web Application中。其中一个原因是GET可能会被网络爬虫等随意访问。POST方法向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
使用GET的时候,参数会显示在地址栏上,而POST不会。所以,如果这些数据是中文数据而且是非敏感数据,那么使用GET;如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用POST为好。表单提交中GET和POST方式的区别归纳如下几点:
(1)GET是从服务器上获取数据,POST是向服务器传送数据。
(2)GET是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。POST是通过HTTP POST机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。对于GET方式,服务器端用Request.QueryString获取变量的值,对于POST方式,服务器端用Request.Form获取提交的数据。
(3)GET传送的数据量较小,不能大于2KB。POST传送的数据量较大,一般被默认为不受限制。
(4)GET安全性非常低,POST安全性较高。
16.3.4 表单提交
在FORM提交的时候,如果不指定Method,则默认为GET请求,Form中提交的数据将会附加在url之后,以?分割URL和传输数据,参数之间以&相连。GET请求请提交的数据放置在HTTP请求协议头中,而POST提交的数据则放在实体数据中; GET方式提交的数据最多只能有1024字节,而POST则没有此限制 。POST传递的参数在doc里,也就http协议所传递的文本,接受时再解析参数部分。获得参数。一般用POST比较好。POST提交数据是隐式的,GET是通过在url里面传递的,用来传递一些不需要保密的数据,GET是通过在URL里传递参数,而POST不是。
简单的url实例:GET方法。以下是一个简单的URL,使用GET方法向helloworld_get.py程序发送两个参数:
1 | http://localhost/cgi-bin/helloworld.py?username=hello&password=world |
简单的url实例:POST方法。以下是一个简单的URL,使用GET方法向helloworld_get.py程序发送两个参数,请注意,查询字符串(名称/值对)是在 POST 请求的 HTTP 消息主体中发送的: 1 | POST cgi-bin/helloworld.py HTTP/1.1 | 2 | Host: localhost | 3 | username=hello&password=world |
【例16-3】使用python语言创建表单参数获取程序。
问题分析:helloworld.py文件的处理GET参数代码,将helloworld.py放在apache安装目录cgi-bin文件夹中。 1 | #coding=utf-8 | 2 | import cgi, cgitb | 3 | form = cgi.FieldStorage() | 4 | username = form.getvalue('username') #获取form表单usrname参数 | 5 | password = form.getvalue('password') #获取form表单password参数 | 6 | | 7 | print "Content-type:text/html\r\n\r\n" | 8 | print "<html>" | 9 | print "<head>" | 10 | print "<title>Welcome to use CGI</title>" #浏览器标题 | 11 | print "</head>" | 12 | print "<body>" | 13 | print "<h2>Welcome Registration Successful your username is %s,\ | 14 | password is %s </h2>" % (username, password) #浏览器显示具体内容 | 15 | print "</body>" | 16 | print "</html>" |
程序分析:1行定义python编码类型为utf-8,2行导入cgi,cgitb。cgitb为cgi调试库。3-5行获取form表单中的username和password的内容。7-16行打印html内容,浏览器解析相应内容并显示。在浏览器地址栏中输入http://localhost/cgi-bin/helloworld.py?username=hello&password=world。看到图16.4所示“Welcome Registration Successful your username is hello,password is world”页面。

图16.4 GET方法显示页面
【例16-4】简单的表单实例:GET方法。
问题分析:以下是一个通过HTML的表单使用GET方法向服务器发送两个数据,提交的服务器脚本同样是helloworld.py文件,helloworld_register_get.py(与helloworld.py放在同一个文件夹下)代码如下: 1 | #coding=utf-8 | 2 | print "Content-type:text/html\r\n\r\n" | 3 | print "<html>" | 4 | print "<head>" | 5 | print "<title>Welcome to use CGI</title>" | 6 | print "</head>" | 7 | print "<body>" | 8 | print "<form action='/cgi-bin/helloworld.py' method='get'>" | 9 | print "username: <input type='text' name='username'>" #用户名输入框 | 10 | print "password: <input type='text' name='password'>" #密码输入框 | 11 | print "<input type='submit'value='Submit' />" #表单提交按钮 | 12 | print "</form>" | 13 | print "</body>" | 14 | print "</html>" |
程序分析:1行定义python编码类型为utf-8,2-14行打印html内容,浏览器解析相应内容并显示,8-12form表单的html内容,action为提交表单的url,username和password本文输入框和提交按钮,读者点击按钮,即可通过GET方式提交表单到action='/cgi-bin/helloworld.py',helloworld.py为获取form表单数据,进行相应的下一步操作。
在浏览器地址栏中输入http://localhost/cgi-bin/helloworld_register_get.py。看到如图16.5所示的页面,如在username中输入“hello”,在password中输入“world”,点击submit按钮,出现图16.6所示的页面,即完成通过GET方式提交用户注册的用户名和密码,完成注册功能。
图16.5和图16.6显示浏览器地址栏中url http://localhost/cgi-bin/helloworld.py?username=hello&password=world可以看到需要传送的参数usename和password的内容。

图16.5 表单GET方法填写表单页面

图16.6表单GET方法提交成功页面

【例16-5】简单的表单实例:POST方法。
问题分析: 使用POST方法向服务器传递数据是更安全可靠的,像一些敏感信息如用户密码等需要使用POST传输数据。与GET方法提交表单相比,只需要改变客户端代码的表单提交方式为POST即可,服务端helloworld.py代码无需改变。
创建helloworld_register_post.py文件,代码如下:
1 | #coding=utf-8 | 2 | print "Content-type:text/html\r\n\r\n" | 3 | print "<html>" | 4 | print "<head>" | 5 | print "<title>Welcome to use CGI</title>" | 6 | print "</head>" | 7 | print "<body>" | 8 | print "<form action='/cgi-bin/helloworld.py' method='post'>" | 9 | print "username: <input type='text' name='username'>" #用户名输入框 | 10 | print "password: <input type='text' name='password'>" #密码输入框 | 11 | print "<input type='submit'value='Submit' />" #表单提交按钮 | 12 | print "</form>" | 13 | print "</body>" | 14 | print "</html>" |
程序分析:1行定义python编码类型为utf-8, 2-14行打印html内容,浏览器解析相应内容并显示,8-12rm表单的html内容,action为提交表单的url,提交方式为POST,username和password文本输入框和提交按钮,读者点击按钮,即可通过POST方式提交表单到action='/cgi-bin/helloworld.py',helloworld.py为获取form表单数据,进行相应的下一步操作。
在浏览器地址栏中输入http://localhost/cgi-bin/helloworld_register_post.py 。看到如图16.7所示的页面,如在username中输入“hello”,在password中输入“world”,点击submit按钮,出现图16.8所示的页面,即完成通过POST方式提交用户注册的用户名和密码,完成注册功能。图16.8显示浏览器地址栏中显示url http://localhost/cgi-bin/helloworld.py没有看到传送的参数usename和password的内容,这个与GET方法不同地方。

图16.7表单POST方法填写表单页面

图16.8表单POST方法提交成功页面
16.3.5 cgitb调试
有时候编写程序的过程中,经常会遇到事件运行异常的情况。这种异常的情况可能会造成程序运行结果的错误,甚至程序的崩溃。上一节编写CGI程序时,如果程序发生错误时,根本无法看到发生错误的原因和具体哪一行发生错误。Python 2.7的标准库中增加cgitb(用于CGI回溯)的模块,导入它并且调用它的enable函数,读者可以看到包含错误信息的网页,从错误信息可以了解到程序哪里发生错误,修改相应地方,得到正确的程序。
【例16-6】cgitb调试程序。
问题分析:创建cgitb _error.py文件,代码如下: 1 | #coding=utf-8 | 2 | import cgitb | 3 | cgitb.enable() | 4 | | 5 | print 1/0 #除0错误信息 |
程序分析:在浏览器地址栏内输入http://localhost/cgi-bin/cgitb_error.py ,可以看到图16.9所示的页面。可以看到错误原因是exceptions.ZeroDivisionError,由于第5行1/0错误,程序中当遇到0为除数时,程序会终止运行,并报出一个“ZeroDivisionError”的错误。修改错误内容,程序即可运行。

图16.9 cgitb除 0错误信息

图16.9所示错误信息页面,最上面“<type 'exceptions.ZeroDivisionError'>”为错误类型。下面“D:\SoftWare\Apache2.2\cgi-bin\ cgitb_error.py”为错误文件存储位置。“=>”标记为错误所在的行数。最下面“<type 'exceptions.ZeroDivisionError'>: integer division or modulo by zero ,args =('integer division or modulo by zero',) ,message = 'integer division or modulo by zero'”为错误的具体解释信息。需要找到py文件,根据错误信息修改相应行数的代码即可。
16.4 个人信息管理系统 上一节两个例子了解到表单提交的GET,POST提交方式,本节将在上一小节的基础上,通过CGI完成个人信息管理系统。在日常生活办公中有很多个人数据,如朋友电话,邮件地址,日程安排,日常记事。这些都可以用个人信息管理系统进行管理和查看。本节将搭建个人信息系统,通过完成个人信息管理系统,加深对CGI和上一章数据库的理解,以及对网站内容的了解。
16.4.1 网站需求说明
(1)登陆与注册
系统的登陆与注册。通过用户名和密码登录系统,注册只包括用户名和密码。
(2)个人基本信息管理模块
系统将对个人信息的管理进行管理,用户姓名、出生日期、性别、民族、学历、登陆名、密码、家庭住址、电话等。 用户可以查看个人信息:登录名,密码、用户姓名、出生日期、性别、家庭住址、电话等信息。 用户可以修改个人信息:登录名,密码、用户姓名、出生日期、性别、民族、家庭住址、电话等信息。
(3)日程安排模块
日程安排模块记录用户的活动安排或者其他有关事项,添加从某一时间到另一时间要做的事情,日程标题,内容,开始时间、结束时间。可以自由查询、修改、删除。
16.4.2 项目数据库设计
上一章已经学过Sqlite3,本章将用Sqlite3创建数据库创建个人信息管理系统用户的数据库和user数据表(字段如表16.1所示用户登录名、用户密码、用户真实姓名、性别、出生日期、电话、用户邮箱、用户地址)。
表16-1 用户表(user) 字段名称 | 字段类型 | 字段长度 | 字段说明 | username | varchar | 30 | 用户登录名 | password | varchar | 30 | 用户密码 | name | varchar | 30 | 用户真实姓名 | sex | varchar | 2 | 性别 | birth | varchar | 10 | 出生日期 | phone | varchar | 20 | 电话 | email | varchar | 30 | 用户邮箱 | address | varchar | 30 | 用户地址 |

图16.11 个人信息管理流程图
执行以下代码创建sqlite3数据库PersonalInformationManagement.db和数据表USER,完成数据库创建。
1 | #coding=utf-8 | 2 | import sqlite3 | 3 | conn = sqlite3.connect(r'd:\book\PersonalInformationManagement.db') | 4 | curs = conn.cursor() #获得游标 | 5 | curs.execute(''' | 6 | CREATE TABLE USER( | 7 | sid INTEGER PRIMARY KEY AUTOINCREMENT, | 8 | username VARCHAR(30), | 9 | password VARCHAR(30), | 10 | name VARCHAR(30) NULL, | 11 | sex VARCHAR(2) NULL, | 12 | birth VARCHAR(10) NULL, | 13 | phone VARCHAR(20) NULL, | 14 | email VARCHAR(30) NULL, | 15 | address VARCHAR(30) NULL | 16 | ) | 17 | ''') | 18 | conn.commit() #事务提交 | 19 | conn.close() |
程序分析:1行定义python编码类型为utf-8,3-4行获取数据库连接和游标,5-17行执行创建表USER语句,18行修改数据库后一定要事务提交。19行关闭数据库。
16.4.3 用户注册模块
用户注册分为用户注册信息界面和注册信息确认两个功能块。通过之前学习的CGI,将用户注册信息界面和注册信息确认写在两个py文件中。
用户注册有两点要求,(1)用户名与已经存在的用户名不能相同。(2)“password”与“confirm password”相同。用户注册确认页面就是依据这两条逻辑。

图16.11 个人信息管理用户注册模块流程图
用户注册是一个网站基本功能,本小节先讲用户注册的流程和实现方式。按照上面的流程图,读者在浏览器地址栏中输入http://localhost/cgi-bin/user_register.py ,显示图16.12所示的用户界面,读者可以看到username和password,confirm password本文输入框和提交按钮,填写相应信息,点击提交按钮,依据用户注册两点要求依次进行判断,先判断用户名是否存在,如存在显示图16.12用户注册用户名存在页面,结束。若不存在,判断password和confirmpassword是否相同,如不同显示图16.14用户注册password和confirm password不同页面,结束。若相同显示图16.15用户注册注册成功返回页面,显示注册成功,结束。

图16.12用户注册表单提交页面

图16.13用户注册用户名存在返回页面

图16.14用户注册password和confirm password不同返回页面

图16.15用户注册注册成功返回页面
用户注册的html页面,显示用户注册填写的表单username、password和confirmpassword的输入框。创建user_register.py文件,代码如下:
1 | #coding=utf-8 | 2 | register_html = '''<html> | 3 | <head> | 4 | <title>registion</title> #浏览器显示标题 | 5 | </head> | 6 | <body> | 7 | <h1>user register</h1> | 8 | <form action='/cgi-bin/user_register_confirm.py' method='post'> #form表单 | 9 | username: <input type='text' name='username'> <br> #用户名输入框 | 10 | password: <input type='password' name='password'><br>#密码输入框 | 11 | confirm password:<input type='password'name='confirmpassword'><br> | 12 | <input type='submit'value='Register' /> #表单提交按钮 | 13 | </form> | 14 | </body> | 15 | </html>''' | 16 | print "Content-type:text/html\r\n\r\n" | 17 | print register_html |
程序分析:1行定义python编码类型为utf-8, 2-15行打印html内容,浏览器解析相应内容并显示,8-13行表单的html内容,action为提交表单的url,提交方式为POST,username和password,confirm password本文输入框和提交按钮,读者点击按钮,即可通过POST方式提交表单到action='/cgi-bin/user_register_confirm.py ',user_register_confirm.py为获取form表单数据,对表单数据进行下一步处理。
获取用户注册信息username、password和confirmpassword参数,完成用户注册功能。创建user_register_confirm.py文件,代码如下:
1 | #coding=utf-8 | 2 | import cgi,cgitb | 3 | import sqlite3 | 4 | cgitb.enable() | 5 | register_confirm_html = '''<html> #注册成功显示的html代码 | 6 | <head> | 7 | <title>registion</title> | 8 | </head> | 9 | <body> | 10 | <h2>%s</h2> | 11 | </body> | 12 | </html>''' | 13 | | 14 | def process(): | 15 | form = cgi.FieldStorage() | 16 | username = form.getvalue('username') #获取表单username参数 | 17 | password = form.getvalue('password') #获取表单password参数 | 18 | confirmpassword = form.getvalue('confirmpassword') #获取confirmpassword参数 | 19 | conn = sqlite3.connect(r'd:\book\PersonalInformationManagement.db') | 20 | curs = conn.cursor() | 21 | query = "SELECT * FROM USER WHERE username = '%s'" %(username) | 22 | curs.execute(query) | 23 | userset = curs.fetchall() | 24 | if len(userset)>0: #判断符合用户是否存在 | 25 | ret_text = "the usename already exists" #返回用户名存在信息 | 26 | elif password!=confirmpassword: | 27 | ret_text = "the password and the confirm password don't same"#返回密码 | 28 | else: #和确认密码不一致错误信息 | 29 | insert_sql = "INSERT INTO USER (username,password) \ | 30 | VALUES ('%s','%s')"%(username,password) | 31 | curs.execute(insert_sql) | 32 | conn.commit() | 33 | ret_text = "you register successfully" | 34 | conn.close() | 35 | print "Content-type:text/html\r\n\r\n" | 36 | print register_confirm_html % (ret_text) #打印浏览器显示html代码 | 37 | | 38 | if __name__ == '__main__': | 39 | process() |
程序分析:1行定义python编码类型为utf-8,2-3行引入cgi,cgitb 和sqlite3,4行打开cgitb 调试功能, 5-12行返回提示信息html内容,浏览器解析相应内容并显示,14-36为注册信息处理函数,16-18行获取表单username,password和confirmpassword三个参数,19-20行获取数据库连接和游标,21-23行过fetchall函数获取所有查询记录,userset是一个列表,记录以元组形式存在列表中。 24-33行根据注册要求进行判断,返回不同的提示信息。29-32行构建insert_sql,添加用户数据,并提交。
16.4.4 用户登录
用户登陆要求是,用户名、密码与存在的用户的用户名,密码均相同,登陆成功。用户登录确认就是遵照此逻辑。

图16.16 个人信息管理用户登陆模块流程图
用户登陆是一个网站基本功能,本小节先讲用户登陆的流程和实现方式。按照上面的流程图,读者在浏览器地址栏中输入http://localhost/cgi-bin/user_login.py ,显示图16.17所示的用户登陆界面,读者可以看到username和password本文输入框和提交按钮,填写相应信息,点击提交按钮,依据用户登陆要求依次进行判断,判断用户名和密码是否与已存在列表进行判断,如不匹配显示图16.18用用户登陆用户名、密码错误在页面,结束。若匹配显示图16.19用户登陆登陆成功页面,显示登陆成功,读者可以自行添加下一步操作结束。

图16.17用户登陆表单提交页面

图16.18用户登陆用户名、密码错误返回页面

图16.19用户登陆登陆成功页面
用户登录界面,显示信息由用户名输入框和密码输入框,以及登陆按钮。创建user_login.py文件,代码如下:
1 | #coding=utf-8 | 2 | login_html = '''<html> | 3 | <head> | 4 | <title>login</title> | 5 | </head> | 6 | <body> | 7 | <h1>user login</h1> | 8 | <form action='/cgi-bin/user_login_confirm.py' method='post'>#form表单 | 9 | username: <input type='text' name='username'> <br>#用户名输入框 | 10 | password: <input type='password' name='password'><br> #密码输入框 | 11 | <input type='submit'value='Login' /> | 12 | </form> | 13 | </body> | 14 | </html>''' | 15 | print "Content-type:text/html\r\n\r\n" | 16 | print login_html #打印浏览器显示登陆页面html代码 |
程序分析:1行定义python编码类型为utf-8, 2-14行打印html内容,浏览器解析相应内容并显示,8-12行表单的html内容,action为提交表单的url,提交方式为POST,username和password本文输入框和提交按钮,读者点击按钮,即可通过POST方式提交表单到action='/cgi-bin/ user_login_confirm.py ',user_login_confirm.py为获取form表单数据,对表单数据进行下一步处理。
获取用户登录的用户名和密码参数,根据用户名和密码查询数据库,获取相应信息,并返回登陆成功信息或用户名、密码错误信息显示页面。创建user_login_confirm.py文件,代码如下:
1 | #coding=utf-8 | 2 | import cgi,cgitb | 3 | import sqlite3 | 4 | cgitb.enable() | 5 | | 6 | login_confirm_html = '''<html> #登陆成功显示html代码 | 7 | <head> | 8 | <title>login</title> | 9 | </head> | 10 | <body> | 11 | <h2>%s</h2> | 12 | </body> | 13 | </html>''' | 14 | | 15 | def process(): | 16 | form = cgi.FieldStorage() | 17 | username = form.getvalue('username') #获取表单username参数 | 18 | password = form.getvalue('password') #获取表单password参数 | 19 | conn = sqlite3.connect(r'd:\book\PersonalInformationManagement.db') | 20 | curs = conn.cursor() | 21 | query = "SELECT * FROM USER WHERE username = '%s' and password= \ | 22 | '%s'" % (username,password) #查询USER表用户名为username的sql语句 | 23 | curs.execute(query) #数据库执行query的sql语句 | 24 | userset = curs.fetchone() #获取数据条目语句 | 25 | if userset != None: | 26 | ret_text = "<a href='/cgi-bin/user_info_look.py?username=%s'>\ | 27 | login successfully,Click the jump</a>"%(userset[1]) #返回登陆成功信息 | 28 | else: | 29 | ret_text = "username or password is wrong" #返回用户或密码错误信息 | 30 | print "Content-type:text/html\r\n\r\n" | 31 | print login_confirm_html % (ret_text) #打印浏览器显示登陆页面html代码 | 32 | conn.close() | 33 | | 34 | if __name__ == '__main__': | 35 | process() |
程序分析:1行定义python编码类型为utf-8,2-3行引入cgi和 cgitb ,4行打开cgitb 调试功能,6行为存在用户名和密码的列表,7-14行返回提示信息html内容,浏览器解析相应内容并显示,16-25为注册信息处理函数,17-19行获取表单username和password两个参数,20-23行根据注册要求进行判断,返回不同的提示信息。
16.4.5 个人信息查看 用户可以查看个人信息:登录名、密码、用户姓名、出生日期、性别、家庭住址、电话等信息。

图16.20查看个人信息页面
根据用户名通过数据库查询语句,获取用户的个人信息。个人信息显示以及相关html代码。创建user_info_look.py文件,代码如下:
1 | #coding=utf-8 | 2 | import cgi,cgitb | 3 | import sqlite3 | 4 | cgitb.enable() | 5 | | 6 | user_look_html = '''<html> | 7 | <head> | 8 | <title>user</title> | 9 | </head> | 10 | <body> | 11 | <table> | 12 | <tr> | 13 | <td> | 14 | <a href='/cgi-bin/user_info_look.py?username=%s'>look person info</a> | 15 | </td> #查看个人信息的<a>标签 | 16 | <td> | 17 | <a href='/cgi-bin/user_info_modify.py?username=%s'>modify person info</a> | 18 | </td> #修改个人信息的<a>标签 | 19 | </tr> | 20 | </table> | 21 | <table bgcolor='#95BDFF'> #表格标签,bgcolor为表格背景颜色属性 | 22 | <tr> | 23 | <td width="100px">username</td> #显示用户名 | 24 | <td width="100px">%s</td> | 25 | </tr> | 26 | <tr> | 27 | <td >password</td> #显示密码 | 28 | <td >%s</td> | 29 | </tr> | 30 | <tr> | 31 | <td >name</td> #显示用户姓名 | 32 | <td >%s</td> | 33 | </tr> | 34 | <tr> | 35 | <td >sex</td> #显示用户性别 | 36 | <td >%s</td> | 37 | </tr> | 38 | <tr> | 39 | <td >birth</td> #显示用户出生日期 | 40 | <td >%s</td> | 41 | </tr> | 42 | <tr> | 43 | <td >phone</td> #显示用户电话 | 44 | <td >%s</td> | 45 | </tr> | 46 | <tr> | 47 | <td >email</td> #显示用户邮箱 | 48 | <td >%s</td> | 49 | </tr> | 50 | <tr> | 51 | <td >address</td> #显示用户地址 | 52 | <td >%s</td> | 53 | </tr> | 54 | </table> | 55 | </body> | 56 | </html>''' | 57 | | 58 | user_error_html = '''<html> | 59 | <head> | 60 | <title>user</title> | 61 | </head> | 62 | <body> | 63 | user do not exits. | 64 | </body> | 65 | </html>''' | 66 | | 67 | def process(): | 68 | form = cgi.FieldStorage() | 69 | username = form.getvalue('username') #获取form表单username参数 | 70 | conn = sqlite3.connect(r'd:\book\PersonalInformationManagement.db') | 71 | curs = conn.cursor() #获取数据库游标 | 72 | query = "SELECT * FROM USER WHERE username = '%s'" %(username) | 73 | curs.execute(query) | 74 | user = curs.fetchone() #获取用户数据 | 75 | if user !=None: | 76 | print 'Content-type:text/html\r\n\r\n' | 77 | print user_look_html %(user[1],user[1],user[1],user[2],user[3], | 78 | user[4],user[5],user[6],user[7],user[8]) | 79 | else: | 80 | print 'Content-type:text/html\r\n\r\n' | 81 | print user_error_html #打印用户名错误信息页面 | 82 | conn.close() | 83 | | 84 | if __name__ == '__main__': | 85 | process() |
程序分析:1行定义python编码类型为utf-8,2-3行引入cgi和 cgitb ,4行打开cgitb 调试功能。6-56行为用户个人信息查看的html源码,11-20行为“个人信息管理的导航栏”,“个人信息查看”和“个人信息修改”的两个链接,点击进入。21-54行显示个人信息登录名、密码、用户姓名、出生日期、性别、家庭住址、电话等的table。58-65行为用户名错误,返回的错误提示页面,防止修改他人信息。67-82行为处理的python程序。68-69行获取GET方法中username参数,以确定修改用户名的username。70-74行连接sqlite3数据库,查询用户名是否存在,获取用户数据。75行通过判断user是否等于None,来判断USER表中是否存有该数据,如不为None执行76-78行代码,否则执行80-81行代码。76-78行返回html内容,浏览器解析相应内容并显示用户个人信息页面。80-81行返回用户不存在提示信息,浏览器解析相应内容并显示。82行关闭数据库连接
16.4.6 个人信息修改
用户可以修改个人信息:密码、用户姓名、出生日期、性别、家庭住址、电话等信息。

图16.21修改个人信息页面
根据用户名查询数据库获取用户个人相关信息,创建用户信息修改的输入框的html代码,进行相关信息修改。创建user_info_modify.py文件,代码如下:
1 | #coding=utf-8 | 2 | import cgi,cgitb | 3 | import sqlite3 | 4 | cgitb.enable() | 5 | | 6 | user_look_html = '''<html> | 7 | <head> | 8 | <title>user</title> | 9 | </head> | 10 | <body> | 11 | <table> | 12 | <tr> | 13 | <td> | 14 | <a href='/cgi-bin/user_info_look.py?username=%s'> | 15 | look person info</a> | 16 | </td> #查看个人信息<a>标签 | 17 | <td> | 18 | <a href='/cgi-bin/user_info_modify.py?username=%s'> | 19 | modify person info</a> | 20 | </td> #修改个人信息<a>标签 | 21 | </tr> | 22 | </table> | 23 | <form action='/cgi-bin/user_info_modify_confirm.py?username=%s' | 24 | method='post'> | 25 | <table bgcolor='#95BDFF'> #表格标签,bgcolor为表格背景颜色属性 | 26 | <tr> | 27 | <td width="100px">password</td> #密码信息修改框 | 28 | <td width="100px"><input name='password' value='%s'> | 29 | </input></td> | 30 | </tr> | 31 | <tr> | 32 | <td >name</td> #用户姓名信息修改框 | 33 | <td><input name='name' value='%s'></input></td> | 34 | </tr> | 35 | <tr> | 36 | <td >sex</td> #用户性别信息修改框 | 37 | <td><input name='sex' value='%s'></input></td> | 38 | </tr> | 39 | <tr> | 40 | <td >birth</td> #用户出生年月信息修改框 | 41 | <td><input name='birth' value='%s'></input></td> | 42 | </tr> | 43 | <tr> | 44 | <td >phone</td> | 45 | <td><input name='phone' value='%s'></input></td> | 46 | </tr> | 47 | <tr> | 48 | <td >email</td> #用户邮箱信息修改框 | 49 | <td><input name='email' value='%s'></input></td> | 50 | </tr> | 51 | <tr> | 52 | <td >address</td> #用户地址信息修改框 | 53 | <td><input name='address' value='%s'></input></td> | 54 | </tr> | 55 | </table> | 56 | </form> | 57 | </body> | 58 | </html>''' | 59 | | 60 | user_error_html = '''<html> | 61 | <head> | 62 | <title>user</title> | 63 | </head> | 64 | <body> | 65 | user do not exits. | 66 | </body> | 67 | </html>''' | 68 | | 69 | def process(): | 70 | form = cgi.FieldStorage() | 71 | username = form.getvalue('username')#获取form表单username参数 | 72 | password = form.getvalue('password')#获取form表单password参数 | 73 | conn = sqlite3.connect(\ | 74 | r'd:\book\PersonalInformationManagement.db') | 75 | curs = conn.cursor() #获取数据库游标 | 76 | query = "SELECT * FROM USER WHERE username = '%s'"%(username) | 77 | curs.execute(query) #执行query查询sql语句 | 78 | user = curs.fetchone() | 79 | if user !=None: | 80 | print 'Content-type:text/html\r\n\r\n' | 81 | print user_modify_html %(user[1],user[1],user[1],\ | 82 | user[2],user[3],user[4],user[5],user[6],user[7],user[8]) | 83 | else: | 84 | print 'Content-type:text/html\r\n\r\n' | 85 | print user_error_html | 86 | conn.close() | 87 | | 88 | if __name__ == '__main__': | 89 | process() |
程序分析:1行定义python编码类型为utf-8,2-3行引入cgi和 cgitb ,4行打开cgitb调试功能。6-67行为用户个人信息查看的html源码,11-22行为“个人信息管理的导航栏”,“个人信息查看”和“个人信息修改”的两个链接,点击进入。21-56行修改个人信息密码、用户姓名、出生日期、性别、家庭住址、电话等的table的form表单。60-67行为用户名错误,返回的错误提示页面,防止修改他人信息。69-86行为处理的python程序。71-72行获取GET方法中username参数,以确定修改用户名的username。73-78行连接sqlite3数据库,查询用户名是否存在,获取用户数据。79行通过判断user是否等于None,来判断USER表中是否存有该数据,如不为None执行80-82行代码,否则执行84-85行代码。80-82行返回html内容,浏览器解析相应内容并显示修改用户个人信息form表单页面。84-85行返回用户不存在提示信息,浏览器解析相应内容并显示。86行关闭数据库连接。
获取表单的个人信息的密码、用户姓名、出生日期、性别、家庭住址、电话,通过用户名更新该用户的数据库条目信息。创建user_info_modify_confirm.py文件,代码如下:
1 | #coding=utf-8 | 2 | import cgi,cgitb | 3 | import sqlite3 | 4 | cgitb.enable() | 5 | | 6 | modify_confirm_html = '''<html> #个人信息修改成功html代码 | 7 | <head> | 8 | <title>user</title> | 9 | </head> | 10 | <body> | 11 | <h2>modify successs! | 12 | <a href='/cgi-bin/user_info_look.py?username=%s'>Click here to homepage </a> | 13 | </h2> #回到首页的链接 | 14 | </body> | 15 | </html>''' | 16 | | 17 | def process(): | 18 | form = cgi.FieldStorage() | 19 | username = form.getvalue('username') #获取form表单username参数 | 20 | password = form.getvalue('password') #获取form表单 password参数 | 21 | name = form.getvalue('name') #获取form表单name参数 | 22 | sex = form.getvalue('sex') #获取form表单sex参数 | 23 | birth = form.getvalue('birth') #获取form表单birth参数 | 24 | phone = form.getvalue('phone') #获取form表单phone参数 | 25 | email = form.getvalue('email') #获取form表单email参数 | 26 | address = form.getvalue('address') #获取form表单address参数 | 27 | | 28 | conn = sqlite3.connect(r'd:\book\PersonalInformationManagement.db') | 29 | curs = conn.cursor() #获取数据库游标 | 30 | sql_update = '''update USER set password = '%s', | 31 | name ='%s', | 32 | sex ='%s', | 33 | birth ='%s', | 34 | phone ='%s', | 35 | email ='%s', | 36 | address ='%s' | 37 | where username = '%s' | 38 | ''' %(password,name,sex,birth,phone,email,address,username) | 39 | curs.execute(sql_update) #执行sql_update的update sql语句 | 40 | conn.commit() #数据库提交 | 41 | conn.close() #关闭数据库 | 42 | print 'Content-type:text/html\r\n\r\n' | 43 | print modify_confirm_html % (username) | 44 | | 45 | | 46 | if __name__ == '__main__': | 47 | process() |
程序分析:1行定义python编码类型为utf-8,2-3行引入cgi和 cgitb ,4行打开cgitb 调试功能。6-15行为显示个人信息修改成功的html源码。 17-43行为处理的python程序。18-26行获取form中用户名、密码、用户姓名、出生日期、性别、家庭住址、电话信息,通过username获取USER表中该条数据,并将密码、用户姓名、出生日期、性别、家庭住址、电话信息进行一一赋值进行修改。28-40行连接sqlite3数据库,通过username和form表单数据,拼接sql_update语句,并执行。42-43行返回html内容,浏览器解析相应内容并显示修改用户个人信息修改成功界面。
本章总结
本章第一部分介绍了CGI和学习了html的一些基础知识。了解到CGI在web技术上的重要作用,编写了第一个html页面,了解html语言的标签<html>、<head>、<body>、<title>、<form>、<input>、<a>、<p>、<h1> - <h6>的功能,加深对html语言的理解。 本章第二部分介绍网站初步搭建,读者了解到如何通过CGI和python搭建网站的过程。了解Apache服务器,下载和安装Apache。编写python语言实现第一个CGI程序,掌握python语言搭建网站的基本流程。了解web表单GET和POST方法,掌握GET和POST的实现方式和两者的不同点,掌握什么时候采用哪种方式。掌握CGI的调试方法cgitb。 本章第三部分为搭建个人信息管理系统了解到一个网站的注册和登陆机制,如何创建用户和使用用户名和密码登录以及数据库的使用。在个人信息查看和修改小节中,学会了如何使用html table标签显示数据和大批量修改数据。相信细心的读者会发现16.4节个人信息管理系统还不够完善,需要进一步修改。在个人信息中,读者可以增加通讯录管理模块、日程安排管理模块、个人文件管理模块等。通讯录管理模块增加同学和朋友之间的信息,方便查询。日程管理模块可以将平时需要做的事情添加进去。

附录A 比较Python 2和Python 3 Python目前有两个比较主流的版本,Python 3.0及之后的版本称为Python 3,而之前的版本则称之为Python 2。本书主要描述Python 2(Python 2.7)版本定义的语言。现在Python最新的版本是Python 3.4.2。Python 3并不是和之前的版本完全不同,大多数函数和语句的工作方式与Python 2完全相同,但由于Python 3的设计理念不支持向下兼容,也就意味着有些Python 2的代码无法使用Python 3的解释器运行。 本附录将介绍Python 2与Python 3之间的主要区别,并对将Python 2代码迁移到Python 3代码提供一些帮助和建议。
(1)future 模块 Python 3中一些Python 2不兼容的关键字和特性可以通过从Python 2的内置__future__模块导入。所以如果需要编写支持Python 3的代码,建议使用__future__模块。例如,如果想要在Python 2中表现 Python 3中的整除,可以通过如下方式导入整除函数 from __future__ import division | __future__模块中其它可被导入的特性和函数列与下表中: 特性 | 效果 | nested_scopes | PEP 227: Statically Nested Scopes | generators | PEP 255: Simple Generators | division | PEP 238: Changing the Division Operator | absolute_import | PEP 328: Imports: Multi-Line and Absolute/Relative | with_statement | PEP 343: The “with” Statement | print_function | PEP 3105: Make print a function | unicode_literals | PEP 3112: Bytes literals in Python 3000 | 如果想要了解更加全面的future模块信息,可以查阅Python中future模块的官方主页:https://docs.python.org/2/library/future.html。
(2)print函数
print语法的变化可能是最广为人知的,在Python 2的print语句在Python 3中已经被print()函数取代,这意味着用于打印的对象必须被小括号包围。 如果使用Python 2的方式使用print,在Python 3中会抛出一个语法异常(SyntaxError)。
Python 2: 1 | from platform import python_version | 2 | print 'Python', python_version() | 3 | print 'Hello, World!' | 4 | print('Hello, World!') | 5 | print "text", ; print 'print more text on the same line' | 运行结果: Python 2.7.8 | Hello, World! | Hello, World! |

Python 3: 1 | print('Python', python_version()) | 2 | print('Hello, World!') | 3 | | 4 | print("some text,", end="") | 5 | print(' print more text on the same line') | 运行结果: Python 3.4.1 | Hello, World! | some text, print more text on the same line |

1 | print 'Hello, World!' |
运行结果:
File "<ipython-input-3-139a7c5835bd>", line 1 print 'Hello, World!' ^SyntaxError: invalid syntax |
(3)除法
除法运算的变化对于代码迁移是十分危险的,因为整除的变化会被认为是正常的语句而不会抛出语法错误。在Python 3中除法运算符总是返回浮点数,而在Python 2中会根据除数和被除数的类型决定返回值类型。可以通过在Python 2的代码中导入__future__模块的division获取该特性: 1 | from __future__ import division |
(4)字符串
在Python 2两种字符串类型:ASCII的str类型和Unicode类型。而在Python 3中只有一种类型,即Unicode类型;Python 2中的str类型在Python 3中属于bytearray类型。 例如Python 2中的"abc"相当于Python 3中的b"abc";而Python 3中的Unicode类型"abc"相当于Python 2中的u"abc"。
(5)range和xrange
在 Python 2中xrange()函数用于创建一个迭代器对象,而range()函数则创建了一个列表。由于xrange()函数的“惰性求值”特性,它的性能比range()函数高出许多。 在Python 3 中,range()相当于Python 2中的xrange(),而Python 2中的xrange()函数将不再存在。 1 | import timeit | 2 | | 3 | n = 10000 | 4 | def test_range(n): | 5 | return for i in range(n): | 6 | pass | 7 | | 8 | def test_xrange(n): | 9 | for i in xrange(n): | 10 | pass |

Python 2: 1 | print 'Python', python_version() | 2 | print '\ntiming range()' % timeit test_range(n) | 3 | | 4 | print '\ntiming xrange()' % timeit test_xrange(n) | 运行结果: Python 2.7.6 | | timing range() | 1000 loops, best of 3: 433 μs per loop | | timing xrange() | 1000 loops, best of 3: 350 μs per loop |
Python 3: 1 | print('Python', python_version()) | 2 | print('\ntiming range()') % timeit test_range(n) | 运行结果: Python 3.4.1 | | timing range() | 1000 loops, best of 3: 520 μs per loop |

参考文献
[1] Chun W J, 吉广. Python 核心编程[M]. 人民邮电出版社, 2008.
[2] Barry P. Head first python[M]. " O'Reilly Media, Inc.", 2010.
[3] McGugan W. Beginning Game Development with Python and Pygame[M]. Will McGugan, 2007.
[4] Hetland M L. Python 基础教程[J]. 2010.
[5] Python 编程入门经典[M]. 清华大学出版社, 2011.
[6] Foord M, Muirhead C. IronPython in action[M]. Manning Publications Co., 2009.
[7] 张志峰. JavaWeb技术整合应用于项目实战[M]. 北京:清华大学出版社, 2013.
[8] 赵家刚. 计算机编程导论——Python程序设计[M]. 人民邮电出版社, 2013
[9] 嵩天. 程序设计基础(Python语言)[M]. 高等教育出版社, 2014.
[10] https://www.python.org/
[11] http://daoluan.net/blog/urllib-source-decode/
[12] http://www.w3cschool.cc/python
[13] http://woodpecker.org.cn/abyteofpython_cn/chinese/index.html
[14] http://blog.csdn.net/imzoer/article/details/8626204
[15] http://www.th7.cn/Program/Python/201305/135739.shtml
[16] http://blog.csdn.net/tianzhu123/article/details/7193455
[17] http://blog.csdn.net/tianzhu123/article/details/7193408
[18] http://www.cnblogs.com/mmix2009/p/3226775.html
[19] http://www.cnblogs.com/daoluanxiaozi/p/3281706.html
[20] http://blog.sina.com.cn/s/blog_b369b20d0101kc0r.html
[21] http://www.cnblogs.com/BeginMan/p/3216093.html
[22] http://www.jb51.net/article/49357.htm
[23] http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html
[24] https://msdn.microsoft.com/zh-cn/library/ae5bf541(VS.80).aspx
[25] http://blog.csdn.net/JINXINXIN_BEAR_OS/article/details/6202784
[26] http://www.cnblogs.com/fnng/archive/2013/04/28/3048356.html
[27] http://www.jb51.net/article/47996.htm
[28] http://www.th7.cn/Program/Python/201405/197007.shtml
[29] http://blog.csdn.net/zhaolei0527/article/details/9331533
[30] http://www.169it.com/article/744469254162602746.html
[31] http://blog.csdn.net/liygcheng/article/details/23054095
[32] http://www.cnblogs.com/skyhacker/archive/2012/01/22/2328789.html
[33] http://www.cnblogs.com/yuxc/archive/2011/08/18/2143606.html
[34] http://justcoding.iteye.com/blog/898562
[35] http://www.jb51.net/article/49357.htm
[36] http://blog.csdn.net/uestcyao/article/details/7896184
[37] http://bbs.chinaunix.net/thread-1681374-1-1.html
[38] http://wenku.baidu.com/link?url=vdzH4RZ3UdcIBqR2bzq7unxgI6BMIpHFhtv6R-n66ObnjP-o
3eGmTy6PiuNDm7f_QII4oVxlL1GAoBRn8L810frCZCBsv4QX8SXUmS6H-Ny
[39] http://www.2cto.com/kf/201412/360046.html
[40] http://www.tuicool.com/articles/nuamMfu
[41] http://blog.sina.com.cn/s/blog_6b60259a0101ftxx.html
[42] http://www.jb51.net/article/49359.htm
[43] http://blog.csdn.net/kirrin/article/details/22078799
[44] http://blog.csdn.net/iloveyin/article/details/8423823

Similar Documents

Free Essay

Computer Science

...Computer science is challenging, and yet dynamic. It requires people in the field to keep learning and pushing the limit. That fast-pacing innovation of technology never stops amazing me, which excites my innate curiosity even more. I, however, had never thought about pursuing computer science as a career until I took an AP Computer Science class in my senior year. It sparked my interest and changed my thoughts after seeing what I could do with some simple line of codes. Despite many countless times staring into the monitor trying to figure out how to solve a problem, those “Aha” moments were more precious and exciting for me. For every minute like that, I was exhilarating and smiled as if I have just won an Olympics’ gold medal in a 100-meter racing swim lap. It fed my hunger for exploration to see what else I could do, maybe something beyond my imagination. Computer science gradually became my hobby in my senior year. I searched many online resources to finally find CodingBat, which allowed me to expand my Java knowledge.These experiences slowly pulled me into the course of a new career, computer science, and at The University of North Carolina at Greensboro pursuing a degree in computer science can provide me knowledge and skills to bring change to people’s daily lives starting from mobile application. I want to be a part of this evolution of technology and I hope to meet other creative, friendly, and ambitious people. My dream is to start my own business partnership during...

Words: 293 - Pages: 2

Premium Essay

The Fluidity of Computer Science

...The Fluidity of Computer Science. Gender Norms & Racial Bias in the Study of the Modern "Computer Science" Computer science or computing science designates the scientific and mathematical approach in computing. A computer scientist is a scientist who specialises in the theory of computation and the design of computers. Its subfields can be divided into practical techniques for its implementation and application in computer systems and purely theoretical areas. Some, such as computational complexity theory, which studies fundamental properties of computational problems, are highly abstract, while others, such as computer graphics, emphasize real-world applications. Still others focus on the challenges in implementing computations. For example, programming language theory studies approaches to description of computations, while the study of computer programming itself investigates various aspects of the use of programming languages and complex systems, and human-computer interaction focuses on the challenges in making computers and computations useful, usable, and universally accessible to humans. Computer science deals with the theoretical foundations of information, computation, and with practical techniques for their implementation and application. History The earliest foundations of what would become computer science predate the invention of the modern digital computer. Machines for calculating fixed numerical tasks such as the abacus have existed since antiquity...

Words: 2298 - Pages: 10

Free Essay

Computer Science

...____________________________________ Computer Science, An Overview ------------------------------------------------- Chapter 00 Introduction Read the introduction to the text. The answers to the following questions will appear in order as you read. What is computer science? ------------------------------------------------- The Role of Algorithms What is an algorithm? What are some examples of algorithms? What is a program? What is programming? What is software? What is hardware? Where did the study of algorithms come from? Once you discover an algorithm, do others need to understand it? When does the solution of a problem lie beyond the capabilities of machines? ------------------------------------------------- The History of Computing What are some of the ancestors of the Computer? Eventually, people began using gears for computing. Who are some of the people involved? Which of the men above produced something that was programmable? What were (and are) some uses of holes punched in cards? What kinds of devices replaced gears? What were some early computers? According to the text, what were the first commercially viable computers, and who built them? What happened in 1981? Who wrote the underlying software for the PC? What important development in computers happened as the Twentieth century was closing? What were two big developments for the Internet? (hint, look for the next two bolded phrases) As computers get smaller and...

Words: 406 - Pages: 2

Premium Essay

Computer Science

...From an early age I’ve always been deeply interested in computing. It was my dad, introducing me to the computer systems at his work place that first sparked this interest. I can always remember the feeling of wanting to know just how computers worked, why they worked and what else they could do. This interest never left me, only growing more profound and passionate with every new discovery I made. From communicating with an artificial intelligence to seeing the wonders of the Internet for the first time, computers have left me fascinated with just how much power yet mystery they hold. My studies have all helped me to develop my understanding of the subject. While Computing has given me a greater insight into the business aspects of the computer industry, 
Physics have helped to improve my analytical and evaluative skills. My interest in computing has not been restricted to the classroom. Within the last few months I’ve used the knowledge that I’ve gained over the past twelve years to set up my own computer related business. This has given me a totally new perspective on how certain things function, and how business operates. The writing of a business plan was a totally alien experience for me, but over the course of three months I researched and planned, and finally when the plan was complete I was rewarded with the satisfaction of knowing that I had completed something that most people would never have the chance to do especially at my age. As well as spending time both...

Words: 357 - Pages: 2

Free Essay

Computer Science

...{ int numGrade = 70; char letter = 'F'; if (numGrade >= 80) { if (numGrade >= 90) letter = 'A'; else letter = 'B'; } else { if (numGrade >= 70) letter = 'C'; else letter = 'D'; } } F | F | F | F | F | F | F | F | F | F | F | F | F | F | F | D | D | D | D | D | D | D | D | D | D | D | D | D | D | D | C | C | C | C | C | C | C | C | C | C | C | C | C | C | C | B | B | B | B | B | B | B | B | B | B | B | B | B | B | B | A | A | A | A | A | A | A | A | A | A | A | A | A | A | A | { int numCookies = 0; while (numCookies < 4) { cout << "Daddy, how many cookies " "can I have? "; cin >> numCookies; } cout << "Thank you daddie!\n"; } 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | { int iUp = 0; int iDown = 10; while (iUp < iDown) { cout << iUp << '\t' << iDown << endl; iUp++; iDown--; } } 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |...

Words: 394 - Pages: 2

Premium Essay

Computer Science History

...Computer science is the scientific and practical approach to computation and its applications. It is the systematic study of the feasibility, structure, expression, and mechanization of the methodical procedures (or algorithms) that underlie the acquisition, representation, processing, storage, communication of, and access to information, whether such information is encoded as bits in a computer memory or transcribed in genes and protein structures in a biological cell.[1] A computer scientist specializes in the theory of computation and the design of computational systems.[2] Its subfields can be divided into a variety of theoretical and practical disciplines. Some fields, such as computational complexity theory (which explores the fundamental properties of Computational and intractable problems), are highly abstract, while fields such as computer graphics emphasize real-world visual applications. Still other fields focus on the challenges in implementing computation. For example, programming language theory considers various approaches to the description of computation, whilst the study of computer programming itself investigates various aspects of the use of programming language and complex systems. Human-computer interaction considers the challenges in making computers and computations useful, usable, and universally accessible to humans. The earliest foundations of what would become computer science predate the invention of the modern digital computer. Machines for...

Words: 284 - Pages: 2

Premium Essay

History of Computer Science

...History of Computer Science Name: Kamyll Dawn Cocon Course, Yr. & Sec.: BSMT 1-D REACTION PAPER The topic of the video which is about the history of computer was kind of interesting since it high lightened our mind about where the computer had really came from. Not only have that, it also made us understand how the computers of today became very wonderful and powerful. Before, computers only existed in the imagination of humans and were believed that creating such monstrous device was impossible. It was a huge leap in the field of computer during the 19th century when Charles Babbage developed the 1st modern computer called the difference machine. The most advantageous feature of this machine is that it reflected Babbage’s attitude of being a perfectionist. Although his work was not finished, the detailed text that was written by Ada was significant in modifying his versions and for documentary purposes of his work. The rapid increase of the American population, on the other hand, triggered the development of a machine that will help the census tabulate such population very fast. Hermin Horrith’s occupation was very helpful to the development of this machine since he used business to gain money to revise his machine which had later evolved into the international business machine. Although war causes devastation to the environment as well as the people involved, it also had contributed to the development of computers, which is the birth of ENIAC, the first large-scale...

Words: 339 - Pages: 2

Premium Essay

The Importance Of Computer Science

...the last piece of the puzzle and making everything click is indescribable, a rush of adrenaline runs through my body and for a brief moment I am on top of the world. My brain is wired to enjoy solving problems logically, which is why I have always been so attracted to computer science, a subject founded on problem solving. Every algorithm that runs smoothly, every piece of hardware that configures without issues, every large project that wraps up on schedule, are all small victories that make it all worth it and fill me with purpose. All aspects of a computer’s inner workings are fascinating to say the least, understanding how a device or service functions can shift one’s perspective of the world and make them see their day-to-day menial tasks in a new light, which is what happened to me....

Words: 700 - Pages: 3

Premium Essay

Computer Science

...CIS101-012 Computer Fundamentals Professor Robotham Wallace, Brein Assignment#1 Date Assignment Given: 9/17/14 Due Date: 9/29/14 Brein Wallace Check Point Assignment #1 True/False True - Electronic components in computers process data using instructions, which are the steps that tell a computer how to perform a particular task. False - Screens for desktops cannot yet support touch. False - Smaller applications, such as at home, typically use a powerful, expensive server to support their daily operations. True - Smartphones typically communicate wirelessly with other devices or computers. False - Data conveys meaning to users, and information is a collection of unprocessed items, which can include text, numbers, images, audio and video. False - As widespread as computers appear to be, most daily activities do not involve the use of or depend on information from them. False - A scanner is a light sensing output device. False - Because it contains moving parts, flash memory is less durable and shock resistant than other types of media. The terms, web, and internet are interchangeable. True - One way to protect your computer from malware is to scan any removable media before using it. True - Operating systems are a widely recognized example of system software. True - You usually do not need to install web apps before you can run them. Multiple Choice 1. An INPUT DEVICE is any hardware component that allows you...

Words: 808 - Pages: 4

Premium Essay

Computer Science

...Von Neumann was a founding figure in computer science.[49] Von Neumann's hydrogen bomb work was played out in the realm of computing, where he and Stanislaw Ulam developed simulations on von Neumann's digital computers for the hydrodynamic computations. During this time he contributed to the development of the Monte Carlo method, which allowed solutions to complicated problems to be approximated using random numbers. He was also involved in the design of the later IAS machine. Because using lists of "truly" random numbers was extremely slow, von Neumann developed a form of making pseudorandom numbers, using the middle-square method. Though this method has been criticized as crude, von Neumann was aware of this: he justified it as being faster than any other method at his disposal, and also noted that when it went awry it did so obviously, unlike methods which could be subtly incorrect. While consulting for the Moore School of Electrical Engineering at the University of Pennsylvania on the EDVAC project, von Neumann wrote an incomplete First Draft of a Report on the EDVAC. The paper, whose public distribution nullified the patent claims of EDVAC designers J. Presper Eckert and John William Mauchly, described a computer architecture in which the data and the program are both stored in the computer's memory in the same address space.[50] John von Neumann also consulted for the ENIAC project, when ENIAC was being modified to contain a stored program. Since the modified ENIAC was...

Words: 625 - Pages: 3

Premium Essay

A Career In Computer Science

...From childhood computers caught my eyes. There was something about them that always attracted my interest. Starting from paint, games , and then finally LOGO, my first ever experience with a programming language. It changed my whole perception about what we can do with computers. Gradually, in my high school I moved on to languages like C, Visual Basic, and C++ . This raised my interest towards computes more and it made me opt for Computer Science as my field in undergraduate course , in Maharaja Surajmal Institute Of Technology. In 2nd year I excelled in courses like software engineering and, java and web development, computer networks, computer architecture, database management system. These subjects fascinated me to so extent that I have...

Words: 886 - Pages: 4

Premium Essay

Computer Science

...(Passwords) One of windows vulnerabilities is that user accounts may have weak, nonexistent or unprotected passwords. The operating system and some third-party applications may create accounts with weak or nonexistent passwords. This in turn causes data to be vulnerable and with respect to user data it could be very damaging to a user’s organization if data is lost or removed without warning by an attacker. Also the connection of these systems to a shared network or perhaps the internet in the scenario of a business organization leaves the system vulnerable to an attacker. With respect to the data that is being sent across the network, there are certain countermeasures that could be taken, such as encrypting data that resides on the computer by using some well-known cryptographic algorithms currently being implemented to secure the system data even after password has been bypassed. Encrypting data provides a level of assurance that even if data is compromised, it is impractical to access the plaintext without significant resources, however controls should also be put in place to mitigate the threat of data exfiltration in the first place. Many attacks occur across a network, while others involve physical theft of laptops and other equipment holding sensitive information. Yet, in most cases, the victims are not aware that the sensitive data are leaving their systems because they are not monitoring data outflows. The movement of data across network boundaries both electronically...

Words: 2126 - Pages: 9

Premium Essay

Computer Science

...Computer architecture covers the design of system software, such as the operating system (the program that controls the computer), as well as referring to the combination of hardware and basic software that links the machines on a computer network. Computer architecture refers to an entire structure and to the details needed to make it functional. Thus, computer architecture covers computer systems, microprocessors, circuits, and system programs. Typically the term does not refer to application programs, such as spreadsheets or word processing, which are required to perform a task but not to make the system run. In designing a computer system, architects consider five major elements that make up the system's hardware: the arithmetic/logic unit, control unit, memory, input, and output. The arithmetic/logic unit performs arithmetic and compares numerical values. The control unit directs the operation of the computer by taking the user instructions and transforming them into electrical signals that the computer's circuitry can understand. The combination of the arithmetic/logic unit and the control unit is called the central processing unit (CPU). The memory stores instructions and data. The input and output sections allow the computer to receive and send data, respectively. Different hardware architectures are required because of the specialized needs of systems and users. One user may need a system to display graphics extremely fast...

Words: 752 - Pages: 4

Free Essay

Computer Science

...Positive and Negative Impact of Computer in Society The Social Impact of Computer in Our Society From the time of the invention of the computers to the present day, computers have met tremendous changes. Time to time incorporation of the latest technical achievement has made the use of computer easier. More and more application have been developed and almost all the areas of the professions have been computerized. Computerization is continuously becoming an important part of many organizations. Computer have proved almost all the fields whether related to numeric processing or non numeric processing or document processing in the developed countries and all the walks of life. Computers have become the part of every organization.  Beneficial or Positive Impact of Computer in our Society * Any professional individual like doctors, engineers, businessmen etc. undergo a change in their style or working pattern after they get the knowledge of computer. * An individual becomes more competent to take a decisions due to the computer because all the information required to take the decision is provided by the computer on time. As a result, any individuals or institutions get success very fast.  * The person working at the managerial level becomes less dependent on low level staff like clerks and accountants. Their accessibility to the information increases tremendously. This improves their working patters and efficiency, which benefit the organization and ultimately affects the...

Words: 2409 - Pages: 10

Free Essay

Computer Science

...Social Issues and Ethics in Computer Science and Engineering Introduction Therac –25 is a medical linear accelerator that was developed by AELC .A linear accelerator (linac) is a particle accelerator, a gadget that increases the energy of electrically charged atomic particles. Linacs are use mainly in hospitals to treat cancer patients .During treatment a patient is exposed to beam of radiation in doses designed to kill a malignancy.(Grolier, 1985) The Software Engineering Code of Ethics and Professional Practice is a practical frame- work for moral decision-making related to problems that software engineers may encounter. (Quinn, 2013) Between June 1995 and January 1987, six patients were seriously injured and some killed by poor administration of radiation from the Therac-25 medical linear accelerator. This paper therefore seeks to explore the causes behind the accidents, the software bugs that were associated with the machine. In addition the paper will also cover some of list the clauses that are violated in the code of ethics of software engineering and explain how they relate to the action or inaction that led to the overexposure incident Technical errors in Therac-25 software One of the major weaknesses that is associated with Therac-25 software was in the lack of formal testing procedures. As results certain errors remained in the software as the product got distributed to the consumers. One of the errors that...

Words: 774 - Pages: 4