Learning Python, 5th Edition Python学习手册 原书第5版 上册 (马克·卢茨) [5 ed.] 1449355730, 9781449355739

Get a comprehensive, in-depth introduction to the core Python language with this hands-on book. Based on author Mark Lut

137 61

English Pages 1643 [796] Year 2013

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
封面
译者序
目录
前言
第一部分 使用入门
第1章 问答环节
人们为何使用Python
软件质量
开发者效率
Python是一门“脚本语言”吗
好吧,Python的缺点是什么
如今谁在使用Python
其他的Python设计权衡:一些难以把握的方面
使用Python可以做些什么
系统编程
图形用户界面(GUI)
Internet脚本
组件集成
数据库编程
快速原型
数值计算和科学计算编程
更多内容:游戏、图像、数据挖掘、机器人、Excel等
Python如何开发并获得支持
开源的权衡
Python有哪些技术上的优点
面向对象和函数式
免费
可移植
功能强大
可混合
相对简单易用
相对简单易学
以Monty Python命名
Python和其他语言比较起来怎么样
本章小结
本章习题
习题解答
Python是工程,不是艺术
第2章 Python如何运行程序
Python解释器简介
程序执行
程序员的视角
Python的视角
执行模型的变体
Python的各种实现
执行优化工具
冻结二进制文件
未来的可能性
本章小结
本章习题
习题解答
第3章 你应如何运行程序
交互式命令行模式
开始一个交互式会话
Windows平台上的交互式命令行在哪里
系统路径
Python 3.3 中的新Windows选项:PATH和启动器
运行的位置:代码目录
不需要输入的内容:提示符和注释
交互式地运行代码
为什么要使用交互式命令行模式
使用注意:交互命令行模式
系统命令行和文件
第一段脚本
使用命令行运行文件
不同的命令行使用方式
使用注意:命令行和文件
UNIX风格可执行脚本:#!
UNIX脚本基础
UNIX env查找技巧
Python 3.3 Windows启动器:Windows也有#!了
点击文件图标
图标点击基础知识
在Windows上点击图标
Windows上输入的技巧
其他图标点击的限制
模块导入和重载
导入和重载基础知识
模块的宏观视角:属性
使用注意:import和reload
使用exec运行模块文件
IDLE用户界面
IDLE启动细节
IDLE基础用法
IDLE功能特性
高级IDLE工具
使用注意:IDLE
其他IDE
其他启动选项
嵌入式调用
冻结二进制可执行文件
文本编辑器启动方式
其他的启动方式
未来的可能
应该选用哪种方式
调试Python代码
本章小结
本章习题
习题解答
第一部分练习题
第二部分 类型和运算
第4章 介绍Python对象类型
Python知识结构
为什么要使用内置类型
Python核心数据类型
数字
字符串
序列操作
不可变性
特定类型的方法
寻求帮助
字符串编程的其他方式
Unicode字符串
模式匹配
列表
序列操作
特定的操作
边界检查
嵌套
推导
字典
映射操作
重访嵌套
不存在的键:if 测试
键的排序:for 循环
迭代和优化
元组
为什么要使用元组
文件
二进制字节文件
Unicode文本文件
其他类文件工具
其他核心类型
如何破坏代码的灵活性
用户定义的类
剩余的内容
本章小结
本章习题
习题解答
第5章 数值类型
数值类型基础知识
数值字面量
内置数值工具
Python表达式运算符
数字的实际应用
变量与基础表达式
数值的显示格式
str和repr显示格式
普通比较与链式比较
除法:经典除法、向下取整除法和真除法
整数精度
复数
十六进制、八进制和二进制:字面量与转换
按位操作
其他内置数值工具
其他数值类型
小数类型
分数类型
集合
布尔型
数值扩展
本章小结
本章习题
习题解答
第6章 动态类型
缺少声明语句的情况
变量、对象和引用
类型属于对象,而不是变量
对象的垃圾收集
关于Python垃圾回收的更多讨论
共享引用
共享引用和在原位置修改
共享引用和相等
动态类型随处可见
“弱”引用
本章小结
本章习题
习题解答
第7章 字符串基础
本章范围
Unicode简介
字符串基础
字符串字面量
单引号和双引号字符串是一样的
转义序列代表特殊字符
原始字符串阻止转义
三引号编写多行块字符串
实际应用中的字符串
基本操作
索引和分片
请留意:分片
字符串转换工具
修改字符串I
字符串方法
方法调用语法
字符串的方法
字符串方法示例:修改字符串II
字符串方法示例:解析文本
实际应用中的其他常见字符串方法
原始string模块的函数(在Python 3 X中删除)
字符串格式化表达式
格式化表达式基础
高级格式化表达式语法
高级格式化表达式举例
基于字典的格式化表达式
字符串格式化方法调用
字符串格式化方法基础
添加键、属性和偏移量
高级格式化方法语法
高级格式化方法举例
与%格式化表达式比较
为什么使用格式化方法
通用类型分类
同一分类中的类型共享同一个操作集
可变类型能够在原位置修改
本章小结
本章习题
习题解答
第8章 列表与字典
列表
列表的实际应用
基本列表操作
列表迭代和推导
索引、分片和矩阵
原位置修改列表
字典
字典的实际应用
字典的基本操作
原位置修改字典
其他字典方法
示例:电影数据库
字典用法注意事项
创建字典的其他方式
请留意:字典vs列表
Python 3 X和2.7中的字典变化
请留意:字典接口
本章小结
本章习题
习题解答
第9章 元组、文件与其他核心类型 ...............................
元组
元组的实际应用
为什么有了列表还要元组
重访记录:有名元组
文件
打开文件
使用文件
文件的实际应用
文本和二进制文件:一个简要的故事
在文件中存储Python对象:转换
存储Python原生对象:pickle
用JSON格式存储Python对象
存储打包二进制数据:struct
文件上下文管理器
其他文件工具
核心类型复习与总结
请留意:运算符重载
对象灵活性
引用vs复制
比较、等价性和真值
Python中True和False的含义
Python的类型层次
类型的对象
Python中的其他类型
内置类型陷阱
赋值创建引用,而不是复制
重复会增加层次深度
注意循环数据结构
不可变类型不可以在原位置改变
本章小结
本章习题
习题解答
第二部分练习题
第三部分 语句和语法
第10章 Python语句简介
重温Python的知识结构
Python的语句
两种不同的if
Python增加的元素
Python删除的元素
为什么采用缩进语法
几种特殊情况
简短示例:交互式循环
一个简单的交互式循环
对用户输入做数学运算
通过测试输入数据来处理错误
用try语句处理错误
嵌套三层深的代码
本章小结
本章习题
习题解答
第11章 赋值、表达式和打印
赋值语句
赋值语句形式
序列赋值
Python 3 X中的扩展序列解包
多目标赋值
增量赋值
变量命名规则
Python中的废弃协议
表达式语句
表达式语句和原位置修改
打印操作
Python 3 X的print函数
Python 2 X的print语句
打印流重定向
版本中立的打印
为什么你要注意:print和stdout
本章小结
本章习题
习题解答
第12章 if测试和语法规则
if语句
一般形式
基础示例
多路分支
复习Python语法规则
代码块分隔符:缩进规则
语句分隔符:行与行间连接符
一些特殊情况
真值和布尔测试
if/else三元表达式
请留意:布尔值
本章小结
本章习题
习题解答
第13章 while循环和for循环
while循环
一般形式
示例
break、continue、pass和循环的else
一般循环形式
pass
continue
break
循环的else
请留意:仿真C 语言的while循环
for循环
一般形式
示例
请留意:文件扫描器
编写循环的技巧
计数器循环:range
序列扫描:while和range vs for
序列乱序器:range和len
非穷尽遍历:range vs分片
修改列表:range vs推导
并行遍历:zip和map
同时给出偏移量和元素:enumerate
请留意:shell命令及其他
本章小结
本章习题
习题解答
第14章 迭代和推导
迭代器:初次探索
迭代协议:文件迭代器
手动迭代:iter和next
其他内置类型可迭代对象
列表推导:初次深入探索
列表推导基础
在文件上使用列表推导
扩展的列表推导语法
其他迭代上下文
Python 3 X新增的可迭代对象
对Python 2 X版本代码的影响:利与弊
range可迭代对象
map、zip和filter可迭代对象
多遍迭代器vs单遍迭代器
字典视图可迭代对象
其他迭代话题
本章小结
本章习题
习题解答
第15章 文档
Python文档资源
#注释
dir函数
文档字符串:__doc__
PyDoc:help函数
PyDoc:HTML报告
改变PyDoc的颜色
超越文档字符串:Sphinx
标准手册集
网络资源
已出版的书籍
常见代码编写陷阱
本章小结
本章习题
习题解答
第三部分练习题
第四部分 函数和生成器
第16章 函数基础
为何使用函数
编写函数
def语句
def语句执行于运行时
第一个示例:定义和调用
定义
调用
Python中的多态
第二个示例:寻找序列的交集
定义
调用
重访多态
局部变量
本章小结
本章习题
习题解答
第17章 作用域
Python作用域基础
作用域细节
变量名解析:LEGB规则
作用域实例
内置作用域
打破Python 2 X的小宇宙
global语句
程序设计:最少化全局变量
程序设计:最小化跨文件的修改
其他访问全局变量的方式
作用域和嵌套函数
嵌套作用域的细节
嵌套作用域举例
工厂函数:闭包
使用默认值参数来保存外层作用域的状态
Python 3 X中的nonlocal语句
nonlocal基础
nonlocal应用
为什么选nonlocal?状态保持备选项
nonlocal变量的状态:仅适用于Python 3 X
全局变量的状态:只有一份副本
类的状态:显式属性(预习)
函数属性的状态:Python 3 X和Python 2.X的异同
请留意:定制open
本章小结
本章习题
习题解答
第18章 参数
参数传递基础
参数和共享引用
避免修改可变参数
模拟输出参数和多重结果
特殊的参数匹配模式
参数匹配基础
参数匹配语法
更深入的细节
关键字参数和默认值参数的示例
可变长参数的实例
Python 3 X的keyword-only参数
min提神小例
满分
附加分
结论
通用set函数
模拟Python 3 X print函数
使用keyword-only参数
请留意:关键字参数
本章小结
本章习题
习题解答
第19章 函数的高级话题
函数设计概念
递归函数
用递归求和
编码替代方案
循环语句vs递归
处理任意结构
函数对象:属性和注解
间接函数调用:“一等”对象
函数自省
函数属性
Python 3 X中的函数注解
匿名函数:lambda
lambda表达式基础
为什么使用lambda
如何(不)让Python代码变得晦涩难懂
作用域:lambda也能嵌套
请留意:lambda回调
函数式编程工具
在可迭代对象上映射函数:map
选择可迭代对象中的元素:filter
合并可迭代对象中的元素:reduce
本章小结
本章习题
习题解答
第20章 推导和生成
列表推导与函数式编程工具
列表推导与map
使用filter增加测试和循环嵌套
示例:列表推导与矩阵
不要滥用列表推导:简单胜于复杂(KISS)
请留意:列表推导和map
生成器函数与表达式
生成器函数:yield vs return
生成器表达式:当可迭代对象遇见推导语法
生成器函数vs生成器表达式
生成器是单遍迭代对象
Python 3.3 的yield from扩展
内置类型、工具和类中的值生成
实例:生成乱序序列
不要过度使用生成器:明确胜于隐晦(EIBTI)
示例:用迭代工具模拟zip和map
为什么你要注意:单遍迭代
推导语法总结
作用域及推导变量
理解集合推导和字典推导
集合与字典的扩展推导语法
本章小结
本章习题
习题解答
第21章 基准测试
计时迭代可选方案
自己编写的计时模块
3.3 版本中新的计时器调用
计时脚本
计时结果
计时模块可选方案
其他建议
用timeit为迭代和各种Python计时
timeit基础用法
基准测试模块和脚本:timeit
基准测试脚本结果
基准测试的更多乐趣
其他基准测试主题:pystones
函数陷阱
局部变量是被静态检测的
默认值参数和可变对象
没有return语句的函数
其他函数陷阱
本章小结
本章习题
习题解答
第四部分练习题
第五部分 模块和包
第22章 模块:宏伟蓝图
为什么使用模块
Python程序架构
如何组织一个程序
导入和属性
标准库模块
import如何工作
1 搜索
2 编译(可选)
3 运行
字节码文件:Python 3.2 及以上版本的__pycache__
实际应用中的字节码文件模型
模块搜索路径
配置搜索路径
搜索路径的变化
sys.path列表
模块文件选择
第三方工具:distutils
本章小结
本章习题
习题解答
第23章 模块代码编写基础
模块的创建
模块文件名
其他种类的模块
模块的使用
import语句
from语句
from *语句
导入只发生一次
import和from是赋值语句
import和from的等价性
from语句潜在的陷阱
模块命名空间
文件产生命名空间
命名空间字典:__dict__
属性名称的点号运算
导入与作用域
命名空间的嵌套
重新加载模块
reload基础
reload示例
请留意:模块重新加载
本章小结
本章习题
习题解答
第24章 模块包
包导入基础
包和搜索路径设置
__init__.py包文件
包导入示例
包的from语句与包的import语句
为什么要使用包导入
三个系统的故事
请留意:模块包
包相对导入
Python 3 X中的变化
相对导入基础知识
为什么使用相对导入
相对导入的适用情况
模块查找规则总结
相对导入的实际应用
包相对导入的陷阱:混合使用
Python 3.3 中的命名空间包
命名空间包的语义
对常规包的影响:可选的__init__.py
命名空间包的实际应用
命名空间包嵌套
文件仍然优先于路径
本章小结
本章习题
习题解答
第25章 高级模块话题
模块设计概念
模块中的数据隐藏
使 * 的破坏最小化:_X和__all__
启用未来语言特性:__future__
混合使用模式:__name__和__main__
以__name__进行单元测试
示例:双模式代码
货币符号:Unicode的应用
文档字符串:模块文档的应用
修改模块搜索路径
import语句和from语句的as扩展
示例:模块即是对象
用名称字符串导入模块
运行代码字符串
直接调用:两种方式
示例:传递性模块重载译注1
递归重载器
另外的代码
模块陷阱
模块名称冲突:包和包相对导入
顶层代码中语句次序很重要
from复制名称,而不是链接
from *会让变量含义模糊化
reload不能作用于from导入
reload、from以及交互式测试
递归形式的from导入可能无法工作
本章小结
本章习题
习题解答
第五部分练习题
封底
Recommend Papers

Learning Python, 5th Edition Python学习手册 原书第5版 上册 (马克·卢茨) [5 ed.]
 1449355730, 9781449355739

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

Learning Python

学习手册

O' RE|LL丫®

旧`

O 归界。琴巳 华章 I T

M,ark Lutz 著 秦鹤林明译 林涵菲审校

原书第5版

Python 学习手册 (上册)

Mark Lutz 著

Beijing• Boston• Farnham• Sebastopol• Tokyo

O'REILL丫@

O'Reilly Media, Inc.授权机械工业出版社出版

机械工业出版社

图书在版编目 (CIP) 数据

Python 学习手册:原书第 5 版/ (美)马克·卢茨 (Mark Lutz) 著,秦鹤,林明译. 一北京:机械工业出版社, 2018.8 (O'Reilly 精品图书系列)

书名原文: Learning Python, Fifth Edition

ISBN 978-7-111-60366-5 I. P··· II. (D 马…@秦.. CT) 林.. III. 软件工具-程序设计-手册 IV.TP3 l l.561-62 中国版本图书馆 CIP 数据核字 (2018) 第 146242 号 北京市版权局著作权合同登记 图字: 01-2013-5994 号

©2013 Mark Lutz. All rights reserved. Simplified Chinese Edition,jointly published by O ' Reilly Media, lnc. and China Machine Press. 2018. Authorized translation of the English edition, 2018 O'Reilly Media, Inc., the owner of all rights to publish and sell the same. All rights reserved including the rights of reproduction in whole or in part in any form. 英文原版由 O' Reilly

Media, Inc.

出版 2013 。

问体中文版由机械工业出版社出版 2018 。 英文原版的翻译得到 O'Reilly

Media, lnc 的授权 。 此间体中文

版的出版和销售得到出版权和销售权的所有者 -~的许可 。 版权所有 , 未得书面许可 、 本书的任何部分和全部不得以任何形式重制 。

封底无防伪标均为盗版 本书法律顾问

北京大成律师事务所韩光/邹晓东



名/

Python 学习手册(原书第 5 版)



号/

ISBN 978-7-111-60366-5

责任编辑/

陈佳媛

封面设计/

Randy Comer, 张健

出版发行/

机械工业出版社



址/

北京市西城区百万庄大街 22 号(邮政编码 100037)



刷/

北京市兆成印刷有限责任公司



本/

178 毫米 X 233 毫米

l6 开本



次/

2018 年 11 月第 1 版

2018 年 11 月第 1 次印刷

r足

价/

219.00 元(共 2 册)

凡购本书 , 如有缺页、倒页 、 脱页 , 由本杜发行部调换 客服热线:

(010)88379426 ,

购书热线 :

(010)68326294 ; 88379649; 68995259 (010)88379604

投稿热线:

88361066

读者信箱: [email protected]

49.75 印张

O'Reilly Media, Inc. 介绍 O'Reilly

Media 通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自 1978

年开始, O'Reilly 一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我 们关注真正重要的技术趋势

通过放大那些“细微的信号”来刺激社会对新科技的应用。

作为技术社区中活跃的参与者, O'Reilly 的发展充满了对创新的倡导、创造和发扬光大。

O'Reilly 为软件开发人员带来革命性的“动物书”,创建第一个商业网站 (GNN) I 组织了 影响深远的开放源代码峰会,以至千开源软件运动以此命名,创立了《Make》杂志,从而 成为 DIY 革命的主要先锋,公司一如既往地通过多种形式缔结信息与人的纽带。 O'Reilly 的 会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思 想。作为技术人士获取信息的选择, O'Reilly 现在还将先锋专家的知识传递给普通的计算机

用户。无论是通过书籍出版,在线服务或者面授课程,每一项 O'ReiJly 的产品都反映了公司 不可动摇的理念~息是激发创新的力量。

业界评论

"0'Reilly

Radar 博客有口皆碑。" -—W订ed

"O'Reilly 凭借一系列(真希望当初我也想到了)非凡想法达立了数百万美元的业务 。 "

— Business 2.0 "O'Reilly Conference 是聚集关键思想领袖的绝对典范。"

— CRN “ 一本 O'Reilly 的书就代表一个有用、有前途、需要学习的主题。"

— Irish Times “Tim 是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照

Yogi Berra 的建议去做了:

'如果你在路上遇到岔路口,走小路(岔路) 。

'回

顾过去 Tim 似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大 路也不错 。 "

— Linux Journal

译者序

如果你想在 2018 年制定一个“学习编程”的年度计划,那么我们会首先推荐 Python 这

门镐程语言。权据目前全球最大的开源代码仓库网站 GitHub 提供的数据,在 2017 年全 球各地的程序员向 GitHub 网站发起了约 100 万次 Python 代码的提交。按照这一指标, Python 无疑是目前世界范围内排名笫二的编程语言。而放眼当今的 IT 界, Python 自己

也像无数励志故事的主角一样,不断成长壮大,并从一众编程语言中脱颖而出,在人工

智能、大数据分析、科学计算、大型网站服务搭建等领域大放异彩。 而这本《Python 学习手册》,正是帮助你入门 Python 或是进阶学习的优质资源。当前

市面上 Python 学习的书籍资源非常丰富,相比之下本书有两个主要特点·一是,本书以 目前史主流的 Python 3 . X 系列为主,同时兼顾到 Python 2.X 的内容,而不是只停留在 2.X

的范畴;二是,本书不贪多,并不企图对所有 Python 的类库都浅尝辄止,而是专注地把 Python 语言的核心知识讲解透彻.正因如此,本书适合作为学习 Python 的笫一本入门书, 同时也可用作强化 Python 核心编程的进阶读物。读者可以放心地用本书构筑起稳固的

Python 核心基础,在此之上,再选购其他书籍,深入学习 Python 在其他特定领域的应用. 当然,这本书的扁幅也十分浩大,难免令人心生畏惧。你也许会有这两个疑惑: Python 核心编程知识真的需要这么多无幅来讲述吗?作者除了讲斛 Python, 是不是也聊了些其

他内容? 确实,如果只是列举 Python 语法和库函数,那么估计几十页的一本小册子就能胜任,但 是如果要帮助读者真正理觯和掌握,就不能跳过知识点背后的原理、思想和例子 。 在翻

译本书的过程中,译者能够真切地体会到作者 Mark Lutz 先生的耐心与细致 。 作者孜孜 不倦地将许多 Python 中看似高深的主题斯开揉碎,妮妮道来 。 对于几乎每一个知识,都 . 会以知识点、思想、示例代码的方式详细展开 。 一方面,读者可以自行选择略读一些段忑 另一方面,这样的讲籽对于理渚复杂的知识非常有益 。

诚然,本书确实聊了一些其他的内容,例如,在笫二部分和笫五部分,深入到 Python 解 释器的层面来讲程序运行和包导入;在笫四部分 , 从软件工程中程序设计的角度来讲函

数的编写;在笫六部分,结合设计模式的思想来讲 Python 中类的编写等。可以说,作者 不满足于让读者会写 Python,

而是希望读者能写得“优雅'' ,做到熟练应用 Python 中

的最佳实践 。

再聊聊如何阅读本书 。 面对这样一本”大部头”,译者自己的笫一感觉是要将它作为工

具书以供查阅 。 但诚如作者在前言中所说,本书更适合作为教材,更值得从头到尾按章 节顺序翻阅学习一遍。不过,如果读者确实已经熟悉 Python, 则可以通过目录来判断某 一章节是否需要跳过,并直接尝试去做跳过章节的“本章习题”。本书附录 D 给出了各

章及各部分的习题答案 。 同时,每一部分的最后一章都用一节来专门总结该章”陷阱", 直接阅读"陷阱"节能够帮助读者快速地补齐很多“知识漏洞”。不过,对于不熟悉的章

节,还是建议读者完整阅读。值得一提的是,书中代码的注释部分本身就带有解释性。因 此,我们希望有能力的读者仔细阅读代码的英文注释,从而加深对代码和概念的理觥。

从内容上来说,本书前五部分(上半册)主要介绍了.语言概览(笫一部分)、核心类

型及操作(笫二部分)、语句和语法(笫三部分)、函数和生成器(笫四部分)、模块 和包(笫五部分) 。 这五部分交代了 Python 语言的基础,其中的翔实讲觥非常适合初学 者系统学习,以及进阶者查缺补漏。本书后四部分(下半册)主要介绍了:类和面向对

象(笫六部分)、异常(笫七部分)、高级主题(笫八部分) 。 对于通常的 Python 轼程, 阅读到笫七部分基本能够胜任。笫八部分可以作为选读部分,在读者遇到相应问题的时 候,再做针对性的补充学习。

本书注释部分包含原书注和译注两部分 。 其中译注部分,旨在帮助读者更好地理斛原书 的意图 。

笫 5 版的整体翻译风格轻松活泼,保持了英文原书原汁原味的风趣幽戏 。

同时,

相较本书之前的中文版 , 我们也做了大量翻译上的调整,从而方便读者阅读 。 在所有翻译决定的背后,是对基本概念讲述的仔细推敲 。 翻译团队内部频繁而密切的交 流,加之与原书作者持续的沟通,我们力求保证翻译的质量 。

当然,最终成书中也不免

会有批漏,希望读者能够包容 , 并不吝斧正 。 如果有任何勘误信息或意见建议,或者希 望与译者交流 , 诗联系邮箱 LearningPython5EChineseBook@outiook. com 玫者 GitHub 仓

库 qinacme/Learning-Python-5E-Chinese-Book 。 哀心希望各位读者能够尽情享受阅读和 学习 Python 的快乐,并把这份热爱带往今后的编程和生活中 。

感谢林涵菲和陈英教授对本书细致的审校 。 感谢龚靖渝、陈灵健在本书翻译过程中提供

的帮助。

译者 2018 年 9 月

目录 上册 、 f

1

削丢............................................................................... 1

第一部分使用入门 第 1 章问答环节............................................................ 21 人们为何使用 Python ......... ...... ................ .. ................... ... ...... . ... .. ................... . ... ... 21 软件质量.. . ....... . .. . ........ . ...... .. ........... .. .... .... ... . ...... . ............................... .. ... ..... 22

开发者效率. . . ........ ........ . . ... .. .. .. . ......... . ......................... . ... . . ... .. ................... . ... . 23

Python 是一门”脚本语言”吗.................. . ........ . ........ ... ... ...... . .. ..... ··········· · ··········23 好吧, Python 的缺点是什么........ . . . . . .. ..... ....... . ......... ............. . ...... ... . ............ ......... 25 如今谁在使用 Python..... .. ................................ . . . ...... .. .......... . ........... .. .... .. .. . ...... . . . . 25

其他的 Python设计权衡:一些难以把握的方面……………………………… . ...… .26 使用 Python 可以做些什么................ .. .. .... .. .... ....... ... ....., ............ . ............................ 28 系统编程.... . ............ .. . . . . ............ . . ... .... .. ....... ........ .... .. . ..... .. . . . ... . .. . . . .. .. .... .. ...... . 28

图形用户界面 (GUI).... . . .. . . ..... .. . .. ... ...... .. .. . ..... .... .... .. ............................. ... . . . 29 Internet脚本... .... ..... ..... ,................................................................................. . . 29

组件集成.............................................................. ··········································30

数据库编程........................................................... ································· · ······· ·30 快速原型......................................................................................... . .. . .. .. ..... .. 31

数值计算和科学计算编程........ . . .. ... .......... ... .... .... .. . .... ... .............. . . ........... ..... 3 1

更多内容:游戏、图像、数据挖掘、机器人、 Excel 等.....………·…..……… ..31 Python 如何开发并获得支持...................... . ......................................................... . . 32 开源的权衡.................................................................................. .. .. ... ............ 33

Python 有哪些技术上的优点 .. ...... .. . . ....... . .... .. . . .. . . . . ................................................ 33 面向对象和函数式... . .. ... ....... . ... . .... . ... ... .... . .... ... ... ....... . .. . ..... . .. . ... .... .. . ........ .. .. 33

免费 .. ..... .... .. .. . . ..................... .. .. .. .... . ..... . . .. ............ ... .. . ..... ..... .......... .. .. . .... . .... .. 34 可移植 ... . . ................ . ......... ... . ........ .. ..... ... .... . .... ... ...... ........ . . ... .. ... . ... . ....... . ..... . 34

:

功能强大.. .. . . .. ... . ..... ..... . .. . . . . .. .... . ............................ . ........... . . . .. .. ... . ................. 35

二单易用

相对简单易学.. .... ... ... .... . ... .... ... ......... .. ...... . .... . .. ..... ............ ..... ....................... 37

以 Monty Python 命名. . .... . ......... .... ........ . . . .. . .......... ................ . .... ··· ··· ···· ······ ···· ·37 Python 和其他语言比较起来怎么样.... ..….. .... ..... . ... . ........... .... .. . . .... ... ................ . .. . 38 本章 I」\结 . . . . ...... . .. . . .. . .. . . .... . ...... . ......... . ... .... .. . . ......... . .... . . .. ......... .. . ................. . . . . .. . .. 39 本章习题.. . ....... . ..... ... ........... . ... . . ... .... . .. ... . . ... ......... .... ...... . . .. ... ... . . .... . ...... .. . .. . . . ..... . . 39 习题解答......... . . . ... .. .... .. ... . ............ . .... ... ....... . . ... . .... ... .... ... . ... .... . . ... .. .... .. . ..... . ... .. . ... 40

Python是工程,不是艺术......... . .............. .. .................. . ............ . .. . . .. . .......... ... 41

第 2 章 Python 如何运行程序........................................... 43 Python解释器简介. . .. . ..... ...... .. . .. . .... .. .... . . .. .... . . ........ . .............................................. 43 程序执行....................................................... . .. . ........ . . .... ........ . ... . ...... ... .. ....... . . .. ... 45 程序员的视角....... . ... . . .. .... . . . ... . ...................................................................... . 45

执行:二言了

::

Python 的各种实现 . . .. . . . . . ... .. .. ... ...... · ····················································...... . .... 49

执行优化工具 . .. . ...... ·· ······· · ·· ·· ·· ·· ····· · · ··· · · ···· · ·· ·· ·········· ··· ·· ·· ······ ·· ···· ··· · · ··· · ··········52 冻结二进制文件..... . ..... . ....... .. .. . . . ... ..... . .. . . .... . ... . . . .... .. . . ... . ...... .. . . . ... . . .. . .. . . . . ...... 53 未来的可能性....... . . . ........ . .... . ......... .... ... . . . .............. .. ... ... .... ...... ............... ... .... 54 本章 IJ \结............ . ... .. .... .... ..... .. ............... ... ...... . .. .... ..... .. ....... .... ..... .... ... . .. ............. . 55

本章习题................ . . .. ................... . .. . .. .. ......... .. .... . ........ . ....................................... 55 习题解答. .......................... . .. . . . ... . ........... . ... . ......... ...... . ........ . .. .... ....................... .. .. 56

第 3 章你应如何运行程序............................... . ............... 57 交互式命令行模式 ..... . ...... .... ..... . .............. . .... . ...... ...... ..... ....... .. . . ......... .... .. . . .. ... .. ... 57 开始一个交互式会话....... . ......... . .................................................... . . .... ... .. ..... 57

Windows 平台上的交互式命令行在哪里 ……...……….. . ……………. ..….. …… … … 59

ii

I

目录

系统路径........................................................................................................ 59

Python 3.3 中的新Windows 选项: PATH和启动器............……·……..………… ...60 运行的位置:代码目录.................................................................................. 61

不需要输入的内容:提示符和注释..…...……......................…...….................. 62 交互式地运行代码......................................................................................... 63 为什么要使用交互式命令行模式..................................................….............. 64

使用注意:交互命令行模式.......................................................................... 65 系统命令行和文件................................................................................................. 67 第一段脚本..................................................................................................... 68

使用命令行运行文件...................................................................................... 69 不同的命令行使用方式.................................................................................. 70

使用注意:命令行和文件.............................................................................. 71 UNIX风格可执行脚本: # !.................................................................................... 72 UN区脚本基础............................................................................................... 72

UNIX env 查找技巧........................................................................................ 73

Python 3.3 Windows 启动器: Windows也有#!了……........令.....…………....…… .73 点击文件图标........................................................................................................ 75 图标点击基础知识......................................................................................... 75

在Windows 上点击图标.................................................................................. 76 Windows上输入的技巧.................................................................................. 77 其他图标点击的限制...................................................................................... 79 模块导入和重载.................................................................................................... 79

导入和重载基础知识....................................................................... ···············80 模块的宏观视角:属性.................................................................................. 82 使用注意: import和 reload............................................................................. 85

使用 exec 运行模块文件...................... . ................. . ................. .. .......... . . . ........ .. ....... 85

IDLE 用户界面....................···················································································86 IDLE 启动细节................................................................................................ 87

IDLE基础用法

88

IDLE 功能特性

89

高级IDLE 工具........... . . . ........ . .............. . .. . .................... ... ..... . . ........... ..... ... ...... 90 使用注意: IDLE............................................................................................ 90

目录

I

iii

其他IDE.. .. ... . .. .. ... . .. . .... . ....... . .. .... ... . ................ . ... .... ....... . ..... .. .. ....... . .. ... .... ............ . 92 其他启动选项........... . . ..... ... ......... .. .. . ... . .................... .. ......... . . .. ..... .. .... ........... .. ... . .. 93

嵌入式调用.......... . ........ . ...... . ................ . .... . .......... .... .... . ..... . ........ . .... . ... ....... ... 93 冻结二进制可执行文件........ . ..... .... . ... ...... . . . ................. . . . . ...... .... .......... . . .. .. .... 94

文本编辑器启动方式.... ..... . . . .. .. .. . ... .. ···· ····· ····· ··· · · ··· ·· · ······ ··· · · · · · · · · · ····· ·· ····· ··· ·· ··94

其他的启动方式. ··· · ········ · ················ · · ··············· ······ · ···· ···· ·· ·· ···· ·· · ·· · ······· · ···· ·····95 未来的可能...... . . . .. .. .. . . .... .... . .. . .... ... . .. ....... ...... ... ..... ..... .. . ............................ ... . . 95 应该选用哪种方式...... ... ...... . ........... ... ........................ .... ... . . ...... . ........................... 95

调试Python代码.. .... .. .. ... . .. . ..... ........... .... ... . ................. . . . ................ ················96 本章 I丿\结 ... ....... .. .. .. .... . . . . .. ....... ..... ... .... ... . . . . ....... . ..... . .............. . .... ... .. . ..... . ... . . . .. . ..... 97

本章习题.... . ... . . . .... . . . .. . .... . ..... . ... . ...... . ....... . .... . . . ..... .. ..... . ..... .. ... . ...... . .. . .............. . .... 97

习题解答

98

第—部分练习题

99

第二部分类型和运算 第 4章介绍 Python对象类型......................................... 105 Python知识结构...... ..... .. .. .. .. .··· ·· ········ ··· ·················· · ········· ·· ·· · ······ ······· · ··········· ····· 105 为什么要使用内置类型.. ..... ....... . .. . ......................... .. . ........ .. .......... ....... ............ .. 106

Python核心数据类型.... ....... ............. . .. . . .. .. .... . .. .... .... . .... .. .... .. .......... · · ·· ····· ··· ······· · 107

数字....· · ········ ···· ···· ··· · · · ·· · ···· · · ············· ··· · · ·········· · · · ······ ··· ··· · · · ···· · ·· · ····· ········ ··· ···· ······ 108 字符串. ....... ......... . ..... ... . .... ..... .... ......... .. ... . . ....... ....... . . ......... .. ... . .... . ... . ........ ... .... . . 110

序列操作... . ...... .... . .... .. .. .. .... ....····· ·· ··· ·· ······· · · · · ··· ·· · ·········· · ······· ·· · ············ · ·· ···· 110 不可变性 . . .. . .. ... ...... . ...... . .............................. . ..................... . ................ . . . .. ... . 112 特定类型的方法

ll3

寻求帮助

ll5

字符串编程的其他方式........... .. ............................................... .. .................. 1 I 6 Unicode字符串............................................................................................ 117 模式匹配...................................................................................................... 119

列表序列操作

}::

特定的操作...................................................·................................................ 121

iv

I

目录

二检查

}::

字典推导



映射操作

125

重访嵌套

126

不存在的键: if 测试

128

键的排序 for 循环

l29

迭代和优化............. . ............... . ................... ... ............................................... 131 元组..... . ....................................... . . . ................. . ......... .. ..................... . .................. 132 为什么要使用元组...........,.................................. . .................. . .....................

133

文件............................................................................................................... ... ...

133

二告!言;:牛



其他类文件工具................................ . .. ........... .... ..... ........ ... .. ..... . ...... ........... 137 其他核心类型............ . ........ . . ........ ... ............... ... ...... . ... . ............................ .. .. . ...... 137

如何破坏代码的灵活性

139

用户定义的类

l40

剩余的内容................................................... . ........................ . .. . . . ......... .. ......

141

本章 IJ \结.............,............................................................................................... 141 本章习题

141

习题解答

142

第 5章数值类型

数值类型基础知识..........................................................

143 143

数值字面量........... ....... . .............. .. ......................................................... .... ... 144 内置数值工具 ...... . ............... . ....... ...... ...... ... .. ..... ................ . ... . .. . ... . ............... 146 Python 表达式运算符.................................................................................... 146 数字的实际应用...... . ... ... .... . ...... . ...... ............ .. .. . .. ..... ............ . .. ........ ................ .... 151 变量与基础表达式...... . .................. . .. . ........ . . ... .... . ........................................

151

数值的显示格式

153

str和 repr显示格式

154

目录

I

V

普通比较与链式比较....... . .............. . ..... . ... ... .......... .. ......... .. ........ . .. ..... ...... . ... 154

除法:经典除法、向下取整除法和真除法..……. . ....……….. . …………………… 156

整数精度.........····················· · · · · ·· ·· · ····· · ···· · ····· · ······· · · · ···· ·· ··· ·················· ··· ··· ··· l60 复数......... . ... .... ... . .......... . ............. . .............. . ........... ... ..... ... .......................... . 160 十六进制、八进制和二进制:字面量与转换.....….......……......……....……… 161 按位操作.... . .. . .... ..... . . . .. . . . .... ... . .. . .. ... .. . ...... . ... . .... ...... . .. .... ......... . ........... .. ...... . 163 其他内置数值工具....... .. . .... ... ..... . ... . ... . .... .......... . . ...... ........ ..... .. . . .. ... . ........... 164 其他数值类型. ... ............................... . .. ... .... .........................................................

167

小数类型

167

分数类型

169

集合............... . .... . ...... . . . ...... . .................. . ... .. .... .. ...........................................

173

布尔型 .... . . . ...... ... . . ..... .. . . ........ . ......... .... ...... . .................... .... ....... . ........... . ...... 180 数值扩展

181

本章小结

l82

本章习题..... .. . .. ... .. ............. .. .... .. ... ...... ... ...... ..................... ... ...... . ............. . ..........

182

习题解答.. . ..... . ............. .. .......... . . . ... ....................... ... ... ...... .... .. ....... .... ..... . ...... . .... 182

第 6 章动态类型.......................................................... 184 缺少声明语句的情况

184

变量、对象和弓 l 用.. . ..... . ............ .. ... . ..... ... ..... .. .. .. . .... .. ..... . ..... .. . ... . .... . . . .. . .. ... . 184 类型属于对象,而不是变量........................................................................ 186

对象的垃圾收集...... . . . ...... . ..... ... ............ . . .... . . .. . ..·· ··· · · ·· ··· ··· ······ ······ ······ ······ ··· 187 关千Python 垃圾回收的更多讨论.. .. ...... .... .. ..... ........ . ... ... .. . . ... ... . ... . . .... .. . ...... 188 共享弓 l 用........ . ...... . . . ....................... . ...... . ... . .... . . . . .... .... .. ...... . . ........................ . ..... 189

共享弓 l 用和在原位置修改. . ....... ...... . . . . . . . .. . ... ..... . . . ... . .. ···· ············ ··· · ···· · ········ · 190 共享弓 l 用和相等.... ......... . . ...... . .... .... .. . . .. . .... . ... . ....... ....... . ... ... .. .. ... ........ ........ . 192

动态类型随处可见 "弱”引用

vi

193 194

本章小结

194

本章习题

194

习题解答... . ..... ....... . . ..... . ........................... . ......................... . ....... . ............. . . . .... ...

195

I

目录

第 7 章字符串基础....................................................... 196 本章范围..................................................................................... . .... .... .. .. ........... 196

Unicode 简介.................................................················································ 197 字符串基础.......................................................................................................... 197

字符串字面量................······················································································ 199 单弓 1 号和双弓 1 号字符串是一样的...............…............................................... 200

转义序列代表特殊字符........................................................................... . .... 200

原始字符串阻止转义... ·················································································203 三弓 1 号编写多行块字符串............................................................................ 205 实际应用中的字符串

206

基本操作

206

索引和分片

207

请留意·分片

2ll

字符串转换工具........................................................................................... 212 修改字符串 I................................................................................................. 214 字符串方法.......................................................................................................... 216 方法调用语法............................................................................................... 216

字符串的方法.................................................................... ···························217

字符串方法示例:修改字符串 II........ …······························…..…................. 218 字符串方法示例:解析文本............................. . .......................................... 220 实际应用中的其他常见字符串方法.......令.....…............................................. 220

原始string模块的函数(在Python 3.X 中删除)……………………···········… ....221 字符串格式化表达式........................................................................................... 223

格式化表达式基础....................................................................................... 224

高级格式化表达式语法... ·············································································225 高级格式化表达式举例................................................................................ 226

基于字典的格式化表达式············································································227 字符串格式化方法调用....................................................................................... 228

字符串格式化方法基础.............................···················································228 添加键、属性和偏移量................................................................................ 229 高级格式化方法语法........................................................................... . ..... . .. 230 高级格式化方法举例......... . .......................................................................... 231

目录

I

vii

与%格式化表达式比较

233

为什么使用格式化方法

236

通用类型分类...................................................................................................... 241

同—分类中的类型共享同一个操作集......................................................… .241 可变类型能够在原位置修改....….......................................................…....... 242

::二::: 习题解答..........................一.................................................................................. 243

第 8章列表与字典....................................................... 245 列表..................................................................................................................... 245 列表的实际应用.................................................................................................. 248

基本列表操作

248

列表迭代和推导

248

索引、分片和矩阵

249

原位置修改列表

250

字典

256

字典的实际应用

258

字典的基本操作

258

原位置修改字典

259

::勹已据库

:::

字典用法注意事项

创建字典的其他方式.

263 .

267

请留意:字典 vs列表.................................................................................... 268

Python 3.X和2.7 中的字典变化.................... ·················································269 请留意:字典接口....................................................................................... 276

二';:::: 习题解答........... . ............ ... .. . ........................... . . ... .. ... . . ..................... .... ............... 277

第9章元组、文件与其他核心类型…………………………. 279 元组........................................................... . ...... . .................... .. ............................ 280

viii

I

目录

元组的实际应用...................................... .. ................................................... 281 为什么有了列表还要元组........ . .. ......... .... ................................. .... .. ....... ...... 283

重访记录:有名元组.................................................................................... 284 文件....................... . ............ . ............. .. ....................... .. ....... .. .... .. ......................... 286

打开文件.................. . .................................................... .. ............................. 287

使用文件...................................................................................................... 288 文件的实际应用................ ... .... . .. ...... . ... . .... .. ..... . .......................................... 289

文本和二进制文件:—个简要的故事................…..….................................. 290 在文件中存储Python对象:转换....…....….............…..….....................…...... 291 存储Python 原生对象: pickle.................................... ..... .. .. .......................... 293

用 JSON格式存储Python对象....................................................................... 294 存储打包二进制数据: struct......................... ... ......... .. . ......................... . ..... 296 文件上下文管理器............ . ... ... ...... .. ............................................................ 297

核心:二1:、结



:::I`:算符重载



弓 I 用 VS 复制... .. .................... . ........ ....... ................................................... .. .. . . . 301

比较、等价性和真值................... . ................................................................ 303 Python 中 True 和 False 的含义... ... .. .... .. .. ... . .................................................... 307

二°;的对:型层次



Python 中的其他类型........................................ ...... ...................................... 311

内置类型陷阱..... .... ..... ...... ...................................... ... ......................................... 311 赋值创建弓 I 用,而不是复制........................................................................ 311 重复会增加层次深度...................... . ..... .. ................................................ ..... . 312

注意循环数据结构.............................................. . ..................... . ... ..... .. . ...... . 313 不可变类型不可以在原位置改变...... .. ......... .. .....令...…...................... ... ......... 313 本章 I」\结.... . ..... . . .. . . ... .... ............................................... . .......................... ....... ..... 314

本章习题................................ . ................................................................ . ........... 314 习题解答

315

第二部分练习题

315 目录

I

ix

第三部分语句和语法 第 10 章 Python语句简介.............................................. 321 重温Python 的知识结构............................................................... . ....................... 321 Python 的语句......................................................................................... . ....... . .... 322 两种不同的 if.................................. .... .............. ........ ........................................... 324 Python增加的元素......................................................... .......................... .... . 325 Python 删除的元素

325

为什么采用缩进语法

327

几种特殊情况.... ... ............................ . .. .... ....... .............. .... .............. .............. 329

简短示例:交互式循环............................................ . .......................................... 331 一个简单的交互式循环..... . ............................. ... ..... .. .... . . ............................. 331 对用户输入做数学运算............ . ........................ .. ... . ........ ........ .... ... .............. 333

通过测试输入数据来处理错误. . ............ . .... ... ................…........................ .... 334 用 try 语句处理错误............................... ............... .... . ........ ....... ......... ...... .. ... . 335 嵌套三层深的代码........................................................................................ 337

::二:: 习题解答........................ .. ........................................ . .............. . ........................... 338

第 1 1 章赋值、表达式和打印...................................… .340 赋值语句.. .... ................................... .. ..................... . . . . ................. .... . . ...... . ........... 340 赋值语句形式............................................................ . .................................. 341

序列赋值...................................................................................................... 342

Python 3.X 中的扩展序列解包...................................................................... 345 多目标赋值... .. .............. . ... .. ................ . .. ............ . ....... ....... . ........ ... .......... ..... . 349 增量赋值................... ........... ....... .... ........................ .. ................................... 350

亡勹:弃协议

:::

表达式语句...................... . ............ ... ...... .. ......... . .... .. . .. ..... . ................. .. ................ 356 表达式语句和原位置修改.................................................... .. .. ... ................. 358 打印操作

X

I

目录

··· ·········· ···· ··········· ··· ·············· ·············· --···························· ···················· · 358

Python 3.X 的 print 函数 Python

359

2X 的 prmt语句

362

打印流重定向........... .. .............. .... .. .............. ... ............................................. 363

版本中立的打印................................. . .......... . ......... . .... . ............................... 367

为什么你要注意: print和 stdout................................................................... 369 本章 IJ \结.................................................................................... ... ...................... 370

本章习题

370

习题解答

370

第 12章 if测试和语法规则............................................ 372 if语句.............. ... ....................... .. . . .... ... . ................... ............ . ........................... .... 372

一般形式.. . ............ . .......... . . .. ..... ........ ... . .. .............................................. . ...... 372

基础示例.............................. . ......... . ......................................................... .. .. 373 多路分支........ . ........ .. ... . .. ..... ...... .... ............................ .. ....... .... ......... ...... . ... .. 373

复习 Python 语法规则........................................................................................... 375 代码块分隔符:缩进规则..................................... ... .................................... 377

语句分隔符:行与行间连接符...... . ......… . .....................................…......…... 378 一些特殊情况

379

真值和布尔测试

381

if/else三元表达式

383

请留意布尔值

384

::二:: 习题解答..................... . . ..................................................................................... . 386

第 13章 while循环和for循环......................................... 387 while循环.............................. . ..... . .......... · ···· · · ····· ·· ············ · · ··· ·········· · ·· ·················387 一般形式...................................................... . ...... . ...... .................. . . .... .. . .. ..... 387 示例..................................................... ..... ........ .... ..................... . .................. 388 break 、 continue、 pass 和循环的 else................................. . ........ .. .......... .. ... ........ . 389

一般循环形式................................ . . . .......... .... .. ............ .... ..... . ... ......... .... .. . ... 389

pass............................................................................................................... 389 continue...................................................................................................... .. 391 目录

I

xi

break....................................................................................................... ...... 391 循环的 else....... . ..................................................................... .. ............ ... .... . . 392 请留意:仿真C 语言的 while循环................................... ...... ...... ... .............. 393 for循环................................................ . ............................................................... 394

一般形式......... . ............................................................................................ 394

示例.............................................. ·········· ··· ····························· ····················· · 395 请留意:文件扫描器 编写循环的技巧

400 402

计数器循环: range..................... ..... ............................................................ 402 序列扫描: while 和 range vs for.................................................................... 403 序列乱序器: range 和 len.............................................. . ............................... 404 非穷尽遍历: range vs分片

405

1疹改列表: rangevs推导......:: : :: :: :: :.:.::.:.: :.: ::......: :.: : :::::::争..::: ::.:...:.:..: 406 并行遍历: zip 和 map.................................................................................... 407 同时给出偏移量和元素: enumerate................................ …·…..............… ....410 请留意: shell 命令及其他................................................... . .......... . . . . ... . ...... 411 本章 I」\结............................................. ..... .. .... ..................................................... 414

本章习题....................... . ..................................................................................... 414

习题解答..................................................................................... ·· ·· ····················414

第 14章迭代和推导..................................................... 4 16 迭代器:初次探索

417

迭代协议:文件迭代器.............................................. . ................................. 417

手动迭代: iter和 next................................................................................... 420

其他内置类型可迭代对象. . ............................................ ······························423 列表推导:初次深入探索................................................................................... 425

列表推导基础............. ····································· · ······································ ·· ····426 在文件上使用列表推导. . .... ..................... ............................. . ......... . .. ..... . .... . 427

扩展的列表推导语法... . ... . . .......... ............ . ........ . . . ..... .... ... ... . .. ....... .. . ............. 428 其他迭代上下文. . ..................... . ....................................................... ....... ............ 430

Python 3.X 新增的可迭代对象..... . . ............... . ................................ . .................. .. . 435 对Python 2.X版本代码的影响:利与弊.....·.....................……............… ········435

xii

I

目录

range可迭代对象........... . .....:········································································436 map 、 zip 和filter可迭代对象........................................................................ 437

多遍迭代器vs 单遍迭代器............................................................................ 439 字典视图可迭代对象

440

其他迭代话题

441

本章小结

442

本章习题

442

习题解答............................................................................................................. 442

第 1 5 章文档............................................................... 444 Python 文档资源

444

#注释

445

如函数

445

文档字符串:

_doc_ …....……………............…·………………………....…………….........……................447

PyDoc: help 函数......................................................................................... 450 PyDoc: HTML报告..................................................................................... 453 改变PyDoc 的颜色........................................................................................ 456 超越文档字符串: Sphinx............................................................................ 461 标准手册集

462

网络资源

463

常见::版编的二:

:::

::;:.二 本章 I」\结............................................................................................................. 465

第三部分练习题,................................................................................................. 467

第四部分函数和生成器 第 16 章函数基础........................................................ 473 为何使用函数...................................................................................................... 474

编写函数...................................................... ·······················································475

目录

I

xiii

.~~~~~~"~~~~~~ ~ ” . ~ i i~~~”~~~ ~~ ~ ~ ~~~~~~“ ~ ”“~~~~~~~

~~ " ~~

~“~~~~~~”“

~i”

”“~~“”.“

.“~

~~~”

~".“”“

••••

.~~~~

~

“~ “ ~ • • •

~~

~~~

~~~~~ii

~"~~

~~~~~~~i

"~

.“”“

“~

.



.

••

“~~“”“~~~~~~

""

~~~}~~~

.~~~~

~~

~~~~

~~~.

~~.“~

.~~"~~~~

.. · ..

“~~~~.

~~~~

"~~

... ..·

~ i

47647747 ~~".~" -i~~”~· --~"~”“~ “”“ i~i~“”~·~

~~~~~.~~ .~~集~~~~~"~

i.

. ~”~~·~”“~”~~~ ~“~~·

~~~交

~时用 ~~~的~ ~“IJ~~~~~~· ~行调~~~~ 歹.~~.~~~

~~中:~~态量.~~

”运和~~态序~~"~. ~千义~~多找~~

~执

..

.行定~~的寻~ "

i. 句句例~. ~“n 。 例 ~i 多变 语 fm示义用 th示义用访部结题答 Py个定调重局小习解 个定调 章章题 一二 第第本本习

f dd ee

第 17章作用域............................................................ 485 Python 作用域基础............................................................................................... 485 作用域细节................................................................................................... 486 变量名解析: LEGB 规则............................................................................. 488

作用域实例................................................................................................... 490 内置作用域................................................................................................... 491

打破Python 2.X 的小宇宙............................................................................. 494 global语句........................................................................................................... 494

程序设计:最少化全局变量........................................................................ 495 程序设计:最 Il \化跨文件的修改................................................................. 497 其他访问全局变量的方式............................................................................ 498 作用域和嵌套函数,........................................................................................ ......499

嵌套作用域的细节....................................................................................... 500 嵌套作用域举例........................................................................................... 500

工厂函数:闭包........................................................................................... 501

使用默认值参数来保存外层作用域的状态.…………………………….......…·… ..503

Python 3.X 中的 nonlocal语句............. .... ........... ···· · ········ ··· ············ · ····· ····· ············507

xiv

I

目录

nonlocal基础................................................................................................ 508

nonlocal 应用.................................... ······················································· ·····509 为什么选 nonlocal? 状态保持备选项.................…..........…................................. 511

nonlocal 变量的状态:仅适用千 Python 3.X ………·……………·…..……·……… ..511 全局变量的状态:只有一份副本..................... .. ......................…….............. 512 类的状态:显式属性(预习)....................... . . . . . ..... .. ... . . ... .. . ... . .. ..... . . . ..... .... 513

函数属性的状态: Python 3.X 和 Python 2.X 的异同.………·…·………...……… .514 请留意:定制 open....... .令.............................................................................. 516 本章/J, 结............................................. .. . . ... . .. .. .. ... ............... . . .. ............................ 518 本章习题............................................................................................................. 518

习题解答............................................................................................................. 520

第 18 章参数

521

参数传递基础...............................................................

521 参数和共享弓 i 用......................................................................................... . . 522 避免修改可变参数

524

模拟输出参数和多重结果

525

:::

特殊::::基勹尸

参数匹配语法............................................................................................... 527

更深入的细节............................................................................................... 528

关键字参数和默认值参数的示例........................................................…...... 529 可变长参数的实例....................................................................................... 532

Python 3.X 的 keyword-only 参数................................................................... 536 min提神 IJ \例........................................................................................................ 539 满分................. . . . ...... ...... ..... . . . .. ........... .. . . . .... . ... ........ . ......... .... ... .... . .............. 540 附加分.......................................................................................................... 541 结论.............................................................................................................. 542

通用 set 函数....................................................................................... . .... . ............ 542 模拟Python 3.X print 函数.................................................................................... 544

使用 keyword-only 参数............................................................................... . . 546 请留意:关键字参数.................................................................................... 547

目录

I

xv

本章 I」\结........... . ............ .... ..... . ... . .......... .. ......... . . .......... .. ... ... .. .......... ... ............... 548

本章习题 ..... .. ....... ... ........ . . ... .... ....... ... ................ ... .... ..... .. .. ... ..... ......... .. .............. 548 习题解答.............................................................................. . ........ .. .................... 549

第 19 章函数的高级话题.............................................. 550 函数设计概念 . ....................................... . ..... . ... . ............. ... . ............... .. ... . ............. 550

递归函数............................... .... ....... . . .. ..... .. ........ . ............ . .................... .. ...... ... ... 552

:::二::: 循环语句 vs递归. . . ......................... ... ........ ... .... .... ... ...... .. . .. ........................... 554

函数处对:尸:::注解 间接函数调用:

:::

“一等“对象................ . ........................................... . ........ 559

函数自省..... . ............. .... ..... .... ..... .... ............. .. ....... .... . .......... ......... ...... . .. . ..... 560

函数属性 ..... ... . ... ...... .................. . . .. ... .... ......... . ....... ...... . . ............ ... . .... ...... .... 561

Python 3.X 中的函数注解......................... ............. ...................................... . 562 匿名函数: lambda....... .. .... . ............... ... ..... .. ....... . .. .. . .. .... .. .. ..... . .... . ... ...... ...... .. ..... 564 lambda表达式基础 . . ..... .... .. ... . ... ... ....... . . .. . . . ..... .. ....... .. ... ... . .. .......... ..... .. ..... ... 564 为什么使用 lambda....... . .... . ........ . ........ . ... .... ................. . .... ............ . ... .. . .... .. .. 566

如何(不)让Python代码变得晦涩难懂 ... ........ .. .... . .......... .. …·…... .. ... …...... 568 作用域: lambda也能嵌套... ... ................................. . .. . ........ . .......... .. . ........... 569 请留意: lambda回调..................................... .. .. .. .............. . .......................... 570

函数式编程工具.. ....... . ......... . . . .... ...... . . . .... .... .. . . ....... . .. . . .. .. .. .. .. . . ............... .. ......... . 570

在可迭代对象上映射函数: map.............. …. .. ......... . .. . ....…............…........… 571 选择可迭代对象中的元素: filter. . .. ... . . ... .. . . . .. .............. .. ............................ .. 573 合并可迭代对象中的元素: reduce..... …...舍.. . .......... . ... ... ................…........... 573 本章 IJ\结.......... .. .......... ... ....... .... ....... . ...... .. .. ... ........ ........... .. . . ......................... ... . 575 本章习题

575

习题解答

575

第 20章推导和生成..................................................... 577 列表推导与函数式编程工具. .... .. .. . . ......... . ... ...... . . . . ... ... .. .... .......... ... ... ... ..... . ... ... ... 577 列表推导与 map.. .............. . ... . ....................................................................... 578

xvi

I

目录

使用 filter增加测试和循环嵌套..................................................................... 579

示例:列表推导与矩阵............................... ·················································582 不要滥用列表推导:简单胜于复杂 (KISS) .…...…………·……………......… ..584 请留意:列表推导和 map........... . ................................................... . ............. 585 生成器函数与表达式........................................................................................... 587 生成器函数: yield

vs return................................. ....................................... 587

生成器表达式:当可迭代对象遇见推导语法………………………………………. 592 生成器函数VS生成器表达式...... . ........ . .........…...…......................…....... . ...... 597 生成器是单遍迭代对象................................................................................ 599

Python 3.3 的 yield from扩展................... .. .................................................. . . 600 内置类型、工具和类中的值生成..... . ...... .. ... . . . .........…............ . .........…....... . 601

实例:生成乱序序列.. . .... ·············· ·· ·· ······ · ··· ···· ·· ··· ························ · · · ······ ·· ·· ·· ·604 不要过度使用生成器:明确胜千隐晦 (EIBTI)

……......……·……………… ...609

示例:用迭代工具模拟zip 和 map................................................. ……. . . .. ..... 611

为什么你要注意:单遍迭代.............................. . ........ . ....... . .... . . . ............... .. 616 推导语法总结......................... . ... . .......................... .. .......................... . . .. .............. 617

作用域及推导变量..... . ................. . ................ .... ...... . .................................... 617

理解集合推导和字典推导…·:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::~~ 620 集合与字典的扩展推导语法

本章/j\ 结.... .. .......... .... .................... . ....... . ....... . ..................... . ......... . .................... 621 本章习题

621

习题解答

621

第 21 章基准测试........................................................ 623 计时迭代可选方案..................................................................................... . ....... .. 623 自己编写的计时模块.......................... . . ....................................... . ............ .. .. 624

3.3 版本中新的计时器调用.. . ...... . ........ . . .... ..... . ......... . .... .. ......... . ................... 627

计时脚本............... ... .... . . .. ........ . .... ......... ... .... ......·· ···· ····· ·· ·· ···· · ······ ·· .. ... . ....... 628

二:可选方案

:::

.

其他建议................... .......... .. . . . . ............... .... ..... . ........ ...... ... . . ................. . .. ... 636 用 timeit 为迭代和各种 Python 计时............................. . ................. .. . . .. .......... . ....... 637

目录

I

xvii

timeit基础用法................. . . . .... . . ........ ... ... ........................ ... ........... .... ........... 637

::

基准测试模块和脚本: timeit. . ......................... .. ..... ..... . .............. . ................ 641

::二二::趣

其他基准测试主题: pystones.... . ...... . ....... .... ... .. .... .. ........ .... ..... . .......... ... ... .... .... . 650 函数陷阱............. ... .... ..... ......... . . .. . .. .. . ... . ... ........ ..... ... . ....................... . ....... .. ..... . .. 651 局部变量是被静态检测的. ..... . .. . . . ... . .. . ... . . . . .. . ...... . .. ..... ... . .... .. .. .. . .................. 651

默认值参数和可变对象

653

没有return语句的函数

655

其他函数陷阱.. . . . .. ....... . .. .. . . . . .. ... . . . . .. . . .... ... .. ... ..... ... .. ..................................... 655

::二:: 习题解答 ... .... . ........ . ....... .. ....... .. ........ .. ....... .... ..... . ...... . ..... . ...... . ............... .... .. .. .. . . 656

第四部分练习题 . . . . .. ................ . ............. . .. ......... . . ... . . ............... . ....... .. ........ .. ........ 657

第五部分模块和包 第 22 章模块:宏伟蓝图.............................................. 663 为什么使用模块... . ................ . .......... ···· ·· · · ·· · · ··· ··· ·· · ····· ······ ·· ······· ··· ···· ·········· ··· ··· ··· 664

Python程序架构..... ······· ··· ····················· ·· · ············ · ·············· ···· ······ · · ······· · ·········· ·· ·664 如何组织一个程序. ... ... . .. ....... ... .. . ..... . ........ ..... .... . .. ............... ...... ......... .... .... 665

导入和属性. ...... .... ..... .. .... . ............ ... ...... . .......... . ............ . .............................. 665 标准库模块 ...... . . .. ...... .. .. . . ........ ........ . . .... ... ...... ... ......... .. ...... .. .... . .. ......... ... .... . 667

import 如何工作..... .. .............. .... . ...... .. .. .................... . .... ..... ............................. . ... 667 1. 搜索............ . ........................................................................................... . .. 668 2. 编译(可选). ... . .. ... ... . ....... .... . . . .. .. . .. . .... ............ . ... . . .. ....... .. ... . .... .. .. . . . ..... .. . 668 3. 运行............................ . . .......... . . .... . . .... ... ....... .. ... . . . ... .. ... . . . ...... . . .... .. ............ 669

字节码文件: Python 3.2及以上版本的 —pycache—.........................................................…..670 实际应用中的字节码文件模型............... . ..................................................... 671 模块搜索路径. .. ... .... . ........................................................................................... 672 配置搜索路径.. . ........ . ................ . ............. . ...... . .. ..... . ...... ... . ... . ...... .. .. .. ..... ...... 674 搜索路径的变化.. . . . . . .. ... . . . .. ............ . ...... .....: . . .... .......... ... ...... . ...... . .. ..... . . . . .. . ... 674

xviii

I

目录

sys.path列表................ ······························ · ··············· · ··································675 模块文件选择............................................................................................... 676 第三方工具:

. distutils.................................................................................. 678

本章 I」\结............................................................................................................. 678

本章习题

679

习题解答

679

第 23 章模块代码编写基础........................................... 680 模块的创建.......................................................................................................... 680

模块文件名................................................................................................... 680 其他种类的模块........................................................................................... 681

模块的使用.......................................................................................................... 681 import语句................................................ ... ........................................ . ....... 681 from语句.............................................................................................. . ..... . . 682 from* 语句.......................................................................................... . ........ 682

导入只发生一次........................................................................................... 683

import和 from 是赋值语句

684

import和 from 的等价性

685

from 语句潜在的陷阱.................................................................................... 686 模块命名空间................................................................................ . ..................... 687

:1;三尸~ ~:~:.·.: .·.: .: .·.: .-.: .: .·.: .·.: .: .·.: .·.:.·.: .·.: .·.:.·.: .·.: .-.:.·.: .·.: .·.: .: .-.: .: :.: .·.: .·.: .: .-.: .·.:.·.:二 属性名称的点号运算.............................................................. . ..................... 689

导入与作用域

690

命名空间的嵌套

69l

重新力且载模块................................................................................................... . .. 692

reload基础............................. .. ..................................................................... 693

reload 示例.................................................................................................... 694

请留意:模块重新加载................................................................................ 695 本章/j\ 结............................................................................................................. 696 本章习题.......... . ................... . ... . .......................................... . ........... ... .... ....... ...... 696

习题解答............................................................................................................. 696

目录

I

xix

第 24 章模块包............................................................698 698 包导入基础

包和搜索路径设置................................................ ... .... ... . ..... ... ................ . ... 699 —init—.PY 包文件.................................................. . ............................. . ........ 700

包导入示例.......................................................................................................... 702

包的 from语句与包的 import语句.........................….............……....…........… .703 为什么要使用包导入 . ... . . ........ .... ............ ... ........ . ............................................... . . 704 三个系统的故事......... .. ............... ... .............. . ............................ ... ................ 705

请留意:模块包......... ........ ... .... . ......................................... .. .. . ........... ..... .... 707

包相:二3X中的变化



相对导入基础知识

709

为什么使用相对导入

710

相对导入的适用情况....... . .................. ... ..................... .. ................... . ............ 713 模块查找规则总结....................................................................................... 713 相对导入的实际应用.................................................................................... 714 包相对导入的陷阱:混合使用............ . ........ . ....…..…......................….....令... .719

Python 3.3 中的命名空间包.................................................................................. 724 命名空间包的语义....................................................................................... 725

对常规包的影响:可选的_init_.py................................ . ........ ... .... . . .......... 726 命名空间包的实际应用.. . ............................................................................. 727

::言了路径 二';:

::: :::

习题解答....................................... . ................................. . ............... . . . ................. 733

第25 章高级模块话题.................................................. 734 模块设计概念.................................. ....... . .... ........................................................ 734

模块中的数据隐藏............................... ...... . ... . ...... ............................................... 736

使*的破坏最 I」\化: _X 和—all—…............…............………........………........…….....……....…..736 启用未来语言特性: _ future_......... ……………………;........…............…...................…......…………… ...737

xx

I

目录

混合使用模式: _name_和_main_…............................……….......……...........令........…........….....738 以—name—进行单元测试............................................................................ 739 示例:双模式代码............................................................................................... 740

货币符号: Unicode 的应用

743

文档字符串:模块文档的立阿.::::::::::::.::.....:::::::::::::::::::::::::::::::::::::::::::::::::::~~;

修改模块搜索路径............................................................................................... 746

import语句和 from 语句的 as 扩展.......................................................................... 747 示例:模块即是对象........................................................................................... 748

用名称字符串导入模块...................................... .. ............................................... 750

运行代码字符串............. . ............................................................................. 751 直接调用:两种方式

示例:传递性模块重载译注1

751 •



••







752

递归重载器................................................................................................... 752 另夕卜的代码................................................................................................... 756 模块陷阱............................... . ............................................................................. 759

模块名称冲突:包和包相对导入...........…......…........…...…….......…....…..... 760

顶层代码中语句次序很重要........................................····························... ·760 from 复制名称,而不是链接.................................. . . .. . ............ ... ............ . .. . .. 761 from* 会让变量含义模糊化......................................................................... 762

reload不能作用于from导入....·····················.. ······················ ······· ······... ••••••••• 762 reload 、 from 以及交互式测试...................................................................... 763 递归形式的 from导入可能无法工作..…·….....................................…............ 764 本章/J, 结............................................................................................................. 765 本章习题.... .... . .. .... ... ..... .. . . ... ..... .... . . .. . . . . .. . . .... . .. ...... . ..... .... ... . ... ..... ... . ................... 766 习题解答

766

第五部分练习题

766

目录

I

xxi

言 铀削 假如你正站在书店中,手里捧着这本书犹豫是否要买下,不妨看看下面三段话:



Python 是一门强大的多范式计算机编程语言,它以程序员的多产、代码的可读及软件 的高质益开发为目标而不断优化。



本书对 Python 语言本身进行了全面而深入的介绍。目标是帮助你在工作中真正使用

Python 之前,掌握 Python 的基本原理并做好充足的准备。秉承本书前四版的一贯思路, 本书这一版同样是所有 Python 初学者的福扯,本书是一个独立的、全方位的学习资源, 不论你将来使用 Python 2.X 、 Python 3.X 或两者兼有。



本书第 5 版已经更新到 Python 的 3.3 和 2.7 发行版,并且进行了充分扩展以反映当今 Python 世界中的 一 线实践。

这里的前言部分较为翔实地讲述了本书的目标、大纲以及结构。尽管是选读材料,但是它

旨在协助你开始本书旅途之前,拥有一个大致的导览。

本书的“生态系统” Python 是一门流行的开源编程语言,广泛用千各个领域的独立程序与脚本化应用中。它不

仅免费、可移植、功能强大,同时相对简单,而且使用起来充满乐趣。从软件业界的任意 一角到来的程序员,都会发现 Python 着眼千开发者的生产效率以及软件质量,因此无论你 的项目是大还是小,选择 Python 都将带来战略性的优势。 不论你处千编程伊始,抑或身为开发专家,本书的目标都是使你跟上 Python 语言的步伐,

这是其他较局限的途径常常无法做到的。在读完本书后,你肯定会全面了解 Python, 并能 自如地将它运用到你想探索的任何应用领域。

从创作之初,本书的定位就是 一 本强调核心 Python 语言本身的教程,而不是 Python 某一 领域的特定应用。同样地,本书意图作为两卷书系列中的第 一 本:



《Python 学习手册》,就是本书,讲述 Python 本身,着眼千横跨多领域的语言基本原理。



《Python 编程》,作为其他备选之一 ,会进 一步向你展示在掌握 Python 后如何使用它。

这种 工 作任务的分配是有意的。尽管使用目标可能因每个读者而不同,但是对有用的语 言

基本原理的介绍则不变。聚焦千应用程序的书籍,如《Python 编程》,会涵盖本书未涉及 的内容,使用真实体量的示例来探索 Python 在常见领域(如 Web 、 GUI 、系统、数据库, 以及文本处理)中充当的角色 。 此外,

《Python 袖珍指南》这本书提供了这里没有包含的

参考材料,可以将其作为本书的补充。 由千本书着眼于 Python 基础,因此它能够运用比许多程序员第一 次学 习这门语言时所见的

更有深度的方式,展现 Python 语 言 的基本原理。其自底向上的叙述方式,以及自包含 、 教 导性的示例构思旨在 一步 一 个脚印地教会读者 Python 语言的全部内容。

在这 一 过程中你收获的核心语 言 技能,将来会应用到你遇见的每 一 个 Python 软件系统 中 一它可以是当今流行的工具,如 Django 、 NumPy 以及 App Engine, 或是其他可能成为

Python 的未来与你编程生涯一部分的工具。 因为本书的前身是一个为期 三天的 Python 培训课程,从始至终带有测试和练习,所以本书 也可作为这门语言的 一 个自学导论。虽然其形式缺少课堂般的现场互动,但是它在深刻性

与灵活性方面进行了补偿,这是书籍所特有的优势。虽然本书有许多使用方法,但是按顺 序阅读的读者会发现它大致相当 千一 门一学期长的 Python 课程。

关千第 5 版 本书第 4 版出版千 2009 年,涵盖了 Python 2.6 与 3 .0 发行版。 注 1 整体上第 4 版处理了在 Python 3.X 系列中引入的许多有时不兼容的更改 。 此外,第 4 版还引入了一款新的 OOP 教程, 以及新的章节,探讨了如 Unicode 文本、装饰器及元类等高级 主题,新的内容既取材千我 本人在教学中实际使用的类,也借鉴自 Python“ 最佳实践"的演化成果。

注 1 :

而 2007 年短暂的笫 3 版涵盖了 Python 2 . 5,

以及其更简单的(并且史简短的)单行

Python 世界 。 参见 http://www.rmi.net/-lutz 荻取关于本书历史的史多细节 。 书的氐幅和复杂度都在增长 , 且直接正比于 Python 自身的增长 。

多年以来,本

由附录 C 可知 , Python

3 .0 在这门语言中引入了 27 个添加项和 57 处更改,本书中已经可以看到它们的身影,而

Python 3.3 延续了这一趋势 。 如今的 Python 程序员面对着两种不兼容的语言系列、三种主 要的范式 、 高级工具的过剩,以及狂风暴雨般的特性冗余 -这其中的大部分在 Python 2.X 和 3.X 系列间都不会整齐地划分 。 不过一切并没有听上去那么令人畏惧(许多工具其实是 同一主题的变体) , 但是全部都是一个兼容并包、综合统筹的 Python 大环坑下的公平消戏 。

2

前言

成书千 2013 年的第 5 版是上一版本的修订本,新版本引入了大量更新,并采用了 Python 3.3

以及 Python 2.7, 它们是 Python 3.X 和 2. X 系列中的当前最新发行版译注 1 。它涵盖了从本 书上一版出版起,每一系列版本中所引入的全部语言特性更改,并且经过了从头至尾的打磨, 因此不仅内容上与时俱进,而且措辞表达上也更加精准严谨。其中:



更新了对 Python 2.X 的介绍,从而包含了像字典和集合推导这样的特性,这些特性 之前只能在 Python 3.X 中使用,但是现在已经向后移植到了 Python 2.7 中,因此在

Python 2.7 中可用。 •

对 Python 3.X 中以下内容的介绍进行了扩增,包括新的 yield 和 raise 语法、 _pycache_字节码模型、 Python 3.3 命名空间包、 PyDoc 的全浏览器模式、 Unicode 字

面量及存储修改,以及 Python 3 . 3 配备的新的 Windows 启动器。



新增了针对 JSON 、 timeit 、 PyPy 、 os.popen 、生成器、递归、弱引用、_mro —、

—iter_、 super 、 _slots_、元类、描述符、 random 、 Sphinx (斯芬克斯)等在内的 许多主题配套的新增或扩展内容,而且也为 Python 2.X 版本兼容问题提供了更多示例

和描述。 这一版还添加了一个新的结尾(第 41 章)、两个新的附录(附录 B 和附录 C) ,以及新的 一章(第 2 1 章,该章扩充了第 4 版中的代码计时示例)。你可以在附录 C 中查阅在第 4 版 和第 5 版之间 Python 语言特性变化的简明总结,以及在本书中对相应内容介绍的指向。这

一附录整体上还对 Python 2.X 和 3.X 之间最初的不同之处进行了小结,这些不同在第 4 版 中首次提出过,其中例如新式类这样的特性横跨了多个 Python 版本,并最终在 Python 3.X 中演化为强制的特性(稍后讲解 Python 2.X 、 3.X 中 “x" 的含义)。 按照前面列出的最后 一 条,本书这一版也扩展了 一 些内容,比如它提供了更多关千高级的

语言特性更为完整的介绍一一我们中的许多人都曾非常努力想要忽视它们,因为它们中的 大多数在过去 10 年都并不常见,但放眼今天的 Python 代码,它们中的大部分已经变得相

当常见。我们也将看到,这些工具的出现使 Python 变得更加强大,但是也提高了初学者的 入门门槛,并且将改变 Python 的领域与定义。由于你可能遇到这其中的任何一个,所以本

书迎难而上,介绍了它们,而不是无视它们。 尽管进行了更新,但本书本版基本上保留了第 4 版中绝大部分结构与内容,同时依旧旨 在为读者提供一套针对 Python 2.X 以及 Python 3.X 的综合学习资源。尽管它主要着眼千

Python 3.3 和 2.7 (3.X 系列的最新版以及可能是 2 . X 系列的最后 一版)的用户,但它也同 时兼顾到了如今还时常能碰到的更老版本的 Python 代码的特性。 译注 1 .截至本书中文版翻译叶, Python 官方的两个主要语言分支分别史新至 Python 3.6 与 2. 7 。 值得一提的是`一方面,从 Python 3.3 到 3.6 已经没有出现显著的基础语言特性变化;另 一方面 , 比对现行见诸市面的 Python 书籍 , 本书采取的 Python 3.3 与 2.7 版本能够更好

地体现并覆盖大部分当前 Python 流行版本的特性 。

前言

I

3

尽管不能预言未来,但是本书强调了近 20 年 一 直有效的基本原理,并且也很可能将它们应

用到未来的 Python 版本中去。照例,我将在本书官方网站上跟进对本书有影响的 Python

更新。在 Python 官方网站文档中的 "What's New in Python" 译注 2 栏目也能够补充 Python 在本书出版后的演化所带来的差异。

Python 2.X 与 3.X 系列 由千 Python 2.X/3.X 的故事在本书中的地位非常重要,我在这里提前说明一下。在本书第 4 版成书的 2009 年, Python 开始了两种风格的演化:



版本 3.0 是通常名为 3 .X 系列中的第一版,这一系列相当千 Python 语 言 的一 次断代升级。



版本 2.6 保留了与已有 Python 代码的庞大身躯向后兼容的特性,并且在整体上是 2.X 系列中的最新版本。

虽然 3.X 大体上是同一门语言,但是它几乎不能运行先前版本中所写的任何代码。它:



采用了 一种 Unicode 模型,对字符串、文件及库产生了广泛的影响。



把迭代器和生成器提升为 一 个更加核心的角色,使它们成为更完整的函数式编程范式 的 一部分。



强制使用新式类 (new-style class) 模型,将类 (class) 与类型 (type) 相合并,也让 新式类模型变得更加强大和复杂。



更改了许多基本工具与库,并对其中的一些进行了替换或移除。

单看将 print 从语句改为函数这一变化,我们就能体会到尽管对语言设计的审美得到了满 足,但也破坏了曾经编写的几乎每 一 个 Python 程序。抛开语 言 设计的战略潜力不谈, 3 . X

强制的 Unicode 及新式类模型,还有随处可见的生成器 , 创造了 一种全新的编程体验。 尽管许多人将 Python 3 . X 视为对 Python 的改进和未来发展的趋势,但是 Python 2.X 仍然 广为使用,并且在未来数年,都将以平行千 Python 3.X 的方式被支持。由千业界 Python 代

码的主体还是 2.X, 向 3.X 迁移的过程注定是缓慢的 。

当今 Python 2.X/3.X 的故事 尽管截至本书第 5 版, Python 已经推出了发行版 3.3 以及 2.7, 但是这个 2. X/3.X 的故事仍

旧大体不变。事实上, Python 现在是一 个二重版本的世界,许多用户根据其软件目标及依赖, 既运行 2. X, 又运行 3.X 。对千许多初学者而言,在 2.X 与 3 . X 之间的选择,演变成了在已 有软件与语言前沿之间的一 次取舍。尽管许多主流 Python 包已经移植到了 3.X, 但是现今 译注 2:

What's New in

Python 是 Python 官方文档中记录每一版本新增语言特性的栏目,网址为

https://docs.python.org/3/whatsnew/index. html 。 感兴趣的读者可以访问查看。

4

前言

的许多其他包仍然只用 2.X 编写。

对千 一 些人来说, Python 3.X 现在被看作探索新思想的沙箱,而 2.X 则被视为经实践检验 过的 Python, 它没有 3.X 的全部特性,但是如同涓涓细流,渗透到每 一 处角落 。 对其他人 来说,将 Python 3.X 视作未来,这一 观点似乎已经被当前核心开发者计划所接受: Python 2.7 将继续被支持,但是将成为 2 . X 的最后 一 个版本,而 3.3 则作为 3.X 系列持续演化进程 中的 一步继续走向未来。

另 一 方面,如 PyPy 这样的方案(当今一款仍然只支持 2.X 的 Python 实现,提供了极其优

异的性能改善)代表了 一 种 2 . X 可能的未来,不过你也可以把它们看作另立大旗、自成一派。 抛开所有的主观看法,在 3 . X 发行将近 5 年后,它尚未取代 2.X, 甚至还未拥有能够与之

匹敌的用户数。作为 一 项评判标准,在当今的 python.org 官方网站,就 Windows 系统而言, 2.X 下载频率仍然比 3.X 高,按理来说这 一 统计应该偏向新用户和最新发行版 。 当然,这样的

统计易千变化,但是它也反映了这 5 年来 3. X 的使用状况译注 3 。对于许多人来讲,已有的 2.X 软件基础库因素仍旧胜过 3 . X 的语言 扩展。此外, 2.7 已是 2 . X 系列的最后 一版,这使

得 2.7 成为某种事实上的标准,因此不受 3 . X 系列恒定幅度变化的影响—一对于寻求稳定基 础库的人而 言 ,有着正面作用 1 对千寻求增长和持续相关性的人而言,则有着负面作用。 就我个人而 言 ,我认为当今的 Python 世界非常广阔,足以同时容纳 3.X 和 2.X; 它们似乎

满足着不同目标 , 吸引着不同的阵营,并且纵观其他语言也不乏先例(例如 C 和 C+ +,有 着长时间的共存,而且它们之间的差异可能比 Python 2.X 同 3.X 之间的差异还大)。此外, 因为它们是如此相似,以至千学习任一 Python 系列所收获的技能几乎都可以完整移植到另

一 系列,尤其在你借助千像本书这样兼顾到版本差异性的学习资源时。事实上,只要你明 白它们之间是如何相互区别的,你将很容易编写在两个版本中都能运行的程序。 与此同时,这种版本间的分裂给程序员和书籍作者都出了 一 道难题,而且这道难题还几乎

不能回避。对千 一本书而言,假装 Python 2.X 从未存在而只涵盖 3.X 的内容,是 一件更简 单的事,但是这将不能满足当今已有的巨大 Python 用户基数的需求。目前海量的已有代码 是用 Python 2.X 编写的,而这些代码不会很快就过时。尽管一 些语言的初学者能够并应该 致力千 Python 3 . X, 但是任何必须使用过去编写的代码的人,都需要涉足当今的 Python 2.X 世界。由千在将许多第三 方库和扩展移植到 Python 3.X 之前仍有数年的时间,所以这一 分

歧将会长期存在。

译注 3:

Python

3.0 发布于 2008 年,到本书英文版出版的 2013 年正好 5 年 。 截至本书中文版翻

译时,最新的比较有代表性的 2 . X 和 3 . X 版本使用 ,情况数据由 Jetbrains 公司提供.在

2016 年 PyCharm 编辑器的用户中 , 72% 采用 2. X, 40% 采用 3 . X (其中包括两个版本 都使用的程序员),统计数据链接 https:!! www.jetbrains .com!pycharm!python-developers-

survey-20161. 前言

5

对 3.X 和 2.X 的介绍 为了解决这一 二 岔分歧,井满足全部潜在读者的需求,本书为此进行更新,涵盖了 Python 3.3 与 2.7, 并且能够迁移到 3.X 和 2.X 系列随后的发行版中。它意图为 Python 2.X 程序员、

Python 3.X 程序员,以及游走千这两个系列之间的程序员提供方便。 也就是说,你可以使用本书来学习两者中的任意一个 Python 系列。尽管 3.X 经常被强调, 对于使用老式代码的程序员而言, 2.X 的不同和工具也需要 一直注意。虽然两个版本系列

大体上是相似的,但它们在一些重要的方面存在差异,而我将在这些差异出现的时候指出来。 例如,我将在大多数示例中使用 3.X 的 print 调用,但是也将讲述 2.X 的 print 语句,这

样你就能理解早期的代码,井且经常使用在两个系列上都能运行的可移植打印技巧。我还 将大址地介绍新的特性,诸如 3.X 中的 nonlocal 语句,以及从 Python 2.6 和 3.0 起可用的

字符串 format 方法,当这样的扩展在老式 Python 中不能使用时我将予以指明。 通过代理,本书也说明了其他的 Python 2.X 和 3.X 发行版,虽然一些老式版本的 2.X 代码

也许不能运行这里的全部示例。例如,虽然从 Python 2.6 和 3.0 起类装饰器是可用的,但是 你不能在还没有这 一特性的老式 Python 2.X 下使用它们。照例,你可以参看附录 C 的更改 表格,从而获取最新 2.X 和 3.X 更改的总结。

我应该选择哪个 Python 版本选择可能由你所效力的组织机构强制规定,但是如果你是 Python 新手并且自主学习,

那么你可能想知道应该安装哪一版本。答案取决于你的目标。下面是一些帮助你做出选择 的建议。

选用 3.X 的情形:新的特性、演化 如果你第一次学习 Python 并且不需要使用任何已有的 2.X 代码,建议你以 Python 3.X 开始。它清理了这门语言中长期存在的一些瑕疵,并修剪了一些陈旧的、不整齐的枝

枝叶叶,同时保留了全部初始的核心思想,还添加了一些令人振奋的新工具。例如, 3.X

的无缝 Unicode 模型,以及对生成器和函数式编程技巧的更广泛使用,被许多用户视为 一 笔财富。很多流行的 Python 库与工具已经或即将在你读到这段话的时候支持 Python 3.X, 尤其是考虑到 3.X 系列持续不断的改进时。所有新的语言演化仅发生在 3.X 中, 这些变化增加了特性并保持与 Python 密切相关,不过也让语言的定义成为一个时常移

动的靶了

一种在语言前沿上的固有权衡。

选用 2.X 的情形:已有代码、稳定性 如果你将使用一个基千 Python 2.X 的系统,那么现今的 3 .X 系列对你可能不是一 个选项。 无论如何,你将发觉本书也能帮你处理忧虑,如果未来迁移到 3.X, 本书将会帮助到你。 你还会发现你其实井不孤单。我在 2012 年培训的每一个小组都只使用 2.X, 并且我仍

6

I

前言

旧定期见到仅以 2.X 形式呈现的实用 Python 软件。此外,不同千 3.X, 2.X 不会再被修

改~也是一笔债务,取决千你所扮演的角色。使用井编写 2.X 代码 是无可厚非的,但是你也会希望尽可能地密切关注 3.X 及其动向。 Python 的未来留待

书写,并且很大程度上将最终由其用户决定,包括你在内。 同时选用两者的情形:版本中立的代码

很可能这里最好的消息就是,在其两个系列中, Python 的基本原理都是相同的一2.X 和 3.X 只会在许多用户觉得细微的方面不同,而本书旨在帮助你学习这两者。事实上, 只要你理解它们不同的原因,就能写出在两个 Python 系列上都可以运行的版本中立的 代码,这也是我们在本书中经常做的。你可以参看附录 C 以获取有关 2.X/3.X 的迁移 指南,以及有关编写针对两个 Python 系列和两类受众的代码的小窍门。 不管一开始从哪个(或哪些)版本上手,你的技能都将直接迁移到你的 Python 工作所指引

的方向上去。

注意:关千 X 记号贯穿本书, “3.X“ 同 “2.X“ 用来整体代指两个系列里的全部发行版。例如, 3.X 包括从 3.0 到 3 . 3 的所有发行版以及未来的发行版 1 2.X 则表示从 2.0 到 2 .7 的全部发行 版 (2.X 预计再没有其他版本)。更加精确的发行版只在一个主题应用千它时会被提起(例

如, 2.7 的集合字面址与 3.3 的启动器和命名空间包)。这种提法偶尔会过千宽泛(一些 这里标注为 2.X 的特性可能不会在早期的 2.X 发行版中出现,而这些早期版本今天已极

少使用),不过我们也要考虑到 2.X 系列已横跨 10 余年的事实。相反, 3.X 的标签则更 加简单、更为准确。

本书的必备知识与意图 我几乎不可能列出本书所需要的绝对必备知识,因为它的实用性和价值不仅依赖千读者的 背景,也同样多地依赖千读者的动机。真正的初学者,以及执拗的编程老手在过去都曾成

功地使用过本书。如果你有学习 Python 的动机,愿意投人时间和精力,那么这本书将非常 适合你。

很多人都会问,学习 Python 到底要花多少时间?尽管答案因人而异,但只要你开始阅读, 就已经离完成不远了。 一 些读者可能将本书当作一个即查即用的工具书,但是大多数希望 精通 Python 的人应该预期会花费数周的时间,也可能是数月的时间,来通读本书,这取决 千他们研究本书示例的深入程度。如前所述,本书大致相当千有关 Python 语言自身为期一

学期的 一 门课程。 这仅仅是对学习 Python 语言本身以及将它使用好的软件技能的预估。尽管本书足以满足基 本的脚本编程目标,但那些希望将软件开发作为职业的读者应该计划在读完本书后,投入

削舌 ',于

| 7

额外的时间到大型项目开发中。如果可能的话,投入额外的时间阅读本书的续集,如《Python

编程》。注 2 对千希望立即精通的人们,那可能不是 一 条受欢迎的消息,但编程并不是一项简单的技能。 (忘了你听过的那些心灵鸡汤吧!)今日的 Python 和通用软件知识,都有足够的挑战性和 较高的回报率,值得你付出像本书这样的综合书籍所暗示的努力。这里是一些有关使用本 书的指南,同时适用千初出茅庐与驾轻就熟的读者。 对于有经验的程序员 你拥有起初的优势,并能够迅速翻阅 一 些靠前的章节 1 但是你不应该跳过其中的那些 Python 专属的核心思想,并可能需要努力放下一些既有的“经验"。笼统地讲,在本

书之前接触任何编程或脚本语言都是有帮助的,因为这能帮助你形成类比。另 一 方面, 我还发现由于在其他语言中根植的成见,先前的编程经验也可能成为一种阻碍。

(根据

所写的第一份 Python 代码,认出课堂中的 Java 或 C+ +程序员真是太容易了!)要用 好 Python 需要你接纳它的思维模式。通过着眼于关键的核心概念,本书旨在以 Python 的方式来帮助你学习编写 Python 代码。 对千真正的初学者

在本书中你能同时学习 Python 以及编程本身 1 但是你可能需要更加努力一点,而且需

要参考阅读其他更加容易的介绍资料来辅助学习。如果你还没有把自己当作一名程序 员,你也有可能发现本书会帮到你,但是你要确保缓慢地前进,并且一 路认真完成示 例和习题。还要牢记,本书会花比讲授编程基础知识更多的时间来讲授 Python 本身。 如果你发现自己因困惑而迷失在这里,我鼓励你在着手阅读本书之前,先了解一下通 用的编程导论。 Python 官方网站提供了许多面向初学者的有用资源链接。 正式地讲,本书旨在成为所有初学者的第一个 Python 读本。对千 一 些之前从没有接触过电 脑的人,它可能不是 一 种理想资源(例如,我们不会花费任何时间来了解电脑是什么), 但是关于你的编程背景和学历,我没有做过多假设。 另一方面,我也不会把读者当作“傻瓜”来事无巨细地解释所有细节,毕竟在 Python 中做

注 2:

标准免责声明:我撰写了本书和先前提过的另一本书,两者可作为组合一起使用:《Python 学习手册》讲授了语言的基本原理,

《Python 编程》讲授了应用程序编写的基础知识,

而《Python 袖珍指南》则可作为前两本的伴侣书目。这三本书全部都起源于 1995 年最初的、 宽泛的《Python 编程》这本书 。 建议你探索当今许多可读的 Python 书籍(在亚马逊网 站 Amazon . com. 我刚刚随意一数就有超过 200 本 ,

因为相关的书目几乎不可计数.而这

还不包括相关主题的书拉,如 Django) 。我自己的出版商最近出版了一系列关于 Python 的书社,有关仪器设各、数据挖掘、 App 引年、数值分析、自然语言处理、 MongoDB 、

AWS 等 -—- 一旦你掌握了这里的 Python 语言基本原理,这些都将成为你可以探索的特定 领域。今天的 Python 故事太过丰富,任何一本书都无法独自讲完 .

8

|

, f,

削舌

些有用的事很容易,而本书将会指引你怎样做。书中也偶尔会拿 Python 同其他语言(如 C 、 C++、 Java) 做对比,但是如果你过去没有使用过这样的语言,你大可放心地忽略这些比较。

本书的结构 为了帮助你上手,本节提供了本书所有主要部分内容和目标的一个快速概要。如果你万分 焦急,想要开始本书的学习,你大可略过这一节(或浏览全书目录)。然而,对于一些读 者而言,这样大部头的一 本书非常值得拥有 一张预先的简明路线图。

经过构思,每一部分都涵盖了这门语言的一个主要功能区,而每一部分都由若干章组成, 这些章节聚焦千本部分功能区的一个特定主题或方面。此外,每一章以习题及其答案结束, 而每一部分以更大型的练习题结束,练习题的解答在附录 D 中给出。

注意:实践很重要:我强烈推荐读者完成本书中的全部测试题与练习题。在编程领域,没有任 何事物能够替代将你的所读付诸实践。无论你在本书中或你自己的项目中是否践行这一 点,实际编写代码都能有效地帮你加深和强化书中提到的思想。

总体上讲,本书的组织形式是自底向上的,因为 Python 语言本身也是如此。随着我们不断 前进,书中的示例与主题也将变得越来越有挑战性。例如, Python 的类基本上仅仅是处理

内置类型函数的包。一且你掌握了内置类型和函数,理解类就相对容易了。由千每一部分 都建立在位千其之前的、逻辑相关的部分之上,对千绝大多数读者来说,按顺序阅读才是 最合适的。以下是你将学习的本书主要部分的预览: 第一部分 我们以 Python 的一个总体概览开始,其中回答了经常会被问到的初步问题一为什么

人们使用这门语言,它能做些什么,等等。第 1 章介绍了潜藏千技术之下的主体思想, 为你提供一些背景知识。本部分的剩余章节将继续探讨 Python 以及程序员运行程序的 方式。主要目的是为你提供充足的信息,以便你能跟上随后的示例与习题。 第二部分 接下来,我们开始 Python 语言之旅,深入学习 Python 的主要内置对象类型如数字、列

表、字典等,以及利用它们所能做的事情。你可以单独使用这些工具完成很多事情, 而它们是每段 Python 脚本的核心。这是本书最重要的部分,因为这部分为随后的章节

奠定了基础。我们还将在这一部分探索动态类型及其引用(这是用好 Python 的关键)。 第三 部分 这一部分我们将继续介绍 Python 的语句,即你输入并创建的代码,同时还将在 Python

中处理对象。这一部分还介绍了 Python 的通用语法模型。尽管本部分关注千语法,但 是它也介绍了 一 些相关的 工 具(如 PyDoc 系统),第 一 次引入迭代的概念,并探索了 编程替代方法。

削丢 , f

?

| 9

第四部分

本部分开始关注 Python 中较高层次的程序结构工具。函数其实是打包代码并重用的 一 种简单方式,它避免了代码冗余。在本部分,我们将探索 Python 的作用域规则、参数 传递技巧,以及时常为人不齿的 lambda 表达式等。我们还将以函数式编程的视角回顾 迭代器,引入用户定义的生成器,并学习如何为 Python 代码计时以测试代码性能。 第五部分

Python 模块允许你将语句和函数组织为更大的组件,本部分展示了如何创建、使用以 及重新加载模块。这里我们还将关注一些更高级的主题,如模块包、模块重新加载、

包相关导入、 3.3 版本的新命名空间包,以及_name_变量。 第六部分

这里,我们探索 Python 的面向对象编程工具,类一 种可选但是强大的组织代码以 便定制和重用的方式,使用它能自然地将代码冗余程度降到最低。正如你将看到的, 类几乎都会重复使用我们到本书目前为止所介绍的思想,而 Python 中的 OOP 基本上就

是关千在相互链接的类组成的类树中查找名称,以及在类的方法中特殊的第一位参数 self。正如你还将看到的,在 Python 中 OOP 是可选的,但是大部分人都觉得 Python 中

OOP 的简洁性远胜于其他语言,而且 OOP 还能够极大地削减开发时间,尤其对千长期 的战略项目研发更为明显。

第七部分 本部分将总结本书关千语言基本原理的介绍,并关注 Python 的异常处理模型及语句, 同时对开发工具进行 一 个简明概览。当你开始编写大型程序时,这些工具将变得更加

有用(例如调试和测试工具)。尽管异常是 一 种相当轻量级的工具,但把这 一 部分安 排在类后面讨论,是由千用户定义的异常在现在的 Python 版本中本质上都是类。这里

我们还介绍了一些更高级的工具,如上下文管理器。 第八部分

在最后这个部分,我们会探索 一 些高级主题: Unicode 与字节串、诸如 property 和描述 符的属性管理工具、函数装饰器和类装饰器以及元类。这些章全部是选读的,因为不 是所有程序员都需要理解它们所应对的课题。另一方面,必须处理国际化文本或 二 进

制数据的读者,或需要开发供其他程序员使用的 API 的读者,应该会在本部分发现 一 些有用的内容。这部分的示例也比本书中的大多数示例庞大,因此能充当自学材料。

第九部分 本书以四个附录的组合圆满结束,它们给出了在不同计算机上安装和使用 Python 的特 定平台相关的小窍门,展示了 Python 3 .3 配备的新 Windows 启动器 1 总结了本书最近

几版涉及的 Python 版本中的更改,井给出了相关网上资源的链接 1 提供了每部分末尾 练习题的解答。而每一章 的“本章 习题”的答案则出现在对应的章 节中。 你也可以参看目录,以获取对本书组成更加细粒度的概览 。

1。

前言

本书不是什么 考虑到多年以来本书相对大规模的读者群体,一些人不可避免地会期待本书扮演其职能外 的角色。因此,尽管我已经告知你这本书是什么,我还想澄清这本书不是什么:



这本书是一本教程,不是参考手册。



这本书介绍语言自身,而不介绍应用程序、标准库或第三方工具。



这本书以系统化的视角,审视一个庞大的主题,而不是一本流水胀般的概览。

因为这些要点对千本书内容十分关键,关千它们我还想预先再讲一些。

它不是一本参考手册,也不是一本特定领域应用程序的指南 本书是一本编程语言教程,不是参考手册,也不是应用程序书籍。这是经过构思的:现今 的 Python 拥有内置类型、生成器、闭包、推导、 Unicode 、装饰器,以及过程式、面向对

象及函数式编程范式的混合。这使得单单核心语言自身就是一个庞大的主题,它同时也是

你未来全部 Python 工作的必备知识,无论你从事哪个领域。当你准备好开始学习其他资源时, 下面是给你的 一 些建议和提醒: 参考资源

正如前面的本书结构描述所暗示的,你可以使用索引和目录来搜寻细节,但是本书没 有任何参考文献附录。如果你正在寻找 Python 的参考资源(且大部分读者很快将会在

他们的 Python 生涯中开始这种寻找),我推荐前面提到的那本书,我把它写为本书的 姊妹篇(《Python 袖珍指南》)。我也建议你采用其他带有快速搜索的参考书目,以 及在 http://www.python.org 维护的标准 Python 参考手册。其中后者是免费的,而且在 持续更新,可以从网络上阅读,也可以在电脑上阅读它。 应用程序和库

如前所述,本书不是一本如 Web 、 GUI 或系统编程这类特定应用程序的指南。取而代 之的是,本书包括了一些你在编写应用程序中将用到的库和工具,虽然这里介绍了一

些标准库和工具(包括 timeit 、 shelve 、 pickle 、 struct 、 json 、 pdb 、 OS 、 urllib 、 re 、 xml 、 random 、 PyDoc, 以及 IDLE) ,然而它们并不是本书主要介绍的内容 。 如果

你是在寻找更多关千这类主题的讲解,或是你已经精通了 Python, 那么我会向你推荐 《Python 编程》这本书。不过,

《Python 编程》一书假设本书的内容作为其必备的基

础知识,因此,请首先确保你扎实的掌握了这门语言的核心内容。特别是在软件这类 工程领域,一个人在跑步之前必须先学会走路。

它不是行色匆匆人们的一本快餐读物 你从本书的篇幅也能判断出书中不乏详尽的细节:它讲述了完整的 Python 语言,而不只 前言

11

是其简化子集的简单概览。 一 路上它也涵盖了整个软件学的原理,这些知识对写出好的

Python 程序是必要的。正如前面提到的,这是 一本需要花费数星期或数月来 阅读的书,旨 在向你传授要从一门有关 Python 的一整学期课程才能掌握的技能水平。 这也是有意而为的。当然,本书的许多读者不需要掌握完整批级的软件开发技能,而且 一 些读者能够以一种零碎的方式吸收 Python 。但与此同时,因为语言的任何部分都可能用于 你将来遇到的代码中,对千大部分程序员来说,书中没有任何一部分内容是真正可跳过的。 此外,即使轻松的脚本编写者和爱好者也需要了解软件开发的基本原理,以便写出更好的

代码以及恰到好处地使用预编写的工具。 这本书力争满足“语言”和“原理”这两个方面的需求,并拥有足够的深度以便更实用。因而,

在你逐章阅读本书并熟练掌握相关的预备知识后,终将发现 Python 中那些所谓高级的工具 (例如对面向对象和函数式编程的支持)其实也都唾手可得。

它以 Python 原本的方式顺序展开 谈到阅读顺序,本书也努力尝试最小化前向引用,但是 Python 3 . X 的改变使这一目标在一

些情形下变得难以实现。(事实上,当你学习时, Python 3.X 有时似乎假设你已经了解它了!) 一些代表性的例子包括 :



打印、排序、字符串 format 方法,以及 一 些依赖由数关键字参数的 diet 调用。



字典键列表与测试,以及许多工具周围使用的 list 调用,暗含有迭代的概念。



使用 exec 运行代码现在假定你已经掌握文件对象与接口的相关知识。



编写新式的异常需要你掌握类和 OOP 的基本概念。



诸如此类-~甚至基本的继承也引人了高级主题(如元类和描述符)。

由简至繁逐步前进仍旧是学习 Python 的最佳方式,因此这里顺序阅读是最有意义的。依然,

一些主题可 能需要非顺序跳读与随机查找。为了将非顺序阅读的程度降到最低,当向后依 赖出现时,本书将会指出它们,并会尽可能地缓解它们的影响。

注意:假如你时间紧张:尽管深度对千掌握 Python 至关重要,但一 些读者很可能时间有限。如

果你希望开启一段快速的 Python 旅程,我建议阅读第 1 章、第 4 章、第 10 章及第 28 章(也

许还有第 26 章) 一一一 个简短的调研有助千激发你对本书剩余部分所讲述的更为完整 故事产生兴趣,这些是大部分读者在现今的 Python 软件世界里所需要的。整体上看,本

书被有意地以此种方式分层,以使其材料更易千读者吸收—一本书的介绍伴随着细节, 因此你能够从概览开始,并随时间的推移探人挖掘。你不必一 口气阅读完本书,其平缓 的介绍方式将帮助你理解它所包含的内容。

12

I

干 f



削舌

本书的程序 通常,本书总是竭力对 Python 版本与平台保持客观中立。它旨在面向全体 Python 用户。 虽然如此,因为 Python 随时间改变,且各个平台在实践中有所不同,所以我会介绍你将在

本书大部分示例中遇到的实际的特定平台。

Python 版本 本书的全部程序示例,都基于 Python 3.3 和 2.7 。此外,许多示例都可以在之前的 3.X 和 2.X 发行版下运行,井且有关在早期版本中语言更改历史的提示会一路搭配在本书中,以照顾 老式 Python 的使用者。 由于本书聚焦千语言核心,因此本书要讲的大部分内容在未来 Python 发行版中都不会有太

大改变。除了少数地方,本书大部分内容也都能应用千较早的 Python 版本 1 当然,如果你 尝试使用一个发行版中新增的扩展,那么情况就与此不同了。根据以往经验,如果你具备 自我提升的自学能力,那么最新的 Python 就是最好的 Python 。 因为本书聚焦千语言核心,所以它的大部分内容还适用于 Jython 和 IronPytbon, 它们是基

千 Java 和.NET 的 Python 语言实现,同样也适合其他 Python 实现,如 Stackless 与 PyPy (在 第 2 章讲述)。这样的替代方案在使用细节上有很大不同,而不是在语言定义上。

平台 本书中的示例曾经运行于 Windows 7 和 8 的超级本,注 3 但是 Python 的可移植性使其成为 一个悬而未决的问题,尤其是在着眼千基本原理的本书中。你会注意到一些 Windows 主义

的事物(包栝命令行提示符、少量的屏幕截图、安装指示,以及一篇有关 Python 3.3 中新 Windows 启动器的附录)。不过它们都反映了这样一个事实,大多数 Python 入门者很可能

从 Windows 平台上开始学习,而其他操作系统的使用人员可以放心地忽视这些内容。 我还会给出像 Linux 这样的其他平台上的启动细节,诸如对“#!“行的使用,但是正如第 3

章和附录 B 所述,即使是此种用法, Python 3.3 的 Windows 启动器也使它成了一种更具移 植性的技巧。

获取本书的代码 本书示例的源代码,以及练习题的解答,可以在下面给出的本书网站上以 zip 文件格式获取: 注 3:

大部分在 Windows 7 环境下运行,但是这与本书主题无关。在写作本书时, Python 安装 在 Windows 8 上 , 并在其桌面模式下运行,这本质上与我所写的没有开始桉钮的 Windows 7 是相同的(你可能需要为从前的开始按钮菜单项创建快捷方式)。而对 WinRT/

Metro " app" 的支持依旧处于持定状态 。 参看附录 A 以荻取更多细节。坦白地讲,当我 写下这段文字时, Windows 8 的未来是不明朗的,因此本书将尽可能地保持版本中立。

削舌 于,'

| 13

http :IIoreil.ly/LearningPython-SE 这个站点既包括本书中的全部代码,也含有包使用说明,因此你可以在其中寻求更多的细节。 当然,在本书的上下文中运行相应的示例能够让你获得最佳的学习体验,而通常你也将需 要一些运行 Python 程序的背景知识来利用这些示例。我们将在第 3 章学习启动细节,因此 请继续学习有关这方面的信息。

使用本书的代码 本书中的代码旨在讲授相关知识,如果代码在力所能及的范围内帮到你,我将非常高兴。 O'Reilly 出版社自身拥有关千一般性重复使用书籍示例的官方政策,我将它们粘贴到这里

以供参考: 本书的目的是帮助你完成工作。通常,你可以在你的程序和文档中使用书中的代码。 你不必联系我们来获得许可,除非你复制了很大一部分的代码。例如,编写一段使用 本书中若干代码块的程序并不需要许可。出售或发布 一张 O'Reilly 出版社书中示例的 CD-ROM 则需要许可。通过引述本书及引用示例代码来回答一个问题不需要许可。在 你产品的文档中包含本书中大量的示例代码则需要许可。

我们感谢,但是不强求你注明引用。引用注明通常包括书名、作者、出版社,以及 ISBN 。 例如,

“Learning Python, Fifth Edition, by Mark Lutz. Copyright 2013 Mark Lutz, 978-1-

4493-5573-9" 。

如果你觉得对代码示例的使用超出合理范围,或在以上给出的许可范围之外,那么请联系 我们,邮箱 [email protected]

字体约定 当然,你开始阅读后就会明白本书的排版机制,不过作为参考,本书使用了如下的排版约定: 斜体 (Italic)

用千电子邮件地址、 URL 、文件名称、路径名称,以及强调第一次引入的新术语。 等宽字体 (Constant width)

用千程序代码、文件内容和命令行输出,以及指明模块、方法、语句和系统命令。 等宽粗体 (Constant

width bold)

在代码片段中使用,显示命令以及需要由用户输入的文本,并且偶尔被用来突出代码 的个别部分。 等宽斜体 (Constant

width italic)

用千可替换的文字和代码片段中的一些注释。

14

I

午 f



削丢

注意:表示一个小贴士、建议或有关附近文本的通用提示。 警告 : 表示附近有关文本的一种警告或警示。

纵观全书,你还会发现偶尔出现的边栏(由方框分隔)和脚注(在页脚),通常为选读内容, 但是提供了有关主题的额外背景。例如,像“请留意:分片”这样的边栏,经常给出示例

使用案例,补充正在被探索的主题。

书籍更新与资源 本书的更新、增补,以及更正(勘误)将在网页上维护,你可以在出版社的网站上或通过 邮件来参与提议。以下是一些主要联系方式: 出版社的网站: http:lloreil.ly!LearningPython-5E

这个网站维护本书的官方勘误列表,以及按照时间记录的用千重印本书的补丁。它还 是前面介绍的本书示例的官方网站。 作者网站: http://www.rmi.net/~lutz/about-lp5e.html

这个网站将用千发布有关本书内容或 Python 自身更为通用的更新一用以应对未来的 变化,你也可以将它看作本书的某种虚拟附录。 我的出版商也有一个电子邮件地址,用来收集有关本书的评论和技术问题:

[email protected] [email protected] 想要了解有关我的出版商的书籍、研讨会、资源中心以及 O'Reilly 网络等的更多信息,请 参看它的综合网站:

http://www.oreilly.com http://www.oreilly.com.cn 想要了解我写作书的更多信息,请参看我自己的书籍支持站点:

http://www.rmi.net/~lutz 如果前面的任何链接因时间久了而变得无效,那么还需要你自己搜索网络来确认 1 毕竟今 天的网络瞬息万变。

致谢 当我在 2013 年写作本书第 5 版时,过去的时光在我脑海中依旧挥之不去。到现在,我已经 使用和推广 Python 20 余年,写作关千它的书籍 18 年,讲授以它为主的实体课程 16 年了。 `',

削舌

I 1s

尽管时间流逝,我现在仍旧不时会惊讶千这门语言所获得的成功——这种成功对千 20 世纪

90 年代的我们来说几乎是无法想象的。因此,冒着被当作一名极度自恋的作者的风险,我 还是希望你能原谅结尾前这里的回顾之语与感激之词。

幕后故事 我自己与 Python 的相识在时间上要早千 Python 1.0 和网络时代(并且可以追溯到这样一个 时间点,那时人们认为安装网络就意味着获取电子邮件信息、拼接、解码,同时祈求一切

都顺利进行)。作为一名受挫的 C++软件开发人员,当我在 1992 年第一次发现 Python 时, 对千它将在我人生中接下来的 20 年产生何种影响,我根本一无所知。我在 1995 年完成了 面向 Python 1.3 的《Python 编程》一书的第 1 版。两年后,我开始了周游全国和全世界的 旅行,并为入门者以及专家讲授 Python 。由于在 l999 年完成了《Python 学习手册》的第 1 版,我成为一名独立的 Python 培训师与作家,某种程度上也多亏了 Python 呈现象级增长

的受欢迎程度。 下面是我折腾到现在的结果。我目前已经撰写了 13 本 Python 书籍,按我自己的统计,总 计大概售出 40 万本。我已经教授 Python 课程 5 年;在美国、欧洲、加拿大及墨西哥讲授

了约 260 堂 Python 训练课;并且一路上遇到了 4000 多名学生。除了推着我向一个飞行常 客的理想国不断前进外,这些课堂帮助我完善了这里的文字以及我的其他 Python 著作。教

书打磨了教材,反之亦然,最终的结果就是我的书籍十分贴近我的课堂,并且能够充当课 堂之外、独立可行的替代解决方案。

至千 Python 本身,随着最近几年的不断成长,它已跻身千全世界使用最广泛的编程语言中 的前 5 ~ 10 名(具体排名依赖千你引用了哪个来源,以及引用的时间)。因为我们将在本

书的第 1 章探索 Python 的地位,所以我会推迟这个故事的剩余部分在那里讲述。

感谢 Python 因为教授 Python 教会了我如何去教课,所以本书很大程度上归功于我的线下课堂。我要感

谢所有参加我课程的学生。伴随着 Python 自身的变化,你们的反馈对千这里文字形成的影 响是无可替代的;没有什么比亲身在现场经历过 4000 人重复一些初学者易犯的错误更具

教育意义了!本书最近几版的修订基千现场教学,主要来源千近期开设的培训班,当然从 1997 年起的每堂培训课都或多或少地为本书的成书提供 了帮助,但根本上也归功千最近的

课堂。我想感谢所有的客户,你们主持了在都柏林、墨西哥城、巴塞罗那、伦敦、埃德蒙顿, 以及波多黎各等地的课堂;这样的经历是我职业生涯中最持久的回报之一。

因为写书教会了作家如何去写作,所以本书很大程度上还要归功千它的读者。我想感谢我 的读者朋友们,在过去的 8 年多,你们抽出时间,以线上和线下的方式,贡献了宝贵建议。

你们的反馈对千本书的改进也是至关重要的,同时也是本书成功的一个重要因素,在开源

16

I

,

f



削吉

的世界,这似乎是一种先天的优势。而读者的评论包罗万象、无处不在,从“你应该被禁

止写作“,到“上帝保佑你写出这本书”;如果说在这样的事情上达成共识是可能的话, 它很可能处千两者之间,借用来自托尔金的 一 句话:书还是太短了。 对千每一位在本书出版过程中贡献力量的人,我都致以衷心的感谢。对千那些多年以来帮

助本书成为一件可靠产品的人们,包括编辑、排版人员、营销人员、技术审校等,我都致 以衷心的感谢。我还想感谢 O'Reilly 出版社,给予我创作 13 本书的机会;这个过程是充满

乐趣的(不过只是有点像电影《土拨鼠之日》译注 4) 。 此外,我还要感谢整个 Python 社区;同大部分开源系统一样, Python 也是为数众多、默默

无闻的贡献者创作的产品。我非常荣幸能够见证 Python 从一个脚本语言的潜懂孩童,成长 为 一种广为使用的工具,并以一种潮流的方式被几乎每一个编写软件的组织机构所使用。 抛开技术性的争论,这个奋进的过程从始至终都是激动人心的。

我还想感谢 O'Reilly .出版社我最初的编辑,已故的 Frank Willison 。本书基本上是 Frank 的 想法。他对千我的职业生涯,以及 Python 新诞生时所取得的成功,都有深远的影响,每当 我忍不住错误使用“只有“这个词时,我都会回想起这份馈赠。

私人感谢 最后是一些更加私人的感谢之词。感谢已故的 Carl Sagan, 你鼓舞了一个 18 岁的威斯康辛 少年。感谢我的母亲,您给我勇气。感谢我的兄弟姐妹,是你们陪伴我发现博物馆中的秘密。

感谢《浅薄》 The Shallows 这本书,为我敲响了急需的警钟。 感谢我的儿子 Michael 和女儿 Samantha 与 Roxanne, 你们令我欣慰。我不知道你们长大后 的模样,但是我为你们现在的一切感到骄傲,并期望看到生活下一步引领你们所去往的方向。

感谢我的妻子 Vera, 你的耐心、不离不弃、健怡可乐以及蝴蝶脆饼。我无比庆幸能在生命 中与你相遇。我不清楚接下来的 50 年会发生什么,但我唯一确信的是,我希望这全部的

50 年都能陪伴在你身旁。

— Mark Lutz,

译注 4:

2013 年春千 Larch

美国电影《土拨鼠之日》讲述了男主人公的生活定格于“土拨鼠日”,不断重复度过同一

天的神奇故事。

,'` 削丢

|

17

第一部分

使用入门

第 1 章

问答环节

如果你已经购买了本书 , 或许你已经知道 Python 是什么,也清楚为什么 Python 是一 个值

得学习的重要工具。如果你还不知道,那么通过学习本书并完成一 两个项目之后,你将会 迷上 Python 。本书第 1 章首先会简要介绍一下 Python 流行背后的一些主要原因。为了构建 一个对 Python 的定义,本章将采用 一 问一答的形式涵盖新手可能提出的 一 些最常见的问题。

人们为何使用 Python 如今有众多可选的编程语言,这往往是入门者首先要面对的问题。鉴千目前大约有 100 万

Python 用户,我的确没有办法完全准确地回答这个问题。开发工具的选择有时取决千特定 的约束条件或者个人喜好。

然而,过去的 16 年中对近 260 个团体组织和 4000 名学生的 Python 培训过程,让我见证了 人们做出这一 选择的 一些共性原因。大部分 Python 用户都提到了下面这些原因: 软件质量 对千很多人而言, Python 更注重可读性、 一 致性和软件质量,这使得它区别千脚本语

言 世界中的许多其他工具。 Python 代码在设计之初就具有良好的可读性,因此具备了 比传统脚本语 言更优秀的可重用性和可维护性。即使代码并非你亲手所写, Python 的 一致性也保证其易千理解。此外, Python 支持软件开发的高级重用机制。例如面向对 象 (object-oriented, 00) 以及函数式编程 (function programming) 。 开发者生产效率

相对千 C 、 C+ +和 Java 等编译/静态类型语言, Python 的开发者效率提高了数倍。

Python 代码的长度往往只有 C+ +或 Java 代码的 1/5~1/3 。这就意味着可以录人更少的 代码,调试更少的代码,并在开发完成之 后维护更少的代码。并且 Python 程序可以立

21

即运行,而无需传统编译/静态语言所必需的编译及链接等步骤 , 进 一 步提高了程序员

的效率。 程序的可移植性

绝大多数的 Python 程序不做任何改变即可在所有 主 流计算机平台上运行。例如,在 Linux 和 Windows 之间移植 Python 代码,只摇简单地在机器间复制代码即可。此外, Python 提供了多种可选的代码库,用于编写包括用户图形界面、数据库接入、基于 Web 的系统等在内的各种程序。其中甚至包括程序启动和文件夹处理这样的操作系统 级接口,也成为 Python 可移植性的 一部分。

标准库的支持

Python 内置了众多预构建井可移植的功能模块,这些功能模块叫作标准库 (standard library) 。标准库支持 一 系列应用级的编程任务,涵盖了从字符模式到网络脚本编程的

匹配等方面 。 此外, Python 可通过自行开发的库或众多第 三 方的应用来支持软件的扩展。 Python 的第 三 方支持 工具包括网站搭建 、 数值计算 、 串口读写、游戏开发等各个方面(参 考接下来的样例)。例如, NumPy 是一 个免费的,与 MATLAB 一 样功能强大的数值 计算开发平台。

组件集成 Python 脚本可通过多种集成机制轻松地与应用程序的其他部分进行通信。这种集成 使 Python 成为实现产品定制和扩展的工具。目前, Python 代码可以调用 C 和 C++ 的库,可以被 C 和 C+ +的程序调用,可以与 Java 和. NET 组件集成,可以与 COM

和 Silverlight 等框架进行通信,可以通过串行端口与设备进行连接,并且可以通过 SOAP 、 XML-RPC 和 CORBA 等接口与网络进行交互。 Python 绝不仅仅是 一 个孤立的 工具。

享受乐趣

Python 的易用性和强大内置 工 具使编程成为 一 种乐趣,而不是琐碎的重复劳动。这是 一 个难以言表的优点,将对开发效率的提升有很重要的帮助。

以上因素中,对于绝大多数 Python 用户而 言 ,前两项(质量和效率)也许是 Python 最具 吸引力的两个优点,值得我们用更加完整的篇幅进行论述。

软件质量 从设计之初, Python 就秉承了 一种独特的简洁而极具可读性的语法,以及 一 种高度 一 致的

编程模型。正如过去某次 Python 会议标语所宣称的那样, Python 似乎是“与人脑思维直觉 吻合”,也就是说, Python 的语 言特性以 一 种一致和受限的方式进行交互,并自然地遵守 一 套紧凑的核心概念。这使得 Python 易千学习、理解和记忆。 事 实上, Python 程序员在阅

读和编写代码时无须经常查阅手册。 Python 是 一个设计风格始终如一 的开发平台,可以保 证开发出具有 一致性的代码。

22

I

第 1 章

从哲学理念上讲, Python 采取了一种所谓极简主义的设计理念。这意味着尽管实现某一编 程任务通常有多种方法,往往只有一种方法是显而易见的,还有一些不是那么明显的方法, 而且贯穿整门语言都采用这一 套紧凑的交互形式。此外, Python 并不会为你做任意的选择; 当交互模棱两可时,使用简洁明了的解决办法要优千“魔法”般的方式。在 Python 的思维

方式中,明确胜千隐晦,简单胜千复杂注 I• 除了以上的设计宗旨, Python 还包含模块化、 OOP 在内的一 些工具来自然地提升程序的可 重用性。而且由千 Python 致力千提升编码质批, Python 程序员也都自然而然地秉承了这一

理念。

开发者效率 20 世纪 90 年代中后期,互联网带来的信息爆炸使有限的程序员与日益繁多的软件开发项

目之间的矛盾愈发严重:开发者开发系统的速度常常要求赶上互联网演变的速度。在这 一 浪潮过后的公司裁员和经济衰退时期,产业图景又悄然改变 。 公司开始要求更少的程序员 来完成相同的开发任务。 无论在以上哪种背景下, Python 作为开发工具均以付出更少的精力完成更多的任务而脱颖

而出 。 Python 致力千开发速度的最优化:其简洁的语法、动态类型、无须编译、内置工具 包等特性使程序员能够快速完成项目开发,而使用其他开发语言则需要几倍的时间。其最

终结果就是,相对千传统的语言, Python 把开发者效率提高了数倍。不论所处的时代是欣

欣向荣还是萧条而不景气,也无论软件行业未来的走势是起还是落,这都是一件值得庆幸 的事。

Python 是一门“脚本语言”吗 Python 是 一 门通用型的编程语 言 ,而它时常扮演着脚本语言的角色 。一 般来说, Python 可 定义为 一 门面向对象的脚本语 言 :这个定义把对面向对象的支持和彻底的面向脚本语 言 的

角色融合在 一起。如果只用 一句话来概括,我想说 Python 是一 门融合了面向过程、函数式 和面向对象编程范式的多目标语言。这句话抓住了今天 Python 涉及的领域及其丰富的内涵。 无论怎样,术语“脚本” 一 词已经同胶水一样黏在了 Python 上,这不同千其他需要编 写 大

屉 繁复代码的语 言 。例如,人们往往用“脚本”

(script) 而不是“程序”

(program) -

词来描述 Python 的代码文件。出于对这项传统的沿袭,本书中“脚本”与“程序”是可以

注 l :

想要更加完整地一览 Python 哲学,在任何 Python 交互式命令行(你将在笫 3 章学会如何 使用它)下轮入命令 import this 。 这将会展示一个在 Python 中隐藏的 " 彩蛋” 一构筑 Python 的设计理念集合,它们漆透到语言本身和用户社区的方方面面 。 缩写 EIBTI 是当 今“明确胜于隐晦 ” 规则的最流行行话 。 这些理念足以成为 Python 的成言和信条,本书 会经常引用它们 。

问答环节

I

23

相互替代的,其中“脚本”往往倾向千描述简单的顶层代码文件,而“程序”则用来描述 那些相对复杂一些的多文件应用。

由千"脚本语言”的意思可谓众说纷纭,因而, 一 些人也认为该词在 Python 中的使用应该 被完全禁止。实际上,人们往往给 Python 冠以以下三个不同的角色,其中有些角色相对其

余的角色更重要: Shell 工具

偶尔当人们听到 Python 是脚本语言时,他们会认为 Python 是 一 个面向系统的脚本语言 代码工具。这些程序往往在命令行运行中,实现诸如文本文件的处理以及启动其他程 序等任务。 Python 程序当然能够以这样的角色工作,但这仅仅是 Python 常规应用范围的很小一部 分。它不只是 一种很好的 Shell 脚本语言。 控制语言 对其他人而言,脚本意味着控制或重定向其他应用程序组件的"胶水”层。 Python 经

常部署于大型应用之中。例如,测试硬件设备时, Python 程序可以调用能够进行硬件 底层访问的相关组件。类似地,在终端用户产品定制的过程中,应用程序可以在策略

点处调用一些 Python 代码,而无需分发或重新编译整个系统代码。

Python 的简洁性使其从本质上能够成为一个灵活的控制工具。然而从技术上来讲,这 也只是 Python 的常规角色之一;许多(或许也是绝大多数) Python 代码作为独立的脚 本执行时无须调用或者了解其他的集成组件。然而, Python 不只是一种控制语言。 使用便捷 可能对“脚本语言“最好的解释,就是 一 类应用千快速编程任务的一种简单语言。对

千 Python 来说,这确实是实至名归,因为 Python 和 C+ +之类的编译语言相比,大大 提高了程序开发速度。其敏捷的开发周期促进了探索、增量式的软件开发模型,而这

些都是必须亲身体验之后才能体会得到的。

但是千万别被迷惑,误以为 Python 仅可以实现简单的任务。恰恰相反, Python 的易用 性和灵活性使编程任务变得简单。 Python 有着一些简洁的特性,但是它允许程序按照 需求以尽可能优雅的方式扩展。也正是基千这一点,它通常应用千快速作业任务和长 期战略开发。 所以, Python 是不是脚本语言呢?这取决千你看待这个问题的视角。一般意义上讲,

"脚

本语言”一词可能最适用千描述一种 Python 所支持的快速和灵活的开发模式,而不是特定

的应用领域的概念。

24

I

第 1 章

好吧, Python 的缺点是什么 在经过 21 年的 Python 使用、 18 年 Python 主题的写作和 16 年 Python 的教学之后,我发现

Python 重大且普遍的唯一缺点就是,在现有的实现方式下,与 C 和 C++这类完全编译井且 较底层的语言相比, Python 的执行速度还不够快。尽管这类任务在今天已经非常少见,你

仍有可能需要使用较底层语言来接近问题的核心,因为这些底层语言同底层硬件的架构有 着更加直接的对应关系。

本书后面将对实现方式的概念进行详细阐述。简而言之,目前 Python 的标准实现方式是将 源代码的语句编译(翻译)为字节码 (byte code) 的形式,之后再将字节码解释出来。由

千字节码是一种与平台无关的格式,因而它具有可移植性。然而,因为 Python 通常不会将 代码编译成底层的二进制代码(例如 Intel 芯片的指令),一些 Python 程序将会比像 C 这

样的完全编译语言慢一些。下一章将要讨论的 PyPy 系统能够通过在程序运行时的进一步编 译来达到在某些代码上 10 到 100 倍的运行加速,但这是一个独立的替代实现。

程序的类型决定了是否需要关注程序的执行速度。 Python 已经优化过很多次,并且 Python 代码在绝大多数应用领域运行的速度也足够快。此外, 一且使用 Python 脚本做一些“现实” 世界的事情,程序实际上是以 C 语言的速度运行的,例如处理某一个文件或构建一个用户 图形界面 (GUI) 。因为在这样的任务中, Python 代码会立即调用 Python 解释器内部已经

编译的 C 代码。究其根源, Python 开发速度带来的效益往往比执行速度带来的损失更为重要, 特别是在现代计算机的处理速度得到极大提升的情况下。 然而即便当今 CPU 的处理速度很快,在一些应用领域仍然需要优化程序的执行速度。例如,

数值计算和动画渲染,常常需要其核心数值处理单元至少以 C 语言的速度(或更快)执行。 如果你在以上领域工作,仍然可以使用 Python: 只需通过分离一部分需要优化速度的应用,

将其转换为编译好的扩展组件,并在整个系统中使用 Python 脚本将这部分应用连接起来。 本书我们将不会再谈论这个扩展的问题,但这却是一个我们先前所提到过的 Python 作为控

制语言角色的鲜活例子。 NumPy 是采用双语言混编策略的一个重要例子:作为一个 Python 的数值计算扩展, NumPy 将 Python 变为一个既高效又简单易用的数值计算编程工具。这些

扩展将会在必要的时候提供强有力的优化工具。

如今谁在使用 Python 在编写本书的时候,乐观估计,此时全球的 Python 用户将达到 100 万(略微有些出入)。 这个估计是基千各种数据统计的。例如,下载率、网页统计和用户抽样调查。因为 Python 开放源代码,没有注册许可证总数的统计,因此很难得到精确的用户总数。此外,在 Linux

的各种发行版、 Mac 电脑和大范围的硬件和产品中都内置了 Python ,进一步模糊了用户数目。

问答环节

I 2s

其他的 Python 设计权衡:一些难以把握的方面 我曾提到过执行速度是 Python 唯一主要的缺点 。 对于绝大多数 Python 用户,尤其是 新手来讲 , 确实是这样的 。 很多人发觉 Python 简单易学 , 使用中充满乐趣,尤其与 它的同时代语言(如 Java 、 C#和 C++) 相比时,这点就愈加明显 。 但是,作为一个喜 欢打破砂锅问到底的人,有必要同时从教学者和开发者的角度介绍一下我沉浸 Python

世界 20 年来注意到的一些不为人所知的设计权衡 。 作为一名教学者 , 我发现有时候 Python 及其库的史新速率是一个负面的东西,偶尔 甚至会影响语言自身的成长 。 这部分是由于教员和书祜作者指望这些前沿的东西过 沽 ---我的工作就是教授这门语言而不管它经常性的变化 , 这任务有点像不时地记录

猫群中猫的数量!无论怎么说,这是一个广泛存在的忧虑 。 我们将在本书中看到 , 虽 Python 起初提出了 “ 保持简单 ” 的主旨,但是在今天 Python 却又常常被归入到追

t.&

求更加复杂觥法 ·朝流中的一员,这当然是以初学者的学习曲线为代价的 。 这本大部头 的书是这一潮流的间接证据 。 另一方面,按大多数标准 Python 仍旧比其他偏程语言简单得多,它至多也只是为了 满足今天所需扮演的种种角色才变得复杂 。 它总体上的连贯性和开放的本质仍是它引

人注目的特征 。 此外,不是所有人都需要站在这一领域的前沿阵地一一Python's 2. X 版本的持续流行很好地说明了这一点 。

作为一名开发者,我有时对 Python 开发的“电池依赖”方法中的内在权衡也会产生质疑 。

Python 对预构建工具的强调有时会增加依赖性(如果你的电池变化了,毁坏了或者弃 用了,会怎么样呢?) , 它对在一般原则下特殊案例觥决方法的鼓励会使用户在长期 受益(当你不知道一件工具的目的,你又如何去评估或是使用它呢?) 。 在本书中 , 我们将会看到关于这两种担忧的例子 。

对于普通的用户,尤其是爱好者和初学者, Python 依赖工具包的特点是一大优势 。 但 当你发现预构建 (precoded) 的工具包不够用时 , 也不必太惊讶,因为你将受益于本

书所传授的相关知识 。 或者,如谚语所云

'`授人以鱼 , 不如授人以渔 。 “本书的工

作很大程度上是教你构达工具而不是仅仅使用工具 。 正如在本章中到处提到的 , Python 和它的工具箱模型也受限于一般开源项目的共有缺

点、 一存在潜在的少数人的个人偏好压倒多数人使用习惯的情况,偶尔还会变得无政

府化甚至精英化 。 这些趋势在 Python 的最新发行版本中正愈发变得严重 。 在本书的末尾,我们将再次回顾这些权衡,在那时你将对 Python 有足够的了觥 , 并 能够得出自己的结论 。 作为一个开源系统, Python” 是 “ 什么由它的用户来定义 。总

的来说,今日的 Python 比以往任何时候都要流行 ,

而它的增长也仍无放 缓 的迹象 。

从某种程度上来讲,这更像是一个事实而不是主观判断 ,

26

I

第 1 章

无论你支持 Python 与否 。

总体来说, Python 从广泛的用户基础和活跃的开发者社区中受益良多。在当今世界最为广 泛使用的编程语言中, Python 可以排在 5~10 名之间(精确的排名随着源码和日期而变化)。 由千 Python 有近 20 余年的发展历史并得到了广泛的应用, Python 也拥有相当的稳定性和

健壮性。 除了被个人用户使用井推进之外, Python 也被一些公司应用千商业产品的开发上。例如, 一 些被人们熟知的 Python 企业用户有:



Google 在其网页搜索系统中大 篮使用了 Python 。



流行的 YouTube 视频分享服务大部分是由 Python 编写的。



Dropbox 存储服务的服务器端和桌面端软件主要都是通过 Python 编写的。



树苺派 (Raspberry P) )单片机推广 Python 作为其教学语言。



EVE Online 这款大 型多人网络游戏 (Massively Multiplayer Online Game, MMOG) 广 泛地使用 Python 。



广为流行的 P2P 文件分享系统 BitTorrent 的起源就 一 个 Python 程序。



工业光魔 (Industrial Light & Magic) 、皮克斯 ( Pixar) 等电影特效制作公司使用 Python 制作动画电影。



ESRI 在其流行的 GIS 地图产品中使用 Python 作为终端用户的定制工具。



Google 的 App Engine 网页开发框架使用 Python 作为其应用程序语言。



lronPort 电 子邮件服务器产品中使用了超过 100 万行的 Python 代码实现其作业 。



Maya 这款强大的集成化 3D 建模和动画系统,提供了一个 Python 脚本编程 API 。



NSA 在加密和智能分析中使用 Python 。



iRobot 使用 Python 开发商用和 军用的机器人设备。



Steam 平台上流行的游戏《文明 IV 》中可定制的脚本化事件是完全用 Python 写的。



One Laptop Per Child (OLPC) 工程使用 Python 构建其用户界面与活动模型。



Netflix 和 Yelp 在它们软件的架构中都记录了 Python 所扮演的角色。



Intel 、 Cisco 、 Hewlett-Packard、 Seagate 、 Qualcomm 和 IBM 使用 Python 进行硬件测试 。



在金融市场预测方面, JPMorgan Chase 、 UBS 、 Getco 和 Citadel 使用 Python 。



NASA 、 Los Alamos 、 Fermilab 、 JPL 等机构使用 Python 实现科学计算任务。

还有许多方面都有 Python 的身影一—尽管这个清单只列举了 一 部分代表, 一 份完整的清单 却远远超出本书的范围,清单本身也无疑会随着时间变化。如果你想获取 一 份最新的新增

问答环节

I

27

Python 用户、应用程序和软件的样本,请浏览下面这些在 Python 站点和维基百科上的网页,

你也可以用喜欢的浏览器进行搜索:



成功案例: http://www.python.org/about/success



应用程序领域: http ://www.python.org!aboutlapps



用户意见: http://www.python.org/about/quotes



维基百科页面: http://en. w巾pedia.orglw ikilList_oj_Python_software

如今贯穿所有使用 Python 公司的唯一 共同思路也许就是: Python 在所有的应用领域几乎无 所不能。 Python 的通用性使其几乎能够应用千任何场合,而不是只能在一 处使用。实际上,

我们这样说也不为过;无论是短期策略任务(例如测试或系统管理),还是长期战略上的 产品开发, Python 都已经证明它能胜任。

使用 Python 可以做些什么 Python 不仅仅是一 个设计优秀的程序语言,它能够完成现实中的各种任务,包括开发者日 复 一 日所做的事情。作为编写其他组件、实现独立程序的工具,它通常应用千各种领域。

实际上,作为一种通用语言, Python 的应用角色几乎是无限的:你可以在任何场合应用 Python, 从网站和游戏开发到机器人和航天飞机控制。

尽管如此, Python 的应用领域仍可大致分为如下几类。下文将介绍一些 Python 如今最常见 的应用领域,以及每个应用领域内用到的 一 些工具。我们不会深入探讨每个工具,如果你 对这些话题感兴趣,可以从 Python 网站或其他一些资源中获取更多的信息。

系统编程 Python 对操作系统服务的内置接口,使其能够成为编写可移植的维护操作系统的管理工具

和部件(有时也称为 Shell 工具)的理想工具。 Python 程序可以搜索文件和目录树 , 可以 运行其他程序,用进程或线程进行并行处理等。 Python 的标准库绑定了 POSIX 以及其他常规操作系统 (OS) 工 具:环境变量、文件、套 接字、管道、进程、多线程、正则表达式模式匹配、命令行参数、标准流接口、 Shell 命令 启动器、文件名扩展、 zip 文件 工具、 XML 和 JSON 解析器、 CSV 文件处理器等。此外, 很多 Python 的系统工具设计时都考虑了其可移植性。例如,复制目录树的脚本无须做任何 修改就可以在几乎所有的 Python 平台上运行。 EVE Online 所采用的 Stackless Python 实现(参

阅第 2 章)还为多处理需求提供 了 高级的解决方案。

28

I

第 1 章

图形用户界面 (GUI) Python 的简洁以及快速的开发周期十分适合开发桌面 GUI 程序。 Python 内置了 tkinter (在 2.X 版本中为 Tkinter) 的标准面向对象接口 Tk GUI API, 使 Python 程序可以生成可移植 的本地化设计感的 GUI 。 Python/Tkinter GUI 不做任何改变就可以运行在微软 Windows 、

X Windows (UNIX 和 Linux) 以及 Mac OS (Classic 和 OS X 都支持)等平台上。一个免 费的扩展包 PMW, 为 Tkinter 工具包增加了一些高级部件。此外,基千 C++平台的工具包

wxPython GUI API 可以使用 Python 构建可移植的 GUI 。 诸如 Dabo 等一些高级工具包是构建在 wxPython 和 tkinter 的基础 API 之上的。通过适当的

库,你可以在 Python 中使用其他的 GUI 工具包。例如,通过 PyQt 使用 Qt, 通过 PyGTK 使用 GTK, 通过 PyWin32 使用 MFC, 通过 IronPython 使用. NET, 以及通过 Jython 或

JPype (Java 版本的 Python, 我们将会在第 2 章中进行介绍)使用 Swing 等。对千运行干 浏览器中的应用或具有一些简单界面需求的应用, Jython 和 Python Web 框架以及服务器端 CGI 脚本(下一节将介绍)都能够作为实现用户界面的选项。

Internet 脚本 Python 提供了标准 Internet 模块,它使得 Python 程序能够广泛地在多种网络任务中发挥作

用,无论是在服务器端还是在客户端。脚本可以通过套接字进行通信;从发送到服务器端 的 CGI 脚本表单中提取信息;通过 FfP 传输文件;解析、生成 XML 和 JSON 文档;发送、 接收、生成和解析 Email, 通过 URL 获取网页;从获取的网页中解析 HTML 文件;通过 XML-RPC 、 SOAP 和 Telnet 等协议进行通信等。 Python 的库使这一切变得相当简单。 不仅如此,从网络上还可以获得很多使用 Python 进行 Internet 编程的第三方工具。例如, HTMLGen 可以从 Python 类的描述中生成 HTML 文件; mod_python 包可以使在 Apache Web 服务器上运行的 Python 程序更具效率,并支持 Python Server Page 这样的服务器端的

网页模板渲染; Jython 系统提供了无缝的 Python/Java 集成,而且支持在客户端运行的服务 器端 Applet 。 此外,雨后春笋般出现的 Python Web 开发工具包,例如, Django 、 TurboGears 、 web2py 、

Pylons 、 Zope 和 WebWare, 使得 Python 能够快速构建功能完善和高质晨的网站。很多这样 的工具包包含了诸如对象关系映射器 (Object-Relational Mapper, ORM) 、模型/视图/控

制器 (Model/View/Controller, MVC) 架构、服务器端脚本和模板系统,以及支持 AJAX 等功能,从而提供了完整的、企业级的 Web 开发解决方案。 最近, Python 扩展并进人到了富互联网应用 (Rich Internet Application, RIA) 领域,可使 用的工具有 Iron Python 的 Silverlight、 pyjs (pyjamas) 和 Python 到 JavaScript 的编译器、

AJAX 框架以及部件集。 Python 通过使用 App Engine 也已进军云计算和其他将在下文数据 库部分提到的领域。互联网发展到哪里, Python 就会很快驻扎到哪里。

问答环节

I

29

组件集成 我们前面介绍 Python 的组件集成角色时,曾把它描述为一门控制语言。 Python 通过 CIC++ 系统进行扩展以及嵌入 CIC++系统的特性,使其能够作为 一 种灵活的黏合语言,可以脚 本化处理其他系统和组件的行为。例如,将一个 C 库集成到 Python 中,就可以让你利用

Python 来进行测试并调用库中的其他组件;将 Python 嵌入到产品中,则可以让你在不需要 重新编译整个产品或分发源代码的情况下,单独对产品进行定制。

SWIG 和 SIP 这样的代码 生成工具可以使已编译组件链接人 Python 便千脚本使用的这 部 分工作自动化,并且 CPython 系统允许允许程序员将代码混合到 Python 和类似 C 的代码

中。此外还有更大 一 些的框架,例如, Python 在微软 Windows 平台上的 COM 支持、基 千 Java 实现的 Jython 、基于 .NET 实现的 IronPython 提供了多种编写组件的替代方式。例 如,在 Windows 中, Python 脚本可利用框架对微软 Word 和 Excel 文件进行脚本处理,访 问 Silverlight 等。

数据库编程 对千传统的数据库需求, Python 提供了对所有主流关系数据库系统的接口,例如, Sybase 、 Oracle 、 Inform ix 、 ODBC 、 MySQL 、 PostgreSQL 、 SQLite 等 。 Python 定义了 一 种通过 Python 脚本存取 SQL 数据库系统的可移植的数据库 API, 这个 API 对千各种底层

应用的数据库系统都是统 一 的。例如,因为厂商的接口实现了可移植的 API, 所以 一 个为

自由软件 MySQL 系统编写的脚本在很大程度上不需改变就可以工作在其他系统上(例如 Oracle) 。一般你只要将底层的厂商接口替换掉就可以实现。还在完善中的 S QLite 嵌入式

SQL 数据库引擎已成为自 2.5 版本后 Python 标准的一部分,它支持快速原型和基本的程序 存储需求。

在非 SQL 部分, Python 的标准 pickle 模块提供了 一 个简单的对象持久化系统:它能够让 程序轻松地将整个 Python 对象保存到文件和类文件载体中,以及从这些载体中恢复。在网

络上,你也可以找到名叫 ZODB 和 Durus 的第 三 方系统,它们为 Python 脚本提供了完整 的面向对象数据库系统;其他诸如 SQLObject 和 SQLA!chemy 系统,实现了对象关系映射 (ORM) ,从而将 Python 的类模型移植到了关系型表; PyMongo 作为 MongoDB 的一个 接口

(MongoDB 是一 种高性能、非 SQL 、开源的 JSON 风格文档数据库),使用了类似千

Python 自有的列表和字典的结构来存储数据,其文本可以使用 Python 自带的标准库 json 模块进行解析和创建。

其他 一 些系统提供更加专业化的数据存储方式,包括 Google App Engine 中的数据存储, 它使用 Python 的类对数据建模井提供良好的扩展性能,此外还涌现了 一 批诸如 Azure 、 PiC/oud 、 OpenStack 和 Stackato 的 云存储方案。

30

I

第 1 章

快速原型 对千 Python 程序来说,使用 Python 或 C 编写的组件看起来都是一样的。正因为如此,我 们可以在 一 开始利用 Python 做系统原型,之后再将组件移植到 C 或 C++这样的编译语言上。

与其他原型工具不同,当确定原型后, Python 不需要重写。系统中不要求执行效率的部分 可以保持不变,从而使维护和使用变得轻松起来。

数值计算和科学计算编程 Python 也被大晕地应用千数值编程,虽然这是 一 个不被传统脚本语言含纳和考虑的领域, 但是却逐渐成长为 Python 最具竞争力的使用案例。我们之前提到过的著名的 NumPy 数值 编程扩展包括很多高级工具,例如,矩阵对象、标准数学库的接口等。通过将 Python 与出 千速度考虑而使用编译语言编写的数值计算的常规代码进行集成, NumPy 将 Python 变成一

个续密严谨并简单易用的数值计算工具,这个工具通常可以替代已有的代码,而这些代码 都是用 FORTRAN 或 C++等编译语言编写的。

其他一 些数值计算工具为 Python 提供了动画、 3D 可视化、井行处理等功能的支持。例如, 常用的 SciPy 和 ScientificPython 扩展,使用 NumPy 作为核心组件并为科学编程工具提供

了额外的库。 Python 的 PyPy 实现(将在第 2 章讨论)也受到了数值计算领域的极大追捧, 部分原因是数值领域这种类型的重型算法代码在 PyPy 中会运行得不可思议得快,通常会快

LO 倍甚至 100 倍。

更多内容:游戏、图像、数据挖掘、机器人、 Excel 等 Python 的应用领域很多,远比这里可涵盖的多得多。例如,你可以找到工具使用 Python 完 成下面的工作:



利用 pygame 、 cgkit 、 pygle( 、 PySoy 、 Panda3D 等进行多媒体和游戏编程。



使用 PySerial 扩展在 Windows 、 Linux 以及更多平台上进行串口通信。



用 PIL 和它的新分支 Pillow 、 PyOpenGL 、 Blender 、 Maya 等工具进行图像处理。



用 PyRo 工具包进行机器人控制编程。



使用 NLTK 包进行自然语言分析。



在树苺派 (Raspberry Pi)



在谷歌安卓和苹果 iOS 系统上用 Python 提供的端口进行移动计算。



使用 PyXLL 和 DataNitro 插件实现 Excel 工作简函数和宏编程。



使用 PyMedia 、 ID3 、 PILIPillow 等进行媒体文件内容和元数据标签的处理。



使用 PyBrain 神经网络库和 Milk 机器学习工具包进行人工智能编程 。

和 Arduino 板上进行设备化。

问答环节

I

31



使用 PyCLIPS 、 Pyke 、 Pyrolog 和 pyDatalog 进行专家系统编程。



网络监管使用的 zenoss 是用 Python 编写和定制的.



使用 ReportLab 、 Sphinx 、 Cheetah 、 PyPDF 等进行文档处理和生成。



使用 Mayavi 、 matplotlib 、 VTK、 VPython 等进行数据可视化。



使用 xml 库包、 xmlrpclib 模块和其他 一 些第三方扩展进行 XML 解析。



使用 json 和 csv 模块进行 JSON 和 CSV 文件的处理。



使用 Orange 框架、 Pattern 包、 Scrapy 和定制代码进行数据挖掘。

你甚至可以使用 PySolFC 程序下棋娱乐。当然,你总是可以在那些不怎么流行的行当里编

写自己个性化的脚本 ,帮助你进行每日的系统管理 、邮件处理、文档和媒体库的整理等。 可以从 PyPI 网站或通过网络搜索(搜索 Google 或在 http:/!www.python.org 上获得具体链接) 找到这些领域的更多支持的链接。 尽管有着广阔的实际应用,这些特定领域当中有许多在很大程度上都是 Python 组件集成

角色的再次例证。为采用 C 这样编译语言编写的库组件添加 Python 作为前端的方式,使 Python 在不同领域广泛地发挥其自身价值。作为 一 种支持集成的通用型语言, Python 的应

用极其广泛。

Python 如何开发并获得支持 作为一个流行的开源系统, Python 拥有一个很大且活跃的开发社区,它以令众多商业软件 开发者认为不凡的速度进行版本更新和开发改进。 Python 开发者使用 一 个源代码控制系统

在线协同地工作。对语言的修改要遵从一个包括写入 PEP (Python Enhancement Proposal, Python 增强提案)或其他文档的正式协议开始,直到通过 Python 的回归测试系统为止的协 议。实际上,今天修改 Python 差不多已经和修改商业软件一样,与早期的 Python 大不相同, 那时候,只需要给 Python 的创始人发一封 E-mail 就够了,但在当今用户最巨大的情况下,

这种新的修改方法更有优势。 一 个正式的非盈利组织 PSF (Python Software Foundation, Python 软件基金会),负责组

织会议并处理知识产权的问题。世界各地举办了大量的 Python 会议, O'Reilly 的 OSCON 和 PSF 的 PyCon 是其中最大的会议。前者还涉及多个开源项目,后者则是专门的 Python 会议,并且近年来规模显著扩大。 PyCon 2012 和 2013 两届的与会者都达到了 2500 人。事 实上, PyCon 2013 在经历了 2012 年令人惊讶的门票售罄(并成功地同时吸引到技术和非 技术公众的广泛关注,我这里不再赘述)之后,已经在这关注度方面几乎达到了极限。早 些年经常会看到与会者翻倍的情况一例如,从 2007 年的 586 名参会者增加到 2008 年的

超过 1000 名,这会给保有多年前回忆的人留下深刻印象,因为早年的与会者只能坐满餐厅 的 一个圆桌。

32

I

第 1 章

开源的权衡 值得注意的是,尽管 Python 拥有着活跃的开发社区,但这也伴随着天生的弊端。开源软件 有时也可以变得糟糕甚至是混乱不堪,并不总是像前文暗示的那样顺利地得到实现。一些 修改也许仍然会与官方协议产生分歧,不过尽管有着过程控制,正如同人类在其他领域的

努力 一样:错误在所难免(例如, Python 3.2.0 在 Windows 下有 一 个失败的控制台 input

函数)。 此外,开源软件牺牲商业利益来换取当下开发者的个人喜好,而别人的个人喜好可能与你 相同,也可能不同。你不再是受 一 个大公司摆布的人质,但却也有可能成为那些能够有时

间去改变系统的人的提线木偶 。 总的效果就是开源软件的演化由少数人推进,但施加到多 数人身上。 尽管如此,在实践中与那些使用已经成熟的系统版本(包括 Python 2. X 和 Python 3.X 的前 期版本)的人相比,这些权衡对那些总是对新版本跃跃欲试的人来说冲击更大。举例来讲,

如果你坚持使用 Python 2.X 中的经典类,你很可能不大关心在 21 世纪头几年里类功能的爆 炸式增加和新式类模型。在 3.X 版本中,这些已经成为强制的语言特性,很多 2 . X 的用户

却可以乐意地绕开这些话题。

Python 有哪些技术上的优点 显然,这是开发者关心的问题。如果你目前还没有程序设计背景,接下来这些内容可能会

显得有些令人费解:别担心,在本书中我们将会对这些内容逐一做出详细解释。不过对千 开发者来说,这将是对 Python 一些最优的技术特性的快速介绍。

面向对象和函数式 从根本上讲, Python 是一种面向对象的语 言 。它的类模型支持多态、运算符重载和多重继 承等高级概念,井且以 Python 特有的简洁的语法和类型为背景, OOP 十分易千使用。事实

上,即使你不懂这些术语,仍会发现学习 Python 比学习其他 OOP 语言 要容易得多。 除了作为 一 种强大的代码组织和重用手段以外, Python 的 OOP 本质使它成为其他面向对象

系统语言的理想脚本工具。例如,通过适当的粘接代码, Python 程序可以对 C++、 Java 和 C#的类进行子类的定制。

OOP 只是 Python 的 一个选择而已,这一 点非常重要。即使不能立马成为 一 个面向对象高手,

但你同样可以继续探人学习 。 就像 C++ 一 样, Python 既支持面向对象编程也支持面向过程 编程的模式。如果条件允许,其面向对象的工具可以立即派上用场。这对策略开发模式十

分有用,该模式常用 于软件开发的设计阶段。

问答环节

I

33

除了最初的过程式(语句为基础)和面向对象(类为基础)的编程范式, Python 在最近几 年内置了对函数式编程的支持一一 一 个多数情况下包括生成器、推导、闭包、映射、装饰器、

匿名 lambda 函数和第一 类函数对象的集合。这是对其本身 OOP 工具的补充和替代。

免费 Python 的使用和分发是完全免费的。就像其他的开源软件一 样,例如, Tel 、 Perl 、 Linux 和 Apache 。你可以从 Internet 上免费获得 Python 的源代码。你可以不受限制地复制 Python,

或将其嵌入你的系统或者随产品 一起发布。实际上,如果你愿意的话,甚至可以销售它的 源代码。

但请别误会:

“免费”并不代表“没有支持“。恰恰相反, Python 的在线社区对用户需求

的响应和商业软件一样快。而且,由千 Python 完全开放源代码,提高了开发者的实力,并

产生了 一 个很大的专家团队。尽管研究或改变一种程序语言的实现并不是对每 一 个人米说 都那么有趣,但是当你知道如果需要的话可以做到这些,该是多么的令人欣慰。你不希要

去依赖商业厂商的智慧,因为最终的文档和终极的净土(源码)任凭你的使用。 Python 的开发是由社区驱动的,是 Internet 大范围的协同合作努力的结果。这个团体包 括 Python 起初的创始者 Guido van Rossum 、 Python 社区内公认的“终身的慈善独裁者”

(Benevolent Dictator for Life, BDFL) 。 Python 语言的改变必须遵循一套规范而有约束力 的程序(称作 PEP 流程),并需要经过规范的测试系统和 BDFL 进行彻底检查。正是这样 才使得 Python 相对千其他语言和系统可以保守地持续改进。尽管 Python 2 . X 和 Python 3.X 版本之间的分裂有力并蓄意地破坏了这项传统,但通常它仍然体现在 Python 的这两个系列 内部。

可移植 Python 的标准实现是由可移植的 ANSI C 译注 1 编写的,可以在目前所有主流平台上编译和 运行。例如,如今从掌上电脑 (PDA) 到超级计算机,随处可见 Python 的运行。 Python 可

以在下列平台上运行(这里只是部分列表)



Linux 和 UNIX 系统



微软 Windows (所有现代版本)



Mac OS (包括 OSX 和经典版)

译注 I:

ANSI C (又称 1S0 C, 或标准 C) ,是指由美国国家标准学会 (American National Standards Institute) 和国际标准化机构 (International Organization for Standardization) 发 布的 C 语言的一系列标准 。 历史上, ANSI C 特指原版以及最佳支持版的 C 语言标准(也

称为 C89 或 C90) .

34

I

第 1 章



BeOS 、 OS/2 、 VMS 和 QNX



实时操作系统,例如 VxWorks



Cray 超级计算机和 IBM 大型机



运行 Palm OS 、 PocketPC 和 Linux 的 PDA



运行 Symbian OS 和 Windows Mobile 的移动电话



游戏终端和 , Pod



运行谷歌安卓系统和苹果 iOS 系统的 平板和智能手机



以及更多

除了语言解释器本身以外, Python 发行时自带的标准库和模块在实现上也都尽可能地考虑

到了跨平台的移植性。此外, Python 程序自动编译成可移植的字节码,这些字节码在已安 装兼容版本 Python 的平台上运行的结果都是相同的(更多详细内容将在第 2 章中介绍)。 这些意味着 Python 程序的核心语言和标准库可以在 Linux 、 Windows 和其他带有 Python 解

释器的平台上无差别地运行。大多数 Python 外围接口都有平台相关的扩展(例如 COM 支 持 Windows) , 但是核心语言和库在任何平台都 一样。就像之前我们提到的那样, Python

还包含了 一 个叫作 tkinter (Tkinter 的 2.X 版本)的 Tk GUI 工具包,它可以使 Python 程序 实现功能完整的,无须做任何修改即可在所有主流 GUI 桌面平台运行的用户图形界面。

功能强大 从语 言 特性的角度来看, Python 是 一 个混合体。它丰富的工具集使它介于传统的脚本语言(如 Tel 、 Scheme 和 Perl) 和系统语言(如 C 、 C++和 Java) 之间。 Python 提供了所有脚本语

言 的简单和易用性,井且具有那些在编译语 言 中才能找到的高级软件工程工具。不像其他 脚本语言不同,这种结合使 Python 在长期大型的开发项目中十分有用。下面是一些 Python 工具箱中的工具简介 : 动态类型

Python 在程序运行过程中跟踪对象的类型,不 需要代码中进行关于复杂的类型 和大小 的声明。事实上,你将在第 6 章中看到, Python 中没有类型或变批声明这种做法。因 为 Python 代码 不约束数据的类型 ,它 往往自动地应用了一种广义上的对象。 自动内存管理 Python 自动为对象分配空间,井且当对象不再使用时将自动撤销空间(“垃圾回收”),

当需要时自动扩展或收缩。正如你将学到的, Python 能够 帮你完成底层的内存管理。 大型程序支持

为了能建立更大规橾的系统, Python 包含了模块、类和异常等 工具。 这些工具允许你

问答环节

I

3s

把系统组织为组件,使用 OOP 重用并定制代码,并以 一 种优雅的方式处理事件和错误。 前面提到的 Python 函数式编程工具,提供了实现相同目标的其他方法。 内置对象类型 Python 提供了常用的数据结构作为语言的基本组成部分。例如,列表 (list) 、字典

(dictionary) 、字符串 (string) 。我们将会看到,它们灵活并易千使用。例如,内置 对象可以根据需求扩展或收缩,可以任意地组织复杂的信息等。 内置工具 为了对以上对象类型进行处理, Python 自带了许多强大的标准操作,包括拼接

(concatenation) 、分片 (slice) 、排序 (sort) 和映射 (mapping) 等。 库工具 为了完成更多特定的任务, Python 预置了许多预编码的库工具,从正则表达式匹配到

网络都支持。当你掌握了语言本身,就能在应用级的操作中使用 Python 的库工具。 第三方工具 由千 Python 是开源的,它鼓励开发者提供 Python 内置工具之外的预编码工具。你可以

在网上找到 COM 、图像处理、数值编程、 XML 、数据库访问等许多免费的支持工具。 除了这一系列的 Python 工具外, Python 保持了相当简洁的语法和设计。综合这一切得到的 就是一个具有脚本语言所有可用性的强大编程工具。

可混合 Python 程序可以以多种方式轻易地与其他语言编写的组件”粘接”在一起。例如, Python 的 C 语言 API 可以帮助 rython 程序灵活地调用 C 程序。这意味着可以根据需要给 Python

程序添加功能,或者在其他环境系统中使用 Python 。 例如,将 Python 与 C 或者 C++写成的库文件混合起来,使 Python 成为一个前端语言和定

制工具。就像之前我们所提到过的那样,这使 Python 成为一个很好的快速原型工具,系统 可以在开发初期出于速度考虑使用 Python 实现,然后转移至 c, 根据不同时期性能的需要 逐步实现系统。

相对简单易用 同其他语言(如 C+ +、 Java 和 C#) 相比, Python 编程对大多数用户来讲出奇得简单。要 运行 Python 程序,你只需简单地键人 Python 程序并运行就可以了。不需要其他语言(如 C 或 C+ +)所必需的编译和链接等中间步骤。 Python 可立即执行程序,这形成了一种交互式 编程体验和不同情况下快速调整的能力,往往在修改代码后儿乎能立即看到程序改变后的

效果。

36

I

第 1 章

当然,开发周期短仅仅是 Python 易用性的 一 方面的体现。 Python 提供了简洁的语法和强大 的内置工具。实际上, Python 曾被称为“可执行的伪代码”。由千它减少了其他工具常见

的复杂性,在实现相同的功能时, Python 程序比采用其他流行语言编写的程序更为简单、 小巧,也更灵活。

相对简单易学 这 一部分引出了本书的重点:尤其同其他广泛使用的编程语言比较时, Python 语言的核心 相当简单易学。实际上,如果你是 一 位有经验的程序员,你可以期望在几天内写出小规模 的 Python 代码,你也许能在几个小时之内习得 Python 的一招 一 式,但是你井不能指望在

如此短的时间内成为专家(忘掉市面上的那些宣传广告吧)。 当然,掌握任何像今天 Python 这样的充实 主题都不是一件轻松事,我们将在本书的剩余部 分致力于此项任务。但是为了掌握 Python 而进行的真正投资是非常值得的-最终你会获

取几乎在每个计算机应用程序领域都适用的编程技能。此外,很多人还发现 Python 的学习 曲线比其他的编程语 言 更加平缓。

这对千那些想学习语言以在工作中应用的专业人员来说是一个好消息,同样对千那些使用 Python 层进行定制和控制的系统的终端用户来说,也是一 个好消息。如今,许多系统都依 赖千这一事实 : 用户可以在没有或者得到很少支持的情况下就学到足够的 Python 知识以便 当场增删他们的 Python 定制化代码。此外, Python 还孕育出一群不以编程为生而以编程为 乐的用户,他们井不需要掌握全面的软件开发技巧。尽管 Python 还是有很多高级编程工具, 但不论对初学者还是行家来说, Python 的核心语言精髓仍是相当简单的。

以 Monty Python 命名 好的,在讲完这么多技术方面的优势后,我想再揭露一个 Python 世界里面令人惊奇而保 守良好的小秘密。尽管 Python 的书和图标中有很多爬行动物,真相却是 Python 以英国喜 剧组 “Monty Python” 命名 一一这是 BBC 在 20 世纪 70 年代喜剧《 Monty Python's Flying Circus》的制片方,也是至今仍在流行的少扯包括《Monty Python and the Holy Grai 》在内

的大电影的制片方。 Python 的最初创作者是 Monty Python 的粉丝,这同其他许多的软件开 发者一样(事实上,这两个领域存在某种对称性......)。

这段有趣的历史无疑增加了 Python 代码例子的幽默属性。例如,作为一般变最名命名传统 的 "foo” 和 “bar” 在 Python 世界中变成了 “spam" 和 “eggs" 。而在 Python 中偶尔出现

的 “Brian",

"ni” 和 “s hrubbery" 表现得也同此类似。它甚至影响了 Python 的整个社区:

在 Python 会议上的一些事件经常被戏称为“西班牙审讯"译注 20 译注 2:

《西琪牙审讯》

(The Spanish

Inquisition) 是 Monty Python 出品的荒诞电 视剧中的一集。

该剧讽刺了中世纪西泉牙天主教审讯的残酷过程 。

问答环节

I

37

当然了,如果你对这部喜剧非常熟悉,就能体会这其中的笑点,但如果不熟悉则相反。你 不必非得熟悉 Monty Python 这部剧来了解从剧中获得灵感的例子(包括你将在本书中看到

的许多例子),但至少你现在知道它们的起源了。

(嗨——我已经告诉你啦。)

Python 和其他语言比较起来怎么样 最后,你也许已经知逍 f ,人们往往将 Python 与 Perl 、 Tel 和 Javat 等语言相比较。这部分

总结这方面的 一 些普遍共识。 我恕预先表明我个人并不喜欢通过祗毁竞争者来获胜——这在长期是行不通的,而且也不 是这里的目的。此外,这并不是一场零和游戏一绝大多数的程序员在 他们的职业 生涯中

都会使用许多语言。尽管如此,编程工具也展示出值得考虑的选择和权衡。毕竞,如果 Python 没有比它的竞争者提供更多的东西,那么它一 开始就不会被人们使用了。 我们之前已经介绍过性能上的权衡,那么这里重点谈一下功能。尽管下面列举的这些语言 也是值得学习和使用的有力 工具,但 人们通常认为 Python:



比 Tel 强大 。 Python 强有力地支持“大规模编程”,使其适用千开发大型系统,它的应

用程序库也更加丰富。



比 Perl 更具可读性。 Python 有若简洁的语法和简单连贯的设计,这反过来使得 Python 更具可读性和更易于维护,同时有助千减少程序 bug 。



比 Java 和 C#更简单、更易千使用。 Python 是 一 门脚本语 言,但 Java 和 C#两者从像

C++这样更加大型的 OOP 系统语言中继承了许多语法和复杂性。



比 C++更简单、更易千使用。 Python 代码比等效的 C++代码更加简单,长度只有其五 分之 一 到 三 分之一。尽管作为脚本语言, Python 有时能扮演许多不同的角色。



比 C 更加简单和高级。 Python 远离底层硬件架构从而降低了代码复杂性,拥有更好的 组织结构,并比 C (C++的祖先)更加友善。



比 Visual Basic 更强大,用途广泛,也更具备跨平台特性。 Python 是更加广泛使用的更 卡宫的语言,它的开源本质意味若它不可能披某一个公司所掌控。



比 PHP 更易懂井且用途更广。 Python 也用来构建 Web 站点,但是,它也应用于几乎每 个计符机领域,从机器人到电影动画和游戏。



比 JavaScript 更强大和用途广泛。 Python 有 一 个更大的工具焦,也并不是牢牢地束缚 千 Web 开发。它也用于科学建模、仪器调试等。



比 Ruby 更具可读性,并更为人们所接受。 Python 的语法混乱更少,尤其在较复杂代码 中,同时它的 OOP 对用 户和和不太使用 OOP 的 工程中是完全可选的。

38

I

第 1 章



比 Lua 更成熟和受到更广泛关注 o- Pythoa 更加庞大的特性集合和更加扩展的库支持给

予其比 Lua (一门和 Tel 一样的嵌入式”胶水”语言)更加宽广的视野。



比 SmallTalk 、 Lisp 和 Prolog 更不晦涩。 Python 拥有这类函数式语言的动态品味,但是 也拥有开发者和定制系统终端用户都可接受的传统语法。

特别是对不仅仅用千个人扫描文本文件,未来会被人们(包括你在内)读到的程序而言,

很多人会发现 Python 比目前任何可用的脚本或编程语言都划得来。不仅如此,除非你的应 用要求最尖端的性能, Python 往往是 C 、 C++和 Java 等系统开发语言的一个不错的替代品:

Pythoa 代码能够常常实现相同的目标,却会减少很多编写、调试和维护的麻烦。 当然,本书的作者从 1992 年就已经是 Python 的正式布道者了,所以尽可能接受这些意见 吧(其他语言的拥护者的利益可能会受到些损失)。然而,所有这些观点的确代表了投入 时间和精力来探索 Python 的众多开发者的 一致看法。

本章小结 以上是本书的“宣传炒作”部分。本章中我们探索了人们选择 Python 完成他们编程任务的 原因,也看到了它实现起来的效果以及当前一些具有代表性的使用 Python 的鲜活案例。然 而我们的目标是教授 Python ,而不是推销它。判断一门语言的最好方法就是在实践中使用它,

所以本书后面的部分将把注意力集中到我们已经在这里简要介绍过的那些语言的细节特性

上。 接下来两章将进行语言的技术性介绍。我们将研究如何运行 Python 程序,窥视 Python 字 节码执行的模式,并介绍保存代码的模块文件的基本概念。目的就是让你能够运行本书其

他部分的例子和练习。直到第 4 章我们才会开始真正的编程,但在此之前,请确保你已经 掌握了继续深人学习的细节。

本章习题 在本书中,我们每一章都会以一个与该章内容有关的快速的开卷小测验作为结束,从而帮 助你复习 一 章中的关键概念。问题的答案会紧随问题之后,建议你独立完成测验后马上查 看参考答案,因为它们有时会给出正文中没有介绍的有用的背景知识。 除了这些每章结尾的测验以外,你还会在本书每一部分的结尾找到一些实验作业,这些作

业是为了帮助你自己动手用 Python 进行编程而设计的。好了,这就是你的第 一 次测验。你

可以根据需要参考本章内容,祝你好运!

I.

人们选择 Python 的 6 个主要原因是什么?

2

请列举如今正在使用 Python 的 4 个著名的公司和组织的名称。

问答环节

I

39

3.

出千什么样的原因会让你在应用中不使用 Python 呢?

4.

你可以用 Python 做什么?

5.

在 Python 中 import this 的表述有什么含义?

6

为什么 “spam" 出现在网上和书中的许多 Python 例子中出现?

7.

你最喜欢的颜色是什么?

习题解答 做得怎么样?这里是本书提供的答案。当然,测验中 一些问题的答案并不唯一。再 一 次强调,

尽管你会认为自己的回答是正确的,我还是建议你参考 一 下我的答案以获得额外的内容。 如果这些答案对干你来说不合理的话,可以在本章的内容中找到详细的信息。

l

软件质量、开发者效率、程序可移植性、标准库支持、组件集成和编码乐趣,其中质 批和效率这两条是人们选择 Python 的主要原因。

2.

谷歌 (Google) 、工业光魔 (Industrial Light & Magic) 、 CCP 游戏、 Jet Propulsion Labs 、 Maya 和 ESRI 等,今天从事软件开发的所有组织几乎都以某种程度使用着

Python , 无论是长期战略产品开发,抑或测试、系统管理这样的短期策略任务都有 Python 的身影。

3.

Python 的主要缺点是它的性能:它不像 C 和 C++这类常规的编译语言运行得那么快。

另一方面,它对于绝大多数应用已经足够快了,并且典型的 Python 代码运行起来速度 接近 C, 因为在 Python 解释器中调用链接了 C 代码。如果速度要求很苛刻的话,应用 的数值处理部分可以采用编译好的扩展以满足应用要求。

4.

你几乎可以在计算机上的任何方面使用 Python: 从网站和游戏开发到机器人和航天飞

机控制。

5.

这在脚注中有提到:在 Python 中运行 import this 会触发内部的一 个彩蛋,它将显示 Python 语言层面之下的设计哲学。下 一章你会学习如何使用这条命令。 "spam” 引用自 Monty Python 剧团的一部著名的荒诞剧,剧中人们试图 在自助餐厅点餐,

6.

却被歌颂火腿的维京人的合唱声淹没。对了,这也是 Python 脚本中的 一 个普通变量名。

7.

蓝色。不,黄色!

(参考前一个问题。)译注 3

译注 3 : 这个问题以及回答出自短剧《Monty

Python And The Holy

Grail 》,被认为是 Monty

Python 剧团最有代表性的幽默桥段 。故事中大概讲述了在通过一庄桥的时候,一个宁桥人 会问三个问题:你叫什么?你干什么?你最喜欢的颜色是什么?而笫三个问题因为与过桥 一事完全无关而引人发笑 。 值得一提的是 , 现在 Python 官方的图标是由两条蟒蛇组成的 十字,而两条蛇的颜色正是篮色与黄色。

40

I

第 1 章

Python 是工程,不是艺术 当 Python 于 20 世纪 90 年代初期出现在软件行业的舞台上时 , 曾经引发其拥护者和 另一个受欢迎脚本语言 Perl 的拥护者之间的冲突、而今天这已成为有关编程语言争论 的一个经典例子 。 我们认为今天这种争论是令人厌倦且没有权据的,因为开发人员都 很聪明 ,

而且可以得出他们自己的结论 。

然而,这是我在培训课程上时常被问到的问

题之一,同时也表明了人们选择使用 Python 的主要理由之一 ;

因此我在这里再对这

几个问题多说几句 。

故事是这样的:

“ 你可以用 Python 做到一切用 Perl 能做到的事,但是 , 做好之后 ,

还可以阅读自己的程序代码 。” 这就是问题所在,两者的领域大部分重叠 ,

但是 ,

Python 更专注于产生可读性的代码 。 就大多数人而言, Python 强化了可读性,转换为 了代码可重用性和可维护性,使得 Python 更迫合用于那些不是写一次就丢掉的程序 。

Pe rl 程序代码很容易写 , 但是可能会很难读 。 由于多数软件在最初的创建后都有较长 的生命周期,所以很多人认为 Python 是史有效的工具 。

这个故事反映出两门语言的设计者的背景 。 Python 出自训练有素的数学家之手,他似 乎自然而然地创造出来一门具有高度一致性和连续性的正交语言 。 Perl 语言由一位语

言学家孕育,他创建了一种接近自然语言的编程工具,并拥有着上下文敏感性和广泛 变化性 。 就像芳名的 Perl 所说的格言 :

Perl 语言及其用户社群在编写代码时 ,

“ 完成的方法不止一种 。 “有了这种思维,

已经历史性地被鼓励脱缰式的表达式自由化 。

一个人的 Perl 代码可能和另一个人的完全不同 。 事实上 , 编写独特、充满技巧性的代

码,

常常是 Perl 用户引以为傲的事 。

但是,做过任何实质性的代码维护工作的人,应该都可以证实,表达式自由度是很棒 的艺术 , 但是,对工程项目来说就令人仄恶了。在工程世界中,我们需要最小化功能 集和可预测性。而表达式自由度会造成维护的噩梦 。 不止一位 Perl 用户向我们透露过, 太过自由的结果通常就是程序很容易重头写起,但修改起来就不是那么容易了 。 这对 工程来说显然就不是那么理想了 。

比如:当人们在绘画或雕塑时 , 他们主要是为自己所做 。 其他人日后去修改他们作品 的可能性很低 。 这是艺术和工程之间关键的差异 。

当人们在编写软件时 , 他们不是为

自己写 。 事实上 , 他们甚至不是专门为计算机编写的 。 实际上 , 优秀的程序员知道 , 代码是为下一个会阅读它而进行维护或重用的人编写的 。 如果那个人无法理斛代码,

在现实的开发场景中 , 就 毫 无用处了 。 换句话说,编程并不事关聪明与深奥-它的 关键是让你的程序能够清晰地表达它的意图 。

这种对可读性的关注就是很多人认为 Python 最有别于其他脚本语言的地方 。

因为

Python 的语法模型几乎会迫使用户编 写 具有可读性的代码 , 所以 Python 程 序会引导

问答环节

I

41

用户向完整的软件开发徙环流程前进 。 此外,因为 Python 强调了诸如有限互动、代 码统一性,以及特征一致性等理念,它会史进一步促进首次编写后能够长期使用的代

码。

长期以来, Python 本身专注于代码质量,提高了程序员的生产力以及程序员的满意度 。 Python 程序员也变得拥有奔放的创意,我们之后也会看到,语言本身的确为某些任务 提供了多种觥决办法 一一有时甚至比今天它能做到的史多,这也是本书中我们会直面 的一个问题 。 事实上 , 这个边栏也可以被斛读为一氐警戒寓言:编码质量其实是脆弱的、

与其依赖于技术,不如史多地依靠人 。 Python 已经历史性地鼓励优秀的工程方式 , 这 是其他脚本语言通常所不具各的,但是接下来的品质故事需要你来书写 .

至少,这是许多使用 Python 的人之间所达成的某些共识 。

当然,你需要通过学习

Python 的内容来形成自己的观点 。 为了帮助你入门 , 让我们进行下一章的学习吧 。

42

I

~,章

第 2 章

Python 如何运行程序

本章和下一章将给出程序执行的简要说明,包括你应该如何开始编写代码以及 Python 是如

何运行代码的。本章我们将学习 Python 解释器通常是如何执行程序的。第 3 章将教你如何 编写自己的程序并执行。 首先介绍的内容本质上是与平台相关的,本章和第 3 章的部分内容也许并不适合你目前工 作的平台,所以当你觉得所讲的内容与希望使用的平台不相关的话,你可以放心地跳过这

些内容。同样,对千 一 些高级用户的读者,也许过去已经使用过类似的工具并希望快点尝 尝 Python 的甜头,也许可以保留这些章的内容”以备以后参考”。对于其他读者,让我们

在学习写程序之前,先看看 Python 是如何运行我们的代码的。

Python 解释器简介 迄今为止,我大多数时候都是将 Python 作为一门编程语言来介绍的。但是,从目前的实现 上来讲, Python 也是 一 个名为解释器的软件包。解释器是一 种让其他程序运行起来的程序。 当你编写了一段 Python 程序, Python 解释器将读取程序,并按照其中的命令执行,得出结

果。实际上,解释器是代码与机器的计算机硬件之间的软件逻辑层。 当 Python 包安装在机器上后,它会生成一些组件:至少包括一个解释器和一套 支持库 。 根

据使用情况的不同, Python 解释器可能采取可执行程序的形式,或是作为链接到另 一 个程 序的一系列库。根据选用的 Python 版本的不同,解释器本身可以用 C 程序 实现,或一些

Java 类实现,或者其他的形式。无论采取何种形式,编写的 Python 代码必须在解释器中运行。 当然,为了实现这一点,首先必须要在计算机上安装 Python 解释器 。

根据平台的不同, Python 的安装细节也不同,若想深入了解,请参照附录 A 。简而言之:

43



Windows 用户可通过获取并运行自动安装的可执行文件,把 Python 安装到自己的机器 上。双击后在所有的弹出提示框中选择“是”或“继续”即可。



Linux 和 Mac OS X 用户也许巳经拥有了 一 个可用的 Python 预先安装在了计算机上:

如今 Python 已成为这些操作系统的标准组件。



一 些 Linux 用户和 Mac OS X 用户(和大多数 UNIX 用户 一 样)可从 Python 的完整源 代码分发包中编译安装。



Linux 用户也可以找到 RPM 文件, Mac OS X 用户可以找到各种特定千 Mac 的安装包。



其他平台有着对应其平台的不同的安装技术。例如, Python 可以在移动电话、平板电脑、 游戏终端和 iPod 上使用,但是由千其安装方法的差异很大,在这里就不详细介绍了。

我们可以通过 Python 官方网站 (http://www.python.org) 下载获得 Python, 也可以在其他 的 一 些发布网站上找到。记住应该在安装 Python 之前确认 Python 是否已经安装 。 如果是 在 Windows 7 或更早的 Windows 版本上工作, 一 般可以在开始菜单中寻找 Python, 如图 2-1 所示。这些菜单的选项将在下一章进行讨论。在 UNIX 或 Linux 上, Python 也许在 lusr 目录下。

, Mamtenance , Mocrosoft 0仕ce MICmmftS,压汕ght

Mozilla So且心rd

,

PlayMem“心凶沁

,

Python 2.7



Python 江 Python 辽

Python 辽

I'IOlE (Python GUO 产 Module Docs .



-- 中一位>ll1INnd I'芒

__

_

_ _

咖咖咖 一

~ Python Manuals

量 Un1nstall Python Re知 for

PC

Roxie Creator U ,艾ype

Sony

`

I Startup 8ack

图 2-1: 本图展示在 Windows 7 和之前的 Windows 版本上安装好 Python 后, Python 在开始

菜单中显示的情况。也许在不同的版本之间会有少许不同,但是 IDLE 会启动一个 GUI 开发环境, 而 Python 会启动一个简单的交互式会话。同肘,这里有一些标准的手册,以及 Pydoc 的文档 引擎 (Docs 模块)。关于 Windows 8 和其他平台的指南,详见第 3 章和附录 A

44

I

第2章

由千安装细节具有很大的平台相关性,所以我们将延后这部分内容的讨论。若想获得更多

安装过程的细节,请参考附录 A 。为了继续本章及下一章的内容,我将假设你已经安装好 Python 井且准备开始使用了。

程序执行 编写或运行 Python 脚本的意义是什么呢?这取决于你是从程序员还是 Python 解释器的角 度去看待这个问题。无论从哪一个角度来看问题,这都会提供给你一个观察 Python 编程的 重要视角。

程序员的视角 就最简单的形式而言,一个 Python 程序仅是一个包含 Python 语句的文本文件。例如,下 面这个命名为 scriptO.py 的文件,是我们能够想到的最简单的 Python 脚本,但它是 一 个功

能完整的 Python 程序:

print('hello world') print(2 ** 100) 这个文件包含了两个 Python print 语句,在输出流中简单地打印一个字符串(引号中的文字) 和 一 个数学表达式的结果 (2 的 100 次方)。不用为这段代码中的语法担心,我们这一章 的重点只是能够让它运行。本书的后面章节将会解释 print 语句,以及为什么在 Python 中 可以计算 2 的 100 次方而不溢出。

你可以用任何自己喜欢的文本编辑器创建由这样的语句所组成的文件。按照惯例, Python 文件是以 PY 结尾的。从技术上来讲,这种命名方案在被“导入”时才是必需的(“导入“ 概念将在下一章中介绍),但是绝大多数 Python 文件为了统一都是以.PY 命名的。 当你将这些语句输入到文本文件后,必须告诉 Python 去执行这个文件。也就是说,从头至 尾按照顺序一个接一个地运行文件中的语句。正如下一章你将看到的那样,可以通过命令行,

从 IDE 中点击其图标或者其他标准技术来运行 Python 程序。顺利的话,在执行文件时,将 会看到这两个打印语句的结果显示在屏幕的某处:一般默认是显示在运行程序的那个窗口。

hello world 1267650600228229401496703205376 例如,这就是我在一个 Windows 笔记本的命令行窗口运行这个脚本的结果,保证上面的语 句没有低级的拼写错误。

C:\code> python scripto.py hello world 1267650600228229401496703205376

Python 如何运行程序

I

4s

你可以参考第 3 章来了解这个过程的详细讲解,尤其当你是 一 个编程新手的话 1 我们将在

第 3 章中对编写和执行程序的细节进行讨论。就现在而言,我们刚刚运行了 一 个打印字符 串和数字的 Python 脚本。我们也许不会因为这段代码获得任何编程大奖,但是这对掌握一 些程序执行的基本概念已经足够了。

Python 的视角 上 一节的简要介绍对千脚本语言来说是相当标准的,并且通常绝大多数 Python 程序员只需 要知道这些就足够了。你在文本文件中输入代码,之后在解释器中运行这些代码 。 然而, 当 Python”运行”时,透过表面,还有 一 些事情发生。尽管了解 Python 内部并不是 Python 编程所必需的要求,但对 Python 的运行时结构有 一 些基本的了解可以帮助你从宏观上把握

程序的执行。 当 Python 运行脚本时,在代码开始进行处理之前. Python 还会执行 一 些步骤。确切地说, 第 一步是编译成所谓的”字节码”,之后将其转发到所谓的“虚拟机”中。

字节码编译 执行程序时, Python 内部(对大多数用户是完全隐藏的)会先将源代码(文件中的语句) 编译成所谓字节码的形式。编译是 一 个简单的翻译步骤,字节码是一种低级的、与平台无

关的表现形式。概括地说, Python 通过把你的每 一 条源语句分解为单 一步骤来翻译成 一 组 字节码指令。这些字节码可以提高执行速度。比起文本文件中原始的源代码语句,字节码 的运行速度要快得多。

你会注意到,上 一 段所提到的这个过程对你来说完全是隐藏起来的。如果 Python 进程在机 器上拥有写入权限,那么它将把程序的字节码保存为一个以.pyc 为扩展名的文件 (".pyc"

就是编译过的 “py" 源代码)。对 Python 3.2 之前的版本,运行程序之后,你会在那些 源代码的附近(也就是说在同一个目录下)看到这些文件。例如,在导入了一个 script.py 文件后你会发现 一 个 script.pyc 文件。在 Python 3.2 以及之后的版本, Python 将把 pyc 字

节码存储在名为_pycache —的子目录中,这个子目录位于与源文件相同的路径下。新版 Python 的_pycache_子目录中的文件命名中包含了编译它们的 Python 的版本信息(例如, script.cpython-33.pyc) 。新的_pycache_子目录能够避免将太多的文件挤在同 一 个路径下,

而新的字节码文件命名规范确保了同 一 台电脑上安装的不同版本的 Python 所生成的字节码 不会相互覆盖。我们将在第 22 章中深入学习这种字节码文件模型,尽管它们都是自动生成 的,与大多数 Python 程序无关,也随着不同版本的 Python 有着不同的形式 。

在上述两种字节码文件模型中, Python 这样保存字节码是作为对启动速度的 一种优化。下 一 次运行程序时,如果你在上次保存字节码之后没有修改过源代码,并且运行使用的是同

一 个 Python 编译器版本,那么 Python 将会加载.pyc 文件井跳过编译这个步骤。这个过程 的工作原理如下:

46

I

第2章



源文件的改变: Python 会自动检查源文件和字节码文件最后一次修改的时间戳,确认

它是否必须重新编译:如果你编辑后又保存了源代码,下次程序运行时,字节码将自 动重新创建。



Python 的版本:导人机制同时检查是否需要因为使用了不同的 Python 版本而重新编译,

这些版本信息在 Python 3.2 版本之前存储在字节码文件中,而在 3.2 版本及之后,存储 在字节码文件名中。 其结果就是源文件的修改和 Python 版本的改变都会触发新的字节码文件的编译。如果

Python 无法在机器 上写入字节码,程序仍然可以工作:字节码会在内存中生成,并在程序 结束时直接被丢弃。然而,由千 pyc 文件能够加速启动,你最好保证在大型程序中能够创 建它们。字节码文件同样是发布 Python 程序的方法之 一 :如果 Python 找到的都是.pyc 文件,

它也很乐意运行这个程序,即便那这里没有原始的.PY 源代码文件(参考本章的"冻结 二 进 制文件” 一 节来了解其他程序发布的选项)。 最后请牢记,字节码只会针对那些被导人 (import) 的文件而生成,而不是顶层的执行脚本(严

格来说,这是一种针对“导入"的优化)。我们将在第 3 章中探索程序运行后的导入机制,

更深入的导入机制将在本书的第四部分介绍。此外,文件仅在程序运行(或者可能编译) 时才会被导人,而且在交互式命令行中输入的命令不会生成字节码。这种利用交互式命令 行的编程模式将在第 3 章中讨论。

Python 虚拟机 (PVM) 一且程序编译成字节码(或字节码从已经存在的 .pyc 文件中载入),之后的字节码发送到 通常称为 Python 虚拟机 (Python Virtual Machine, 简写为 PVM) 的程序上来执行。 PVM

听起来比它本身给人的印象更深刻一些。实际上,它不是一个独立的程序,不需要安装。 本质上, PVM 就是迭代运行字节码指令的 一 个大循环,一个接一个地完成操作。 PVM 是

Python 的运行时引绞,它时常表现为 Python 系统的一部分,并且是实际运行脚本的组件。 从技术上讲,它只是所谓 “Python 解释器”的最后一步。

图 2-2 展示了这里介绍的运行时的结构。请记住所有的这些复杂性都是有意地对 Python 程 序员进行隐藏的。字节码的编译是自动完成的,而且 PVM 也仅仅是安装在机器上的 Python

系统的 一部分。所以,程序员只需简单地编写代码并运行包含语句的文件,而 Python 会负 责所有运行这些文件的逻辑。

性能的含义 熟悉 C 和 C+ +这类完全编译语言的读者或许已经发现了 Python 模式中的一些不同之处。 其中之 一是,在 Python 的工作中通常没有构建或 “make" 的步骤:代码在写好之后立即运

行。另外一 个就是, Python 字节码不是机器的 二进制代码(例如 Intel 或 ARM 芯片的指令)。 字节码是特定千 Python 的 一种表现形式。

Python 如何运行程序

I

47



,

` `

,' ·'.

,· #

,'

"

_

二J



q



.. ,、

T

二J

5

圈 2-2: Python 的传统运行肘执行模型:你输入的琼代码转换为字节码,之后字节码在 Python 虚拟机中运行。代码会自动被编译,之后再被解释 这就是 Python 代码无法运行得像 C 或 C+ +代码 一样快的原因,就像第 1 章描述的那样:

PVM 循环(而不是 CPU 芯片)仍需解释字节码,并且字节码指令比 CPU 指令需要更多的 工作。另 一 方面,与其他经典的解释器不同,这里仍有内部的编译步骤: Python 并不需要

反复地重新分析和重新分解每 一 行源代码语句的文本。最终的效果就是纯 Python 代码的运 行速度介千传统的编译语言和传统的解释语言之间。更多关于 Python 性能的描述请参看第

1 章。

开发的含义 Python 执行模型所导致的另 一 个结果是其开发和执行的环境实际上并没有区别。也就是说, 编译和执行源代码的系统是同 一 个系统。这种相似性对千拥有传统编译语言背景的读者来 说,更有意义,然而在 Python 中,编译器总是在运行时出现,井且是运行程序系统的 一部分。

这将大大缩短开发周期。在程序开始执行之前不需要预编译和链接 1 只需要简单地输入井 运行代码即可。这同样让 Python 带上了更浓厚的动态语言色彩:在运行时, Python 程序去 构建并执行另 一个 Python 程序是有可能的,而且往往是非常方便的。例如, eval 和 exec 内 置模块能够接受并运行包含 Python 程序代码的字符串。这种结构是 Python 能够实现产品

定制的原因:因为 Python 代码可以动态地被修改,用户可以改进系统内部的 Python 部分, 而不需要拥有或编译整个系统的代码。 从更基础的角度来说,牢记我们在 Python 中真正拥有的只有运行时:完全不需要初始的编

译阶段,所有的事情都是在程序运行时发生的。这甚至还包括了建立函数和类的操作以及 模块的链接。这些事情对千静态语 言往往是发生在执行之前的,而在 Python 中是与程序的

执行同时进行的。就像我们将看到的那样,这就使得 Python 带来了比一 些读者所习惯的程 序语 言 更加动态的编程体验。

执行模型的变体 现在,我们学习了前一 节所介绍的内部执行流程,而这种流程也反映了今天 Python 的标准 实现形式,但这实际上并不是 Python 语 言 本身所必需的 。 正是因为这一 点,执行模型也 会

48

I

第2章

随时间而演变。事实上,现在已经出现了 一 些系统修改了图 2-2 所描述的情况。在继续学 习之前,让我们花些时间探索一下这些变体中最主流的 一些改进吧。

Python 的各种实现 严格来说,在本书写作的过程中, Python 语言有 5 种主要实现方式---CPython, Jython 、 lronPytho11 、 Stackless 和 PyPy 。尽管在这些 Python 实现方式之间充满了思想和工作的杂交,

它们中的每一 个都是独立安装的软件系统,拥有各自的开发者和用户群体。另外的潜在备 选者包括 Cython 和 Shed Skin 系统,但它们将在之后作为优化工具进行讨论,因为它们并

没有实现标准的 Python 语言(前者是 Python 和 C 的混合体,后者是隐式的静态类型化)。

简要地说, CPython 是标准的实现,也是大多数读者将选择使用的(如果不确定的话,你 也极可能是他们中的一员)。这也是本书所使用的版本,尽管这里所展示的核心的 Python 语言与其他版本儿乎一模一样。其他的 Python 实现都有特定的目标和用途,这些目标和用

途通常被 CPython 所包涵。所有这些都用来实现 Python 语言,只是通过不同的形式执行程 序而已。

例如, PyPy 是一 个现成的 CPython 替代品,因为它可以更快地运行大多数的程序。类似地, Jython 和 IronPython 是完全独立的 Python 实现,用来为不同的运行时环境架构编译 Python 源代码,从而能够提供直接的 Java 和. NET 组件的使用接口。你也可以通过 CPython 程序

访问 Java 和. NET 软件一例如 JPype 和基千.NET 的 Python 系统,允许标准 CPython 代 码调用 Java 和 .NET 组件。 Jython 和 IronPython 通过提供 Python 语言的全面实现,提供了 更为完整的解决方案。 以下将简要地介绍今天最主流的 Python 实现。

CPython: 标准 Python 当你要与 Python 的其他实现方式进行区分时,最初的、标准的 Python 实现方式通常称作 CPython (或者也可以直接称为 “Python") 。这个名字来自它是由可移植的 ANSI C 语 言代码编写而成的这个事实。这就是你从 http://www.python.org 获取的,从 ActivePython 和 En thought 的发行版中得到的,以及从绝大多数 Linux 和 Mac

OS X 机器上自动安装的

Python 。如果你在机器上发现了 一个预安装版本的 Python, 那么很有可能就是 CPython, 除非你的公司将 Python 用在更加特殊的场合。

除非你想要使用 Python 脚本化 Java 和.NET 应用,或是利用 Stack.Jess 和 PyPy 的编译优势,

否则通常你只需要使用标准的 CPython 系统。因为 CPython 是这门语言的标准参照实现, 所以与其他的替代系统相比,它运行速度最快、最完整、最新,而且也最健全。图 2-2 反 映了 CPython 的运行时体系结构。

Python 如何运行程序

I

49

Jython: 基千 Java 的 Python Jython 系统(最初称为 JPython) 是 一 种 Python 语言的可选实现方式,其目的是与 Java 编 程语言集成。 Jython 包含 Java 类,这些类将 Python 源代码编译成 Java 字节码,并将得到

的字节码定向到 Java 虚拟机 (JVM) 上。程序员仍然可以像平常一 样,在文本文件中编写 Python 语句; Jython 系统的本质是将图 2-2 最右边两个方框中的内容替换为基千 Java 的等 效实现。 Jython 的目标是让 Python 代码能够脚本化 Java 应用程序,就好像 CPython 允许 Python 脚

木化 C 和 C++组件 一样 。 它实现了与 Java 的无缝集成。由千 Python 代码被翻译成 Java 字 节码,因此在运行时看起来就像真正的 Java 程序 一样。 Jython 脚本可以应用千开发 Web applet 和 servlet, 构建基于 Java 的 GUI 等。此外, Jython 包括对集成的支持,允许 Python 代码导人并使用 Java 的类(把这些类当作用 Python 编写的 一 样),以及让 Java 代码能 够把 Python 代码当作内嵌的语言来运行。虽然相较千 CPython, Jython 既不快也不够健 壮,但是它解决了希望 寻 找一 门作为前端脚本语言的 Java 开发者的诉求。参见 Jython 网站

http://jytho11.org 以获取更多信息。

Iron Python: 基千. NET 的 Python Python 的第 三种实现方式是 IronPython (比 CPython 和 Jython 都要新),其设计目的是让 Python 程序可以与 Windows 平台上的. NET 框架以及与之对应的 Linux 上开源的 Mono 编 写成的应用相集成。本着像微软早期的 COM 模型 一 样的精神,. NET 和其 C#程序语言的

运行系统旨在成为语言无关的对象通信层。 IronPython 允许 Python 程序同时扮演客户端和 服务器端的角色,与 NET 语言进行来回访问,以及在 Python 代码中利用诸如 Silverlight

框架的 . NET 的技术。 在实现上, Iron Python 很像 Jython (实际上两者都是由同 一 个创始人开发的) :它替换

了图 2-2 中最后的两个方框,将其换成 . N ET 环境的等效执行方式。并且与 Jython 一样,

IronPython 拥有特定的目标:它主要是为了满足希望在 NET 组件中集成 Python 的开发者。 IronPython 起初由微软公 司 开发,现在成了 一 个开源项目。它同时也为了得到更优异的性 能而利用了 一 些重要的优化工具 。 关千更多细节,参见 http:/lironpython.net 和其他网站上

的资源。

Stackless: 注重并发的 Python 还有一些其他的方案可以用来运行 Python 程序,它们具有更加专注的目标。例如,

Stackless Python 系统是标准 CPython 针对并发性而优化的 一 个增强版实现,因为 Stackless Python 不会在 C 语言调用栈上保存状态,这使得 Python 更容易移植到较小的栈架构中,提

供了更高效的多处理选项,井且促进了像协程 (coroutine) 这样的新颖的编程结构的出现。

50

1

第2章

在 Stack less 为 Python 带来的所有优化中,微线程是 Python 原生多线程工具的一个更高效 和更轻址的替代品。微线程带来了更好的程序结构、更可读的代码以及更强的开发效率。 “EVE 在线”的创建者 CCP 游戏公司就是 Stackless Python 的忠实用户,它也是 Python 最 成功的业界案例之 一 。详情参见加p:J/stackless.com 。

PyPy: 注重速度的 Python PyPy 系统是 CPython 标准的另 一 个实现,它更注重性能。它提供了 一 个带有即时编译器

(just-in-time, JIT) 的 Python 快速实现,能够在安全环搅中运行不信任代码的“沙箱“模 型的工具,默认对前一 节中 Stackfess Python 系统的支持以及对大型并行需求的微线程支持。 PyPy 是原先的 Psyco 即时编译器的继任者,并将 Psyco 纳入 一 个追求速度的纯 Python 实现。 即时编译器事实上只是 PVM 的一个扩展(就是图 2-2 最右边的方框中所代表的),它将你 的字节码中的部分直接转换成运行速度更快的 二 进制机器码。这 一 切发生在你的程序运行

的时候,而非运行前的编译阶段。而且即时编译器能够通过追踪程序中的对象数据类型, 创建针对 Python 语言动态特性的机器代码。通过这种方式部分地替换字节码,程序将在运 行的时候越跑越快。此外, 一 些 Python 代码在 PyPy F运行也会占用更少的内存。 本书写作时, PyPy 支持 Python 2.7 (还未支持 3.X) ,运行在 Intel x86 (IA-32) 和 x86_64 平台(包括 Windows 、 Linux 和近来的 Mac 版本),同时针对 ARM 和 PPC 架构的支持正

在开发中。 PyPy 能够运行大多数的 CPython 代码,尽管其中的 C 扩展模块通常必须重新编 译,而且 PyPy 有 一 些细小而微妙的语言特性差别,包括回避了 一些常见编程模式的垃圾回

收机制 。 例如,它的非引用计数方案意味着临时文件将不会立即被关闭或者从缓冲区中释放, 而且甚至在某些情况下需要手动关闭。 作为回报,你的代码将运行得更快。 PyPy 如今号称在一 系列测评代码上比 CPython 提速 5.7 倍(参见 http://speed.pypy.org) 。在某些情况下,它利用动态优化机会的优势让 Python 代

码运行得跟 C 代码一样快,甚至有时超越 C 。这点对箕法密集和计算密集型的程序尤为明显。 例如,在第 21 章的 一 个测评中我们将看到, PyPy 如今比 CPython 2.7 快 10 倍左右,而比

CPython 3.X 快 100 倍左右。尽管其他测评结果会有 一 定出入,但这种程度的提速对许多领 域来说都是 一 种令人不可抗拒的优势,甚至从某种意义上比 Python 前卫的语言特性更受人 追捧。同样重要的是, PyPy 也对内存空间进行了优化-—-在一次发布的测评中, PyPy 使用 247MB 的内存花 10.3 秒完成了编译,大大超过了 CPython 的 648MB 和 89 秒。 PyPy 的编译链工具也通常足够支持包括 Pyrolog ( 一 个使用 Python 编 写 ,利用 PyPy 翻译

的 Prolog 解释器)在内的其他语言。可搜索 PyPy 网站以获取更多信息。 PyPy 现在的官网 是加p:llpypy.org, 当然你也能通过搜索网络找到有价值的信息 。 关干它的当前性能的总览,

可参考 http:/lwww.pypy.org/pe1Jormance.htmf 。

Python 如何运行程序

I

s1

注意:就在本书编写后不久, PyPy 2.0 发布了添加 ARM 处理器支持的测试版,同时保留了仅

针对 Python 2.X 的实现。在它的 2.0 测试版文档中这样写道: "PyPy 是一个兼容标准的 Python 解释器,几乎是 CPython 2.7.3 的一个很方便的替代者。 它的快速的原因在千它集成了回溯即时编译器。本次发行版支持运行 Linux 32/64 位、

Mac OS X 64 位和 Windows 32 位的 x86 架构机器。它同时支持运行 Linux 的 ARM 架构 机器。" 这里的说明十分准确。使用将在第 21 章所提到的计时工具,我在测试中发现 PyPy 通常

比 CPython 2.X 和 3.X 快一个数最级 (10 倍左右),甚至更多。这已经考虑到 PyPy 是 在我的 Windows 32 位机器上进行测试,而 CPython 使用了更快的 64 位编译的事实。 当然,对你自己的代码进行测评才是最关键的,不过也存在 CPython 获胜的情况。例如, PyPy 的文件迭代器使用了在今天相对较慢的时钟。然而,考虑到 PyPy 注重性能而不是 语言特性的更新,尤其是它对计算领域的强大支持,许多人今天仍将 PyPy 视作 Python

的一条重要发展方向。如果你要编写 CPU 密集型的代码,那么 PyPy 值得你拥有。

执行优化工具 CPython 和前一节中介绍的大部分实现变体都是以相似的方式实现 Python 语言的:通过把 源代码编译为字节码,然后在适合的虚拟机上执行这些字节码。其他的系统,包括 Cython

混合语言、 Shed Skin C++转换器,以及 PyPy 与 Psyco 中的即时编译器,则试着优化了基

本执行模块。这些系统并不是你现阶段学习 Python 的必备知识,但是简要地了解它们在执 行模型中所处的位置能够帮助你揭开执行模型的神秘面纱。

Cython: Python 和 C 的混合体 Cython 系统(基千 Pyrex 项目所完成的工作)是一 种混合的语言,它为 Python 代码配备了 调用 C 函数以及使用变扯、参数和类属性的 C 类型声明的能力。 Cython 代码可以编译成使 用 Python/C API 的 C 代码,随后可以再完整地编译。尽管与标准 Python 并不完全兼容, Cython 对于包装外部的 C 库以及提高 Python 的 C 扩展的编码效率都很有用。该项目的细 节和近况参见 http://cython.org 。

Shed Skin: Python 到 C++的转换器 Shed Skin 是一个新兴的系统,它采用了一种不同的 Python 程序执行方法:尝试将 Python 代码翻译成 C++代码,然后使用机器中的 C++编译器将得到的 C++代码编译为机器代码。

通过这种方式,它以一种平台无关的方式来运行 Python 代码。 在写作本书的时候, Shed Skin 仍处千活跃的开发中。它现在支持 Python 2.4 到 2.6 的代码, 给 Python 程序施加了一种隐式的静态类型约束,这在其他语言中十分常见,但在 Python

中看上去有点格格不入,所以我们不再深入了解其中的 一 些细节了。不过初步结果显示它

s2

I

第2章

具有超越标准 Python 代码以及类 Psyco 扩展的潜质。请通过网络搜索以获得更多细节以及 项目目前的发展状况。

Psyco: 原先的即时编译器 Psyco 系统并不是 Python 的另一种实现方式,而是一个可以让程序运行得更快的扩展字节 码执行模块组件。今天, Psyco 是一个过时的项目:它仍然可以被下载,但是已经跟不上 Python 的演化,并且不再被积极地维护了。然而,它的思想被融入到前面介绍的更加完整

的 PyPy 系统中。不过, Psyco 所开创的思想的重要性,使得它还是值得你简要了解一下的。 如图 2-2 所示, Psyco 是一个对 PVM 的增强工具,这个工具在程序运行时收集并使用类型 信息,可以将部分程序的字节码转换成底层的真正的二进制机器代码,从而实现更快的执 行速度。在开发过程中, Psyco 无须修改代码或独立的编译步骤即可完成这一转换。

粗略地讲,当程序运行时, Psyco 会收集正在传递过程中的对象的类别信息,这些信息可以 用来裁剪对象的类型,从而生成高效的机器代码。机器代码一旦生成后,就替代了对应的 原始字节码,从而加快程序的整体执行速度。实际的效果就是,通过使用 Psyco, 使程序在

整个运行过程中执行得越来越快。在理想的情况下,一些通过 Psyco 优化的 Python 代码的 执行速度可以像编译好的 C 代码一样快。

因为字节码的转换在程序运行时发生,所以 Pysco 往往被看作一个即时编译器 (JIT) 。不 过 Pysco 实际上与一些读者曾经在 Java 语言中了解的 JIT 编译器稍有不同。 Psyco 是一个 专有的 JIT 编译器:它生成机器代码将数据类型精简至程序实际所使用的类型。例如,如 果程序的 一部分在不同的时候采用了不同的数据类型, Psyco 可以生成不同版本的机器码以 支持每一个不同的类型组合。

Psyco 已经证实能够大大提高一些 Python 代码的速度。根据其官方网站介绍, Psyco 提供了 “2 倍至 100 倍的速度提升,通常是 4 倍,在没有改进的 Python 解释器和不修改源代码的基础上,

仅仅依靠了动态可加载的 C 扩展模块"。同等重要的是,最显著的提速是在以纯 Python 写 成的算法代码上实现的一确切地讲,是为了优化往往需要迁移到 C 的那部分代码。关于

Psyco 的更多信息,可以搜索网络或者看看前面提到的它的继任者 PyPy 项目。

冻结二进制文件 有时候人们需要一个“真正的 “Python 编译器,实际上他们真正需要的是一种能够让

Python 程序生成独立的二进制可执行代码的简单方法。这是一个比执行流程概念更接近千 打包发布概念的东西,但是两者之间或多或少有些联系。你可以通过从网络上获得的一些 第三方工具将 Python 程序转为可执行程序,它们在 Python 世界中被称作冻结二进制文件

(frozen binary) 。这些程序可以不安装 Python 环境而独立运行。

Python 如何运行程序

I

s3

冻结 二进制文件能够将程序文件的字节码、 PVM (解释器)以及任何程序所需要的 Python 支持文件捆绑在 一 起形成一个单独的文件包。这个过程存在着 一 些不同的变体,但最终的

结果是一 个单独的可执行二进制程序(例如, Windows 系统中的 exe 文件),这个程序可 以很容易地向客户发布 。 如图 2-2 所示,这就好像将字节码和 PVM 混合在一起形成 一个独 立的组件-—-冻结 二进制文件。

如今,有许多系统能够生成随平台特性而变化的冻结二进制文件: py2exe (为 Windows 平 台提供了全面的支持)、 PyInstaller (与 py2exe 类似,它能够在 Linux 及 Mac

OS X 上使用,

并且能够生成自安装的 二进制文件)、 py2app (创建 Mac OS X 应用程序)、 freeze (最初 的版本)和 cxJreeze (同时提供 Python 3.X 和跨平台支持)。你可以单独获得这些工具,

它们也是免费的。 这些工具还在不断地演化中,请参考 http://www.python.org 以及使用你喜欢的搜索引擎以获

得有关这些工具的更多信息。这里我们给出 一些信息,方便你了解这些系统的应用范围, 例如 py2exe 可以封装那些使用了 tkinter 、 PMW 、 wxPython 和 PyGTK GUl 库的独立程序;

应用 pygame 进行游戏编程的程序; win32com 客户端的程序等。 冻结 二进制文件与真实的编译器输出结果有所不同:它们通过虚拟机运行字节码。因此, 除了必要的初始改进,冻结 二进制文件和最初的源代码程序运行速度完全相同。冻结 二进

制文件并不小(它们包括一 个 PVM) ,但是以目前的标准来衡量,它们的文件也不是特别大。 因为在冻结 二进制文件中嵌人了 Python, 接收端并不帣要安装 Python 来运行这些冻结二进 制文件。此外,由千代码嵌入在冻结 二进制代码之中,对千接收者来说,代码都是隐藏起来的。 对商业软件的开发者来说,单文件封装的构架特别有吸引力。例如, 一个旧 Python 编写的

基千 tkinter 工具包的用户界面可以封装成 一个可执行文件,并且可以在光盘中或网络上作 为独立程序进行发售。终端用户无须安装(甚至没有必要知道) Python 就可以运行这些发

售的程序。

未来的可能性 最后,值得注意的是,这里简要描述的运行时执行模型事实上是 当 前 Python 实现的一 种产物, 并不是语言本身。例如,或许在本书的销售过程中会出现一种完全将 Python 源代码变为机

器代码的传统编译器。

(尽管在最近的 20 年里还没有 一 款这样的编译器,所以好像不太可

能!) 未来也许会有新的 字 节码格式和实现方式的变体将被采用。例如:



Parrot 项目的目标就是提供一种对千包括 Python 的多种编程语 言通用的字节码格式、

虚拟机以及优化技术。 Python 自己的 PYM 运行 Python 代码比 Parrot 效率更高(正如

s4

I

第2章

众所周知的一次软件会议上的蛋糕挑战所示,详情参见网络

译注 l

),但 Parrot 将如何

发展还不明晰。详情参见 http://parrot.org 。



先前名叫 Unladen Swallow 的 一 个由谷歌工程师开发的开源项目,试图将标准 Python

提速至少 5 倍,从而可以在许多应用场景下快到能够取代 C 语言。这曾是 CPython (特 指 Python 2.6) 的一个优化分支,目的在于通过添加一个即时编译器来提高 Python 的

兼容性和速度。正如我在 2012 年写的,这个项目似乎注定失败(根据这个项目被撤回 的 PEP,

“带着一丝挪威的忧郁不舍地离去”)。然而,它带给我们的教训能使我们

在另一方面受益,你可以在网上检索更多的重大进展译注 20 尽管未来的实现方案有可能从某种程度上改变 Python 的运行时结构 (runtime structure),

但就接下来的一个时期内来看,字节码编译仍然将会是 一 种标准。字节码的可移植性和运 行时的灵活性对千很多 Python 系统来说是很重要的特性。此外,为了实现静态编译,而增

加类型约束声明将会破坏这种灵活、紧凑、简明以及所有代表了 Python 编码精神的特性。 由千 Python 本身高度动态的本质,今后的任何实现方式都可能保留诸多当前的 PVM 的产物。

本章小结 本章介绍了 Python 的执行模型 (Python 是如何运行程序的),井探索了这个模型的一些变 体(即时编译器以及类似的工具)。尽管编写 Python 脚本并不要求你了解 Python 的内部实现,

通过本章介绍的主题获得的知识会帮助你从一开始编码时就真正理解程序是如何运行的。 下一章 ,我们将开始实际运行编写的代码。那么,首先让我们开始常规的章节测试吧。

本章习题 J.

什么是 Python 解释器?

2.

什么是源代码?

3

什么是字节码?

4.

什是 PVM

5.

请列举两个或多个 Python 标准执行模型的变体的名字。

?

译注 I : 有一次 Python 和 Parrot 的创始人打赌,如果谁的实现更快`谁就可以朝对方的脸上扔一 块蛋糕 。 最终, Python 创始人嬴了,并在 OSCON 会议的讲台上朝 Parrot 创始人的脸上

扔了一块蛋糕。详情参见 http://archive.orei/ly.comlpub/aloscon2004/fridaylindex.html 和 http://www.artima.com/forums/flat.jsp?forum=l22&thread=27898 。

译注 2 据原书作者的说法,现在已经有相当多和编译器相关的工作正在进行中,包括 Numba (有

关数值工作)、 Py」ion (来自微软公司)、 Pyston (基于 LLVM) 、 Transcrypt (从 Python 到 JavaScript)

以及 Pythran (从 Python 到 C++) 。

Python 如何运行程序

I

55

6.

CPython 、 Jython 以及 IronPython 有什么不同?

7.

什么是 StackJess 和 PyPy?

习题解答 1.

Python 解释器是运行你所编写的 Python 程序的程序。

2.

源代码是为程序所写的语句:它由文本文件中的文本组成,通常以. PY 作为后缀名。

3.

字节码是 Python 将程序编译后所得到的低级形式。 Python 自动将字节码保存到后缀名 为 .pyc 的文件中。

4.

PVM 是 Python 虚拟机,它是 Python 的运行时引擎,能解释编译得到的字节码。

5.

Psyco 、 Shed Skin 以及冻结 二进制文件都是执行模型的变体。除此之外,在接下来的两

个问题中提到的其他 Python 实现,以替代字节码和虚拟机或是添加工具和 JIT 的形式, 对 Python 的执行模型进行改进。

6.

CPython 是 Python 语言的标准实现。 Jython 和 IronPython 分别是 Python 程序在 Java 和 NET 环境中的实现,它们都是 Python 的替代编译器。

7.

Stackless 是一种针对并发性而增强的 Python 版本,而 PyPy 是针对速度而增强的 Python 实现。 PyPy 作为 Psyco 的继任者,融合了 Psyco 中的 JIT 概念。

56

I

第2章

第3章

你应如何运行程序

好了,是时候开始编写程序了。现在你已经掌握了程序执行模型的知识,终千可以准备开

始一些真正的 Python 编程了。假设你已经在计算机上安装好了 Python, 如果还没有的话, 请参照上一章的开头和附录 A 来获取一些在各个平台安装和配置的提示。本章的目标是学

习如何运行 Python 程序代码。 我们已经介绍了许多执行 Python 程序的方式。本章中所讨论的都是当前常用的程序启动技

术。与此同时,你将学习如何交互地输入程序代码,以及如何将代码保存至一 个文件从而 以你喜欢的方式运行:可以在系统命令行中运行,点击图标运行,模块导入, exec 调用, 以及 IDLE GUI 中的菜单选项等。

与上 一章一 样,如果你之前有过编程经验并且迫不及待地想开始探索 Python, 那么你可以 大致浏览本章后直接跳到第 4 章。但是不要跳过本章开始的预备知识和说明约定、代码调

试技巧总览,以及对模块导入的概述一一这些都是理解 Python 程序架构的必要知识,我们

要等到后面的部分才会重温它们。我同时建议你阅读一下关千 IDLE 和其他 IDE 的小节, 去发现更适合你的工具,从而使你能开发更为复杂的 Python 程序。

交互式命令行模式 我们从这一节开始介绍交互式编程的基础知识。因为它是我们运行代码的最初体验,我们

也会包含一些预备知识,如设置工作目录和系统路径。如果你是一位编程新手,一定要阅 读完本节。本节还会解释一些全书中使用的约定,因此多数读者都应该快速浏览一遍。

开始一个交互式会话 也许最简单的运行 Python 程序的办法就是在 Python 交互式命令行中输入这些程序,它们

57

有时也称为交互式提示模式。你有多种方式可以开始这样的命令行:在 IDE 、系统终端中等。 假设解释器已经作为一个可执行程序安装在系统中,对千各平台打开交互解释器会话的通

用方式,一般是在操作系统的提示命令行下输人 python, 且不带任何参数。例如: % python

Python 3.3.0 (v3.3,0:bd8afb90ebf2, Sep 29 2012, 10 :57: 17) [MSC v.1600 64 bit... Type "help", "copyright", "credits" or "license" for more information.

»> "Z 在系统命令行下输入 “python" 后即可开始一个交互式的 Python 会话;这里列出的代码开

始处的"%”字符代表通用的系统提示符,该字符不需要自己输入。在 Windows 系统上, 你可以键入 结束这个会话,在 UNIX 系统,你可以用 。

注意这里的系统命令行的概念是通用的,但你启动命令行的方式可能在不同的平台上会有 所不同:



在 Windows 中,你可以在 DOS 终端窗口(一个名为 cmd.exe 的程序,通常称为命令行)

中输入 python 。如果想获取这个程序的更多细节,参见本章下面的边栏 “Windows 平 台上的交互式命令行在哪里?”。



在 Mac OS X 中,你可以通过双击 Application -

Utilities 一 Terminal, 然后在打开的窗

口中输人 python 来开始一个 Python 交互式解释器。



在 Linux (和其他 UNIX 系统中)中,你可以在 shell 窗口或终端窗口中(例如,在 xterm 或控制台中运行的 ksh 或 csh 这样的 shell) 输入 python 即可。



其他的系统可以采用类似的方法或平台特定的工具。例如,在手持设备上,你可以点 击主窗口或应用程序窗口中的 Python 图标来启动一个交互式会话。

在大多数平台上,你可以通过不需要输入命令的其他方式来打开交互式命令行,但是这些 快捷键在不同平台上的差异甚至更大:



在 Windows 7 及之前的版本,除了在 shell 窗口中输入 python 以外,你也可以通过打开

IDLE GUI (将在后面讨论),或者从开始菜单中选择 “Python (command line)” 菜单项(第 2 章的图 2-1) 来打开一个类似的交互式会话。两种方法都能得到一个 Python 交互式命 令行,其结果和使用 “python" 命令是类似的。



在 Windows 8 系统上,你没有 一 个开始按钮(至少在我写作本书的时候是如此),但 是那里也有获取之前所描述工具的其他方法。包括开始屏幕中的图标阵列、搜索、文 件资源管理器和开始屏幕下的”所有程序”入口。参阅附录 A 以获取该平台下的更多

指南。



其他平台也有着无需输入命令就可以开始 Python 交互式会话的类似方法,不过由千它

们相对繁琐,因而不适合在这里提及,你可以参阅相关系统的文档来获取细节。

58

I

第3章

任何时候你见到>>>提示符,就进入了一个交互式 Python 解释器的会话中了一一价;可以输

入任何 Python 语句或者表达式并立即运行它们。我们很快就会这样做,但首先我们衙要说 明少蜇的启动细节以确保所有读者都可以继续前进。

Windows 平台上的交互式命令行在哪里 那么你如何在 Windows 下开启命令行界面呢?一些 Windows 读者已经知道,但是 UNIX 开发者和初学者可能不知道;比起 UNIX 系统来, Windows 的终端或控制台窗

口并不那么出名 心 下面是一些帮助你找到命今行的指南,权据每个 Windows 版本的 不同可能会稍有变化。

在 Windows 7 和更早的版本,这经常位于开始一所有程序菜单的附件部分,或者你可 以在开始一”运行…“对话框或开始菜单的搜索框中轮入 cmd 来运行它 。 如果愿意气

你可以创这一个桌面快捷方式图标来更快地完成这一切 。 在 Windows 8 下,你可以通过右击屏幕左下角的预览图弹出的菜单进入命令行;或者

可以通过开始屏幕中的”所有程序”中 Windows 系统部分找到它;或者通过屏幕右 上角下拉出现的搜索框中榆入 cmd 或 command prompt 。很可能存在其他方法,触摸屏

也给予相似的途径。如果你嫌这些操作太麻烦,可以把命令行放在桌面任务栏以便下 次能快速启动 。

这些步骤很可能会随时间变化,也很有可能随着电脑和用户变化 。 我试图避免把本书 变成一本讲觥 Windows 的书,所以我会在这里结束对这一话题的简短讨论。如有疑问, 尝试一下系统的帮助界面。

(当然,它们的使用可能也会和它们为之提供帮助的工具

一样随着时间而变化!) 对于任何阅读这个边栏并开始觉得自己像是一条脱离水的鱼的 UNIX 用户,诗注意:

你可能也对 Cygwin 系统感兴趣,它实现了 Windows 下完埜的命今行 。 参阅附录 A 以 荻取史多内容 。

系统路径 当我们在上一小节启动的一 个交互式会话中输入 python 时,我们依赖于系统帮我们在程序 搜索路径中定位 Python 程序的位置。根据 Python 版本和平台的不同,如果你没有设置系

统的 PATH 环境变最来包括 Python 的安装路径,可能需要用你机器上的 Python 可执行文件

的完整路径来代替单词 “python" 。例如,在 UNIX 或 Linux 上,你可以输入/usr/local/ bin/python (或/ usr/bin/python3) 。在 Windows 上,你可以输入 C:\Python33\python (对

千 3 . 3 版本) :

c:\code> c:\python33\python Python 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit... 你应如何运行程序

1

59

Type "help", "copyright", "credits" or "license" for more information. >» "Z 或者,你可以在输入 python 之前运行 “cd" 目录改变命令进入到 Python 的安装目录下, 你可以在 Windows 中使用 cd c:\python33, 例如:

c:\code> cd c:\python33 c: \Python33> python Python 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit... Type "help", "copyright", "credits" or "license" for more information. »> "Z 不过,你会更希望设置 PATH 变量,使得简单的 “python" 就可以启动。如果你不知道 PATH 是什么或如何设置它,参阅附录 A, 其中涵盖了这种在每个平台上都不同的环境变量,也

包括在本书中不太会使用的 Python 命令行参数。 Windows 用户的简单方法:参看控制面板 系统选项的高级系统设置。如果你在使用 Python 3.3 及之后的版本,如同下节所讲,这在 现有的 Windows 上是全自动的。

Python 3.3 中的新 Windows 选项: PATH 和启动器 接下来的一小节和本章大部分主要描述了 Python 3.3 之前的 2.X 和 3.X 版本的通用运行状

态。从 Python 3.3 开始, Windows 安装程序有一个选项,在选中时能够使 Python 3.3 的目 录自动添加到系统的 PATH 。如果你使用了这个选项,不必再输人目录的路径或像上一节一 样敲一个 “cd" 来运行 python 命令。如果你愿意,在安装时确定勾选此选项,因为默认情 况下该选项关闭。 更有用的是, Python 3.3 自带了一个新的能自动安装的 Windows 启动器,这是一个携带可 执行程序的系统, py 会打开一个控制台而 pyw 不会,它们都被放置千系统路径的目录中,

是开箱即用的,因而不需要配置 PATH, 改变目录或目录路径前缀:

c:\code> PY Python 3.3.0 (v3.3,0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit ... Type "help", "copyright", "credits" or "license" for more information.

»> "Z c:\code> PY -2 Python 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)] Type "help", "copyright", "credits" or "license" for more information.

»> "Z c: \code> PY -3.1 Python 3,1.4 (default, Jun 12 2011, 14:16:16) [MSC v.1500 64 bit (AMD64)] Type "help", "copyright", "credits" or "license" for more information. »> "Z 就像最后两条命令所展示的那样,这些可执行程序在同一命令行(或如后面所讨论的,在 脚本头部 UNIX 风格的#!行)也接受 Python 的版本号,当点击时也会像原始的 python 可

60

I

第3章

执行程序一样打开相关的 Python 文件。 python 可执行程序依然可以像它们之前那样工作, 但在某种程度上已经被启动器的新程序所超越。 启动器是 Python 3.3 的标准组件,也能同其他的版本单独使用。我们将在本章和后续章节 看到更多关千新启动器的内容,并简要介绍它对#!行的支持。然而,因为只有 Windows

用户对这部分内容感兴趣,同时只有在 3.3 版本或独立安装的情况下才有启动器,所以我 把关千启动器的几乎所有细节都安排在附录 B 中。 如果你在 Windows 下使用 Python 3.3 或是更新的版本,建议你不妨先绕路到附录一探究竟。 因为它提供了一种可替代的、在某种程度上更好地运行 Python 命令行和脚本的方法。简单

来说,启动器的用户可以在本书的大部分系统命令中输入 PY 来替代 python, 并避免一些配 置步骤。特别是在那些有着多个不同版本 Python 的电脑上,新的启动器让你能更好地显式 掌控运行代码的 Python 版本。

运行的位置:代码目录 现在我已经开始介绍如何运行代码,但我想预先说一下在哪里运行代码。简单起见,在本

章和本书中大多数时候我会从自己的 Windows 电脑上所创建的 C:\code 工作目录(文件夹) 来运行代码,这是我的主驱动的一个顶级子目录。那是我开始大部分交互式会话的地方, 也是我要保存和运行绝大多数脚本文件的地方。这也意味着示例创建的文件多数都会在这 个目录中展示。

如果你想继续学习,你就得在开始前做一些类似的事情。如果你需要一些设置工作目录的 帮助,这里是一些小建议:



在 Windows 系统上,你可以使用文件资源管理器或命令行窗口创建工作代码目录。在

文件资源管理器中,在文件菜单中或右击选择新建文件夹。在命令行下,通常在你 cd

进入到满意的父目录后,输入井运行 mkdir 命令(例如 cd c\ :和 mkdir code) 。你 的工作目录可以放在你喜欢的任何地方,能以你愿意的任何方式命名,并且不必是 C:\

code (因为它在提示符下很简短,所以我才选择这个名字)。不过在同一个目录下运行, 能够帮助你跟踪工作井简化你的任务。想要了解更多关千 Windows 的相关内容,参见 本章关千命令行的边栏,也可参见附录 A 。



在以 UNIX 为基础的系统(包括 Mac OS X 和 Linux) 上,你的工作目录也许通过在一 个 shell 窗口或与平台相关的 GUI 文件资源管理器中输入 mkdir 命令来创建,并位于

lusr/home 下。不过它们用到的概念是相同的。虽然目录的名字可能会变(比如/home 、 !cygdrive!c) ,但 Windows 上的类 UNIX 系统 Cygwin 也是如此。 你也可以在将代码存储在 Python 的安装目录下(如 Windows 上的 C:\Python33) 来简化一

些命令行,从而无需设置 PATH 。但是你最好不要这样做,因为该目录是留给 Python 自身的,

你应如何运行程序

I

61

所以如果 Python 自身被移动或卸载了,那么你的文件也将不能幸免。 一 且你创建成功了工作目录,总在那进行本书的示例学习吧。本书中我运行代码并展现目 录的提示符反映了我 Windows 笔记本上的工作目录;当你看见 C:\ code> 或是%,可以把它

们当作自己目录的位置和名字。

不需要输入的内容:提示符和注释 关千提示符,本书有时会把系统提示符表示为通用的%,有时会是 Windows 形式完整的(;\

code> 。前者表示平台是不可知的(来源千早期的 Li nux 版本) ,后者则用在 Windows 的 特定上下文中。同时,本书中提示符的后面都有一个空格以增加可读性。不管你的机器上

的具体情况如何,本书中系统命令行开头的%字符代表各平台系统的提示符。例如,在我 的机器上%代表 Windows 命令行下的 (:\code> ,在 Cygwin 安装下则代表$。 致初学者:你不要输入在本书交互式代码段中出现的%字符(或者有时是 C:\code 代表的 系统提示符),因为这是系统打印的文本。你只需要输入这些系统提示符后面的文本。类似地, 不要输入在交互式列表解释器的行首显示的">>>”和“···"字符,这些是 Python 自动展

示作为交互式代码入口的可视化引导的提示符。只要在这些 Python 提示符的后面输入文本。 例如,

"... "提示符在一些 shell 中用千行的延续,但是井不在 IDLE 中出现,

" ... "有时

在本书的一些列表但不是全部的列表中出现 1 如果在界面中没有,不要自己输入它。 为了帮助你记住这点,本书中用户输入以粗体显示,而提示符并不是粗体的。在 一 些系统 上(例如,第 2 章讲述的专注于性能的 PyPy 实现使用 4 字符的">>>>”和“.·"),提示 符会有所不同,但这条规则依然适用。牢记在这些系统和 Python 提示符后面输入的命令将 会被立即执行,通常也不会保存在我们要创建的源文件中,我们很快会看到为什么这个区

别很重要。

同样地,正常情况下你在本书的列表中也不必输入以#字符开始的文本,你会学到,这些 是注释,而不是可执行代码。除了在 UNIX 脚本或 Python 3.3 Windows 启动器脚本的顶部

#作为 一个指令外,你可以放心地忽略#后面的文字(了解更多关于 UNIX 和启动器的信息, 参阅本章和附录 B) 。

注意:如果你继续往后学习,第 17 章中的交互式列表会去掉大部分的".. . "连接提示符,以 此来方便读者从电子书或其他地方对如由数和类等大型代码进行剪切和粘贴,或者, 一

次粘贴或输入 一 行并谝掉提示符。至少在起步阶段,手动输入代码很重要,这培养你对

语法细节和错误的感觉。 一些例子或是通过其自身列出,或是通过本书实例包(前言 ) 中的命名文件列出。我们会经常转变列表格式。当你感到疑惑的时候,如果看见“>>>", 就意味着你在交互式输入代码。

62

I

第3章

交互式地运行代码 有了那些预备知识后,让我们开始输入一些实际的代码吧。

Python 交互对话刚开始时会打印两行信息文本以说明 Python 版本号并给予 一 些提示(为了 节省章节内容在这里省略了这个例子),然后显示等待输入新的 Python 语句或表达式的提 示符”>>>"。在交互式命令行下工作,输人代码的结果将会在按下 Enter 键在“>>>”这一

行之下显示。例如,这里是两条 Python print 语句的结果 (print 在 Python 3.X 中确实是 一个函数调用,但在 Python 2.X 中不是,因此,这里的括号只在 Python 3.X 中需要) :

% python

» > print('Hello world I') Hello world! >» print(2 ** 8) 256 就是它了,我们刚刚运行了 一 些 Python 代码(别怕,这可不是“西班牙审讯")。现在还

不需要为这里显示的 print 语句的细节担心(我们将会在下 一 章开始深入了解语法)。简 而言之,这两行语句打印了 一 个 Python 的字符串和一 个整数,正如每个“>>>"输入行下

边的输出行显示的那样(在 Python 中, 2 ** 8 的意思是 2 的 8 次方)。 像这样在交互式命令行下工作,想输入多少 Python 命令就输人多少,每一 个命令在输人回 车后都会立即运行。此外,由千交互式对话自动打印输人表达式的结果,在这个提示模式下, 往往不需要每次都刻意地输入 “print"

>» lumberjack ='okay' >» lumberjack 'okay' >» 2 ** 8 256

>>>

^Z

#

Use Ctrl-D (on UNIX) or Ctrl-Z (on Windows) ro exit

% 此处,第 一 行把一 个值赋给了 一 个变量 (lumberjack) 从而保存它,变量通过赋值语句创 建;最后两行的输入为表达式 (lumberjack 和 2**8) ,它们的结果是自动显示的。像这里 一样退出交互对话并回到系统 shell 提示模式,在类 UNIX 系统中输入 退出 1 在

Windows 系统中输入 退出。在随后讨论到的 IDLE GUI 中,输入 退出或 简单地关闭窗口来退出。 注意清单右侧的斜体注释(这里以“#”开始)。我会 一直使用这些来说明什么正在被阐述, 但是你不必自己输人这些文本。实际上,就像系统和 Python 提示符一样,你不需要在系统 命令行上输入这个 1 Python 把“#”部分当作注释,但在系统提示符里可能会是一个错误。

现在,我们对这次会话中的代码并不是特别的了解:仅仅是输入 一 些 Python 的打印语句和

你应如何运行程序

I

63

变最赋值的语句,以及一些表达式,这些我们都会在稍后进行深入的学习。这里最重要的

事情就是,注意到解释器在每行代码输入完成后,也就是按下回车后立即行。 例如,当在“>>>”提示符下输入第一条打印语句时,输出( 一 个 Python 字符串)立即回

显出来。没有必要创建一个源代码文件,也没有必要在运行代码前先通过编译器和链接器, 而这些是以往在使用类似 C 或 C++语言时所必需的。在本章后面你将看到,也可以在交互

提示符中运行多行语句,在你输入了所有语句行并且两次按下 Enter 键添加一个空行之后, 会立即运行这条语句。

为什么要使用交互式命令行模式 交互提示模式根据用户的输入运行代码并响应结果,但是,它不会把代码保存到一个文件中, 尽管这意味着你不能在交互会话中编写大量的代码,但交互提示仍然是体验语言和测试编

写中的程序文件的好地方。

实验 由千代码是立即执行的,交互命令行模式变成了实验这个语言的绝佳地方。这会在本书中

示范较小的例子时常常用到。实际上,这也是需要牢记的第一条原则:当你对一段 Python 代码的运行有任何疑问的时候,马上打开交互命令行并实验代码,看看会发生什么。

例如,假设你在阅读一个 Python 程序的代码并遇到了像 'Spam!'* 8 这样一个不理解其含 义的表达式。此时,你可能要花上 10 分钟翻阅手册、书本或网页来尝试搞清楚这段代码做

什么,或者你也可以直接交互式地运行它: % python

»>'Spam!'* 8 'Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam!'

#

Learning by trying

通过交互提示模式接收到的直接反馈,通常是搞清楚 一 段代码到底做什么的最快的方式。 这里,它清楚地显示:这条语句重复字符串,在 Python 中,*表示数字相乘,但对千字符

串来说,表示重复,就像重复地把一个字符串连接到其自身(本书第 4 章将详细介绍字符串)。 这种体验方式不会带来任何破坏(至少目前还没有),这是不错的。要进行真正的破坏,

例如删除文件并运行 shell 命令,你必须尝试显式地导入模块(你还需要对 Python 的系统 接口了解更多,才能变得这么有危险性)。直接的 Python 代码总是可以安全运行的。 例如,当你在交互提示模式中犯了一个错误的时候,看看会发生什么情况:

»> X Traceback (most recent call last): File "", line 1, in NameError: name'X'is not defined

64

I

第3章

#

Making mistakes

在 Python 中,给一个变量赋值之前就使用它,这总是一个错误;否则,如果名字总是填充 了默认值,一些错误将变得无法检测。这意味着你在增加计数器之前必须将其初始化为零,

你在扩展列表之前必需将其初始化等;你不需要声明变量,但是变量必须在访问它们的值 之前被赋值。

我们稍后将更详细地了解这一点,这里的重要之处在千,当你犯了这样的一个错误的时候, 不会导致 Python 或计算机崩溃。相反,你会得到 一 条有意义的出错消息,指出该错误以及

出错的代码行,并且你可以继续自己的会话或脚本。实际上,一且你熟悉了 Python, 其出 错消息通常能够提供你所需的调试支持(在“调试 Python 代码"边栏中了解更多关千调试

选项的知识)。

测试 除了充当学习语言的体验工具,交互式解释器也是测试已经写人到文件中的代码的好地方。

你可以交互地导入模块文件,并且通过在交互提示模式中运指如飞地输入命令,从而在它 们定义的工具上运行测试。

例如,下面的代码在 Python 的标准库所附带的一个预编码的模块中测试一个函数(它显示 出我们当前所工作的目录的名称,成对的反斜杠只代表一个),但一旦开始编写自己的模 块文件,也可以做同样的事情:

>» import os >» os. getcwd () 'c:\\code '

#

Testing on the fly

更为常见的是,交互提示模式是一个测试程序组件的地方,不需要考虑其源代码,你可以 在 Python 文件中导入并测试面数和类,通过输入命令来连接 C 函数,在 Jython 中使用的 Java 类等。部分是因为这种交互的本身特性, Python 支持了一种实验性和探索性的编程风格, 当开始使用的时候,你就会发现这种风格是很方便的。尽管 Python 程序员也使用文件中的

代码(稍后我们将在本书中学习使其简化的方法)进行测试,对大多数程序员来讲,交互 命令行模式依然筑起了测试保卫战的第一道防线。

使用注意:交互命令行模式 交互命令行模式简单易用,不过这里还有一些初学者需要牢记的技巧。在本章中,我列出 了一些如下的常见错误列表以供参考,但是,如果你提前阅读它们的话,会帮助你避免一 些令人头疼的问题:



只能输入 Python 命令。首先,记住只能在 Python 交互式命令行下输入 Python 代码, 而不要输入系统的命令。这里有一些方法可以在 Python 代码中使用系统命令(例如使 用 os.system) ,但是并不像简单地输入命令那么直接。

你应如何运行程序

I

6s



print 语句仅在文件中才是必需的。在交互解释器中自动打印表达式的结果,不需要在 交互式命令行下输入完整的打印语句。这是一个不错的特性,但是换成在文件中编写

代码时,用户就会产生一些困惑:在文件中编写代码,必须使用 print 语句来进行输出, 因为表达式的结果不会自动显示。记住,在文件中需要写 print, 在交互式命令行下则

是可选的。



在交互命令行模式下不需要缩进(目前还不需要)。当输入 Python 程序时,无论是在 交互式命令行下还是在 一 个文本文件中,请确定所有没有嵌套的语句都在第 一 列(最

左边)。如果不是这样, Python 也许会打印 “SyntaxError" 的信息。在第 10 章以前, 你所编写的所有语句都不需要嵌套,所以这条法则目前都还适用。记住,

每行开头的

空格也会产生错误的消息,所以不要在交互式命令行下以 一 个空格或制表符开始,除

非这是 一 段嵌套代码。



留意复合语句下的提示符变化。我们在第 4 章之前不会见到复合(多行)语句,笼统

地讲是在第 10 章之前。但是,为了预先有个准备,当在交互模式下输入两行或多行的 复合语句时,提示符会发生变化。在简单的 shell 窗口界面中,交互提示符会在第 2 行

及后边的行由“>>>“变成“..." ;在 IDLE 的 GUI 界面中,第 1 行之后的行会自动 缩进。 在第 10 章中将看到这为什么如此重要。就目前而言,如果在代码中输入,偶然碰到“.. . “ 这个提示符或空行 , 这可能意味着让交互式命令行的 Python 误以为输入多行语句。试 着点击回车键或 组合键来返回主提示模式。也可以改变”>>>”和“... "

(它

们在内置模块 sys 中定义),但是在本书的例子中,假定并没有改变过这两个提示符。



在交互命令行模式中,用一个空行结束复合语句。在交互命令行模式中,要告诉交互 式 Python 已经输入完了多行语句,必须要插人一个空行(通过在一行的起始处按下 Enter 键)。也就是说,你必须按下 Enter 键两次,才能运行一条复合语句。相反,在 文件中空行是不需要的,并且如果有的话也将会忽略。在交互式命令行下工作的时候, 如果你没有在一 条复合语句的末尾按两次 Enter 键,将会陷人尴尬的境地,因为交互式 解释器根本什么也不会做,它等着你再次按下 Enter 键。



交互命令行模式一次运行一条语句。在交互提示模式中,你必须运行完 一 条语句,然 后才能输入另 一 条语句。对千简单语句来说,这很自然,因为按下 Enter 键就可以运行 输入的语句。然而,对千复合语句,记住必须提交一个空行来结束该语句,然后运行它,

之后才能输入下一 条语句。

输入多行语句 冒着重复的风险,在更新本章内容的时候,我收到了受最后两项错误伤害的读者的邮件,

因此,这两项错误还是值得强调的。我将在下 一 章中介绍多行(复合)语句,并且我们将 在本书后面更正式地介绍其语法。由千它们在文件中和在交互命令行模式中的行为略有不 同,因此,这里有两点要注意。

66

I

第3章

首先,在交互命令行模式中,注意像结束 for 循环和 if 测试那样,用一个空行结束多行复 合语句。换言之,必须按两次 Enter 键来结束整个多行语句,然后让其运行。例如(这里的 双关不是有意的) :

>» for x in'spam• : print(x)

HPress Enter twice here to make this loop run

在脚本文件中,复合语句后面不需要空抎只在交互命令行模式下,才需要该空行。在文件中,

空行不是必需的,如果出现了,将会直接忽略掉在交互命令行模式中,它们会结束多行语句。 记住:先前的"..."延续行提示符是作为可视化引导由 Python 自动打印的 1 你的界面(例 如 IDLE) 中可能并没有它,本书有时也会将其遗漏。当它不在的时候,不要自己输入这个 字符。

还要记住,交互命令行模式每次只运行一条语句:必须按两次 Enter 键来运行一个循环或其 他的多行语句,然后才能输入下一 条语句:

>» for x in'spam': print(x) •.. print('done') File "", line 3 print('done')

#

Press Enter twice before a new statement

^

SyntaxError: invalid syntax 这意味着不能在交互命令行模式中复制并粘贴多行代码,除非这段代码的每条复合语句的

后面都包含空行。这样的代码最好在一个文件中运行,下一小节将讨论这一 话题。

系统命令行和文件 尽管交互命令行对千实验和测试来说都很好,但是它也有 一 个很大的缺点: Python 一且执 行了输入的程序之后,它们就消失了。在交互式命令行下输入的代码是不会保存在 一 个文

件中的,所以为了能够重新运行,不得不从头开始输入。复制 - 粘贴和命令重调在这里也 许有点用,但是帮助也不是很大,特别是当输入了相对较大的程序时。为了从交互式会话 中复制-粘贴代码,不得不重新编辑清理出 Python 提示符、程序输出以及其他一些东西, 这实在不是一 种现代的软件开发方法论。

为了能够永久保存程序,需要在文件中写入代码,这样的文件通常叫作模块。模块是一 个 包含了 Python 语句的简单文本文件。 一且编写完成,可以让 Python 解释器多次运行这样

的文件中的语句,并且可以以多种方式去运行:通过系统命令行,通过点击图标,通过在

IDLE 用户界面中选择等方式。无论它是如何运行的,每 一次当你运行模块文件时 , Python 都会从头至尾地执行模块文件中的每一 条代码。

你应如何运行程序

I

67

这一部分的术语可能会有某些变化。例如,模块文件常常作为 Python 写成的程序。也就是

说,一个程序是由一系列预编写好的语句构成,保存在文件中,从而可以反复执行。可以 直接运行的模块文件往往也叫作脚本(一个顶层程序文件的非正式说法)。有些人将“模 块“这个说法应用于袚另一个文件所导入的文件,而将“脚本”应用千一个程序的主文件,

我们通常也会这样做。

(之后会为大家继续解释``顶层”

“导入“

”主文件”的含义)。

不论你怎样称呼它们,下面的几部分内容将会探索如何运行输人至模块文件的代码。这一 节将会介绍如何以最基本的方法运行文件 : 通过在系统命令行模式下的 python 命令行列出

它们的名字。对某些人来说,这似乎有些粗糙简单,但可以通过使用如 IDLE (后面讨论) 这类 GUI 避免。对千很多程序员而言,一个系统 sheU 命令行窗口加上一 个文本编辑器窗口,

这就组成了他们所需的一个集成开发环境的主力部分。

第一段脚本 让我们开始吧。打开文本编辑器(例如, vi 、 Notepad 或 IDLE 编辑器),在命名为 script]. PY 的新文本文件中输入如下 Python 语句,并把它保存在之前创立的工作代码目录:

Afirsr Pyrhon scripr import sys print(sys.platform) print(2 ** 100) x ='Spam!' print(x * 8)

#

# Load a library module #

Raise 2 to a power

#

String repetition

这个文件是我们第一个正式 Python 脚本(不算第 2 章中仅 2 行的那个脚本)。对千这个文 件中的代码,我们应该不会担心太多,但是,简要来说,这个文件:



导入一 个 Python 模块(附加工具的库),以获取系统平台的名称。



运行 3 个 print 函数调用,以显示脚本的结果。



使用一个名为 x 的变址,在创建的时候对其赋值,保存一个字符串对象。



应用我们将从下一章开始学习的各种对象操作。

这里的 sys. platform 只是 一个字符串,它表示我们所工作的计算机的类型,它位千名为 sys 的标准 Python 模块中,我们必须导入以加载该模块(稍后将详细介绍导人)。

为了增加乐趣,我在这里还添加了一些正式的 Python 注释,即#符号之后的文本。我先前 提到过这些,既然它们现在出现在脚本里,就得更加正式一些。注释可以自成一 行,也可 以放置在代码行的右边。#符号后的文本直接作为供人阅读的注释而忽略,并且不会看作语 句的语法的一部分。如果你要复制这些代码,也可以忽略掉注释,它们只是提供信息。在

本书中,我们通常使用 一 种不同的格式体例来让注释更加容易识别,但是,在代码中,它 们是作为正常文本显示的。

68

I

第3章

此外,现在不要在意这个文件中的代码的语法,我们随后将学习其所有的语法。主要要注 意的是,我们已经把这段代码输入到一个文件中,而不是输人到交互命令行模式中。在这 个过程中,我们已经编写了一个功能完整的 Python 脚本。 注意,这个模板文件叫作 script].py 。对千所有的顶层文件,也应该直接叫作脚本,但是,

要导入到客户端的代码的文件必须用.PY 后缀。我们将在本章稍后学习导入。此外,一些文 本编辑器通过.PY 后缀来检测 Python 文件,如果没有这个后缀,可能无法使用如语法着色 和自动缩进等功能。

使用命令行运行文件 保存如这个文本文件,可以像下面这样将其完整的文件名作为一条 python 命令的第一个参 数,在系统 shell 命令行中输入(不要在 Python 交互式命令行下输入,如果下面的代码不工作, 继续阅读下一段),从而要求 Python 来运行它:

% python script1.py win32 1267650600228229401496703205376

Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! 再次,我们可以在系统所提供的用千命令行的任何入口(例如一 个 Windows 命令行窗口、

一个 x term 窗口,或者类似的窗口)中,输入这样的 一个系统 shell 命令。但是确保在所保 存脚本文件的相同工作目录下运行它(如果需要 “cd" 到那里),并确保在系统命令行下

运行它,而不是 Python 的">>>”提示符。也要记住,如果你的 PATH 设置没有配置的话, 要像前面一样,用完整的目录路径替换 “python" 。但是 “py" Windows 启动器程序并不

要求如此, 3.3 及后续版本也可能不要求。 对初学者的另 一 个提示:不要在上 一节创建的 script I.py 源文件中输入任何前面的文本。

这些文本是一个系统命令和程序输出,不是程序代码。这里的第一行是用来运行源文件的 shell 命令,接下来的一些行是源文件 print 语句产生出来的结果。再次,记住%代表系统

提示符

不要自己输入它(不要嫌捞叨,这是很普遍的一个初级错误)。

如果一切工作按计划进展,这条 shell 命令将使得 Python 一行 一行地运行这个文件中的代码, 并且,我们将会看到该脚本的 3 条 print 语句的输出: Python 所知道的底层平台的名称、 2

的 100 次方,以及我们前面见过的相同的字符串重复表达式的结果(第 4 章将介绍后两者 的更多细节)。

如果 一 切没有桉计划进行,你会得到 一 条错误消息,确保已经在文件中精确地输入了这里 所示的代码,并再次尝试。下一节有对这一过程的额外选项和指南,我们将在边栏“调试

Python 代码”部分介绍调试选项,但是,目前你可能只能死记硬背。如果所有这一切都失 败的话,你也许需要尝试下在前面讨论的 IDLE GUI 中运行,这是一个包装了一些启动细

节的工具,但有时却是以牺牲你使用命令行时更明确的控制为代价。

你应如何运行程序

I

69

如果你不嫌复制代码单调乏味或者易出错的话,你也可以从网站上获取示例代码,但在开

始阶段输入代码会帮助你学习避免语法错误。参阅前言来了解如何获取本书示例文件的细

节。

不同的命令行使用方式 由千这种方法使用 shell 命令行来启动 Python 程序,所有常用的 shell 语法都适用。例如, 我们可以使用特定的 shell 语法,把 一 个 Python 脚本的输出定向到 一 个文件中,从而保存

起来以备以后使用或查看: % python script1.py > saveit.txt 在这个例子中,前面运行中的 3 个输出行都存储到了 saveit.txt, 而不是打印出来。这通 常叫作流重定向 (stream redirection) ,它用千文本的输入和输出,而且在 Windows 和类 UNIX 系统上都可以使用。这对测试来说是很棒的,因为你可以编写监视其他程序输出变

化的程序。它几乎和 Python 不相关 (Python 只是支持它而已),因此,我们在这里略过有 关 shell 重定向语法的细节。

如果你仍然在 Windows 平台上工作,这个例子也同样有效,但是,系统提示符和先前描述 的通常有所不同:

C:\code> python script1.py win32 1267650600228229401496703205376

Spam!Spam!Spam!Spam!Spam!Spam!Spam !Spam! 通常,如果你没有把 PATH 环境变量设置为包含 python 的完整路径,确保在自己的命令中 包含它,或者首先执行切换目录命令来进人该路径:

(:\code> C:\python33\python script1.py win32 1267650600228229401496703205376

Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! 另一种情况,如果你在使用 Python 3.3 (之前描述过)

中新的 Windows 启动器, PY 命令

有着同样的效果,但是它并不要求 一 个目录路径或 PATH 设置,它也允许你在命令行指定 Python 版本号:

c:\code> py -3 script1.py 社 n32

1267650600228229401496703205376

Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! 在较新的 Windows 版本上,我们也可以只是输入脚本的名字,井省略掉 Python 本身的名字。 由千新的 Windows 系统使用 Windows 注册表(又称文件名关联)找到用哪个程序来运行一

70

I

第3章

个文件,我们不需要在命令行上显式地使用名字 “python" 来运行一个.PY 文件。例如,在

大多数 Windows 机器上,前面的命令可以缩写如下,并可以被 3.3 之前版本的 python 或是 3.3

及之后版本的 PY 自动运行,就好像你在文件资源管理器中点击了文件图标一 样(下面介绍 关于此选项的更多内容) C: \code> script1. py 最后,如果你所在的目录与工作的目录不同,别忘了给出脚本文件的完整路径。例如,如

下的系统命令行,运行自 D:\other, 假设 Python 在你的系统路径中,但要运行的文件存放 在其他地方:

C:\code> cd D:\other D:\other> python c:\code\script1.py 如果你的 PATH 没有包含 Python 的目录,你没有在使用 Windows 启动器 PY 程序,并且 Python 和脚本文件都没有位千你所工作的目录中,那么针对两者都使用完整的路径:

D:\other> C:\Python33\python c:\code\scriptl.py

使用注意:命令行和文件 从系统命令行开始运行程序文件是相当直接明了的启动选择,特别是在通过你之前的日常

工作巳熟悉了命令行的使用时。由千几乎每台电脑都有某种命令行和目录结构的概念,所 以这也许是运行 Python 程序最便捷的方式 。 对千初学者来说,我们提示大家注意这些新手 陷阱以帮助你避免一些挫折感:



注意 Windows 和 IDLE 上的自动扩展名。如果使用 Windows 系统的记事本编写程序文 件,保存文件时要注意选择所有文件类型,并显示地指定文件后缀为.PY 。否则记事本 会自动将文件保存成扩展名为 txt 的文件(例如保存成 spam.py. txt) ,导致在某些项目 中难以使用;如此,它不会是便捷的。

更糟糕的是, Windows 默认隐藏文件扩展名,所以除非改变查看选项,否则你可能没 有办法注意到你编写的文件是文本文件而不是 Python 文件。文件的图标可以给我们 一 些提示:如果图标上没有 一 条小蛇的话,你可能就有麻烦了。发生这样问题的其他一 些

症状还包括 IDLE 中的代码没有颜色高亮,以及点击时没有运行而变成了打开编辑文件。

Microsoft Word 默认文件扩展名为. doc 。更糟糕的是,它增加了 Python 语法中不合乎 语法的 一些格式字符。作为 一 条简要的法则,在 Windows 下保存文件时,永远要选择 所有文件,或者使用对程序员更加友好的文本编辑器,例如 IDLE 。 IDLE 甚至不会自

动添加.PY 后缀 : 这个特性程序员也许会 喜欢,但是 一 般用户不会。



在系统命令行模式下使用文件扩展名和目录路径,但是在导入时别使用它们。在系统 命令行中别忘记输入文件的完整文件名。也就是说,使用 python scriptl. py 而不是

你应如何运行程序

I

71

python scriptl 。相反地,我们将会在本章后边提到 Python 的导入语句,忽略 PY 文件 后缀名以及目录路径(例如 import

scriptl) 。这看起来琐碎,混淆两者却是一个常

见的错误。 在系统命令行模式下,你是在一个系统的 shell 中,而不是 Python 中,所以 Python 的 模块文件的搜索规则不再适用了。正因如此,必须同时包括 .PY 后缀,如果必要的话,

指向运行文件的绝对目录路径。例如,运行一个不在你工作目录中的文件时,一般都

需要列出其绝对路径(例如 python d:\tests\spam.py) 。然而,在 Python 代码中, 你可以只写 import spam, 并依靠 Python 模块搜索的路径定位文件,这将稍后进行介绍。



在文件中使用 print 语句。是的,我们已经这样说过了,但这是一 个常见错误,值得我 们在这里重复说明。不像交 互 式命令行的编程,我们往往要使用 print 语句来看程序

文件的输出。如果没有看到如何输出,确保在你的文件中已经使用了 “print" 。在交互 式会话中是不需要 print 语句的,因为 Python 自动响应表达式的结果 1 这里的 print

无伤大雅,但确实是不必要的录入。

UNIX 风格可执行脚本:#! 我们下 一 项启动技能其实是上 一 个的特殊形式,不管本节的标题,这在今日的 UNIX 和 Windows 系统上都可应用千程序文件的运行。由千它生根发芽千 UNIX, 所以让我们的故 事从这里讲起吧。

UNIX 脚本基础 如果在 Python 、 Linux 及其他的类 UNIX 系统上使用 Python, 可以将 Python 代码编程为可

执行程序,就像使用 Shell 语言编写的 csh 或 ksh 程序一样。这样的脚本往往叫作可执行脚本。 简而言之, UNIX 风格的可执行脚本只是包含了 Python 语句的一般文本文件,但是有两个

特殊的属性:



它们的第一行是特定的。脚本的第一行往往以字符#!开始(常常叫作 “hash bang" 或 "shebang") ,其后紧跟着机器 Python 解释器的路径。



它们往往都拥有可执行的权限。脚本文件往往通过告诉操作系统它们可以作为顶层程

序执行,而拥有可执行的权限。在 UNIX 系统上,往往可以使用 chmod +x file.py 来 实现这样的目的。

让我们看一个类 UNIX 系统的例子。再次使用文本编辑器创建一 个名为 brian 的 Python 代

码文件:

#!/usr/local/bin/python print('The Bright Side'+'of Life...')

72

1

第3章

# + means concatenate for strings

文件顶端特定的一行告诉系统 Python 解释器保存在哪里。从技术角度讲,第一行是 Python 注释。就像之前介绍的一样, Python 程序的注释都是以#开始,直到本行的结束为止;

它们是为阅读代码的读者提供额外信息的地方。但是当第一行和这个文件一样的话,它在 UNIX 上就有特定的意义,因为操作系统使用它找到解释器来运行文件其他部分的程序代码。

并且,注意这个文件命名为 brian, 而没有像之前模块文件一样使用. PY 后缀。给文件名增 加 PY 也没有关系(也许还会提醒你这是一个 Python 程序文件),但是因为这个文件中的 代码并不打算被其他模块所导入,这个文件的文件名是没有关系的。如果通过使用 chmod

+x brian 这条 shell 命令赋予了这个文件可执行的权限,你就能够在操作系统的 shell 中运 行它,就好像这是一个二进制文件一样(下面的例子中,要么确保” . "

(当前工作目录)

在你的系统 PATH 设置中,要么通过./brian 运行)

% brian

The Bright Side of Life.. .

UNIX env 查找技巧 在一些 UNIX 系统上,可以避免在脚本文件中硬编码 Python 解释器的路径,而可以在文件 特定的第一 行注释中像这样写:

#!/usr/bin/env python ... script goes here... 当这样编写代码的时候, env 程序可以通过系统的搜索路径的设置(在绝大多数的 UNIX Shell 中,通过搜索 PATH 环境变最中罗列出的所有目录)定位 Python 解释器。这种方法可 以使代码更具可移植性,因为没有必要在所有的代码中的第一行都硬编码 Python 的安装路

径。如此一来, 一旦脚本移动到新机器,或 Python 移动到新位置,你只需更新 PATH, 而 不是全部的脚本。

假设在任何地方都能够使用 env, 无论 Python 安装在系统的什么地方,你的脚本都可以照 样运行。事实上,这样的 env 形式比较一般化,比起如/usr/binlpython 的形式更值得推荐,

因为 一 些平台可能会到处安装 Python 。

当然,这是 env 在任何系统中都是相同的路径的前

提下(有些机器上,有可能在 ls bin 、 /bin 或其他地方) 1 如果不是的话,所有的可移植性

也就无从谈起了。

Python 3.3 Windows 启动器: Windows 也有#!了 对运行 Python 3.2 及之前版本的 Windows 用户的提示:这里介绍的方法是一个 UNIX 小花

你应如何运行程序

I

73

招,它在你的平台上可能不会工作。不必担心,直接使用前面探索过的基本命令行技巧吧。

在显式的 python 命令行上列出该文件的名称:注 l C:\code> python brian The Bright Side of Life... 在这一案例中,你不需要顶部的特殊#!注释(尽管当其出现时 Python 也会忽略它),而该 文件也不需要被赋予可执行的特权。实际上,如果你想要在 UNIX 和微软 Windows 之间可

移植地运行文件(当你总是使用基本命令行方式,而不是 UNIX 风格的脚本)来启动程序时, 你的生活就会更加简单。 如果你使用 Python 3.3 或之后的版本,或单独安装了它的 Windows 启动器,其结果是

UNIX 风格的#!行在 Windows 上确实也意味着什么。除了提供前面提到过的 PY 可执行特 权之外,前面提到的新 Windows 启动器会企图解析护行来决定启动哪 一个 Python 版本运 行脚本的代码。此外,它允许你以完整或部分的形式给出版本号,在这 一 行分辨出最常见 的 UNIX 模式,包括/usr/bin/env 形式。

当你从命令行运行带有 PY 的程序脚本,以及点击 Python 文件图标时(在这种情况下 PY 通 过文件名关联隐式地运行),启动器的护解析机制开始起作用。不同千 UNIX, 想要这个 在 Windows 上工作,你不必用可执行权限标记文件,因为文件名关联取得类似的结果。 例如,下面的第 一 段代码通过 Python 3.X 运行,第二段代码通过 Python 2.X 运行(没有显 式的编号,启动器默认为 2.X, 除非你设置了 PY_PYTHON 环境变量)

c: \code> type robin3. py #l/usr/bin/python3 print ('Run','away I...') c:\code> py robin3.py Run away!.. .

c: \code> type robin2.py #!python2 print'Run','away more!...' c:\code> py robin2.py Run away morel...

注 I:

# 3.Xftmction # Run file per#! line version

# 2.X statement # Run file per#! line version

在我们探索命令行时已经计论过,所有新的 Windows 版本也允许你在系纨命令行上只轮 入 PY 文件的名称一一 它们使用注册表决定该文件应该用 Python 打开(轮入 brian.py 同 轮入 python

brian.py 是等价的) 。 这一命令行模式精神上同 UNIX 的#!是类似的,

尽管 Windows 上它是全系纹的,而不是逐文件的 。 它也要求显示. PY 扩展名·没有它文 件名关联不能工作 。 一些程序可能实际中在 Windows 上觥释和使用开头的#!行,很像

在 UN1X 上所做的那样(包括 Python 3.3 的 Windows 启动器) , 但是 Windows 上系统级 shell 自身会简单地忽略它 。

74

I

第3 章

除了在命令行上传递版本号外这可以工作(前面为了开启交互命令行模式我们简要地见过 这个),当启动脚本文件时它也同样工作:

c:\code> py _3.1 robin3.py Run away!...

#

Run per command-line argument

总的效果就是启动器允许 Python 版本既可以逐文件指定,也可以逐命令行指定,分别通过 使用#!代码行和命令行参数实现。至少这是启动器非常简短的介绍。如果你在 Windows

上正在使用 Python 3.3 或之后的版本,或是将来打算使用,我推荐你顺便看一下附录 B 中 对启动器的完整介绍(如果你还没有这样做的话)。

点击文件图标 如果你不是一位命令行狂热分子,一般你可以通过点击文件图标,使用开发 GUI 和运用其

他随平台变化的架构,启动 Python 脚本,从而避免使用命令行。让我们快速看一下这些替 代方案中的第一个吧。

图标点击基础知识 图标点击在绝大多数平台上以 一 种或另一种形式被支持。以下是这些如何在你的电脑上构 建的 一个纲要:

Windows 图标点击 在 Windows 下,注册表使通过点击图标打开文件变得很容易。安装过后,点击 Python

程序文件, Python 使用文件名关联将其自身自动注册为打开它们的程序。正因如此,

你可以通过使用鼠标简单地点击(或双击)程序的文件图标来启动 Python 程序。 专门地,被点击的文件会通过两个 Python 程序中的一个运行,这依赖千其扩展名和你 正在运行的 Python 版本。在 Python J.2 和之前的版本中, PY 文件在控制台(命令行)

窗口通过 python.exe 运行,而 pyw 文件不需要控制台,通过 pythonw.exe 文件运行。 如果点击的话,字节代码文件也通过这些文件运行。根据附录 B, 在 Python 3.3 和之后

的版本中(那里它被分别安装),新的 Windows 启动器的 py.exe 和 pyw.exe 程序起着 相同的作用,各自打开.PY 和.pyw 文件。 非 Windows 图标点击

在非 Windows 系统中,也能够使用相似的技巧,但是图标、文件资源管理器导航方案 以及很多方面都有少许不同。例如,在 Mac OS X 系统上,你可能使用应用程序文件夹 下的 MacPython (或 Python N.M) 文件夹中的 PythonLauncher, 通过在 Finder 中点击 来运行。

在一 些 Lin肛和其他 UNIX 系统上,也许需要在文件资源管理器的 GUl 中注册 PY 的扩 展名,从而可以使用前一节介绍的#!行技巧使脚本成为可执行的程序,或者通过编辑

你应如何运行程序

I

75

文件,安装程序,或者使用其他工具,以使文件的 MIME 类型与应用程序或命令相关联。

如果一开始点击后不能正常工作,请参考文件资源管理器的文档以获得更多细节。 换言之,图标点击在你的平台上通常如你所期待的那样可以工作,但是请确保参考 Python 标准手册集中的平台使用文档 “Python 的安装和使用”获取所需的更多细节。

在 Windows 上点击图标 为了讲清楚,让我们继续使用前面编写的 scriptl.py 脚本,这里重复如下以节省篇幅:

# Afirst Python script

import sys print(sys.platform) print(2 ** 100) x ='Spam!' print(x * 8)

#

Load a library module

# Raise 2 to a power # String repetition

我们已经介绍了,总是可以从一个系统命令行来运行这个文件:

(:\code> python scriptl.py win32 1267650600228229401496703205376 Spam!Spam!Spam!Spam!SpamlSpam!Spam!Spam! 然而,点击图标让你不需要任何输人即可运行文件。为了实现这点,你得在电脑上找到这 个文件的图标。在 Windows 8 上,你可以右击屏幕左下角来打开文件资源管理器。在更早 的 Windows 版本上,你可以在开始按钮的菜单中选择“计算机”

(XP 中是“我的电脑”)。

还有其他方法能够打开文件资源管理器,一且你这么做了,在 C 驱动器上一直前进下去直 到进入你的工作目录。 此时此刻,你将会得到与图 3-1 的截图类似的文件资源管理器窗口(这里使用了 Windows 8) 。 注意 Python 文件的图标是如何出现的:



源文件在 Windows 上有白色背景。



字节码文件有黑色背景。

根据上一章,在此图中我通过在 Python 3.1 中导入来创建字节码文件, 3.2 及之后的版本是 在 _pycache_子目录中存储字节码文件,这里也显示了出来,我也是通过在 Python 3 .3 中

导入来创建。正常情况下,你需要点击(或者运行)白色的源代码文件,以便得到你最新 的更改,而不是点击字节码文件一如果你直接启动字节码, Python 不会去检查源代码文 件是否发生了更改。为了启动这里的文件,直接点击 script J.py 的图标。

76

I

第3章

@.个 I

• Com四er

.j. Documents

一”心心_

Pictures

沁n

息 Videos

, robtn2.py 乒 robin3.py

嗡 Computer

,

• -

·



.-

-

• saiptl.pyc

.:) CDDri代 (0:) U3'.

兀terns



卢 saip11”

Local Dislc (C:)



V

v

C,

somegUI.pyw

p

Search code

Date modified

Name

"

M心c

,, Removable Oislc (

Local Di士 (C:) 片 code 令



lype

10/31/20123:27 PM file fol如 10/31/2012 3:16 PM file 10/31/2012 3:15 PM Python File 10/31/20123:15 门~ Python File 10f30心tt3:36 叩行加,n F.!l_e 10/31/20123:28 PM Compiled 叩nF阮 10/30/2012 3:36 PM Python File (no console)

< 御巴

1 item selected 211 byt氐

图 3-1 :在 Windows 上, Python 程序文件在文件资源管理器窗口中显示为一个图标,并通过 昆标双击能够自动运行(尽管你采用这种办法也许不会看到打印的输出或错误提示)

Windows 上输入的技巧 遗憾的是,在 Windows 上,点击文件图标的结果也许不是特别令人满意。事实上,就像 刚才一样,这个例子的脚本在点击后可能产生一个令人困惑的"一闪而过"的结果一不

是 Python 程序的入门者通常所期盼的那种结果反馈。这不是 bug, 但是与 Windows 版本的 Python 处理打印输出的方式有关。 在默认情况下, Python 会生成一个弹出的黑色 DOS 控制台窗口(命令行)作为被点击文件 的输入和输出。如果脚本打印后退出了,那么它就是打印后退出了~制台窗口出现了,

文本在那里打印了,但是在程序退出时,控制台窗口关闭并消失。除非你反应非常快,或 者机器运行非常慢,否则看不到任何输出。尽管这是很正常的行为,但是这也许并不是你

所想象的那样。 幸运的是,这样的问题很好解决。如果需要通过图标点击运行脚本,脚本输出后暂停, Python 3.X 中可以简单地在脚本的最后添加内置 input 函数的一条调用语句 (Python 2.X

中是 raw_input: 参见前面的注释)。例如: # A first Python script

import sys print(sys.platform) print(2 ** 100) x =' Spam!' print(x * 8) input()

# Load a library module # Raise 2 to a power # String repetition

# C:\python33\python »> import script1 win32 1267650600228229401496703205376 Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! 以上代码可以运行,但是在默认情况下,只是在每次会话运行一次(真的,不信你可以试

一 下)。在第一 次导入之后,其他的导人都不会再工作,甚至在另 一 个窗口中改变并保存 了模块的源代码文件也不行:

... Change script1.py in a text edit window to print 2

**

16...

»> import scriptl » > import scriptl 这是有意设计的结果,导入是一个开销很大的操作,以至千每个文件、每个程序不能重复

运行多千一次。当你学习第 22 章时会了解到,导入必须找到文件,将其编译成字节码,并 且运行代码。

但是如果真的想要 Python 在同 一 次会话中再次运行文件,而不停止和重新启动会话,需要 调用 imp 标准库模块中可用的 reload 函数(这个函数也是一个简单的 Python 2.X 内置函数,

但在 Python 3.x 中不是内置的) :

8O

I

第3章

» > from imp import reload # Must load from module in 3.X (only) »> reload(scriptl) win32 65536 Spam!Spam!5pam!Spam!Spam!Spam!Spam!Spam!

»> 这里的 from 语句直接从一 个模块中复制出 一个名字(稍后更详细地介绍)。 reload 函数自 身载入并运行了文件当前版本的代码,如果已经在另 一 个窗口中修改并保存了它,那将反 映出修改变化 。

这允许你在 当 前 Python 交互会话中立即编辑并改进代码。例如,这次会话中,在第 一 个

import 和 reload 调用这段时间里, script].py 中的第 二个 print 语句在另 一 个窗口中改成 了打印 2 **16一因此结果是不同的。 reload 函数希望获得的参数是 一个已经被加载了的模块对象名称,所以在重载之前 , 请确 保已经成功地导人了这个模块(如果导入报告了一个错误,你就不能重新加载,而必须再

次导入)。值得注意的是, reload 函数还希望在模块对象的名称周围有圆括号,而 import 则不需要。 reload 是一个被调用的函数,而 import 是一个语句。 这也就是为什么你必须把模块名称传递给 reload 函数作为括号中的参数,并且这也是在重

载时得到了额外的 一 行输出的原因。最后一行输出是 reload 调用后的返回值的打印显示, reload 函数的返回值是一个 Python 模块对象。第 16 章我们将会学习更多关千函数使用的 常见知识,从现在起,当你听见“函数”时,牢记需要圆括号来进行调用。

注意:版本差异提示: Python 3.X 把 reload 内置函数移到了 imp 标准库模块中。它仍然像以前

一样重载文件,但是,必须导入它才能使用。在 Python 3.X 中,运行 import imp 并使 用 imp.reload(M) ,或者像这里所示的,运行 from imp import 并使用 reload(M) 。我们

将在下一 节介绍 import 和 from 语句,并且会在后面更加正式地讨论这些内容 。 如果你在使用 Python

2. X, reload 可以作为内置函数使用,因此,不需要导入。在

Python 2.6 和 2 .7 中, reload 可以以这两种形式使用一—内置函数与模块函数一—这有 助千向 Python 3.X 的过渡。换句话说,在 Python 3 .X 中仍然可以使用重载,但是需要一 行额外的代码来导入对 reload 的调用。 向 Python 3 . X 迁移,可能部分动机是由 一 些众所周知的问题所引起的,这些问题包括

我们将在下一节讨论的 reload 和 from 语句。简而 言之,用 一 个 from 载入的名字不会 通过一个 reload 直接更新,但是用 一 条 import 语句访问的名字则会。如果你的名字在 reload 后没有改变, 尝 试使用 import 和 module.attribute 名称引用。

你应如何运行程序

l

81

模块的宏观视角:属性 导入和重载提供了 一 种自然的程序启动选项,因为导入操作将会在最后 一 步执行文件。从 更宏观的角度来看,模块扮演了一个工具库的角色,这将在第五部分详细介绍。然而,基

本的思想是直截了当的:模块往往就是变员名的包,即众所周知的命名空间,而在那个包 中的变量名称为属性。属性简单说就是绑定在特定的对象(如模块)上的变量名。

在典型的应用中,导入者得到了模块文件中在顶层所定义的所有变氮名的访问权限。这些

变量名通常被赋值给通过模块函数、类、变批等来导出的工具—一这些工具往往用在其他 文件或程序中。表面上来看, 一 个栈块文件的变盟名可以通过两种 Python 语句读取一— import 和 from, 以及 reload 调用。 为了讲清楚,请使用文本编辑器,在你的工作目录中创建 一 个名为 myfile.py 的单行 Python

模块文件,其内容如下所示:

title= "The Meaning of Life" 这也许是世界上最简单的 Python 模块文件了(它只包含单 一 的赋值语句),但是它已经足 够讲明白基本的要点。当文件导入时,它的代码运行并生成了模块的属性。也就是说,这 个赋值语句创建了 一 个名为 title 的变批和模块属性。 你可以通过两种不同的办法从其他组件获得这个模块的 title 属性。首先,你可以通过使 用 一 个 import 语句将模块作为一个整体载人,然后对模块名使用点号语法后跟一个属性名

来获取它(注意,这里我们让解释器自动地打印) :

% python

# Start Python

»> import myfile >» myfile.title

#

#

Run file; load module as a whole Use its attribute names:'.' to qualify

The Meaning of Life 一 般来说,这里的点号表达式语法 object.attribute 可以让你从任何的 object 中取出其任 意的属性,并且这是 Python 代码中最常用的操作。在这里,我们已经使用它去获取在模块

myfile 中的字符串变扯 title---换言之,就是 myfile.title 。 作为替代方案 , 你也可以通过 from 语句从棱块文件中获取(实际上是复制出)变址名: % python

# Start Python

>» from myfile import title >» title

#

Run file; copy its names II Use name directly: no need to qualify

The Meaning of life 稍后你将会了解更多细节, from 和 import 很相似,只不过增加了对载人组件的变址名的额 外的赋值。从技术上讲, from 复制了模块的属性,以便属性能够成为接收者的直接变屎一

82

I

第3章

因此,这一 次你能够直接以 title (变批)引用导入的字符串而不是 myfile.title (属性引 用)。

注 3

无论使用的是 import 还是 from 去执行导入操作,模块文件 myfile.py 的语句都会执行,并 且导入的组件(对应这里是交互命令行模式)获得在文件顶层赋值的变最名的访问权。在

这个简单的例子中只有一个这样的变扯名一变量 title 袚赋值给 一 个字符串)。但是如 果开始在模块中定义对象,例如函数和类时,这个概念将会更加有用:这样 一 些对象就变 成了可重用的软件组件,可以通过变蜇名披 一 个或多个客户端模块读取。 在实际应用中,模块文件往往定义了 一 个以上的可在文件内部和外部使用的变呈名。下面 这个例子中定义了 三 个变最名:

a='dead' b ='parrot' c ='sketch' print(a, b, c)

# Define three attributes # Exported to other Jiles

# Also used in this file (in 2.X: print a, b, c)

文件 threenames.py 给 三 个变批赋值,因此对外部世界生成了 三个属性。它还在 3.X print 语句中使用它自有的 三 个变批,就像在将其作为顶层文件运行时我们看到的多余的换行符 一样(在 Python 2.X 中, print 略有不同,因此这里省略其外层的括号来精确地匹配输出, 留心第 11 章关于这些内容的更加完整的解释)

% python threenames.py

dead parrot sketch 这个文件的所有代码运行起来就和第一 次从其他地方导入后 一样,无论是通过 import 或

from 。这个文件的客户端使用 import 得到了具有属性的模块,而使用 from 的客户端,则 会获得文件变扯名的复本:

% python

>» import threenames dead parrot sketch

# Grab the whole module: it runs here

>>>

>» threenames. b, threenames. c ('parrot','sketch')

# Access its attributes

>>>

>» from threenames import a, b, c >» b, C

#

Copy multiple names out

('parrot','sketch') 这里的结果打印在括号中,因为它们实际上是元组一— 通过输入里的逗号创建的一种类型 的对象(本书的下一 部分将会介绍),目前我们可以安全地忽略它们。 注3

注意 import 和 from 这两者都将模块文件的名称简单地列为 myfife. 而没有加上其.PY 扩 展名后缀。你在笫五部分将会学到,当 Python 寻找实际的文件时 , 它会知道在其搜索 步骤中包含后级名 。 再次,在系统的 shell 命令行中,你必须包含. PY 后缀名,但是在 import 语句中不必如此 。

你应如何运行程序

I

83

一旦你开始这样在模块文件编写多个变址名,内置的 dir 函数开始发挥作用了,你可以 使用它来获得模块内部的可用的全部变盐名的列表。下面代码以方括号的形式返回一个

Python 字符串列表(我们将从下一章开始学习列表) :

»> dir(threenames) ['_builtins_",'_doc_','_file—','_name_','__package_','a','b ' ,'c'] 这一列表的内容巳被编辑,因为它们会随 Python 版本变化。值得注意的是,当 dir 函数像

这样被调用,括号中是已导入模块的名称,它返回那个模块内部的所有属性。其中返回的 一些变址名是“免费”获得的:一些以双下划线开头与结尾的变量名 (_x __),这些通常

都是由 Python 预定义的内置变最名,对千解释器来说有特定的意义,但是在本书的此时此刻, 它们并不重要。那些通过代码赋值而定义的变量 (a 、 b 和 c) 在 dir 结果的最后显示。

模块和命名空间 模块导入是一种运行代码文件的方法,但是就像稍后我们即将在本书中展开讨论的那样, 模块同样是 Python 程序最大的程序结构,也是这门语言的首要关键概念之一 。 正如我们见过的, Python 程序往往由多个模块文件构成,通过 import 语句连接在 一起。每

个模块文件是一个变量包( 一 个命名空间)。同样重要的是,每个模块都是自包含的命名空间: 一个模块文件不能看到其他文件中定义的变量名,除非它显式地导人了那个文件。因为这点,

模块文件在代码文件中起到了最小化命名冲突的作用,因为每个文件都是一个独立完备的 命名空间,即使在它们拼写相同的情况下,一个文件中的变最名是不会与另一个文件中的 变量名冲突的。 实际上,

Python 竭尽全力将变量打包成组件以避免名称冲突,而模块就是实现这个目标的

少址的方法之 一。 我们将会在本书后面章节进一步讨论模块和其他命名空间结构,包括类 和函数定义的局部作用域。就目前而言,模块作为一个不需要重复输入而可以反复运行代

码的方法,迟早会派上用场。

注意:

import vs from, 应该指出, from 语句在某种意义上战胜了模块的名称空间分隔的目的, 因为 from 把变量从一 个文件复制到另一个文件,这可能导致在导人的文件中相同名称 的变量被覆盖,井且,如果发生这种情况的话,不会发出警告。这根本上会导致名称空

间重叠到 一 起,至少在复制的变址上会重叠。 因此,有些人建议总是使用 import 而不是 from 。然而,我不建议这么做,不仅因为

from 更短(在交互命令行模式下是 一 个优点),而且它传说中的问题在实际中几乎不是 问题。此外,这是由你来控制的问题,可以在 from 中列出想要的变量;只要你理解它 们将被赋值为目标模块中的值,这不会比编写赋值语句更危险一一这很可能是你想要使 用的另 一功能!

84

I

第3章

使用注意: import 和 reload 由于某种原因, 一旦 人们发现关千使用 import 和 reload 运行文件的知识`很多人就会倾 向千仅使用这个方法,而忘记了能够运行当前版本的代码的其他启动选项(如图标点击、 IDLE 菜单选项以及系统命令行)。然而,这种方式会很快让人变得困惑:你需要记住是何

时导人的,才能知道能否 reload, 你需要记住(只有)当调用 reload 时需要使用括号,并 且要记住让代码的当前版本运行时首先要使用 reload 。此外, reload 是不可传递的(重载 一 个模块的话只会重载该模块,而不能够重载该模块所导入的任何模块),因此有时候必 须重载多个文件。

由千这些复杂的因素(并且我们将会在后边碰到其他的麻烦,包括本章前面的提示中所讨 论的 reload/from 问题),从现在开始就要避免使用 import 和 reload 启动程序,这通常 是 一 个好主意。例如,下一节介绍的 IDLE Run 一 Run Module 的菜单选项,提供了一个更 简单并更少出错的运行文件的方法,并且总是运行代码的当前版本。系统 shell 命令行提供

了类似的优点。如果使用这些技术,不需要使用 reload 。

此外,这里如果用不同寻常的方法使用模块,可能会遇到麻烦。例如,如果想要导入 一 个 模块文件,而该文件保存在其他目录下而不是现在的工作目录,你必须跳到第 22 章学习关

千模块搜索路径的知识。现在起,如果必须导入,为了避免复杂性,请将所有文件放在同

一 目录下,同时将这个目录作为你的工作目录。注 4 也就是说, import 和 reload 已经证明是 Python 类中 一 种常用的测试技巧,并且你可能也喜 欢使用这种方法。然而,通常如果你发现自己碰壁了,那就别再继续碰壁!

使用 exec 运行模块文件 严格地讲,除了这里介绍的方法,还有更多的方法可以运行模块文件中保存的代码。例如,

exec(open('module.py').read( ))内置函数调用 ,是从交互命令行模式启动文件而不必导 人以及随后重载的另 一 种方法。每个这样的 exec 都运行从文件读取的当前版本的代码,而 不需要随后的重载 (script].py 保留我们在前面小节中一次重载它之后的样子)

% python

» > exec (open('scriptl. py'). read()) win32 65536

Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! 注4

如果你非常好奇,不想再等,简单来说导入就是 Python 在 sys.path 里列出的每一个 目录中搜索导入的模块 sys.path 是 sys 模块里目录名称字符串的 Python 列表,它从

PYTHON PATH 环境变量中获得初始化 ,加上一个标准目录集。如果你想从不是你当前工作 目录的目录中导入模块,通常那个目录必须在你的 PYTHON PATH 设置中列出 。 想要了伴史 多细节,参见笫 22 章和附录 A 。

你应如何运行程序

I

85

... change scriptl.py in a text edit window to print 2

**

32...

»> exec(open('script1.py').read(}) win32 4294967296

Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam! exec 调用 有着类似于 import 的效果,但是它实际上不会 真 地导入栈块 。默 认情况 下,每次 以这种方式调用 exec 的时候,它都重新运行文件,就好像我们把文件粘贴到了调用 exec

的地方。因此, exec 不需要在文件修改后进行模块重载,它忽略了常规的模块导人逻辑。 缺点是,由千 exec 的工作机制就好像在调用它的地方粘贴了代码一 样,和前面提到的 from 语句 一样,有可能默默地覆盖掉当前正在使用的变量。例如,我们的 script].py 赋值了 一 个

名为 x 的变量。如果这个名字也在 exce 调用的地方正在使用,那么这个名称的值将被段盖:

»> X = 999 >» exec(open('scriptl.py').read())

HCode run in this namespace by defa11/1

... same outout...



X

# Its assignments can overwrite names here

'Spam!' 相反,基本的 import 语句每个进程只运行文件一次,井且它会把文件生成到一个单独的模

块命名空间中,以便它的赋值不会改变你的作用域中的变批。为模块名称空间分隔所付出 的代价是在修改之后需要重载。

注意:版本差异提示:除了允许 exec(open('module.py' ))的形式 , Python 2.X 也包含 一个 execfile('module.py') 内置函数,这两个函数都会自动读取文件的内容。这两种 形式 都等同千 exec(open('module.py').read( ))的形式,后者更为复杂,但是在 Python 2.X 和 Python 3.X 中都可以运行。 遗憾的是,两种较简单的 Python 2.X 形式在 Python 3.X 中都不可用,这意味着我们必须

理解文件及其读取方法,以便现在完全理解这一技术(这似乎是针对 Python 3.X 的实用 性的美学痛击)。实际上, Python 3.X 中的 exec 形式需要如此多的录人,以 至干最佳

建议都是干脆不要使用它,通常启动文件的最好方式是输入系统 shell 命令行或者使用 下一节所介绍的 IDLE 菜单选项。 要了解 Python 3.X 的 exec 形式所使用的文件接口的更多内容,请参阅第 9 章。要了解

关于 exec 和它的小伙伴、 eval 和 compile 的更多内容,请参阅第 10 章和第 25 章。

IDLE 用户界面 到目前为止,我们看到了如何通过交互命令行模式、系统命令行、 UNIX 风格脚本、图标点击、 模块导入和 exec 调用来运行 Python 代码。如果你希望找到更可视化的方法, IDLE 提供了 做 Python 开发的用户图形界面 (GUI) ,而且它是 Python 系统的一 个标准并免费的部分。

86

[第 3 章

它往往被认为是 一 个集成开发环境 (IDE) ,因为它在一个单独的界面中绑定了很多不同的

开发任务。

注5

简而言之, IDLE 是 一 个能够编辑、运行、浏览和调试 Python 程序的桌面 GUI, 所有能够 在一个单独的界面实现。它在绝大多数 Python 平台上可移植地运行,包括微软 Windows 、

X Windows (用千 Linux 、 UNIX 以及类 UNIX 平台)以及 Mac OS (无论是 Classic 还是 OSX) 。 对千很多人来说, IDLE 代表了 一 种简单易用的命令行输入的替代方案,一种比点击图标出 问题的可能性更小的替代方案,并且是初学者开始编辑和运行代码的 一 种伟大方式。此外

作为平衡,你将牺牲一些控制,但是之后在你的 Python 生涯中这通常变得非常重要。

IDLE 启动细节 大多数读者应该能够立即使用 IDLE, 因为在现今的 Mac

OS X 系统和大多数 Linux 安装套

件上,它都是 一个标准组件,而在 Windows 系统上使用标准的 Python 自动安装。然而,

因为平台细节有所不同,所以在打开 GUI 之前我需要给出一些指点: 技术上讲, IDLE 是一个 Python 程序,它使用标准库的 tkinter G Ul 工具箱(在 Python 2.X 中名为 Tkinter) 来构建其窗口。这使得 IDLE 是可移植的(它在所有主流桌面平台上都同 样地工作),但是它还意味着你需要在 Python 中获得 tkioter 支持来使用 IDLE 。这种支持 在 Windows 、 Mac 和 Linux 系统上都是标准的,但是在某些系统上会出现一 些警告,而启

动也会随平台变化。这里是一些平台特定的小贴士:



在 Windows 7 及之前的版本上,启动 IDLE 很容易,它总是在 Python 安装后,在

Windows 7 和之前版本的开始按钮的 Python 菜单中有 一个菜单项( 见图 2-1) 。你也 能够通过右键点击 Python 程序图标进行选择,通过点击位千 Python Lib 目录下的子目 录 idlelib 中的文件 idle .pyw 或 idle .py 图标进行启动。在这一模式下, IDLE 是位于 C:\ Python33\L巾\idlelib 、 C:\Python27\Lib\idlelib 或类似的目录中的一 个可点击的 Python

脚本,如果愿意可以拖出它到一个快捷方式,以方便单次点击。



在 Windows 8 上,通过搜索勹dle", 通过浏览开始屏幕显示的“全部程序”,或通过 使用文件资源管理器查找前面提到过的 idle.py 文件,以在开始磁贴中查询 IDLE, 这里

你可能想要一 个快捷方式,因为在桌面模式下没有开始按钮菜单(至少现在是这样的 I 参考附录 A 获得更多指南)。



在 Mac OS X 系统上, IDLE 所需要的 一 切都作为操作系统的标准组件出现。在应用程 序的 MacPython (或 Python N. M) 程序文件夹下,应该可以启动 IDLE 。这里有一个提 示:由干细微的版本依赖, 一 些 OS X 版本可能要求安装更新过的 tkinter 支持,这里

注 5:

官方上 IDLE 是 IDE 的一种降格版本,但是它如此命名实际上是为了向 Monty Python 的 成员 Eric Idle 致敬 。 如果你不确定为什么 ,参 见笫 1 章 。

你应如何运行程序

I

87

我会使读者免受这些困扰,参见 python.org 的 Download 页面获取更多细节。



在 Linux 系统上,现今 IDLE 通常也作为标准组件出现。在你的路径下它可能采用 idle 可执行文件或脚本的形式;在 shell 中输入这个进行检测。在 一 些机器上,它可能需要 安装(参阅附录 A 获取相关指南),而在另外 一 些平台上,你可能需要使用命令行或 图标点击来启动 IDLE 的顶层脚本:运行位千 Python 的 /usr/lib 目录的 idlelib 子目录

中的 idlepy 文件(执行 find 命令获取准确的位置)。

因为 IDLE 只是标准库中模块搜索路径上的一个 Python 脚本,你通常也可以在任意平台上 以及从任意目录中运行它,在系统命令行 shell 窗口输人下面的命令(即在 Windows 上的

命令行窗口),但是你得参阅附录 A 获取关千 Python 的- m 标签的更多内容,参阅第五部 分获取关干这里要求的".“包语法的更多知识(在本书的这里盲目相信这里所说的足够了):

c:\code> python -m idlelib.idle

# Run idle.py in a package 011 module path

想了解更多关千 Windows 和其他平台上的安装问题和使用提示方面的内容,确保参阅附录

A, 还有 Python 标准手册中的 “Python 安装与使用”里的关千你的平台的提示。

IDLE 基础用法 让我们看一个示例。图 3-3 显示了 Windows 上启动 IDLE 后的场录。 Python shell 窗口是主

窗口,一开始就会袚打开,并运行交互会话(注意到>>>提示符)。这个工作起来与其他 任何交互对话一样(这里你输入的代码在输入后马上运行),并且可以作为 一 种测试和实

验工具。 IDLE 可以使用友好的菜单并配合键盘快捷键进行绝大多数操作。为了在 IDLE 中创建一个 新的脚本文件,使用 File 一 New: 在主窗口,选择 File 下拉菜单,并选择 New 打开一个 新的文本编辑窗口(那里你可以输人)。使用 File 一 Open …也可以,打开一个新的文本编 辑窗口,显示已有文件的代码,你可以编辑并运行它。

尽管这不会在本书的插图中进行详细的讲解,但 IDLE 使用了语法相关的颜色高亮,对在 主窗口输人的代码和文本编辑窗口的关键字使用的是一种颜色,常量使用的是另一种颜色 等。这有助千给代码中的组件一个更好的外观(甚至可以帮助你发现错误,例如连续的字

符串都是一种颜色)。 为了运行在 IDLE 中编辑的代码文件,使用那个文件的文本编辑窗口的 Run 一 Run 模块。 也就是说,选中该文件的文本编辑窗口,并打开那个窗口的 Run 下拉菜单,选择列举在那

里的 Run Module 选项(或者使用等效的键盘快捷键,快捷键在菜单中已给出)。如果已经 在文件打开后或最后一次保存后改变了文件,而你又忘记保存更改, Python 将会提醒你需

要首先保存文件~。

88

I

第3章

屯 即

二一。一

?四lCJII_S阮妒 fdit

Shell[lebug

..今一·一·

盯 tbon 如

3.3 .0

Qpt叩

'tf,ndows 一

J:ielp ·一

(v3.3.0:bd8星众90心,r2,

“copyright 们

s叩 29

2012, 10:55:48) -~

~

[匹C

v . 1600

32 以t

(工ntel)

l on win32

二J

“Cz.dit·“O工 ''licens.( ) ”«o工 mo工.红让o口心已on

>» 2 • 100 200 >» 'Spam!' • 15 'Sp...1 匀心! Sp工nI 匀立,. l

»> X »> X



'sp邱`

+

' N工 '

Spam I Spam I Sp...,Spam I Sp_,. I Sp""'I Sp...ISpa,nlSp心3,Spa,nISp...,



'spamNI'

»>

R王T

>>>

win32 12 67 65060022822 94014 9670320537 6 匀心! Sp皿 !Sp己n I Spam I Spam I Spam I Spam I Sp皿 I >» >>> 江0 吐 o墓,."

»>

0寡. g.tc晴d()

•c: \\code • >>>.“`心tZo=

. .,in32 ' > > > . “ ` `th

['C : \\code', 'C: \\盯 thon33\\Lib\\id.l.olib', •c :\\wnm叩S\\S了STEIC32\\python33 . zip', c • \\Python33\\lib' , • c : \\盯thon33 •, •c : \\Python33\\lib\ \霆土七•-p..ck 皇ge·']

• c : \ \行 thon33\\D立怎 ' ,

'

>>>

»> holp (bin) `已p on built-in fonction bin

in 皿心吐e built 丘,

bin( ... ) bin(n“心rJ

->

str江g

Return tho binary

n,pr詹sent,,t土on

of an in teg,,r .

»> l.."1p0rt th.i.j

,

~

1

iln: 32ieot 1s

图 3-3: IDLE 开发 GUI 的主 Python shell 窗口,在 Windows 上运行。使用 File 菜单开始(新 窗口)或改变 (Open... )一个源文件;使用文件编辑窗口的 Run 菜单去运行那个窗口的代码 (Run

Module) 按照这种方式运行时,脚本的输出结果和可能生成的任何错误信息出现在主交互窗口

(Python shell 窗口)中。如图 3-3 所示,窗口中部附近的 “RESTART" 后面的三行反映了 在独立编辑窗口打开的 script].py 脚本的执行情况。

“RESTART" 信息告诉我们用户脚本

的进程重新启动以运行编辑的脚本,并对脚本输出进行分割(如果 IDLE 已经在没使用用 户代码子进程的情况下启动了,它将不会显示。稍后会介绍关千这一模式的更多信息)。

IDLE 功能特性 同大多数 GUI 环境一 样,学习 IDLE 最好的方法可能是为你自己测试驱动它,但是 一 些关 键的用法要点可能不是那么明朗。例如,如果想要在 IDLE 的主窗口中重复前一条命令,

可以使用 组合键回滚,找到命令行的历史记录,并用 向前寻找(在一 些 Mac 系统上,可以试试使用 和 ) 。之前的命令可以重新调用井显示,并 且可以编辑改变后重新运行。

你也可以将光标放到命令上,然后点击和按下 键,在输入提示符下插入其文本,

以重新运行该命令,或使用标准的复制粘贴的操作,尽管这些技巧看起来 需 要花费更多力

你应如何运行程序

I

89

气(有时也许会被偶然地触发)。在 IDLE 之外, Windows 上的交互式命令行对话环堍中,

可以使用方向键重新调用使用过的命令。 除了命令行历史和语法高亮外, IDLE 有着如下的使用特征:



在编辑器中,为 Python 代码自动缩进或不缩进(退格键后退 一 级)



在输入时,按下 键,单词自动补全



当进行函数调用,输入其开始的飞”时,会有气球帮助弹出窗门出现



当在对象名称后面输入“. ",然后停顿或按下 键,会有对象属性的弹出选择列 表

这其中的一些可能不会在每个平台上都工作,如果你发觉它们的默认功能妨碍了你实现自 身编程风格的话,也可以重新配置或弃用。

高级 IDLE 工具 除了基本的编辑、运行函数和上一节的可用性工具, IDLE 提供了更多高级的特性,包括 一 个点击即可的图形化调试器和一个对象浏览器。 IDLE 调试器通过 Debug 菜单启用,对象

浏览器通过 File 菜单启用。该浏览器允许你通过模块搜索路径导航到文件和文件中的对象; 在文件或对象上点击则在文本编辑窗口中打开对应的源文件。

你通过在主窗口中选择 Debug 一 Debugger 菜单选项来初始化 IDLE, 然后通过在文本编辑 窗口中选择 Run-+ Run 模块选项来 启动你的脚本。一且调试器袚启用,你就可以在代码中 设置断点,在文件编辑窗口的代码行上右击会阻止它的执行,以及显示变量值,等等。你 还可以在调试过程中监视程序的执行一一当你逐步地运行代码时,会注意到当前代码行。 为了实现更加简单的调试操作,你也可以用鼠标右击错误信息的文本来迅速跳到出错的那

行代码-;i! ¾-

1- 1MiJ5,

能简单快速地修补和再次运行代码。除此以外, IDLE 的文 本

编辑器提供了一个程序员友好的大型工具集,包括这里我们不会介绍的高级文本和文件搜

索操作。因为 IDLE 使用直观的 GUI 交互,你应该用系统进行实时实验,以切身体会它的 其他工具。

使用注意: IDLE IDLE 是免费、简单易用、可移植并自动支持绝大多数平台的。我通常向 Python 新手们推荐它, 因为它简化一些启动细节,并且不假定使用者先前拥有系统命令行的经验。但是与 一些更

高级的商业化的 IDE 相比,它同样有一些局限,而对一些人来讲,它可能显得过露了。为 了帮助你避免一些 共同的陷阱,这里是一个 IDLE 新手应该在心中牢记的问题列表:



当保存文件时,你必须显式地添加 “.py" 。在讲到一般文件的时候提到过这 一 点,但

90

I

第3章

是这是一个常见的 IDLE 的障碍,尤其是对于 Windows 用户来说。 IDLE 不会在文件保

存时为文件名自动添加.PY 扩展名。第一次保存文件时,需要亲自小心地输入.PY 扩展名。 如果不这样的话,尽管你仍可以从 IDLE (以及系统命令行)运行文件,但是无法交互 式地或从其他模块导入文件。



通过在文本编辑窗口中选择 Run ➔ Run 模块运行脚本,而不是通过交互式的导入和重 载。本章前边我们看到了通过交互式命令行导入运行一个文件是可能的。然而,这种 机制会变得复杂,因为在文件发生改变后蒂要手动重载文件。与之相反,使用 IDLE 菜 单选项的 Run-+Run Module 总是运行文件的最新版本 。如果必要的话,它同样会首先

提醒你保存文件(另一个 IDLE 之外会发生的常见错误)。



你只需要重载交互地测试的模块。像 shell 命令行一样 , IDLE 的 Run 一 Run Module 菜 单选项一般只是运行顶层文件以及它导入的任何模块的最新版本。因此, Run_, Run

Module 避免了常见的令人混淆的嵌套导入。你只需要重载那些将在 IDLE 中交互地导 入和测试的模块。如果选择 import 和 reload 来替代 Run_, Run Module, 要记住使用

快捷键调用前 一 条命令。 •

你可以对 IDLE 进行定制。改变 IDLE 的 字体和颜色,在任何一个 IDLE 窗口中选择 Option 菜单中的 Configure 选项。你也可以配置快捷键组合的动作、缩进设置、自动补 全等,参考 IDLE 的帮助下拉菜单以获得更多提示。



在 IDLE 中没有清屏选项。这个选项看起来是一个经常提到的要求( 也许是由千在其他 的 IDE 中都有类似的功能选项),并且也有可能最终增加这个功能。尽管这样,目前 还没有清空交互式命令行窗口文字的办法。如果想要清理掉窗口文字,可以 一 直按着

键或者输入 一 个打印 一 系列空白行的 Python 循环(当然,没有人真会用后一 种方法,不过它听上去比按下 键更高超)。



tkinter GUI 和线程程序有可能不适用千 IDLE 。因为 IDLE 是一个 Python/tkinter 程序, 如果使用它运行特定类型的高级 Python/tkinter 程序,有可能会没有响应。这对千使用 较新版本的 IDLE 在一个进程中运行用户代码、在另一个进程中运行 IDLE GUI 本身问

题会变得小很多,但是一些程序(尤其是那些使用多线程的程序)仍然会发生 GUI 没 有响应的情况。即使只是在代码中调用 tkinter 的 quit 函数退出 GUI 程序的正常方法,

如果在 IDLE 中运行也足够导致程序的 GUI 没有响应(这里只有 destroy 是更好的)。 你的代码也许不会碰到这样的问题,但是作为经验之谈,如果使用 IDLE 编辑 GUI 程

序是永远安全的,最好使用其他的选项启动运行它们,例如,图标点击或系统命令行。 有疑问时,如果代码在 IDLE 中发生错误,请在 GUI 外再试试。



如果发生了连接错误,试一下通过单进程模式启动 IDLE 。在最新的 Python 版本中,这 个问题表面上已经消失了,但是仍旧会影响使用较老 Python 版本的读者。由千 IDLE

要求在其独立的用户和 GUI 进程间通信,有时候它会在特定的平台上发生启动错误(特 别是在一 些 Windows 机器上,它会不时地出现启动错误)。如果运行时碰到了这样的

你应如何运行程序

I

91

连接错误,常常可以通过系统命令行使 IDLE 运行在单一 进程的模式下进行启动,从而

避免通信的问题: -n 命令行标志位可以强制进入这种模式。例如,在 Windows 上,可 以开启一个命令行提示窗口,井从 C:\Python33\lib\idlelib (如果必要的话,使用 cd 切 换到这个目录下)运行系统命令行 idle.py -n 。 python

-m idlelib.idle -n 命令可以

从任何地方工作(参见附录 A 获得关于- m 标签的内容)



谨慎使用 IDLE 的一些可用的特性。对千新手来说, IDLE 让 工作变得更容易 ,但是有 些技巧在 IDLE GUI 之外并不能够使用。例如, IDLE 可以在 IDLE 的环境中运行脚本, 代码中的变量自动在 IDLE 交互对话中显示:不需要总是运行 import 命令去获取己运

行的顶层文件的变扯名。这可以很方便,但是在 IDLE 之外的环境会让人很困惑,变显 名只有在从文件中显式地导入时才能使用。

当你运行一 个代码文件, IDLE 还自动把目录修改为那个文件的目录,并且将其添加到 模块导入搜索路径中~允许你在没有搜索路径设置的时候使用 文件和导入模块。但是,当你运行 IDLE 之外的文件时,该功能无法同样地工作。使用

这样的功能没有问题,但是别忘了它是 IDLE 行为,而不是 Python 行为。

其他 IDE 由千 IDLE 是 一 个免费、可移植的 Python 标准组件,如果希望使用 IDE 的话,它是 一 个

不错的值得学习的首选开发工具。如果你是一个新人,本书建议你在本书的练习中使用 IDLE, 除非你已经对基千命令行的开发模式非常熟悉了。尽管这样,这里有 一些为 Python

开发者提供的 IDE 替代品,与 IDLE 相比,其中一些工具相当强大和健全。除 IDLE 之外、 这里是一 些 Python 最常用的 IDE: Eclipse 和 PyDev

Eclipse 是 一个高级的开源 IDE GUI 。最初是用来作为 Java IDE 的, Eclipse 在安装 了 PyDev (或类似的)插件后也能够支持 Python 开发。 Eclipse 是一个流行和强大的

Python 开发工具,它远远超过了 IDLE 的特性。它包含了对代码完成、语法突出显示 、

语法分析、重构、调试等功能的支持。其缺点就是需要安装较大的系统,并且对于某 些功能可能需要共享扩展(经过 一 段时间后这可能会有所变化)。尽管这样,当你希 望从 IDLE 升级时, Eclipse/PyDev 这个组合是值得你注意的。

Komodo 作为 一 个全功能的 Python (及其他语言的)开发环境 GUI, Komdodo 包括了标准的语 法着色、文本编辑、调试以及其他特性。此外, Komodo 提供了很多 IDLE 所没有的高 级特性,包括项目文件、源代码控制集成和正则表达式调试。在编写本书时, Komodo 不是免费的,但是参看网页获取其当前的状态一可以 Acti veS tate 网站 http://www. activestate.com 下载,这个网站还提供了附录 A 中提到的 ActivePython 安装包。

92

I

第3章

NetBeans IDE Python 版 NetBeans 是一款强大的开源开发环境 GUI, 针对 Python 开发者支持很多高级功能:代

码完成、自动缩进和代码着色、编辑器提示、代码折叠、重构、调试、代码覆盖和测试、 项目等。它可以用来开发 CPython 和 Jython 代码。与 Eclipse 一样,要获得超越 IDLE

GUI 的那些功能, NetBeans 也需要 一 些安装步骤,但很多人认为值得这么做。请搜索 Web 以查找最新的信息和链接。

Python Win Python Win 是 一个只 能在 Windows 平台上使用的免费 Python IDE, 它是作为 ActiveState 的 ActivePython 版本的一部分来分发的( 也可以单独从 http://www.python.

org 上获得)。大致来看,它很像 IDLE, 井增加了一些有用的 Windows 特定的扩展。

例如, Python Win 支持 COM 对象。如今, IDLE 也许比 Python Win 更高级(例如 , IDLE 的双进程构架使其远离挂起的现象) 。尽管如此, PythonWin 为 Windows 开发者 提供了 IDLE 没有的工具。查看 http://www.activestate.com 以了解更多信息。 Wing 、

Visual Studio 等

其他一些 IDE 在 Python 开发者中也是非常流行的,包括最为商业化的 Wing IDE, 使用 插件的 Microsoft

Visual Studio, 还有 PyCharm 、 PyScripter 、 Pyshield 和 Spyde,---f且

是这里我没有篇幅来讲述它们,讲解更多无疑会显得超时。实际上,目前几乎每 一 种 程序员友好的文本编辑器都对 Python 开发有某种程度的支持,无论它是预安装的还是

单独获取的。例如 Emacs 和 Vim, 都有足够的 Python 支持。 IDE 的选择经常是主观的,因此我鼓励你广泛搜罗来寻找适合你开发风格和目标的工具。 想要了解更多信息,查看 http:/lwww.python.org 上的资源,或者在网络上搜索 “Python IDE", 等等。现在对 “ Python editors" 的搜索会把你带到 一 个 wiki 页面,那里包含

了用千 Python 编程的许多 IDE 和文本编辑器选项。

其他启动选项 到现在为止,我们已经看到了如何运行代码交互地输入,以及如何以各种不同的方式启动 保存在文件中的代码:在系统命令行中运行、图标点击、 import 和 exec 、使用 IDLE 这样 的 GUI 等。这基本上涵盖了常规使用中的大部分技巧,对于运行你在本书中见到的代码也 是足够了。然而,还有运行 Python 代码的其他方法,其中大多数都有专门或有限的用途。 为了完整性和便千参考,下面几个小节我们将简要介绍这些方法。

嵌入式调用 在一些特定的领域, Python 代码也许会在 一 个封闭的系统中运行。在这样的情况下,我们 说 Python 程序被嵌人在其他程序中运行。 Python 代码可以保存到 一个文本文件中,存储在

你应如何运行程序

I

93

数据库中,从一个 HTML 页面获取,从 XML 文件解析等。但是从执行的角度来看,另 一 个系统(而不是你)会告诉 Python 去运行你创建的代码。 这样的嵌入式执行模式 一 般用来支持终端用户定制的。例如 一 个游戏程序,也许允许用户 进行游戏定制(及时在策略点存取 Python 代码)。用户可以提供或修改 Python 代码来定

制这种系统。由千 Python 代码是解释性的,不必重新编译整个系统以融人修改(参见第 2 章更详细地了解 Python 代码是如何运行的)。 在这种模式下,当使用 Jython 系统的时候,运行你的代码的外围系统可能是使用 C 、 C++ 或者甚至 Java 编写的。作为 一 个示例,从 C 程序中通过调用 Python 运行时 AP! 中 (Python 在机器上编译时创建的由库输出的 一 系列服务)的函数来创建并运行 Python 代码字符串是

可行的:

#include ... Py_Initialize(); PyRun_SimpleString("x ='brave'+'sir robin'");

// This is C, not Python // But it runs Python code

C 代码段中,用 C 语言编写的程序通过连接 Python 解释器的库嵌入了 Python 解释器,井

传递给 Python 解释器 一 行 Python 赋值语句字符串去运行。 C 程序也可以通过使用其他的

Python API 工具获取 Python 的对象,井处理或执行它们。 本书并不主要介绍 Python/C 集成,但是你应该意识到,按照你的组织打算使用 Python 的 方式,你也许会(或者也许不会)成为实际上运行你创建的 Python 程序的那个人。不管怎样, 你仍将很有可能使用这里介绍过的交互和基千文件的启动技术去测试代码,那些袚隔离在

这些封闭系统中并最终有可能被这些系统使用的代码。注 6

冻结二进制可执行文件 如第 2 章所介绍的那样,冻结二进制的可执行性是集成了程序的字节码以及 Python 解释器

为 一 个单个的可执行程序的包。通过这种方式, Python 程序可以像其他启动的任何可执行 程序(图标点击、命令行等) 一 样被启动。尽管这个选择对千产品的发售相当适合,但它 井不是一 个在程序开发阶段适宜使用的选择。 一 般是在发售前进行封装(在开发完成之后) 。 看上一 章了解这种选择的更多信息。

文本编辑器启动方式 像之前提到过的 一 样,尽管不是全套的 IDE GUI, 大多数程序员友好型文本编辑器都支持 注 6:

参阅《 Python 偏程》

(O'Reilly) 一书荻得关于在 CIC++程序中嵌入 Python 的更多细节 。

嵌入 API 可以直接调用 Python 函数,加载模块,以及史多 。 此外,注意 Jython 系统允许 Java 程序使用基于 Java 的 API (一个 Python 觥释器类)来调用 Python 代码 。

94

I

第3 章

Python 程序的编辑甚至运行等功能。这样的支持也许是内置的,或者可通过网络获取。例如,

你如果熟悉 Emacs 文本编辑器,可以在这个编辑器内部实现所有的 Python 编辑以及启动功 能。可以通过查看 http://www.python.org/editors 或者在网络上搜索 “Python editors" 来获

得更多细节。

其他的启动方式 根据你所使用的平台,也许有其他启动 Python 程序的方法。例如,在一 些 Macintos h 系统

上,你也许可以通过拖拽 Python 的程序文件至 Python 解释器的图标让程序运行。在 一 些 Windows 系统上,你总是能够通过开始菜单中的运行选项启动 Python 脚本。最后, Python

的标准库有一些工具允许单独进程中的其他 Python 程序来启动 Python 程序(如 os.popen 和 os.system) ,并且像 Web 这样的较大的环境也可能产生 Python 脚本(例如 一 个 Web 页 面可能调用服务器上的 一 个脚本),不过这些工具超出了本章的内容范围。

未来的可能 尽管本章反映的是当前的实际情况,其中很多都具有与平台和时间相关的特定性。确实, 这里描述的执行和启动的细节是在本书前儿版的销售过程中提出来的。作为程序启动的选 择,很有可能时不时地会有新的程序启动选择出现。

新的操作系统以及已存在系统的新版本,也许会提供超出这里列举的执行技术。一般来 说,由于 Python 与这些变化保持同步,你可以通过任何对千你使用的机器合理的方式运行

Python 程序,无论现在还是将来~在虚拟现实中拖 拽图标,或者在与你的同事交谈中喊出脚本的名字。 实现的变换也会对启动原理有某种程度的影响(例如, 一 个全功能的编译器也许会生成 一 般的可执行文件,就像如今的冻结二进制那样启动)。如果我知道未来会怎样,我可能会

去找一个股票经纪人谈谈,而不是在这里写下这些话。

应该选用哪种方式 在所有这些选择中,真正的初学者可能很自然地问:哪一种最适合我?一般来说,如果你 是刚刚开始学习 Python, 应该使用 IDLE 界面做开发。它提供了 一 个用户友好的 GUI 环境, 并能够隐藏一些底层配置细节。为编写脚本,它还提供了一个与平台无关的文本编辑器,

而且它是 Python 系统中 一个标准且免费的部分。

从另 一方面来说,如果你是 一 个有经验的程序员,也许觉得这样的方式更恓意一些,简化

成在 一 个窗口使用你选择的文本编辑器,在另 一个窗口通过命令行或点击图标启动编写程 序(事实上,这是我如何开发 Pytho n 程序的方式,但是在很久之前我偏向千 UNIX) 。因

你应如何运行程序

I

95

为开发环境是很主观的选择,本书很难提供统 一 的标准。 一 般来说,你最喜欢使用的环境

往往就是最适合你用的环境。

调试 Python 代码 一般来说,我们的读者或学生不会在他们的代码里包含错误(在此置之一笑吧),但 为了极少数可能遭遇不幸的朋友,这里快速介绍现实世界的 Python 程序员调试代码 时候常用的一些策略:

什么也不做。我这么讲并不是说 Python 程序员不要调试自己的代码 , 但是,当你



在一个 Python 程序中犯错的时候,会得到一条非常有用且容易读懂的出错消息(如 果你已经有了一些错误的话,很快会见到这些消息) 。 如果你已经了觥 Python, 特别是如果你已经熟悉自己的代码了,那么这么做通常就够了 -一一阅读出错消息,

并修改标记的行和文件 。 对于很多人来说,这就是 Python 中的调试 。 但是,对于 你没有偏写过的那些大型系统来说,这并不总是理想的做法 .

插入 print 语句。可能 Python 程序员调试自己的代码的主要方式 ( 以及我调试



Python 代码的方式)

,就是插入 p rint 语句并再次运行 。 由于 Python 在修改后立

即运行,这通常是荻取比出错消息所提供的更多信息的一种快捷方式。 print 语

句不必很复杂,一条简单的 “I am here " 或变量值的显示,通常就能够提供你所 需的足够的背景信息 。 只是别忘了,在发布你的代码之前,删除掉或注释拌(如

在前面添加一个#)用来调试的 print 。

使用 IDE GUI 调试器。对于 你没有编写过的较大的系统,以及对于那些想要更



详细地跟踪代码的初学者,大多数 Python 开发 GUI 都有某种指向点击调试器 。 IDLE 也有一个调试器,但是它在实际中似乎并不常用,可能是因为它没有命令行 ,

或者可能是因为添加 print 语句通常比设置一个 GUI 调试会话要快 。 要了觥史多 内容,诗查阅 IDLE 的帮助,或者直接自己尝试;其基本界面如本章前面的“高

级 IDLE 工具”一节所述 。 其他的 IDE (如 Eclipse 、 NetBeans 、 Komodo 和 Wing IDE) 也都提供了高级的指向点击调试器,如果你使用这些 IDE, 诗查阅它们的

文档 。

使用 pdb 命令行调试器。为了实现最终控制 , Python 附带了一个名为 pdb 的原代



码调试器,可以作为 Python 的标准库中的一个模块使用 。 在 pdb 中,我们扴入命 令来一行一行地步进执行,显示变量,设置和肴除断点,继续执行到一个断,点或

错误,等等 。 你可以通过导入交互式地启动 pdb, 或者作为一个项层脚本启动 。 不管采用哪种方式,由于我们可以轮入命今来控制会话,它都提供了强大的调试

工具 。 pdb 还包含了一个 postmortem 函数 (pdb.pm())

,可以在异常发生后执行

它,从而荻取发生错误时的信息 。 参见 Python 的库手册以及本书笫 36 章了觥关

于 pdb 的更多细节 , 参见附录 A 荻得相关示例,或使用 Python 的- m 命今参数将 pdb 作为脚本运行 。

96

I

第3章



使用 Python 的- i 命令行参数。除了添加打印或在 pdb 下运行外,你仍然能够看 见是什么出了错。如果你从命令行运行脚本,并在 python 和脚本名称之间传递了 -i 参数(即 pyth on

-i m.py) ,当你的脚本退出时, Python 就会进入它的交互

解释器模式(>>>提示符),无论它是成功地结束或是产生错误。此时此刻,你

可以打印变量的最终值来获得关于代码中所发生事情的更多细节,因为它们处于 顶层的命名空间。甚至这之后你还可以导入和运行 pdb 调试器以了斛更多相关背 景;如果脚本运行失败,它的算后检查模式将允许你检查最新的错误。附录 A 也

展示了使用中的- i 。



其他选项。如果有史具体的调试需求,你可以在开源领域找到其他工具,包括支 持多线程程序、嵌入式代码和进程附件的工具。例如,

Winpdb 系统是一个独立的

调试器,具有高级的调试支持、跨平台的 GUI 和控制台界面 。 随着我们开始编写较大的脚本,这些选项将变得更加重要 。

然而,关于调试最好的消

息可能是在 Python 中检测出并报告错误,而不是默默地传递错误或最终导致系统崩溃。 实际上,错误本身是一种定义良好的机制,称为异常,我们可以捕荻并处理它们(史

多关于异常的讨论在本书笫七部分进行)。当然犯错并不好玩,但是正如某人回忆到, 当进行调试意味着最终得出一个十六进制计算器和仔细钻研成堆的内存转储输出的时 候.有了 Python 的调试器支持,所犯的错误不会像没有调试器的情况下那样令人扁

苦不堪 。

本章小结 在本章我们学习了启动 Python 程序的一般方法:通过交互地输入运行代码,通过系统命 令行运行保存在文件中的代码、文件图标点击、模块导入、 exec 调用以及像 IDLE 这样的

IDE GUI 。本章介绍了许多实际中入门的细节。本章的目标就是给你提供足够的信息,让 你能够开工,完成我们将要开始的本书下一 部分的代码。那里,我们将会以 Python 的核心

数据类型(作为程序主题的那类对象)作为开始。 尽管如此,我们还会按照常规完成本章习题,练习本章学到的东西。因为这是本书这一部 分的最后一章,在习题后边会紧跟着一些更完整的练习,测试你对本部分的主题的掌握程度。 为了了解之后这些问题的答案,或者只是想换种思路,请查看附录 D 。

本章习题 I.

怎样才能开始一个交互式解释器的会话?

2

你应该在哪里输入系统命令行来启动一个脚本文件?

3

指出运行保存在一个脚本文件中的代码的四种或更多的方法。

你应如何运行程序

I

97

4.

指出在 Windows 下点击文件图标运行脚本的两个缺点。

5.

为什么你需要重载模块?

6.

在 IDLE 中怎样运行一 个脚本?

7.

列举 2 个使用 IDLE 的缺点 。

8.

什么是命名空间,它和模块文件有什么关联?

习题解答 I.

在 Windows 7 和之前版本上可以通过点击“开始”按钮,选择“程序”,点击 “Python",

然后选择 “Python (command line)” 菜单选项来开始一个交互会话。在 Windows 下可 以在系统终端窗口(在 Windows 下的一 个命令提示窗口)输入 python 作为 一 条系统命 令行来实现同样效果。另 一 种方法是启动 IDLE, 因为它的主 Python shell 窗口是 一 个

交互式会话窗口。依赖千你的平台和 Python 版本,如果你没有设置系统的 PATH 变拭来 找到 Python, 需要使用 cd 切换到 Python 安装的地方,或输入 python 的完整路径而不 是仅仅输入 python (例如,在 Windows 上输入 C:\Python33\python, 除非你正在使用

3 . 3 启动器)。

2.

在输入系统命令行的地方,也就是你所在的平台提供给作为系统终端的地方:在

Windows 下的系统命令行;在 UNIX 、 Linux 或 Mac OS X 上的 xterm 或终端窗口等。 你在系统的提示符下输入这个,而不是在 Python 的交互解释器的“>>>”提示符下输 入一—当心不要混淆这些提示符。

3.

一 个脚本(实际上是模块)文件中的代码可以通过系统命令行、文件图标点击、导人 和重载、 exec 内置函数以及像 IDLE 的 Run

--+

Run Module 菜单选项这样的 IDE GUl

选取来运行。在 UNIX 上,还可以使用#!技巧来运行,井且 一些平台还支持更为专用 的启动技术(如拖拽和拖放)。此外, 一 些文本编辑器有运行 Python 代码的独特方式, 一 些 Python 程序作为独立的"冻结二 进制”可执行文件提供;并且一 些系统在嵌入式

模式下使用 Python 代码,其中代码由 C 、 C++或 Java 等语言编写的 一 个封闭程序自动 运行。后几种技术通常用来提供 一 个用户定制层级。

4.

打印后退出的脚本会导致输出文件马上消失,这在你能够看到输出之前(这也是 raw_ input 这个技巧之所以有用的原因) ;脚本产生的同样显示在输出窗口的错误信息,会

在查看其内容前关闭(这也是对千大多数开发任务,系统命令行和 IDLE 这类 IDE 之 所以更好的原因)。

5

在默认情况下, Python 每个进程只会导人(载入) 一 个模块 一 次,所以如果你修改了

它的源代码,并且希望在不停止或者重新启动 Python 的情况下运行其最新的版本,必 须重载它。在你重载 一 个模块之前至少已经导人了 一 次。在系统命令行中运行代码,

98

I

第3章

或者通过图标点击,或者使用 IDLE 这类 IDE, 这不再是一个问题,因为这些启动机制 往往每次都是运行源代码的最新版本。

6

在你希望运行的文件所在的文件编辑窗口,选择窗口的 Run

-+

Run Module 菜单选项。

这可以将这个窗口的源代码作为顶层脚本文件运行,并在交互 Python shell 窗口显示其 输出。

7.

IDLE 在运行某种程序时会失去响应一一特别是使用多线程(本书话题之外的 一 个高级 技巧)的 GUI 程序。并且 IDLE 有 一 些方便的特性,在你一 且离开 IDLE GUI 时会伤害你: 例如,在 IDLE 中一个脚本的变量是自动导入到交互的作用域中的,当你运行文件时工 作目录会变更.但是通常 Python 自身不会采用这些步骤。

8

命名空间就是变量(变批名)的封装。它在 Python 中以 一 个带有属性的对象的形式出现。 每个模块文件自动成为 一 个命名空间:也就是说, 一 个对变量的封装,这些变量对应 了顶层文件的赋值。命名空间可以避免在 Python 程序中的命名冲突,因为每个模块文 件都是独立完备的命名空间,文件必须显式地导人其他文件才能使用这些文件的变晕

名。

第一部分练习题 是时候自己开始编写程序了。这第一部分的练习相当简单,但是这样设计是为了确保你为

学习钻研本书的剩余部分做好准备,而这里的一些问题提示了未来章节的一些主题。一定 要查看附录 D 的第 一 部分以获得答案;练习题及其解答有时候包含了一些并没有在这部分 主要内容中的补充信息,所以即使你能够独立回答所有的问题,也应该看看解答。

I.

交互。使用系统命令行、 IDLE 或者任何在你的平台上工作的其他方法,开始 Python

交互命令行(>>>提示符),并输入表达式 “Hello World!" (包括引号)。这行字符串 将会显示出来。这个练习的目的是确保己配置 Python 运行的环境。在某些情况下,你

也许需要首先运行一 条 cd shell 命令,输入 Python 可执行文件的绝对路经,或者增加 Python 可执行文件的路径至 PATH 环境变量。如果想要的话,你可以在 cshrc 或.kshrc

文件中设置 PATH, 使 Python 在 UNIX 系统中永久可用;在 Windows 上,环境变益 GUI 通常就是你想要的。参照附录 A 获得坏境变扯设置的帮助。

2

程序。使用你选择的文本编辑器,写 一 个简单的包含了单个打印 “Hello module world!" 语句的模块文件,并将其保存为 modulel.py 。现在,通过使用任何你喜欢的启

动选项来运行这个文件:在 IDLE 中运行,点击其文件图标,在系统 shell 的命令行中

将其传递给 Python 解释器程序(例如 python module1. py) 等。实际上,尽可能地使 用本章所讨论到的启动技术运行你的文件去实验。哪种技术看起来最简单?

(当然,

这个问题没有正确的答案。)

3.

模块。紧接着,开始一 个 Python 交互命令行(>>>提示符),并导入你在练习 2 中所

你应如何运行程序

I

99

写的模块。试着将这个文件移动到 一 个不同的目录,并再次从其原始的目录导入(也

就是说,当导入时在原来的目录运行 Python) 。发生了什么?

(提示:在原来的目录

中是否仍然有一个 module1.pyc 的 字节码文 件,或者在_pycache_ 子目录下有类似 的

文件?)

4.

脚本。如果你的平台支持的话,在 module].py 模块文件的顶行增加 一行 #!,赋予这个 文件可执行的权限,并作为可执行文件直接运行它。在第一行需要包含什么? #! 一般 在 UNlX 、 Linux 和 UNIX 类平台(如 Mac OS X) 有意义;如果你在 Windows 平台上 工作,遇过“开始一运行“对话框或类似的操作,然后试着在命令行窗口不在其前边

加 “python" 这个词而直接列出其名字来运行这个文件(这在最近版本的 Windows 上

有效)。如果你正在使用随着操作系统安装的 Python 3.3 或 Windows 启动器,改变脚

本的#!代码行进行实验,来启动你可能在电脑上已安装的不同 Python 版本(或等效地, 根据附录 B 中的学习手册一步步完成)。

5.

错误和调试。在 Python 交互命令行中,试着输入数学表达式和赋值。首先输入 2 500 和 1 /

**

0, 并且像我们在本章前面所做的那样引用 一个未定义的变量名。发生了什

么? 你也许还不知道,但是当你犯了一个错误,你正在做的是异常处理:这将会在第七部

分深入探索。如同你将会在那里学到的,从技术上正在触发所谓的默认打印标准错误 信息的异常处理逻辑。如果你没有获得错误信息,那么默认的处理模块获得并作为回

应打印了标准的错误信息。 异常总是和 Python 中的调试概念密切相关的。当你第一次开始的时候, Python 关千异

常的默认错误消息总是为你的错误处理提供尽可能多的支持,它们给出错误的原因,

并且在代码中显示错误发生时所执行的行。要了解关千调试的更多内容,参见本章的“调 试 Python 代码”部分。

6.

中断。在 Python 命令行中,输入:

L

= [ 1,

2)

L.append(L) L

Make a 2-item list Append Las a single item to itself # Print L: a cyclic/circular object

# #

发生了什么?如果使用的是比 1.5 版更新的 Python, 你也许能够看到一个奇怪的输出,

我们将会在本书的下一部分讲到。如果你用的 Python 版本比 1.5. l 还老,在绝大多数 平台上 组合键也许会有用。为什么会发生这种情况? 警告:如果你有一个比 1.5.1 版更老的 Python, 在运行这个测试之前,保证你的机 器能够通过中断组合键 中止一个程序,不然的话你得等很长时间。

7.

文档。在你继续感受标准库中可用的 工具和文档集的结构之前 ,至少花 l5 分钟浏览 一 下 Python 库和语言手册。为了熟悉手册集的主题,至少得花这么长的时间。 一 且你这

100

I

第3章

样做了,将很容易找到你所需要的东西。你可以在一些 Windows 版本上的 Python 开 始按钮菜单项,或者在 IDLE 的 Help 下拉菜单上的 Python Docs 选项里,或者在网址

http://www.python.org/doc 上找到这个手册。本书将会在第 15 章用部分内容描述一些 手册及其他可用文档资源中的内容(包括 PyDoc 以及 help 函数)。如果还有时间,前 往 Python 的网站以及 PyPI 第 三方扩展的网站看看。特别留意 Python.org (http://www.

python.org) 的文档和搜索页面,它们是至关重要的资源。

你应如何运行程序

I , 01

第二部分

类型和运算

第4章

介绍 Python 对象类型

本章我们将开始学习 Python 语言。从非正式的角度来说,在 Python 中,我们使用“材料”

来处理”事务”注 l 。

”事务”指的是像加法以及拼接这样的操作形式,而“材料”指的就

是我们操作的对象。在本书这一部分中,我们将注意力集中在“材料”上,以及程序运用 这些“材料”可以做的”事务”。

从更正式的角度来讲,在 Python 中,数据以对象的形式出现一无论是 Python 提供的内置

对象,还是使用 Python 或是像 C 扩展库这样的扩展语言工具创建的对象。尽管我们在之后 才能形成这 一 概念,但对象无非就是内存中的一部分,包含数值和相关操作的集合。正如

我们将看到的, Python 脚本中的一切都是对象,甚至简单的带有值(如 99) 或支持运算操 作(加法、减法等)的数字也是如此。 由千对象是 Python 中最基本的概念,从这一章开始,我们将会全面地体验 Python 的内置

对象类型。之后的章节会补充这里没有提到的细节。这里我们的目标是简要地介绍一下基 础知识。

Python 知识结构 在接触代码之前,先来简单地了解本章如何概括 Python 全貌。从更具体的视角来看, Python 程序可以分解成模块、语句、表达式以及对象,如下所示: 1.

程序由模块构成。

2.

模块包含语句。

3.

语句包含表达式。

注]

诗容忍我的咬文嚼字,这也许是作为一个计算科学家的职业习惯 。

105

4.

表达式创建井处理对象。

在第 3 章中对模块的讨论介绍了这个等级层次中的最高 一 层。本部分的章节将会从底层开 始,探索你在编程过程中将使用的内置对象和表达式。 我们将在本书的下一部分学习语句,你会发现它们很大程度上是在管理我们现在遇到的对 象。更进一步地,当我们接触到本书“面向对象编程”部分的对象时,你会发现 Python 允 许我们通过使用或模仿在这一部分探索到的对象类型,定义属千我们自己的对象类型。因 为这一系列原因,内置对象是所有 Python 之旅中不可错过的登机口。

注意:传统介绍编程时着重强调编程的 三大支柱 :顺序语句(执行这一 条,然后下一 条)、选 择语句(当那个值为真时,执行这一条)和循环语句(重复执行多次这一条)。 Python 拥有全部这三类语句,同时也有一些相应的定义一函数和类。这些概念能够帮助你更 早地建立思维模式,但是它们相对精巧和简洁。例如列表推导的复杂表达式,实际上只 是循环语句和选择语句的结合, Python 中部分术语可能有多重含义,而且后来的许多概

念将与此不尽相同。在 Python 中,更为统一的原则就是对象,以及对象的操作。欲知详情, 请继续阅读。

为什么要使用内置类型 如果你使用过较低层语言(如 C 或 C++) ,应该知道很大一部分工作集中千使用对象(或 者叫作数据结构)去表现应用领域的组件。你需要部署内存结构,管理内存分配,实现搜

索和读取例程等。这些工作听起来非常乏味(且容易出错),并且往往背离程序的真正目标。 在典型的 Python 程序中,这些令人头痛的大部分工作都消失了。因为 Python 提供了强大

的对象类型作为语言的组成部分,在你开始解决问题之前,往往没有必要编写对象的实现。 事实上,除非你有内置类型无法提供的特殊对象要处理,否则最好使用内置对象而不是使 用自己的实现。下面是部分原因:



内置对象使程序更容易编写。对于简单的任务,内置类型往往能够表现问题领域的所 有结构。 Python 直接提供了强大的工具,例如集合(列表)和搜索表(字典),你可 以立即使用它们。只是使用 Python 内置对象类型就能够完成很多工作。



内置对象是可扩展的组件。对千较为复杂的任务,或许仍需要使用 Python 的类或 C 语 言的接口提供你自己的对象。但就像本书稍后要介绍的内容,我们手动实现的对象往

往建立在像列表和字典这样的内置类型的基础之上。例如, 一 个栈数据结构也许会实 现为管理和定制内置列表的类。



内置对象往往比定制的数据结构更有效率。在速度方面, Python 的内置类型使用了已 优化的用 C 实现的数据结构算法来加速。尽管你可以实现属千自己的类似的数据类型, 但往往很难达到内置数据类型所提供的性能水平。

106

I

第4章



内置对象是语言标准的一部分。从某种程度上来说, Python 不但借鉴了依靠内置工具 的语言(如 LISP) ,而且汲取了那些依靠程序员提供自己实现的工具或框架的语言(如

C++)的优点。尽管在 Python 中可以实现独一 无二的对象类型,但在开始阶段并没有 必要这样做。此外,因为 Python 的内置工具是标准化的,它们通常都是一 致的。另一 方面,独创的框架则在不同的环境都有所不同。 换句话说,与我们从零开始所创建的工具相比,内置对象类型不仅仅让编程变得更简单,

而且它们也更强大和更高效。无论你是否实现新的对象类型,内置对象都构成了每一个 Python 程序的核心部分。

Python 核心数据类型 表 4-1 是 Python 的内置对象类型和一些编写其字面量 (literal) 所使用到的语法,也就是能

够生成这些对象的表达式注 2 。如果你使用过其他语言,其中的一些类型也许对你来说很熟 悉 。 例如,数字和字符串分别表示数字和文本的值,而文件对象则提供了处理保存在计算 机上的真实文件的接口。 表 4-1: 内置对象 .之

对象类血

字面量/构造示例

数字

1234, 3.1415, 3+4j,Ob111,Decimal(),Fraction()

字符串

'spam', "Bob's",b'a\xOlc',u'sp\xc4m'

列表

[ 1, [ 2,'three'],4. 5], list (range(10))

字典

{'food':'spam','taste':'yum'}, dict(hours=lO)

元组

(1,'spam',4,'U'),tuple('spam'),namedtuple

文件

open('egg.txt'),open(r'C:\ham.bin','wb')

集合

set ('abc'), {'a','b','c'}

其他核心类型

类型、 None 、布尔型

程序单元类型

函数、模块、类(位于本书第四、五、六部分)

Python 实现相关类型

己编译代码、调用栈跟踪(位千本书第四、八部分)

不过对千一些读者来说,表 4-1 中的对象类型可能比你习惯的类型更通用也更强大 。 例如, 你会发现列表和字典就是强大的数据表现工具,省略了在使用底层语言的过程中为了支持

集合和搜索而引入的绝大部分工作。简而言之,列表提供了其他对象的有序集合,而字典 则通过键存储对象。列表和字典都可以嵌套,可以随需求扩展和删减,并能够包含任意类

型的对象。 注 2

在本书中,字面量可以单纯理鲜为那些产生对象的表达式——- 有时也称为常量 。需要 注意 , “常量”一词并不意味着对象或者变量具有不可变性(换句话说,此处的“常量”与 C++ 的 const 和 Python 的 “不变 性”的区别 -我们将在“不变性”一节中进行介绍) .

介绍 Python 对象类型

I

107

同样如表 4-1 所示,像函数、模块和类这样的编程单元(我们将在本书后面部分见到)在 Python 中也是对象。它们由 def 、 class 、 import 和 lambda 这样的语句和表达式创建,可 以在脚本间自由地传递,存储在其他对象中,等等。 Python 还提供了一组与实现相关的类型, 例如编译过的代码对象,它们通常更多地关系到工具生成器而不是应用程序开发者,本书

后续部分也会讨论它们,但由千它们特殊的身份将不会进行很深人的讨论。 比起表 4-1 的标题,表中的内容并不一 定完整,因为在 Python 程序中处理的每样东西都是 对象。例如,在 Python 中进行文本模式匹配时,创建了模式对象,进行网络脚本编程时, 使用了套接字对象。这些其他类型的对象往往都是通过在模块中导入或使用函数来建立的 (例如在 re 和 socket 模块中的模式和套接字),而且它们都有各自的行为。 我们通常把表 4-1 中的对象类型称作核心数据类型,因为它们是在 Python 语言内部高效创 建的,也就是说,有一些特定语法可以生成它们。例如,运行下面带有引号的代码: , , »> spam

实际上,你正在运行 一 个字面量表达式,这个表达式生成井返回 一 个新的字符串对象。这 是 Python 用来生成这个对象的一个特定语法。类似地,一个方括号中的表达式会生成一个 列表,大括号中的表达式会建立一个字典等。尽管这样,就像我们看到的那样, Python 中 没有类型声明,运行的表达式的语法决定了创建和使用的对象的类型。事实上 , 在 Python 语言中,诸如表 4-1 中的那些对象生成表达式就是这些类型起源的地方。

同等重要的是,一且创建了一个对象,它就和操作集合绑定了~只能对字符串进行字 符串相关的操作,对列表进行列表相关的操作。用更规范的语言描述,这意味着 Python 是

动态类型的(它自动地跟踪你的类型而不是要求声明代码),但是它也是强类型语言(你 只能对一 个对象进行适合该类型的有效操作)。 我们将会在后续章节学习表 4- 1 中的每一种对象类型。在深入介绍细节之前,让我们先迅 速地浏览在实际应用中的 Python 的核心对象。本章的余下部分将提供一 些操作,而在本章 以后的章节我们将会更深入地学习这些操作。别指望在这里能够找到所有答案一一本章的

目的仅仅是激发你学习的欲望,并介绍 一 些核心的概念。好了,最好的入门方法就是迈出 第一步,所以让我们看一些真正的代码吧。

数字 如果你过去曾经编写过程序或脚本,表 4-] 中的 一 些对象类型看起来会比较眼熟。即使你

没有编程经验,数字也是比较直接的。 Python 的核心对象集合包括常规的类型:整数(没 有小数部分的数字)、浮点数(概括地讲,就是后边有小数部分的数字)以及更为少见的

类型(有虚部的复数、固定精度的十进制数、带分子和分母的有理分数以及全功能集合等)。

108

1

第4章

Python 内置的数字类型足以表示从你的年龄到你的银行存款的绝大部分数值量,但同时还

有更多第三方库提供着更为强大的功能。 尽管提供了一些多样的选择, Python 的基本数字类型还是相当基本的。 Python 中的数字支

持一般的数学运算。例如,加号(+)代表加法,星号(*)表示乘法,双星号(**)表示幕。

»> 123 + 222 345 »> 1.5 * 4 6.0 »> 2 ** 100 1267650600228229401496703205376

# lnteger addition # Floating-point multiplication # 2 to the power JOO, again

注意这里的最后一个结果: Python 3.X 的整数类型在需要的时候会自动提供额外的精度, 以用于较大的数值(在 Python 2.X 中,一个单独的长整型会以类似的方式处理那些对千普 通整型来说太大的数值)。例如,你可以在 Python 中计算 2 的 1 000 000 次幕。

(但是你

也许不应该打印结果:有 300 000 个数字以上,你就得等一会儿了!)

>» len(str(2 ** 1000000)) 301030

# How many digits in a really BIG number?

这些嵌套的调用从内向外进行运算一首先将**的运算结果利用内嵌的 str 函数转型成字

符串,接着用 len l!II 数得到该字符串的长度。最后的结果就是位数的个数。许多对象类型 都支持 str 和 len ;随着学习的深入,我们将经常遇到它们。 在 Python 2.7 和 Python 3.1 以前的版本,一且你开始接触浮点数,很可能会遇到一些乍看 上去有些奇怪的事情:

»> 3.1415 * 2 6.2830000000000004 »> print(3.1415 • 2) 6.283

#

repr: as code (Pythons< 2.7 and 3.1)

#

str: user-friendly

第一个结果并不是 Bug, 这是显示的问题。这是由千存在两种打印 Python 对象的方式:全

精度(就像这里的第一个结果显示的那样)和用户友好的形式(如第二个结果所示)。一 般来说,第一种形式看作对象的代码形式 repr, 第二种是它的用户友好形式 str 。在早期

的 Python 版本中,浮点数的 repr 函数有时可以显示出比你预期的更加精确的结果。当我 们使用类时,这两者的区别将会表现出来。就现在而言,如果有些东西看起来比较奇怪,

试试使用原生的 print 语句显示它。 当升级到 Python 2.7 或者最新的 Python 3.X 后,浮点数的显示将更加智能,通常带有更少

的额外位数。由千本书是基千 Python 2.7 和 3.3, 以下是全书中通用的浮点数显示格式:

>»3.1415*2 6.283

# repr:as code (Pythons>= 2.7 and 3.1)

介绍 Python 对象类型

I

109

除了表达式外,随 Python 一起安装的还有一些常用的数学模块,模块只不过是我们导入以

供使用的一些额外工具包。

»> import math »> math.pi 3.141592653589793 »> math.sqrt(85) 9.219544457292887 math 模块包括更高级的数学工具,如函数,而 random 模块可以作为随机数字的生成器和随 机选择器(这里,从被方括号括起的 Python 列表中选择。列表是其他对象的有序集合,将 会在本章后面进行介绍)。

» > import random »> random.random() 0.7082048489415967 »> random.choice([1, 2, 3, 4]) 1

Python 还包括了 一 些较为少见的数字对象(如复数、固定精度十进制数、有理数、集合和

布尔值)和第三方开源扩展领域等(矩阵、向量和可扩展精度的数字)。本书稍后会讨论 这些类型。

到现在为止,我们已经把 Python 作为简单的计算器来使用。想要更好地利用内置类型,就

来看一下字符串。

字符串 字符串用来记录文本信息(如你的名字)和任意的字节集合(如图片文件的内容)。它们 是我们在 Python 中提到的第一个作为序列(一个包含其他对象的有序集合)的例子。序列

中的元素包含了一个从左到右的顺序一序列中的元素根据它们的相对位置进行存储和读 取。从严格意义上来说,字符串是由单字符的字符串所组成的序列,其他更一般的序列类

型还包括列表和元组(稍后介绍)。

序列操作 作为序列,字符串支持假设其中各个元素包含位置顺序的操作。例如,如果我们有一个包

含在 一对引号内的四个字符的字符串(字符串具有不变性),可以通过内置的 len 函数验 证其长度,并通过索引操作得到其各个元素:

»> S ='Spam' »> len(S)

# Make a 4-character string, and assign it to a name # Length

4

>» s[o]

's' 110

I

第4章

#

The first item in S, indexing by zero-based position

»> S[l) ,

p

# The second iremfrom the left

,

在 Python 中,索引是按照从最前面的偏移量进行编码的,也就是从 0 开始,第一项索引为 o, 第 二项索引为 I, 依此类推。

注意,这里是如何把字符串赋给一个名为 S 的变量的。我们随后将详细介绍这是如何做到 的(特别是在第 6 章中),但是, Python 变址不需要提前声明。当给一个变扯赋值的时候

就创建了它,可能赋的是任何类型的对象,并且当变量出现在一个表达式中的时候,就会 用其值替换它。在使用变摄的值之前必须对其赋值。我们需要把一个对象赋给一个变最以

便保存它供随后使用,要学习本章内容,知道这一点就足够了。

在 Python 中,我们能够反向索引,从最后一个开始(正向索引是从左边开始计算,反向索 引是从右边开始计算)。

ss 一一』-

---12 >m>a >·>· >,>, -

# The last item from the end in S

# The second-to-last item from the end

一 般来说,负的索引号会简单地与字符串的长度相加,因此,以下两个操作是等效的(尽 管第一个要更容易编写且不容易发生错误) :

»> S[ -1]

# The Last item in S

.m'

>» S[len(S)-1)

.' m

# Negative indexing, the hard way

值得注意的是,我们能够在方括号中使用任意表达式,而不仅仅是使用数字字面量一—只 要 Python 需要一个值,我们可以使用一个字面晕、一个变量或任意表达式。 Python 的语法

在这方面是完全通用的。 除了简单地从位置进行索引,序列也支持一 种所谓分片 (slice) 的操作,这是一种一步就 能够提取整个分片 (slice) 的方法。例如:

»> s

# A 4-characrer string

'Spam'

»> S[l:3] ,

# Slice of S from offsets I through 2 (not 3)

pa `

也许认识分片的最简单的办法就是把它们看作从一个字符串中一次就提取出一部分的方法。 它们的 一 般形式为 X[I:J] ,表示“取出在 X 中从偏移量为 I. 直到但不包括偏移量为 J 的 内容”。结果就是返回一个新的对象。例如,上边的最后一 个操作,给出在字符串 s 中从

偏移 1 到 2 (从 1 到 3-1) 的所有字符作为 一 个新的字符串。效果就是切片或“分离出“中 间的两个字符。

介绍 Python 对象类型

I

111

在一个分片中,左边界默认为 0, 并且右边界默认为分片序列的长度。这引入了 一 些常用

法的变体:

»> 5[1:] ,

pam

,

# Everything past the first (1 :len(S))

s

# S itself hasn't changed

'Spam' »> S[0:3] 'Spa'

# Everything but the last

»> S[ :3]

# Same as S[0:3]

'Spa' »> S[:-1] 'Spa'

# Everything but the last again, but simpler (0:-1)

»> S[:]

# All of Sas a top-level copy (0:len(S))



'Spam' 注意在“从第二到结尾”的命令中,负偏移量是如何用作分片的边界,上面最后 一 个操作

如何有效地复制整个字符串。正像今后将学到的那样,没有必要复制一个字符串,但是这 种操作形式在列表这样的序列中很有用。

最后,作为 一个序列,字符串也支持使用加号进行拼接 (Concatenation) (将两个字符串合 并为一个新的字符串),或者重复(通过再重复 一次创建一 个新的字符串)

»> s 'Spam'

»> S +'xyz'

# Concatenation

'Spamxyz'

»> s

# S is unchanged 'Spam' »> S * 8 # Repetition 'SpamSpamSpamSpamSpamSpamSpamSpam'

注意加号(+)对千不同的对象有不同的意义:用千数字表示加法,用于字符串表示拼接。 这是 Python 的 一 般特性,也就是我们将会在本书后面提到的多态。简而言之, 一 个操作 的意义取决千被操作的对象。正如将在学习动态类型时看到的那样 , 这种多态的特性给 Python 代码带来了很大的简洁性和灵活性。由千类型并不受约束, Python 编写的操作通常 可以自动地适用千不同类型的对象,只要它们支持一种兼容的接口(就像这里的+操作一 样)。这成为 Python 中很重要的概念。关千这方面,你将会在后面的学习中学到更多的内容。

不可变性 注意,在之前的例子中,没有通过任何操作对原始的字符串进行改变。每个字符串操作都 被定义为生成新的字符串作为其结果,因为字符串在 Python 中具有不可变性一在创建后 不能原位置 (in place) 改变。换句话说,你永远不能覆盖不可变对象的值。例如,不能通

过对其某一位置进行赋值而改变字符串,但你总是可以建立一个新的字符串并以同一个变

112

I

第4章

量名对其进行赋值。因为 Python 在运行过程中会清理旧的对象(之后你将会看到),这并 不像听起来那么复杂:

»>

s

'Spam'

>» s[o] ='z'

# Immutable objects cannot be changed ... error text omitted... TypeError:'str'object does not support item assignment

>» S ='z'+ S[l:] >» s `

zpam

# But we can ru11 expressions ro make new objects

.

Python 中的每一个对象都可以归类为不可变的或者可变的。在核心类型中,数字、字符串 和元组是不可变的列表、字典和集合不是这样一—它们可以跟你用类创造出来的对象一样, 完全自由地改变。这种特性在 Python 中是至关重要的,但是我们现在还不能全面地介绍它。

在其他方面,这种不可变性可以保证在程序中保持一个对象固定不变,可变对象的值能够 随时改变(无论是否如你所愿)。 严格来说,你可以在原位置改变基于文本的数据。这需要你将它扩展成 一个由独立字符构

成的列表,然后不加人其他字符把它重新拼接起来。另外一种方法就是使用在 Python 2.6 和 3.0 及以后新增的 bytearray 类型:

»> S ='shrubbery' »> L = list{S) »> L

# Expand to a list: […]

('s','h','r','u','b','b','e','r','y']

»> L(1] ='c' >»''. join{L)

# Change it in place # Join with empty delimiter

'scrubbery'

>» B = bytearray(b'spam') »> B.extend{b'eggs') »> B

# A bytes/list hybrid (ahead) # 'b'needed in 3.X, not 2.X # B[i] = ord(c) works here too

bytearray(b'spameggs') »> B.decode{} , spameggs

# Translate to normal string

byte array 支持文本的原位置转换,但仅仅适用千字符编码至多 8 位宽(如 ASCII) 的文本。 其他所有的字符串依然是不可变的。 bytearray 融合了不可变的字节字符串(它所用到的 b'...' 的语法在 Python 3.X 是必需的,而在 Python 2.X 是可选的)和可变列表(用[]编 写和显示)两者的特性。我们将通过进一步学习这些和 Unicode 的相关知识来深入理解这 些代码。

特定类型的方法 目前我们学过的每 一 个字符串操作都是 一 个真正的序列操作。也就是说,这些操作在

介绍 Python 对象类型

I

113

Python 中的其他序列中也能使用,包括列表和元组。尽管如此,除了 一 般的序列操作,字

符串还有独有的 一些作为方法存在的操作。方法指的是依赖并作用千对象上的函数,并通 过一个调用表达式触发。

例如,字符串的 find 方法是一个基本的子字符串查找的操作(它将返回 一 个传入子字符串 的偏移扯,或者在没有找到的情况下返回- 1)' 而字符串的 replace 方法会对全局进行搜 索和替换,这两种操作都针对它们所依赖和被调用的对象而进行。 , »> s ='Spam' »> S.find('pa')

#

Find the offset of a substring ins

#

Replace occurrences of a substring with another

1

»> s 'Spam ' >» S.replace('pa','XYZ') 'SXYZm' >» s 'Spam'

尽管这些字符串方法的命名有改变的含义,但在这里都不会改变原始的字符串,而是会创

建 一 个新的字符串作为结果

因为字符串具有不可变性,这种做法是唯一存在的 一 种可

能。字符串方法是 Python 中文本处理的头号工具。其他的方法还能够实现通过分隔符将字

符串拆分为子字符串(作为一种推导的简单形式),大小写变换,测试字符串的内容(数字、 字母等),去掉字符串后的空格字符:

>» line ='aaa,bbb,ccccc,dd' »> line.split(',') ['aaa','bbb','ccccc ' , ' dd ' ] »> S ='spam' >» S.upper() 'SPAM' »> S.isalpha() True »> line='aaa,bbb,ccccc,dd\n' »> line = rstrip() ' aaa,bbb,ccccc,dd' »> line.rstrip().split(',') ['aaa','bbb','ccccc','dd']

#

Split it on a delimiter into a list of substrings

#

Upper- and lowercase conversions

# Content tests: isalpha, isdigit, etc.

#

Remove whitespace characters on the right side

# Combine two operations

注意这里的最后一行命令一一它在调用 split( )方法前调用了 rstrip( )方法。 Python 遵循 从左到右的执行顺序,每次前一步方法调用结束,都会为后一步方法调用产生一个临时对象。 字符串还支持一个叫作格式化的高级替代操作,可以以 一 个表达式的形式(最初的)和 一 个字符串方法调用 (Python 2.6 和 Python 3.0 中新引入的)形式使用。其中,在 Python 2.7

和 3 . 1 中的字符串方法调用允许你省略相应的参数序号:

»>'%s, eggs, and %s'% ('spam','SPAM!') 'spam, eggs, and SPAM!'

114

I

第4章

#

Formatting expression (all)

>»'{o}, eggs, and {1}'.format('spam','SPAM!') 'spam, eggs, and SPAM!' ·

# Formatting method (2.6+, 3.0+)

>»'{}, eggs, and {}'.format('spam','SPAM!') 'spam, eggs, and SPAM!'

# Numbers optional (2.7+, 3.1+)

字符串格式化具有丰富的形式,我们将在本书后面的部分对此详细介绍。这些技巧对生成 数值报告的时候十分重要:

»>'{:,.2f}'.format(296999.2567) '296,999.26 ' »>'%.2f I %+osd'% (3.14159, -42) · 3.14 I -0042'

/tSeparators, decimal digits #

Digits, padding, sign:;

注意:尽管序列操作是通用的,但方法不通用(虽然某些类型共 享某些方法名,字符串的 方法只能用千字 符串)。 一 条简明的法则是, Python 的工具库是 呈 层级分布的:可作用千

多种类型的通用操作都是以内置函数或表达式的形式出现的(如 len(X) 、 X [o]) ,但是类 型特定的操作是以方法调用的形式出现的(如 a String. upper()) 。如果经常使用 Python, 你会更顺利地从这些分类中找到你所需要的 工 具,下一节将会介绍 一些马上能使用的技巧。

寻求帮助 上 一 节介绍的方法很具有代表性,但这仅仅是少数的字符串的例子而已。 一 般来说,这本

书看起来并不是要详尽地介绍对象方法。对千更多细节,你可以调用内置的 dir 函数。这 个函数列出了在调用者作用域内,形式参数的默认值;更加有用的是,它会返回 一 个列表,

其中包含了对象的所有属性。由千方法是函数属性,它们也会在这个列表中出现。假设 s 是一 个字符串,这里是其在 Python 3 . 3 中的属性 (Python 2.X 中略有不同) :

>» dir(S) ,'_dir_','_doc_', ['_add_','_class_','_contains_','_delattr_',' '_eq_','_format_', '—ge_','_getattribute_','_getitem_', ,'__ hash_','_init_','_iter_','_le_', ,_getnewargs_ ','_gt_ ','_hash_','_init_','_iter_','_le_' , ' l e n ' , ' l t ' , ' m o d ' , ' m u l ' , ' _ _ne_','_new_','_reduce_' ' , '_reduce_ex_',' —repr_',' —rmod—',' _rmul—','_setattr_',' —sizeof_', ' _ str_','_subclasshook_','capitalize','casefold','center ' ,'count', 一 一一 —' 'encode','endswith','expandtabs','find','format','format_map','index', 'isalnum','is alpha','isdecimal','isdigit','isidentifier','is lower', 'isnumeric','isprintable','isspace','istitle','isupper','join','ljust', 'lower','ls trip','maketrans','partition','replace','rfind','rindex', 'rjust','rpartition','rsplit','rstrip','split','splitlines','startswith', 'strip','swapcase','title','translate','upper','zfill'] 也许只有在本书的稍后部分你才会 对这个列表的变 量 名中有双下划线的内容感兴趣,那时

我们将在类中学习重载:它们代表了 字 符串对象的 实 现方式, 并 支持定制 。 例如 字符串的 _add_ 方法是 真 正执行字 符串拼接的函数。虽然 Python 内部将前者映射到 后者,但是你也

应 当 尽量避免直接使用 第二种形式(这不仅十分晦涩,而且还会让程序运行得更慢) :

介绍 Python 对象类型

I

11 s

»> S +'NI I' 'spamNI !'

»> s._add_{'NI!') 'spamNI !' 一般来说 , 以双下划线开头并结尾的变量名用来表示 Python 实现细节的命名模式。而这个 列表中没有下划线的属性是字符串对象能够调用的方法。

dir 函数简单地给出了方法的名称 。 要查询它们是做什么的,你可以将其传递给 help 函数。

»> help(S,replace) Help on built-in function replace: replace(...) S.replace(old, new[, count]) -> str Return a copy of S with all occurrences of substring old replaced by new. If the optional argument count is given, only the first count occurrences are replaced. 就像 PyDoc (一个从对象中提取文档的工具)一样, help 是一个随 Python 一起安装的面 向系统代码的接口。本书后面你会发现 PyDoc 也能够将其结果生成可以显示在浏览器上的 HTML 格式。

你也能够对整个字符串提交帮助查询(如 help(S)) ,但是你将看到过多或过少的信息一

所有在旧版本的 Python 中出现的方法,这些方法在新版本中也许根本用不着,因为字符串 被特殊对待。因此,通常推荐针对具体的方法进行查询 。

dir 和 help 命令都可以作用于 一个真实对象(比如我们这里的字符串 s) 或是 一 种数据类 型(如 str 、 list 和 diet) 。其中 help 命令相较千 dir 命令而言,在返回相同的函数列表 的同时,还提供了完整的类型细节,并允许你使用类名查询一个具体的方法(例如, help str.replace 来查询 str.replace 方法)。 想获得更多细节,可以参考 Python 的标准库参考手册或者商业出版的参考书,但是 dir 和

help 提供了 Python 中最原生的文档。

字符串编程的其他方式 到目前为止,我们学习了 字 符串对象的序列操作方法和类型特定的方法。 Python 还提供了 各种编写字符串的方法,我们将会在下面进行更深入的介绍。例如,反斜线转义序列表示 特殊的字符,在 Python 中表示为\ xNN 的十六进制,除非它们是可打印的字符。

»> S = ' A\nB\tC' >» len(S)

# \nis end-of-line, \tis tab # Each stands for just one character

5

>» ord('\n') 116

I

第4章

#\nis a byte with the binary value JO in ASCJ/

10 »> S ='A\08\0C' »> len(S)

# \0, a binary zero byte, does not terminate string

s

»> s •a\xOOB\xOOC'

# Non-printables are displayed as\xNN hex escapes

Python 允许字符串包括在单引号或双引号中-它们是相同的,而采用不同的引号可以 让另外一种引号被包含在其中(大多数程序员倾向千使用单引号)。 Python 也允许在三

个引号(单引号或双引号)中包括多行字符串字面量。当采用这种形式的时候,所有的行 都合并在一起,并在每一行的末尾增加换行符。这是一个语法上微妙的便捷方式,但当在 Python 脚本中嵌入像多行 HTML 、 XML 或 JSON 这样的代码时十分有用。这种方式能暂时

减少代码行数--{Jl 需要在代码段上下都加上三个双引号:

>» msg = """ aaaaaaaaaaaaa bbb'''bbbbbbbbbb""bbbbbbb'bbbb cccccccccccccc »> msg '\naaaaaaaaaaaaa\nbbb\'\'\'bbbbbbbbbb""bbbbbbb\'bbbb\ncccccccccccccc\n' Python 也支持原始 (raw) 字符串字面量,即去掉反斜线转义机制。这样的字符串字面量

是以字母 “r" 开头,并对诸如 Windows 下的文件路径的表示十分有用(如 r'C:\text\ new') 。

Unicode 字符串 Python 还支持完整的 Unicode 字符串形式,从而支持处理国际化的字符文本。例如日文 和俄文的基本字符,并没有被编码在 ASCII 字符集中。这种非 ASCII 字符能够在网页、 Email 、图形界面、 JSON 和 XML 等情形下被展示。处理这样的字符需要 Unicode 的支持。 Python 也原生支持 Unicode, 然而这些支持的形式随着 Python 版本的不同而变化。可以说 对 Unicode 的支持是 Python 不同版本间最大的差异之 一 。 在 Python 3.X 中,基本的 str 字符串类型也能够处理 Unicode (ASCII 文本是一种简单的 Unicode) ,并且用 一种独特的 bytes 字符串类型表示原始字节值(包括媒体和文本编码) ; 在 Python 2.X 中的 Unicode 常扯在 Python 3.3 以及之后所有兼容 Python 2.X 的版本中被支

持(它们与 Python 3.X 中的 str 字符串效用相同) :

>»'sp\xc4m' , , spAm >» b'a \x01c' b'a\xo1c' >» u'sp\uOOc4m' , spAm'

# 3.X: normal str strings are Unicode text #

bytes strings are byte-based data

#

The 2.X Unicode literal works in 3.3+: just str

介绍 Python 对象类型

I

117

在 Python 2.X 中,通常的 str 字符串既能处理 8 位的基于字符的字符串(包括 ASCU 文本),

也能处理原始字节值 1 一 个独特的 unicode 字符串类型用千表示 Unicode 文本 1 而 3.X 的 字节字面量能够支持在 2.6 及之后 2.X 中的可移植性(它们袚当作 2.X 中通常的 str 字符

串进行处理) :

» > print u'sp\xc杠' spiim »>'a\x01c' 'a\xo1c' »> b'a\xOlc' 'a\xOlc'

# 2.X: Unicode strings are a distinct type # Normal str strings contain byte-based text/data #

The 3.X bytes literal works in 2.6+: just str

正式地说,在 2.X 和 3.X 中,非 Unicode 字符串在可能的情况下是由 ASCII 码打印的 8 位

字节序列,而 Unicode 字符串是 Unicode 码序列。也就是说, Unicode 字符串能够分辨数字 和字符,但在编码到文件或存储到内存时不 一 定要将文本字符映射成单字节。事实上,字 节的概念并不适用于 Unicode: 一 些 Unicode 码的一个字符所占的位置大千一个字节,而且 即使是简单的 7 位 ASCII 文本在某些编码和内存存储机制下,也不是存储为一 个字符一字 节的形式:

spam spam »>'spam' . encode('utf8') b'spam »>'spam'.encode('utf16') b'\xff\xfes\xoop\xooa\xoom\xoo'

>>>

#

Characters may be 1, 2, or 4 byres in memory

# Encoded to 4 bytes in UTF-8 in files #

Bue encoded to JO bytes in UTF-16

3.X 和 2.X 也都支持前面的 bytearray 字符串类型,这实际上是一种 bytes 字符串 (2. X 中 的 str) ,能够支持大部分列表对象的原位置可变操作。 3 . X 和 2.X 还都支持编码非 ASCII 字符(带有\x 十六进制或短\ u 和长\U 的 Unicode 转义符, 以及在程序源文件中声明的以文件为范围的编码)。这里是我们在 3.X 中 三 种非 ASCII 编 码的字符(你可以在 2.X 中增加一个开头的 “u" ,井使用 “print" 语句来获得结果) : »>'sp\xc4\uooc4\Uooooooc如'

'spAAAm' 这些值的意义及其使用方式在不同的文本字符串和字节字符串中是不同的:其中文本字符

串包括 3.X 中的普通字符串和 2.X 中的 Unicode 字符串,而字节字符串包括 3.X 中的字节 串和 2.X 中的普通字符串。所有这些转义符可以被用千在文本字符串中嵌人实际的 Unicode 码原始值整数。相反,字节字符串只使用\ x 十六进制转义符来嵌入文本的被编码形式,而 非文本的解码值(只有对少数的一部分编码方式和字符来说,被编码的字节和码本身是一

样的) :

» >'\uooA3','\uOOA3'. encode('latinl'), b'\xA3'. decode('latinl') ('王 ',b'\xa3','£')

118

1

第4章

这里有个区别值得注意, Python 2.X 允许在 一个表达式中混合使用其普通字符串和 Unicode 字符串,只要所有的普通字符串都由 ASCII 字符组成,相反, Python 3.X 拥有一个更严格

的模型,禁止在没有显式转型的情况下,将其普通字符串和字节串混合使用:

b, ', ',++ ,y y', xx uu

, b, ', ',++ ,y xx y' uu

# Works in 2.X (where b is optional and ignored) # Works i11 2.X: u'xy'

# Fails in 3.3 (where u is optional and ignored) #

'x'+ b'y'.decode() 'x'.encode() + b'y'

Works in 3.3:'xy'

# Works in 3.X if decode bytes ro str:'xy' #

Works in 3.X if encode srr to bytes: b'xy'

除了这些字符串类型, Unicode 的操作大部分都能归为与文件之间进行文本的来回传输:文 本在存入文件时会被编码成字节,而在读入内存时会被解码成字符(也称为"码”)。一

旦它被载 入,我们通常只处理文本解码后的 字符串。 由于这 一 模型,文件在 3.X 中也是和内容相关的:文本文件实现了特定的编码,井接受和

返回 str 字符串,但是 二进制文件通过 bytes 字节串处理原始的二进制数据。在 Python 2.X 中,普通文件的内容是 str 字节,同时有一个特殊的 codecs 模块负责处理 Unicode 和展示 类型为 unicode 的内容。 我们将在本章后面介绍文件的时候再次遇到 Unicode, 不过我们会把 Unicode 剩下的故事放

到本书后面的部分进行介绍。它简要地出现在第 25 章中和货币符号相关的例子中,但大部 分内容会推迟到本书的高级主题部分。 Unicode 在一 些领域中十分重要,不过大部分程序员 只需要稍微了解这方面的知识。如果你的数据都是 ASCII 文本,在 2.X 和 3.X 中的字符串 和文本的操作基本上是 一样的。如果你刚开始学习编程,你可以放心地跳过 Unicode 的细节,

并在你已经精通字符串基础内容后再回过头来学习。

模式匹配 在继续学习之前,值得关注的一点就是没有任何字符串对象自己的方法能够支持基千模式 的文本处理。文本的模式匹配是本书范围之外的 一 个高级工具,但是有其他脚本语言背景 的读者也许对在 Python 中进行模式匹配很感兴趣,这里我们需要导入一个名为 re 的模块。

这个模块包含了类似搜索、分割和替换等调用,但由千我们能够利用模式来定义子字符串, 因此可以进行更通用的匹配:

>>> import re »> match = re.match('Hello[ \t]*(.*)world','Hello >» match.group(1) 'Python'

Python world')

这个例子的目的是搜索子字符串,这个子字符串以 “Hello" 开始,后面跟着零个或几个制 表符或空格,接着任意字符并将其保存至匹配组中,最后以单词 “world." 结尾。如果找到

介绍 Python 对象类型

I

119

了这样的子字符串,与模式中括号包含的部分匹配的子字符串的对应部分保存为组。例如,

下面的模式取出了 三 个被斜线所分割的组,你也可以类似地将它们替换成其他模式:

, >»match= re.match('[/:](.*)[/:](.*)[/:](.*)',' • *)[/: ](. *)','/usr/home/:lumberjack') »> match.groups() ( ' usr','home','lumberjack') >>> re.split('[/:]','/usr/home/lumberjack') ['','usr','home','lumberjack'] 模式匹配本身是一个相当高级的文本处理工具,但是在 Python 中还支持更高级的语言处理 工具,包括 XML 、 HTML 解析和自然语言处理等。我们将在第 37 章末尾看到其他有关模

式和 XML 解析的例子。不过,我们已经在本书中介绍了足够多的字符串,所以让我们开始 介绍下 一 个类型吧 。

列表 Python 的列表对象是这个语 言 提供的最通用的序列。列表是 一个任意类型的对象的位置相 关的有序集合,它没有固定的大小。与字符串不同,列表是可变的,通过对相应偏移量进

行赋值可以定位地对列表进行修改,另外还有其他一 系列的列表操作。相应地,它们提供 了 一 种灵活地表示任意集合的工具,例如 一 个文件夹中的文件、 一 个公司里的员工,或你

的收件箱中的邮件等。

序列操作 由于列表是序列的一种,它支持所有我们对字符串所讨论过的序列操作。唯 一 的区别就是

其结果是列表而不是字符串。例如,有一个有三个元素的列表:

»> L = [123,'spam', 1,23) >» len(l) 3

# A list of three different-type objects # Number of items in the list

我们能够对列表进行索引 、 切片等操作,就像对字符串所做的操作那样:

»> l[o] 123 »> L[:-1] [123,'spam']

# Indexing by position #

Slicing a list returns a new list

»> L + [4, 5, 6] # Concatlrepeat make new lists too [123,'spam', 1.23, 4, 5, 6] »> L * 2 [123,'spam', 1.23, 123,'spam', 1.23] »> L [123,'spam', 1.23]

120

I

第4章

#

We're not changing the original list

特定类型的操作 Python 的列表与其他语言中的数组有些类似,但是要强大得多。其中 一 个方面就是,列表 没有固定类型的约束。例如,上个例子中接触到的列表,包含了 三个完全不同类型的对象( 一

个整数、 一 个字符串和一个浮点数)。此外,列表没有固定大小,也就是说能够按照需要 增加或减小列表大小,用来响应其特定的操作:

>» L.append{'NI'} »> L

#

Growing: add object at end of list

#

Shrinking: delete an item in the middle

[123,'spam', 1.23,'NI']

>» L.pop(2) 1. 23



L

# "def L{2]" deletes from a list too

[ 123,'spam','NI'] 这里,列表的 append 方法增大了列表的大小,并在列表的尾部插入一项; pop 方法(或者 等效的 del 语句)移除给定偏移量的 一项,从而让列表减小。其他的列表方法可以在任意 位置插入 (insert) 元素,按照值移除 (remove) 元素,在尾部添加多个元素 (extend) 等。 因为列表是可变的,大多数列表的方法都会原位置改变列表对象,而不是创建一个新的列表:

>>> M = ['bb','aa','cc']

>» M.sort() >» M ['aa','bb','cc']

»> M.reverse() >» M ['cc','bb','aa'] 例如,这里的列表 sort 方法,默认按照升序对列表进行排序,而 reverse 对列表进行翻转。 这些例子中,这些方法都直接改变了列表。

边界检查 尽管列表没有固定的大小, Python 仍不允许引用不存在的元素。超出列表末尾之外的索引 总是会导致错误,对列表末尾范围之外赋值也是如此:

»>

L

[123,'spam','NI')

»> L[99] ... error text omitted... lndexError: list index out of range

>» L[99] = 1 ... error text omitted... IndexError: list assignment index out of range 这是有意而为之的,由于给 一 个列表边界外的元素赋值,往往会得到 一 个错误(而在 C 语

介绍 Python 对象类型

I

121

言中情况比较糟糕,因为它不会像 Python 这样进行错误检查)。在 Python 中,井不是默 默地增大列表作为响应,而是会提示错误。为了让一个列表增大,我们可以调用 append 这

样的列表方法。

嵌套 Python 核心数据类型的一个优秀特性就是它们支持任意的嵌套。能够以任意的组合对其进 行嵌套,并可以多个层次进行嵌套。例如,能够让一个列表包含一个字典,并在这个字典 中包含另一个列表等。这种特性的一个直接应用就是实现矩阵,或者 Python 中的“多维数

组”。一个嵌套列表的列表能够完成这个基本的操作(你将会在某些应用界面中看到第二行、 第三行开头部位的“.. "续行符,但却不会在 IDLE 中得到这样的结果)

»> M = [[1, 2, 3], # A 3 x 3 matrix, as nested lists [4, s, 6], # Code can spa11 lines;jbracketed [7, 8, 9]] »> M [[1, 2, 3], [4, s, 6], (7, 8, 9]] 这里,我们编写了一个包含 3 个其他列表的列表,其效果就是表示了一个 3x3 的数字矩阵。

这样的结构可以通过多种方法获取元素。 >,、/

鼠“5H

]6] [ ,[ 1 1 >4> >[> ][ 2--

6

# Get row 2

# Get row 2, then get item 3 within the row

这里的第一个操作读取了整个第二行,第二个操作读取了那行的第三个元素(正如之前字

符串的 strip 和 split 方法的连续调用, Python 从左向右依次执行代码)。串联起索引操作

可以逐层深入地获取嵌套的对象结构注 30

推导 处理序列的操作和列表的方法中, Python 还包括了一个更高级的操作,称作列表推导表达 式 (list comprehension expression) ,从而提供了一种处理像矩阵这样结构的强大工具。例 如,假设我们需要从列举的矩阵中提取出第二列。因为矩阵是按照行进行存储的,所以通

过简单的索引即可获取行,使用列表推导同样可以简单地获得列。

注 3:

这种矩阵结构适用于小规模的任务,但是对于处理史大量的数据,你会希望使用 Python 提供的数值扩展,如开源的 NumPy 和 Sci Py 系织 。这些工具相比于我们自己的嵌套列 表结构,能更高效地存储和处理大矩阵。 NumPy 已经被称为能够将 Python 变成一个和

MATLAB 系纽一样自由甚至更加强大的系统,而且例如 NASA 、 Los Alamos 、 JPL 和其 他的许多机构都将这一工具用于科学和金扯领域的任务。搜索互联网以荻取史多细节.

122

I

第4章

»> col2 »> col2 [2,

s,

= [row[1]

for row in M]

# Collect the items in column 2

8]

»> M [[1, 2, 3], [4,

#

s,

The matrix is unchanged

6], [7, 8, 9]]

列表推导源自数学中集合的概念。它是一种通过对序列中的每一项运行一个表达式来创建 一个新列表的方法,每次一个, 从左至右。列表推导是编写在方括号中的(提醒你在创建

列表这个事实),并且由使用了同一个变量名的(这里是 row) 表达式和循环结构组成。之 前的这个列表推导表达基本上就是它字面上所讲的:

“把矩阵 M 的每个 row 中的 row[1],

放在一个新的列表中。“其结果就是一个包含了矩阵的第 二 列的新列表。

实际应用中的列表推导可以更复杂:

>» [row[l] + 1 for row in M] [3, 6, 9] >» [row[1] for row in M if row[l] % 2 [2, 8]

#

==

o]

Add 1 to each item in column 2

# Filter out odd items

例如,这里的第一个操作,把它搜集到的每一个元素都加了 I, 第二个使用了一个 if 条件 分句,通过使用%取模表达式(取余数)过滤了结果中的奇数。列表推导创建了新的列表 作为结果,但是能够在任何可迭代对象(我们将很快介绍这个术语)上进行迭代。例如,

这里我们使用列表推导去单步处理坐标的 一个硬编码列表和一 个字符串:

»> diag = [M[i][i] for i in [o, 1, 2]] >» diag

#

Collect a diagonal from matrix

(1, 5, 9]

>» doubles = [c * 2 for c in'spam'] >» doubles

# Repeat characters in a string

['s s','pp','aa','mm'] 这些表达式同样可以用于存储多个数值,只要将这些值包在 一 个嵌套的集合中。下面的代

码展示了原生函数 range 生成连续整数的功能。在 Python 3.X 中你需要将其包在 list() 中 来显示 (Python 2.X 中的 range 直接生成了一个列表)。

>» list(range(4)) [o, 1, 2, 3) »> list(range(-6, 7, 2))

#

0..3 (list() required in 3.X)

#

-6 to +6 by 2 (need list() in 3.X)

[-6, -4, -2, o, 2, 4, 6)

** 2, x ** 3) for x in range(4)] # Mulriple values, "if'filrers [[o, o], [1, 1), [4, BJ, [9, 27]] »> [[x, x / 2, x * 2] for x in range(-6, 7, 2) if x > o] [[2, 1, 4), [4, 2, 8), [6, 3, 12)) >» [[x

正如你的直觉告诉你的那样,列表推导以及相关的内容函数 map 和 filter 比较复杂,这 一 章节不 过多讲述了。在这里进行简要说明的目的是告诉你 Python 中既有简单的工具,也有

介绍 Python 对象类型

I

123

高级的工具。列表推导是一个可选的特性,在实际中非常有用,并常常具有处理速度上的

优势。它们也能够在 Python 的任何序列类型中发挥作用,甚至一些不属千序列的类型。你 将会在本书后面学到更多这方面的内容。

然而作为前瞻,我们会发现在 Python 的最近版本中,列表推导被赋予了更一 般的用法:它 现在不仅仅只被用作生成列表。例如,括号中的推导语法也可以用来创建产生所需结果的 生成器。例如,内置的 sum 函数将序列中的元素求和一在下面的例子中,按照要求将矩 阵中行的元素相加:

»> G = (sum(row) for row in M) »> next(G)

# Create a generator of row sums # iter(G) not required here

6

»> next(G) 15 »> next(G) 24

# Run the iteration protocol next()

内置的 map 可以通过 一 个函数,按照请求一次生成 一 个项目,实现类似的功能。就像

range, 在 Python 3.X 中将它包在 list 里面会强制它返回所有的值 1 这在 2.X 版本中是不 必要的,因为在那里 map 创建了一个包含所有结果的列表,在其他自动迭代的上下文中也 是不必要的,除非需要用到多次扫描或者类似列表的行为:

»> list(map(sum, M)) [6, 15, 24]

# Map sum over items in M

在 Python 2.7 和 3 . X 中,推导语法也可以用来创建集合和字典:

>» {sum(row) for row in M} {24, 6, 15}

# Create a set of row sums

>» {i : sum(M[i]) for i in range(3)} {O: 6, 1: 15, 2: 24}

# Creates key/value table of row sums

实际上,在 Python 3.X 和 2.7 中,列表、集合、字典和生成器都可以用推导来创建:

>» [ord(x) for x in'spaam'] # List of character ordinals (115, 112, 97, 97, 109] »> {ord(x) for x in'spaam'} #Sets remove duplicates {112, 97, 115, 109} »> {x: ord(x) for x in'spaam'} # Dictionary keys are unique {'p': 112,'a': 97,'s': 115,'m ': 109} # Generator of values >» (ord(x) for x in'spa am') import struct »>packed= struct.pack('>i4sh', 7, b'spam', 8) >» packed b'\xOO\xOO\xOO\x07spam\xOO\xo8' »> >» file = open('data. bin','wb') »> file.write(packed) 10 »> file.close()

# Create packed binary data # IO byres, not objects or text

# Open binary output file # Write packed binary data

读取并还原二进制数据实际上是一个对称的过程,并非所有的程序都需要触及如此底层的 字节领域,但 Python 中的二进制文件简化了这个过程:

»> data= open('data.bin','rb'),read() >» data b'\xoo\xoo\xoo\xo7spam\xoo\xo8' »> data[4:8] b'spam' »> list(data) [o, o, o, 1, 11s, 112, 97, 109, o, 8] »> struct.unpack('>坏sh', data) (7, b'spam', 8)

# Open/read binary datafile # IO bytes, unaltered # Slice bytes in the middle # A sequence of 8-bit bytes II Unpack into objects again

Unicode 文本文件 文本文件用于处理各种基千文本的数据,从备忘录到邮件内容到 JSON 再到 XML 文档。如 今的世界数据互联更广泛,尽管我们不能真的脱离文本的种类来讨论它们~也必须知 道文本的 Unicode 编码类型,不论是因为它与你所在的平台默认类型不同,还是因为你不

能依靠这一默认类型实现数据移植。 值得庆幸的是,这比听上去要容易很多。如果一个文件的文本没有采用我们所用平台的默

认编码格式,为了访问该文件中的非 ASCII 编码的 Unicode 文本(如同这一章早些时候提

到过的),我们可以直接传入一个编码名参数。在这种模式下, Python 文本文件自动在读 取和写人的时候采用你所指定的编码范式进行解码和编码。在 Python 3.X 中:

» > S = ' s p \ x c 4 m ' # Non-ASCII Unicode text »> s , spAm , »> S[2] # Sequence of characters ,入'

» > file = open('unidata. txt','w', encoding='utf-8') >» file.write(S)

# Write/encode UTF-8 text # 4 characters written

4

» > file. close() » > text = open('unidata. txt', encoding='utf-8'). read() »> text spAm >» len(text)

# Read/decode UTF-8 text

# 4 chars (code points)

介绍 Python 对象类型

1

135

4 通常情况下,这些自动编码和解码能够满足你的需求。因为文件在传输时处理编码事务, 你可以直接把文本当作内存中的 一 个由字符构成的简单字符串进行处理,而不必担心其中 的 Unicode 编码原型。如果有需要,你同样可以通过进人 二进制模式来查看文件中真正存

储的内容:

»> raw= open('unidata.txt','rb').read{) »> raw b'sp\xc3\x84m' »> len(raw) 5

It Read raw encoded bytes

# Really 5 bytes in UTF-8

如果你从 一 个非文件的源中得到 Unicode 数据,你同样可以手动编码和解码 ~l 如从 emai [信息或网络连接中进行解析:

»> text.encode('utf-8')

# Manual encode to bytes

b'sp\xc3\x84m' »> raw.decode('utf-8') spAm

# Manual decode to str

了解文本文件如何自动地在不同的编码格式下编码同 一 字符串是十分有益的,这提供了 一

种将数据翻译成不同编码的方式一只要提供了正确的编码方式名称,文件中不同种类的 字节可以被解码成内存中相同的字符串。

»> text.encode('latin-1') b'sp\xc4m' »> text.encode('utf-16') b'\xff\xfes\xoop\xoo\xc4\xoom\xoo'

#

Bytes differ in others

»> len(text.encode('latin-1')), len(text.encode('utf-16')) (4, 10)

>>> b'\xff\xfes\xoop\xoo\xc4\xoom\xoo'.decode('utf-16') spAm

# But same string decoded

这些方法或多或少都能够在 Python 2.X 中发挥同样的效用,但在 2.X 中 Unicode 字符串 袚编码成以一个 “u" 开头的形式,而字节码并不需要或显示一个字母 “b" 的开头,而且

Unicode 文本文件必须使用 codecs.open (跟 3.X 版本中的 open 接受编码格式名称的用法 一样)打开,并且使用特殊的 unicode 字符串表示内存中的内容。在 2.X 版本中,由千正 常文件就是基千字节的数据,因此二进制文件模式看起来像是可选的,但如果出现行结尾 时就需要避免修改它(本书将在后面对此进行详细讨论) :

136

» > import codecs »> codecs.open('unidata. txt', encoding='utf8').read()

# 2.X: read/decode text

u'sp\xc4m' >>> open ('unidata. txt','rb'). read ()

#

I

第4章

2.X: read raw byres

'sp\xc3\x84m'

» > open('unidata. txt'). read()

#

2.X: rawlundecoded too

'sp\xc3\x84m' 如果仅需处理 ASCH 文本,你通常不必考虑这种特殊情况。但 Python 的字符串和文件机制 将在你处理二进制数据(包含绝大多数的多媒体文件)或者包含国际化字符集的文本(包 含了今天互联网上绝大多数的内容)时成为一笔财富。 Python 同时支持非 ASCll 文件名(不 仅仅是文件内容),但这很大程度上是自动化的;诸如 walkers 和 listers 等工具在必要时提 供了更多的控制,相关的细节留到第 37 章讨论。

其他类文件工具 open 函数能够实现在 Python 中对绝大多数文件进行处理。尽管这样,对于更高级的任务, Python 还有额外的类文件工具:管道、先进先出队列 (FIFO) 、套接字、按键值访问的文件、

持久化对象 shelve 、基千描述符的文件、关系型数据库接口和面向对象数据库接口等。例如, 描述符文件 (descriptor file) 支持文件加锁和其他的底层工具,而套接字提供网络和进程

间通信的接口。虽然在本书中我们并不全部介绍这些话题,但是在开始使用 Python 编程时, 你一定会发现这些都是很有用的。

其他核心类型 到目前为止,除了我们看到的核心类型外,还有其他的或许能够称得上核心类型的类型, 这取决千我们定义的分类有多大。例如,集合是最近增加到这门语言中的类型,它不是映

射也不是序列,相反,它们是唯一 的不可变的对象的无序集合。集合可以通过调用内置 set 函数而创建,或者使用 Python 3 . X 和 2 . 7 中新的集合字面最和表达式创建,并且它支持一

般的数学集合操作(新的用千集合字面量的{...}语法是有意义的,因为集合更像是一个 无值的字典的键) :

»>

X = set('spam') >>> Y = {'h','a','m'}

# Make a set with set literals in 3.X and 2.7

>» X, Y

# A tuple of two sets without parentheses

#

Make a set out of a sequence in 2.X and 3.X

({'m','a','p','s'}, {'m','a', ' h'})

»>

X & Y

# Intersection

{'m','a'}



X

I

Y

{'m','h','a','p','s'} >>> X - Y {'p','s'} » >X >Y False

»> {n ** 2 for n in [1, 2, 3, 4]} {16, 1, 4, 9}

#

Union

#

Difference

# Superset

#

Set comprehensions in 3.X and 2.7

介绍 Python 对象类型

I

137

即使是不太精通数学的程序员也往往把集合当作处理例如过滤重复对象,分离集合间差异,

进行非顺序的等价判断等任务的利器。在列表、字符串和其他所有可迭代对象中:

»> list(set([1, 2, 1, 3, 1])) [1, 2, 3] »> set('spam') - set('ham') {'p','s'} >» set('spam') == set('asmp'} True

#

Filtering out duplicates (possibly reordered)

#

Finding differences in collections

# Order-neutral equality tests(== is False)

集合同时支持 in 函数的成员测试操作,而所有 Python 中其他的集合类型也是如此:

>»'p'in set('spam'),'p'in'spam','ham'in ['eggs','spam','ham'] (True, True, True) 此外, Python 最近添加了一些新的数值类型:十进制数(固定精度浮点数)和分数(有 一 个分子和一个分母的有理数)。它们都被用来解决浮点数的局限性和内在的不精确性:

>» 1 / 3 O. 3333333333333333 »> (213) + (1/2) 1.1666666666666665

# Floating-point (add.0 in Python 2.X)

»> import decimal »> d = decimal.Decimal{'3.141') >» d + 1 Decimal('4,141')

#

Decimals: fixed precision

»> decimal.getcontext().prec = 2 >» decimal. Decimal ('1. 00') / decima 1. Decimal('3. 00') Decimal('0.33') »> from fractions import Fraction >» f = Fraction{2, 3) >» f + 1 Fraction(s, 3) >» f + Fraction(1, 2) Fraction(7, 6)

# Fractions: numerator+denominator

Python 最近还添加了布尔值(预定义的 True 和 False 对象实际上是定制后以逻辑结果显示 的整数 l 和 O) ,以及长期以来一直支持的特殊的占位符对象 None (它通常用来初始化变 呈和对象) :

138

»> 1 > 2, 1 < 2 (False, True) >» bool('spam') True

# Booleans #

Object's Boolean value

»> X = None »> print(X) None >» L = [None] • 100

#

None placeholder

I

第4章

# Initialize a list of JOO Nones

>» L [None, None, None, None, None, N'o ne, None, None, None, None, None, None, None, None, None, None, None, None, None, None,... a list of 100 Nones... ]

如何破坏代码的灵活性 本书稍后将对所有的这些对象进行介绍,但此处仍有一点需要强调。内置泊数 type 返回的 type 对象是一 个能告知其他对象类型的对象,该函数返回的结果在 Python 3.X 中略有不同, 因为类型 (type) 已经完全和类 (class) 合并起来了(我们将在本书第五部分的新式类部 分中介绍)。假设 L 仍然是前面小节中的那个列表: # In Python 2.X:

»> type(L}

»> type(type{L}}

In Python 3.X: >» type{L)

»> type(type(L}}

# Types: type of Lis I儿t type object #

Even f)•pes are objects

#

# 3.X:

#

types are classes, and vice versa

See Chapter 31 for more on class types

除了允许交互地访问对象,这个函数的实际应用是,允许编写代码来检查它所处理的对象

的类型。实际上,在 Python 脚本中至少有 3 种方法可做到这点:

»> if type(L} == type([]): print('yes') yes »> if type(L} == list: print('yes') yes >» if isinstance(L, list): print('yes')

#

Type testing, if you must...

# Using the type name

# Object-oriented tests

yes 现在本书已经介绍了所有类型检验的方法,尽管这样,我们不得不说,就像在本书后边看 到的那样,在 Python 程序中这样做基本上都是不推荐(这也是一个有经验的 C 程序员刚开

始使用 Python 时的 一个标志)。在本书后面,当我们开始编写函数这样较大的代码单元的 时候,才会澄清其原因,但这是一个(可能是唯一的)核心 Python 概念。在代码中检验了 特定的类型,实际上破坏了它的灵活性,即限制它只能使用 一 种类型工作。没有这样的检测, 代码也许能够使用整个范围的类型工作。

这与前边我们讲到的多态的思想有些关联,它是由 Python 没有类型声明这一特征发展出来 的。就像你将会学到的那样,在 Python 中,我们编写对象接口(被支持的操作)而不是类型。

这意味着,我们关注一个对象能做什么,而非它是什么。不关注千特定的类型意味着代码

介绍 Python 对象类型

I

139

会自动地适应它们中的很多类型:任何具有兼容接口的对象均能够工作,而不管它是什么 对象类型。尽管支持类型检测(即使在一些极少数的情况下,这是必要的),你将会看到 它井不是一个 “Python 式”的思维方法。事实上,你会发现多态也许才是使用 Python 的一 个关键思想。

用户定义的类 在本书后面的内容中,我们将深入学习 Python 中的面向对象编程(这门语言一个可选的但

很强大的特性,它可以通过支持程序定制而节省开发时间)。用抽象的术语来说,类定义 了新的对象类型,扩展了核心类型,所以这里对这些内容做一个概览。也就是说,假如你

希望有一个对象类型对职员进行建模。尽管 Python 里没有这样特定的核心类型,下边这个 用户定义的类或许符合你的需求:

>» class Worker: def _init_(self, name, pay): self.name= name self.pay = pay def lastName(self): return self.name.split()(-1] def giveRaise(self, percent): self.pay *= (1.0 + percent)

#

Initialize when created

# self is the new object

#

Split string 0,1 blanks

# Update pay inplace

这个类定义了一个新的对象种类,有 name 和 pay 两个属性(有时候叫作状态信息),也有 两个小的行为编写为函数(通常叫作方法)的形式。当我们像函数那样去调用类,就会创

建新类型的实例,并且在调用类的方法(其中的 self 参数)时,类的方法自动获取披处理 的实例:

»>bob= Worker('Bob Smith', 50000) »> sue = Worker('Sue Jones', 60000) »> bob.lastName() 'Smith' »> sue.lastName() 'Jones' »> sue.giveRaise(.10) »> sue.pay 66000.0

# Make two instances # #

Each has name and pay atcrs Call method: bob is se/f

# sue is the self subject

# Updates sue's pay

隐含的 "self" 对象是我们把这叫作面向对象模型的原因:一个类中的函数总有一个隐含的

对象。尽管如此,一般来说基千类的类型建立在核心类型之上,并使用了核心类型。例如, 这里的一个用户定义的 Worker 对象,仅仅是一个字符串和数字(分别为 name 和 pay) 的集 合,附加了用来处理这两个内置对象的函数。

类的继承机制使 Python 支持了软件层次,使其可以通过扩展进行用户定制化服务。我们通

过编写新的类来扩展软件,而不是改变目前工作的类。你应该知道类是 Python 可选的一个 特性,并且与用户编写的类相比,像列表和字典这样的更简单的内置类型往往是更好的工具。

140

I

第4章

到这里已经远超出介绍面向对象教程的范围了,所以你可以把它当作预习。然而为了获取 更多用户定制化的细节,你需要继续往后阅读。因为类建立在其他 Python 工具之上,它们

将成为本书的主要侧重点之一。

剩余的内容 就像之前说过的那样, Python 脚本中能够处理的所有事情都是某种类型的对象,而我们的 对象类型介绍是不完整的。尽管在 Python 中的每样东西都是一个“对象”,只有我们目前

所见到的那些对象类型才被认为是 Python 核心类型集合中的一部分。其他 Python 中的类 型有的是与程序执行相关的对象(如函数、模块、类和编译过的代码),这些我们将在后

面学习;有的是由导入的模块函数实现的,而不是语言语法。后者也更倾向于特定的应用 领域的角色,例如,文本模式、数据库接口、网络连接等。

此外,记住我们学过的对象仅是对象而已,并不一定是面向对象。面向对象是一种往往要 求有继承和 Python 类声明的概念,我们将会在本书稍后部分学到。 Python 的核心对象是你 可能碰到的每一个 Python 脚本的重点,它们往往是更多的非核心类型的基础。

本章小结 到此,我们就完成了初步的数据类型之旅。本章简要介绍了 Python 核心对象类型,以及可

以对它们进行的一些操作。我们学习了一些能够用千许多对象类型的通用操作(例如索引 和分片这样的序列操作),以及可以作为方法调用的特定类型操作(例如字符串分隔和列

表增加)。在学习的过程中已经定义了一些关键的术语,例如不可变性 、序列和多态。 在这个过程中,我们看到了 Python 的核心对象类型,比像 C 这样的底层语言中的对应部分 有更好的灵活性,也更强大。例如,列表和字典省去了在底层语言中为了支持集合和搜索

所进行的绝大部分工作。列表是其他对象的有序集合,而字典是通过键而不是位置进行索 引的其他对象的集合。字典和列表都能够嵌套,能够根据需要增大或减小,以及可以包含

任意类型的对象。此外,它们的内存空间在不再使用后会被自动清理。我们同时也看到了 字符串和文件联手支持着二进制和文本文件的多样性。 本书在这里尽量跳过了细节以便提供 一个快速的介绍,所以别指望这 一章所有的内容都讲 透彻了。在随后几章中,我们将会更深入地学习,填补这里所忽略了的 Python 核心对象类

型的各种细节,以便你能够更加深入地理解。我们将会在下一 章深人了解 Python 数字。那么, 首先让我们进行另 一个测验来复习吧。

本章习题 后面的几章将会探索本章所介绍的概念的更多细节,所以这里仅介绍一 些大致的概念:

介绍 Python 对象类型

I

141

I.

列举 4 个 Python 中核心数据类型的名称。

2.

为什么我们把它们称作“核心“数据类型?

3

”不可变性”代表了什么,哪 三种 Python 的核心类型被认为是具有不可变性的?

4.

“序列”是什么意思,哪三种 Python 的核心类型被认为是这个分类中的?

5.

"映射”是什么意思,哪种 Python 的核心类型是映射?

6.

什么是“多态”,为什么我们要关心多态?

习题解答 1.

数字、字符串、列表、字典、元组、文件和集合 一 般被认为是核心对象(数据)类型。 类型、 None 和布尔型有时也被归为这一 分类中。还有多种数字类型(整数、浮点数、 复数、分数和十进制数)和多种字符串类型 (Python 2.X 中的 一般字符串和 Unicode 字 符串,以及 Python 3.X 中的文本字符串和字节字符串)。

2.

它们被认作是“核心”类型是因为它们是 Python 语言自身的 一部分,并且总是有效的, 为了建立其他的对象,通常必须调用被导入模块的函数。大多数核心类型都有特定的 语法创建其对象:例如,' spam' 是 一 个创建字符串的表达式,而且决定了可以被应用 的操作的集合。正是因为这一点,核心类型与 Python 的语法紧密地结合在一起。与之 相比较,必须调用内置的 open 函数去创建一 个文件对象(尽管它通常也被认为是 一种

核心类型)。

3.

一个具有“不可变性”的对象是一个在其创建以后不能够被改变的对象。 Python 中的 数字、字符串和元组都属千这个分类。尽管无法原位置改变一 个不可变的对象,但你

总是可以通过运行一 个表达式创建一 个新的对象。在 Python 新版本中,字节串数组为 文本提供了可变性,不过它们井非普通的字符串,而且仅仅支持 8 位编码的文本文件(如 ASCII) 。

4.

一 个“序列”是系列具有位置顺序的对象的集合体。字符串、列表和元组是 Python 中 所有的序列。它们共同拥有一般的序列操作,例如索引、合并和分片,但又各自有自

己的类型特定的方法调用。 一 个相关的术语

“可迭代性”,意味着任何 一 个能够按需

提供它的内容的物理或虚拟序列。

5.

术语“映射”,表示将键与相关值相 互 关联映射的对象。 Python 的字典是其核心类型

集中唯 一 的映射类型。映射没有从左至右的位置顺序,它们支持通过键获取数据,并 包含了类型特定的方法调用。

6.

“多态”意味着 一个运算符(如+)的意义取决千被操作的对象。这是让你用好 Python 的关键思想之 一 (或许可以去掉之 一 吧) :不要把代码限制在特定的类型上,使代码 自动适用千多种类型。

142

I

第4章

第 5 章

数值类型

本章我们将进行更深入的 Python 语言之旅。在 Python 中,数据采用了对象的形式——无论

是 Python 所提供的内置对象,或是我们使用 Python 工具以及像 C 这样的其他语言所创建 的对象。事实上,对象是一切 Python 程序的基础。因为对象是 Python 编程中的最基本的概念, 所以对象也是本书第一个关注的焦点。

在上一章中,我们对 Python 的核心对象类型进行了概览。尽管上 一章已经介绍了最核心的 术语,但受限千篇幅,并没有涉及更多的细节。本章将对数据类型概念进行更详尽的学习, 以此补充之前略过的细节。让我们开始探索第一种数据类型的分类: Python 的数值类型和

运算。

数值类型基础知识 Python 中大部分的数值类型都是相当典型的,而且如果你有其他编程语言的经验,应该会

很熟悉。它们可以用来记录你的银行收支、地球到火星的距离、访问网站的人数以及其他 任何数值量。

在 Python 中,数字并不真的只是一种对象类型,而是一组相似类型的分类。 Python 不仅支 持通常的数值类型(整数和浮点数),还提供了字面量来直接创建数字和表达式以处理数字。 此外, Python 为更高级的工作提供了很多高级数值编程支持和对象。完整的 Python 数值类 型工具包括:



整数和浮点对象



复数对象



小数:固定精度对象

143



分数:有理数对象



集合:带有数值运算的集合体



布尔值:真和假



内置函数和模块: round 、 math 、 random 等



表达式 1 无限制整数精度;位运算 1 十六进制、八进制和二进制格式



第三方扩展:向量、库、可视化、作图等

由千上述清单第一项的类型在 Python 代码的使用中最为常见,因此本章从基本的数字和知 识开始讲起,然后介绍这个清单上的其他类型,它们都有着特殊的作用。这里我们也将学

习集合,它同时拥有数值和集合体的性质,但是通常认为更像前者而不是后者。不过,在 开始介绍代码之前,下面两个小节首先概述了如何在脚本中编写和处理数字。

数值字面量 Python 提供了整数(包括正整数、负整数和零)以及浮点数(带有小数部分的数字,有时 简称为“浮点”)作为其基本类型。 Python 还允许我们使用十六进制、八进制和二进制字 面量来表示整数;提供了 一 个复数类型;并且允许整数具有无限的精度一只要内存空间 允许,整数可以增长为任意位数的数字。表 5-l 展示了 Python 数值类型在程序中编写为字 面量或构造函数调用时的显示样子。 表 5-1: 数值字面量和构造函数 -

字面量

-n

n

-

-

ll.lll

-

-

-



解释

瞩-.亡·心”“

1234, - 24, o, 99999999999999

整数(无大小限制)

1.23, 1., 3.14e-10, 4E210, 4.oe+210

浮点数

Ool 77, OX9ff, Ob101010

Python 3.X 中的八进制、十六进制和二进制字面量

0177, Ool 77, Ox9ff, Ob101010

Python 2.X 中的两种八进制、十六进制和二进制字面量

3+4j, 3.0+4.0j, 3]

复数字面量

set('spam'), {1, 2, 3, 4}

集合: 2.X 和 3.X 中的构造形式

Decimal('1.o'), Fraction(l, 3)

小数和分数扩展类型

bool{X), True, False

布尔类型和字面量

一般来说, Python 的数值类型是很容易使用的,但是有些编程的概念值得在这里强调 一下: 整数和浮点数字面量

整数写成十进制数字的串。浮点数带 一 个小数点,也可以加上 一 个科学计数标志 e 或 者 E 。如果你编写一个带有小数点或幕的数字,那么 Python 会将它创建为一个浮点数

对象,并且当这个对象用在表达式中时,将启用浮点数(而不是整数)的运算法则。

144

I

第5章

浮点数在标准 CPython 中采用 C 语言中的“双精度”来实现,因此,其精度与用来构

建 Python 解释器的 C 编译器所给定的双精度一样。 Python 2.X 中的整数:一般整数和长整数 Python 2.X 中有两种整数类型:一般整数(通常 32 位)和长整数(无限精度),并且 一个整数可以以 1 或 L 结尾,从而强制它转换为长整数。由千当整数的值超过为其分配 的位数的时候会自动转换为长整数,因此你永远不需要自己输入字母 L一当需要额外 的精度时, Python 会自动地转换为长整数。

Python 3.X 中的整数:单独的一种类型 在 Python 3.X 中,一般整数和长整数类型已经合二为 一 了一只有整数这一种,它自

动地支持 Python 2.X 中长整数类型所拥有的无穷精度。因此,整数在程序中不再有末 尾的 1 或 L 表示,并且整数也不再显示这个字符。除此之外,这一修改并不会影响到大 多数程序,除非某些代码通过类型测试检测了 Python 2.X 的长整数。 十六进制数、八进制和二进制字面盐

整数可以编写为十进制(以 10 为基数)、十六进制(以 16 为基数)、八进制(以 8 为基数) 和二进制(以 2 为基数)形式,后 三 者在 一 些编程领域是很常见的。十六进制数以 Ox 或 ox 开头,后面接十六进制的数字 o-9 和 A-F 。十六进制的数字编写成大写或小写都 可以。八进制数字面量以数字 Oo 或 00 开头(数字 0 后面加小写或大写的字母 “o"),

后面接着数字 0 - 7 构成的数字串。在 Python 2.X 及更早的版本中,八进制字面最也可 以写成前面只有 一 个 o 的形式,但在 Python 3.X 中不能这样~种最初的八进制形 式很容易与十进制数混淆,因此用新的 Do 的形式替代了,这也能够在 Python 2.6 起的

Python 2.X 版本中使用。 Python 2.6 和 Python 3.0 中的新 二进制字面最以 ob 或 08 开头, 后面跟着二进制数字 (o 一 1) 。

要注意,所有这些字面量在程序代码中都产生 一 个整数对象,它们仅仅是特定值的不 同语法表示而已。内置函数 hex(I) 、 oct(I) 和 bin(I) 把一个整数转换为这 3 种进制表

示的字符串,并且 int(str,

base) 根据每个给定的进制把一 个运行时字符串转换为 一

个整数。 复数

Python 的复数字面量写成实部+虚部的写法,这里虚部是以 j 或 J 结尾。其中,实部 从技术上讲可以省略,所以虚部可以独立于实部单独存在。从内部看来,复数是通过

一 对浮点数来实现的,但是对复数的所有数字运算都会按照复数的运算法则进行。复 数也可以通过内萱函数 complex(real, imag) 来创建。 编写其他的数值类型

我们将在本章后面看到表 5-1 末尾的其他数值类型,它们扮演着更加高级或更加专用的 角色。你需要导入某些模块并调用其函数来创建一些数值类型(如小数和分数),其

他的 一 些拥有它们自己的字面扯语法(如集合)。

数值类型

I

14s

内置数值工具 除了在表 5-1 中显示的内置数字字面最和构造调用之外, Python 还提供了一系列处理数字 对象的工具:

表达式运算符 十、-、*、/、>>、**、&等。 内眢数学由数

pow 、 abs 、 round 、 int 、 hex 、 bin 等。 工具模块

random 、 math 等。 随着学习的深入,所有这些我们都会见到。

尽管数字主要是通过表达式、内置函数和模块来处理,但它们如今也拥有很多专用于类型

的方法,这些内容也将在本章中介绍。例如,浮点数拥有 一 个 as_integer_ratio 方法,它 对千分数数值类型很有用 1 还有一 个 is_integer 方法可以测试数字是否是一个整数。整数 有各种各样的属性,包括在 Python 3.1 中引入的新的 bit_length 方法,它给出表示整数对 象的值所需的比特位数 (number of bits, 二进制位数)。此外,集合同时具有集合体与数

字的性质,而集合也支持集合体与数字的方法和表达式。 由千表达式对千大多数数值类型而言是最基本的工具,因此我们先开始介绍它们。

Python 表达式运算符 表达式是处理数字最基本的工具,表达式的定义是:数字(或其他对象)与运算符相组合, 并被 Python 在执行时计算为 一个值。在 Python 中,你可以使用一般的数学记号与运算符

号来编写表达式。例如,让两个数字 X 和 Y 相加,就可以写成 X+Y, 这告诉 Python, 对名 为 X 和 Y 的变最值运用+的操作。这个表达式的结果就是 x 与 Y 的和,也就是另 一 个数字

对象。 表 5-2 列举了 Python 中所有的运算符表达式。其中大部分都是 一 目了然的,例如,常见的

数学运算符(+、一、气/等)。如果你曾用过其他编程语 言 ,那么一定对其中 一些运算符 很熟悉: %是求余数运算符、<<执行左移位、&执行按位与等。另一些运算符则更 Python 化一 些,而且本质上并不都是与数值相关的:例如, is 运算符会测试对象的身份(也就是 内存地址,更严格意义上的等价性),而 lambda 会创建匿名函数。

146

I

第5章

表 5-2: Python 表达式运算符及程序 运算符

描述

yield x

生成器函数 send 协议

lambda args: expression

创建匿名函数

x if y else z

三元选择表达式(仅当 y 为真时, x 才会被计算)

x or y

逻辑或(仅当 x 为假时, y 才会被计算)

x and y

逻辑与(仅当 x 为真时, y 才会被计算)

not x

逻辑非

x in y, x not in y

成员关系(可迭代对象、集合)

xis y, xis not y

对象同—性测试

X

< y,

X

== y,

X

I

y,

X

>= y

y

值等价性运算符 按位或、集合并集

y

按位异或、集合对称差集

x ^ y

按位与、集合交集

X

&y

X

« y,

X

+ y

X

» y

将 x 左移或右移 y 位

加法、拼接 减法、集合差集

y

X -

大小比较、集合的子集和超集

X

*y

乘法、重复

X

%y

求余数、格式化

y,

XI

XII

y

真除法、向下取整除法

-x, +x

取负、取正

~x

按位非(取反码)

X

**

y

幕运算(指数)

x[i]

索引(序列、映射等)

x[i:j:k]

分片

x(...)

调用(函数、方法、类,其他可调用对象)

x.attr

属性引用

(...)

元组、表达式、生成器表达式

[... ]

列表、列表推导

{... }

字典、集合、集合与字典推导

由千本书既介绍 Python 2.X 又涉及 Python 3 . X, 因此这里给出关千表 5-2 中运算符的版本

差异和新添加内容的一些提示 :



在 Python 2.X 中,值的不相等可以 写 成 X != y 或 X y 。在 Python 3.X 中, XY 的

数值类型

I

147

形式被移除了,因为它是多余的。在 2.X 与 3.X 中,使用 Xl=Y 是进行值不等价测试的

最佳实践。



在 Python 2.X 中,一个反引号表达式、 X 、与 repr(X) 的作用相同,目的是转换对象以 显示字符串。由千它不易理解,因此 Python 3.X 移除了这个表达式;你应当使用更容

易理解的 str 和 repr 内置函数,本章后面的“数值的显示格式”一节中介绍了这点。



在 Python 2.X 和 Python 3.X 中, XII Y 对应的向下取整除法表达式总是会把商的小数

部分去掉。在 Python 3.X 中, X I Y 表达式会执行真除法(保留了商的小数部分)以及

Python 2.X 中的经典除法(截断为整数)。参阅后面的“除法:

经典除法、向下取整

除法和真除法”一节。



[...]语法同时用千列表字面量、列表推导表达式。列表推导表达式会执行隐式的循环, 并把表达式的结果收集到一个新列表中。参阅第 4 章、第 14 章和第 20 章中的示例。



(...)语法用千元组和表达式分组,以及生成器表达式。生成器是一种能够按需产生结 果的列表推导,而不是一次性创建完整的结果列表。参阅第 4 章和第 20 章的示例。在 上面所有 3 种用法中,圆括号有时候可以省略。



{...}语法用千字典字面量,并且在 Python 3.X 和 2.7 中可以表示集合字面批以及字典

和集合推导。参阅本章、第 4 章、第 8 章、第 14 章和第 20 章中关千集合的示例。



yield 和 三元 iflelse 选择表达式在 Python 2.5 及之后的版本中可用。 yield 会在生成 器中返回 send(... )参数 1 三元 iflelse 选择表达式是一个多行 if 语句的简化形式。

如果 yield 不是单独地位千一条赋值语句的右边,需要用圆括号。



比较运算符可以链式使用: X

1 / 2.0

o.s 而且除了 print 和自动显示之外,还有很多种方式能显示计算机中数字的位数(下面的例 子全部是在 Python 3.3 中运行的,因此在老版本中可能略有不同)

»> num = 1 / 3,0 >» num 0. 3333333333333333 >» print(num) O. 3333333333333333

# Auto-echoes # Print explicitly

>»'%e'% num '3.333333e-01'

# String formatting expression

>>>'认. 2f'%

# Alternative floating-point format

num '0.33' »>'{o:4.2f}'.format(num) '0.33'

# String formatting method: Python 2.6, 3.0, and later

数值类型

I

1s3

这些方式中的最后三种使用了字符串格式化,这是一种可以灵活地进行格式化的工具,我 们将在后面关千字符串的章节(第 7 章)中介绍它。其产生的结果字符串,通常用干打印 或报告。

str 和 repr 显示格式 从技术上来说,默认的交互式命令行显示和 print 的区别,相当于内置 repr 和 str 函数的区别:

>» repr('spam') " ' spam ' " »> str('spam') 'spam'

# U.ved by echoes: as-code form #

Used by print: user-friendly form

str 和 repr 都会把任意对象转换成对应的宇符串表示. repr (以及跌认的交互式命令 行显示)会产生看起来像代码的结果. st r (和 print 操作)转换为一种通常对用户

史加友好的格式 。 对象同时拥有这两种方式: str 用于一般用途, repr 带有额外细节 。 我们在学完宇符串以及类中的运算符重载之后,会重新认识这一概念,同时你也将在 本书后面学习更多关于内置工具的内容。 除了为任意对象提供可打印的字符串, str 内置函数也是宇符串数据类型的名称,而

且在 Python 3.X 中可以传入一个编码名称,从一个字节串中斛码出一个 Unicode 字符

串(例如 str(b'xy','utf8')) ,这也是笫 4 章见过的 bytes.decode 方法的替代方案 。 我们将在笫 37 章中学习这一高级功能。

普通比较与链式比较 到目前为止,我们已经介绍了标准数值运算(加法和乘法),不过数字(如同 Python 的全

部对象)还可以进行比较。一般的比较能像我们所期待的那样作用千数字,它们会比较操 作数的相对大小,并且返回一个布尔类型的结果,我们 一 般会对这个布尔结果进行测试,

并以此决定在更大的语句和程序中接下来要执行的内容:

»> 1 < 2 True »> z.o >= 1 True >» 2.0 == 2.0 True >» 2.0 I= 2.0 False

#

Less than

# Greater than or equal: mixed-type 1 converted to 1.0

# Equal value #

Not equal value

再次注意数值表达式中是如何允许混合类型的(仅限干数值表达式) !在上面的第 二个测 试中, Python 比较了更为复杂的浮点类型的值。

1s4

I

第5章

有趣的是, Python 还允许我们把多个比较链接起来执行范围测试。链式比较是更大的布尔 表达式的简写。简而言之, Python 允许将相对大小比较测试链接起来,形成如范围测试的

连续比较。例如,表达式 (A< B < C) 测试了 B 是否位千 A 和 C 之间;它等同千布尔测试 (A

> ______

接下来的两个表达式有着相同的效果,但第一个表达式更简单且便千录入,同时因为 Python 只需计算一次 Y, 所以它运行起来可能略快一 点;

»> X < Y < Z

# Chained comparisons: range tests

True

>» X < Y and V < Z True 获得 False 结果也是一样的,并且允许任意的链式长度:

>» X < Y > Z False

»> X < Y and Y > Z False



1

< 2 < 3.0 < 4

True

>» 1 > 2 > 3.o > 4 False 你可以在链式测试中使用其他的比较,不过最终的表达式可能会变得很晦涩,除非你按照 Python 的方式来计算。例如,如下表达式结果是 False, 因为 l 不等千 2:

»> 1 == 2 < 3

# Same as: 1 = = 2 and 2 < 3 # Not same as: False< 3 (which means O < 3, which is true!)

False

Python 并不会把表达式 1 == 2 的 False 的结果与 3 进行比较,这样做的话,从技术上的含 义与 0»

1.1 + 2.2 ==

3.3

#

Shouldn't this be True?...

False

»> 1.1 + 2.2 3.3000000000000003 >» int(1.1 + 2.2) == int(3.3) True

# Close to 3.3, but not exactly: limited preciswn

# OK if convert: see also round, floor, trunc ahead # Decimals and fractions (ahead) may help here too

数值类型

I

155

这是由千浮点数因为有限的比特位数,而不能精确地表示某些值的事实~是数值编程

的一个基本问题,而不只是在 Python 中出现。我们后面会学到小数和分数,它们是能够避 免这些限制的工具。然而,首先让我们继续 Python 核心数值运算的旅程,并开始深入地探 索除法。

除法:经典除法、向下取整除法和真除法 你已经在上一节中见过了除法的工作原理,因而你现在也应该知道除法的行为在 Python 3.X

和 Python 2.X 中略有差异。实际上, Python 中有三种风格的除法,以及两种不同的除法运 算符,其中一种运算符在 Python 3.X 中有所变化。这部分内容非常细节化,但它是 Python 3.X

中另一个主要的变化,而且可能会破坏 2.X 的代码,所以下面直接给出除法运算符的描述:

XI Y 经典除法和真除法。在 Python 2.X 或之前的版本中,这个操作对千整数会省去小数部分, 对千浮点数会保持余项(小数部分)。在 Python 3.X 中将会变成真除法,即无论任何类型,

最终的浮点数结果都会保留小数部分。

XII Y 向下取整除法。这是从 Python 2.2 开始新增的操作,在 Python 2.X 和 Python 3.X 中均 能使用。这个操作不考虑操作对象的类型,总是会省略结果的小数部分,剩下最小的

能整除的整数部分。它的结果类型取决千操作数的类型。 Python 中引入真除法,是为了解决原本的经典除法的结果依赖千操作数类型(这种结果在 Python 这样的动态类型语言中很难预料)的现象。由千这一限制, Pyt hon 3.X 移除了经典 除法: /和//运算符在 Python 3 . X 中分别实现了真除法和向下取整除法。 Python 2.X 默认

为经典除法和向下取整除法,不过你也可以在 2.X 中启用真除法。概括来讲:



在 Python 3 . X 中,/现在总是执行真除法,不管操作数的类型,都返回包含任何小数 部分的一个浮点数结果。//执行向下取整除法,它截除掉余数并针对整数操作数返回 一个整数,如果有任何一个操作数是浮点数类型,则返回一个浮点数。



在 Python 2.X 中,/表示经典除法,如果两个操作数都是整数的话,执行截断的整数 除法,否则,执行浮点除法(保留余数)。//执行向下取整除法,并且像在 Python 3.X 中一样工作,对千整数执行截断除法,对千浮点数执行浮点除法。

下面是两个运算符在 Python 3.X 和 Python 2.X 中的用法——其中/运算符是 2.X 与 3.X 最

显著的区别:

(:\code> C:\Python33\python

»> >»

10 / 4

#

Differs in 3.X: keeps remainder

10 / 4.0

#

Same in 3.X: keeps remainder

2.5

»> 156

I

第5章

2.5

»>

10 // 4

#

Same in 3.X: truncates re1nainder

10 // 4.0

#

Same in 3.X: truncates to floor

2

»> 2.0

C:\code> C:\Python27\python

»> >» 10 / 4

#

This might break on porting to 3.X!

2

»>

10 / 4.0

2.5 »> 10 // 4

# Use this in 2.X

if truncation needed

2

»>

10 // 4.0

2.0

要注意,在 Python 3 .X 中,/ I 的结果的数据类型总是依赖千操作数的类型:如果操作数中

有 一个是浮点数,结果就是浮点数 1 否则,结果是一个整数。尽管这可能与 Python 2. X 中 的类型依赖行为类似(正是该因素引发了在 Python 3.X 中的变化),但返回值的类型差异

比返回值本身的数值差异要轻微得多。 此外,由千//运算符是作为依赖千整数截断除法的程序(这种程序比你想象得要常见很多) 而引入的一种兼容性工具,因此它必须为整数返回整数。当必须要求整数截断运算时,请 在 Python 2.X 中使用//来替代/,从而获得 3.X 的兼容性。

支持两个 Python 版本 尽管 /的行为在 Python 2.X 和 Python 3.X 中有所不同,但我们仍然能够在自己的代码中支

持这两个版本。如果你的程序依赖千截断整数除法,在 Python 2.X 和 Python 3.X 中都使 用刚刚提到的//。如果你的程序对于整数需要带小数部分的浮点数结果,请在 2 .X 中使用 float 调用将一个操作数强制转换成浮点数:

X = Y // Z

# Always truncates, always an int result/or ints in 2.X and 3.X

X =YI float(Z)

# Guarantees float division with remainder in either 2.X or 3.X

作为替代方案,我们可以使用一个从_future _模块的导入,从而在 Python 2.X 中开启

Python 3.X 的/真除法,而不是用 float 转换来强制它: C:\code> C:\Python27\python >» from _future_ import division

»>

#

Enable 3.X"/" behavior

10 / 4

2.5 >» 10 // 4

# Integer II is the same in both

2

当像这样交互式输入时,这一 特殊的 from 语句适用千你会话的剩余部分,当在 一 个脚本文

数值类型

I

1s7

件中使用时,必须作为第一个可执行行出现(在 Python 中我们可以从将来导入,而不是从

过去。

向下取整除法 vs 截断除法 一个细节是: //运算符有一个非正式的别名,叫作截断除法,不过更准确的说法应该是向

下取整除法一一//把结果向下截断到它的下层,即真正结果之下的最近的整数。其直接效 果是向下舍入,井不是严格地截断,并且这对负数也有效。你可以使用 Pytho n 的 math 模

块来查看其中的区别(在使用模块中的内容之前,必须先导入模块,本书后面将更详细地 介绍模块的导入)

:

» > import math »> math.floor(2.5)

H Closest number below value

2

»> math.floor(-2.5) -3 »> math.trunc(2.5)

# Truncate fractional part (toward zero)

2

>» math.trunc(-2.5) -2

在执行除法操作的时候,其实只是截断了正的结果。因此对千正数,截断除法和向下取整 除法是相同的;对千负数,除法操作就是 一 个向下取整结果(实际上,除法总是向下取整,

但对千正数来说,向下取整和截断总是相同的)。下面是在 Python 3.X 中的情况:

(:\code> c:\python33\python >» 5 / 2, 5 / -2 (2.5, -2.5) >» 5 // 2, 5 // -2 (2, -3)

# Truncates to floor: rounds to Ji rst lower integer

# 2.5 becomes 2, -2.5 becomes -3

»> 5 / 2.0, 5 / -2.0 (2.5, -2.5) »> 5 // 2.0, 5 // -2.0 (2.0, -3.0)

#

Ditto for floats, though res11/t is float too

Python 2.X 中的情况类似、但是 I 的结果再次有所不同: C:\code> c:\python27\python >» 5 / 2, 5 / -2 (2, -3) »> 5 // 2, 5 // -2 (2, -3) »> 5 / 2.0, 5 / -2.0 (2.5, -2.5) >» 5 // 2.0, 5 // -2.0 (2.0, -3.0)

158

I

第5章

# Differs in 3.X

#

This and the rest are the same in 2.X and 3.X

如果你真想获得趋向千零的截断,而不管正负,那么总是可以将一个浮点数除法结果传入

math.trunc 函数,而不管是什么 Python 版本(请查阅内置函数 round 来了解相关的功能, 内置函数 int 有着相同的效果,不过 int 不需要导入)

C:\code> c:\python33\python » > import math >» 5 / -2

#

Keep remainder

- 2. 5

»> 5 // -2

# Floor below result

-3

>» math.trunc(s / -2)

#

Truncate instead offloor (same as int())

-2

(:\code> c:\python27\python >>>加port math »> 5 / float(-2)

# Remainder i11 2.x

-2.S

»> 5 / -2, 5 // -2 (-3, -3) >» math. trunc(s / float(-2))

# Floor in 2.X

# Truncate in 2.X

-2

为什么截断很重要 作为 一 个小结,如果你正在使用 Python 3.X, 下面的除法运算符的简短故事可以作为参考 :

»> (5 I 2), (5 / 2.0), (5 / -2.0), (5 I -2) (2.5, 2.5, -2.5, -2.5)

# 3.X true division

>» (5 II 2), (5 // 2.0), (5 // -2.0), (5 // -2) (2, 2.0, -3.0, -3)

# 3.X floor division

»> (9 I 3), (9.0 / 3), (9 II 3), (9 II 3.0) (3.0, 3.0, 3, 3.0)

# Both

对千 Python 2.X 的用户,除法工作如下(整数除法的三个加粗的输出与 Python 3.X 不同) :

»> (5 / 2), (5 / 2.0), (5 / -2.0), (5 / -2) (2, 2.5, -2.5, -3)

# 2.X classic division (differs)

»> (5 // 2), (5 // 2.0), (5 // -2.0), (5 // -2) (2, 2.0, -3.0, -3)

# 2.Xfloordivision (same)

>» (9 I 3), (9.0 I 3), (9 II 3), (9 II 3.0) (3, 3.0, 3, 3.0)

# Both

Python 3.X 中 I 的非截断行为还是可能会影响到大量的 Python 2.X 程序。可能是因为 C 语言的遗留原因,很多程序员仍然依赖于整数的截断除法,因此他们必须学习在这些场 景下使用//。今天,你应当在编写所有新的 Python 2.X 和 Python 3.X 代码中这样做一

在 Python 2 .X 中这样做是为了向 Python 3 . X 兼容,而在 Python 3.X 中这样做是因为/在

Python 3.X 中不再截断。参阅第 13 章中的一个简单的素数 while 循环的示例,以及第四部

数值类型

I

1s9

分末尾的一个相关的练习,它们说明了/在 Python 3.X 中的修改可能会影响到的代码种类。

同时第 25 章将给出更多本节中 from 语句的内容。

整数精度 除法可能在 Python 的各个版本中有所区别,但它仍然是相对标准的。下面是一些看上去比 较奇怪的内容。如前所述, Python 3.X 整数支持无限制的大小:

»> 999999999999999999999999999999 + 1 1000000000000000000000000000000

# 3.X

Python 2.X 针对长整数有一个单独的类型,不过它会自动把任何太大了以至千无法存储到 一般整数中的数字转换为这种类型。因此,我们不必编写任何特殊的语法就能够使用长整型, 并且,你唯一可以在 2.X 识别出正在使用长整数的方式,就是长整数的末尾会显示一个 “L":

» > 999999999999999999999999999999 + 1 1000000000000000000000000000000L

# 2.x

无限制精度整数是一个方便的内置工具。例如,你可以直接在 Python 中以美分为单位来计

算美国国家财政赤字(如果你决意如此,并且计算机内存能放得下今年的预算的话译注 2) 。 这也是它们能够用来计算第 3 章中 2 的高次幕的原因。下面分别是在 Python 3.X 和 Python 2.X 中的情况:

»> 2.. 200 160693804425899027554196209234116260252220299378279283530 1376

>» 2 ** 200 160693804425899027554196209234116260252220299378279283530 1376L 由千 Python 需要为支持扩展精度而进行额外的工作,因此在实际应用中,长整型数的数学

运算通常要比正常的整数运算更慢。然而,如果你确实要求精度,更应该庆幸 Python 为你 提供了内置的长整数支持,而不是抱怨其性能上的不足。

复数 尽管复数比我们之前介绍的这些类型要少见一些,但 Python 中的复数是一种独特的核心对 象类型。它们通常在工程和科学应用程序中使用。如果你知道复数是什么,你就会知道它 的重要性;如果不知道的话,那么请把这一部分作为选读材料。

复数表示为两个浮点数(实部和虚部)并接在虚部增加了 j 或 J 的后缀。我们也可以把实 部非零的复数写成实部与虚部相加的形式,并以+号相连。例如,一个复数的实部为 2, 虚

部为- 3, 可以写成 2 + -3j 译注 3 。下面是一些复数运算的例子 : 译注 2:

作者在这里用诙谐的方式调侃了美国的国家财政赤字问题。

译注 3:

当然对于虚部为负的情况,也可以直接写为 2-3j 。

160

I

第5章

>>> 1j * 1J

(-l+Oj)

*3 (2+3j) »> (2 + lj) * 3 (6+3j) >» 2 + lj

复数允许我们将它的实部和虚部作为属性来访问,并支持所有一般的数学表达式,同时还 可以通过标准的 cmath 模块(复数版的标准 math 模块)中的工具进行处理。因为复数在绝 大多数编程领域都较为罕见,因此这里我们将跳过复数的其他内容。参阅 Python 的语言参 考手册来获取更多的细节。

十六进制、八进制和二进制:字面量与转换 Python 的整数能以十六进制、八进制和二进制记数法来编写,这作为我们迄今一直在使用

的常见的以 10 为底的十进制记数法的补充。对千十指生物而言,其他这三种进制第一眼看 上去充满异域风情,但是一些程序员会认为它们在描述一些数值时十分方便,尤其是它们

能很容易地对应到字节和位。本章前面已经简要介绍了编码规则,这里让我们来看一些实 际的例子。

记住,下面这些字面最只是指定一个整数对象的值的一种替代方式。例如,在 Python 3.X

和 Python 2.X 中编写的如下字面量会产生具有上述 3 种进制底数的常规整数。在内存中, 同一个整数的值是相同的,它与我们为了指定它而使用的底数无关:

>» 001, 0020, Oo377 (1, 16, 255) »> oxo1, ox10, OxFF (1, 16, 255) >» ob1, ob10000, Ob11111111 (1, 16, 255)

# Octal literals: base 8, digits 0-7 (3.X, 2.6+) #

Hex literals: base 16, digits 0-9/A-F (3.X, 2.X)

# Bi,uiry literalss: base 2, digits 0-1 (3.X, 2.6+)

这里,八进制值 00377 、十六进制值 oxFF 和二进制值 ob11111111 ,都表示十进制的 255 。例如, 十六进制数值中的 F 数字代表十进制的 15 和二进制的 4 位 1111, 且反映了以 16 为幕。因此, 十六进制数值 oxFF 和其他数值能像下面这样转换为十进制数值:

»> oxFF, (15 * (16 ** 1)) + (15 * (16 ** o))

# How hex/binary map to decimal (255, 255) »> ox2F, (2 * (16 ** 1)) + (15 * (16 ** o)) (47, 47) »> oxF, Obl卫1, (1*(2**3) + 1*(2**2) + 1*(2**1) + 1*(2**0)) (15, 15, 15)

Python 默认用十进制值(以 10 为底)显示整数数值,但它提供了内置函数,能帮我们把整

数转换为其他进制的数字字符串(以 Python 字面量的形式) -当程序或用户期望看到以 给定数字为底的数值时,这很有用:

数值类型

I

161

>» oct(64), hex(64), bin(64) ('00100','ox40','ob1000000')

#

Numbers=>digit strings

oct 函数会将十进制数转换为八进制数, hex 函数会将十进制转换为十六进制数,而 bin 函 数会将十进制数转换为 二进制。反过来,内置函数 int 会将一 个数字的字符串转换为 一 个

整数,并能通过可选的第二位参数来确定转换后数字的进制。这对千从文件中作为字符串 读取的(而不是在脚本中直接编写的)数字来说很有用:

>» 64, Oo100, Ox40, Ob1000000 (64, 64, 64, 64)

#D屯its=>numbers

in scripts and strings

» > int ('64'), int('100', 8), int('40', 16), int ('1000000', 2) (64, 64, 64, 64) >» int('ox40', 16), int('ob1000000', 2) (64, 64)

# literal forms supported too

本书稍后介绍的 eval 函数,将会把字符串作为 Python 代码来运行。因此,它也具有类似

的效果,但往往运行得更慢:它实际上会把字符串作为程序的一个片段编译并运行,并且 它假设当前运行的字符串属千一个可信的来源。耍小聪明的用户也许会提交 一 个删除你机 器上文件的字符串,因此小心使用 eval 调用:

>» eval('64'), eval(000100'), eval('ox40'), eval('ob1000000') (64, 64, 64, 64) 最后,你也可以使用字符串格式化方法调用和表达式(它只返回数字,而不是 Python 的字

面量字符串),将整数转换为指定底的字符串:

»>'{o:o}, {1:x}, {2:b}'.format(64, 64, 64) '100, 40, 1000000'

# Numbers=>digits, 2.6+

»>'%0, %x, %x, %X '% (64, 64, 255, 255) '100, 40, ff, FF'

#

Similar, in all Pythons

字符串格式化将在第 7 章中详细介绍。 在继续学习之前,有两点需要注意。首先,如本章开头所述, Python 2.X 的用户要记住在

编写八进制时,你可以直接用 一 个 0 开头,也就是 Python 中原本的八进制格式:

>» (1, »> (1,

001, 0020, 00377 16, 255) 01, 020, 0377 16, 255)

# New octal format in 2.6+ (same as 3.X) # Old octal literals in all 2.X (error in 3.X)

但在 Python 3 . X 中,上面第 二 组例子的语法将产生错误。即便它在 Python 2.X 中不是一 个错误,除非你真的想要表示 一 个八进制的值,否则要小心不要用 o 开始 一 个数字串。

Python 2.X 会将其当作八进制数,但可能会不像你所期待的那样工作,例如, 010 在 Python

2.X 中总是八进制数,而不是十进制数(不管你是否这样认为)。也就是说,为了保持与

162

I

第5章

十六进制和二进制的形式对称, Pyth_on 3.X 修改了八进制的形式,在 Python 3.X 中必须使 用 00010, 并且在 Python 2. 6 和 2.7 中,为了清晰以及向前兼容干 Python 3.X, 你也应当尽 可能使用 00010 。 其次,要注意这些字面批可以产生任意长度的整数。例如,下面的例子创建了十六进制形 式的 一 个整数,然后先用十进制形式显示它,再使用转换函数将其转换为八进制和 二进制 的形式(这里在 Python 3.X 中运行:在 Python 2.X 中,十进制和八进制显示的尾部有一 个 L,

表示其单独的长类型,并且八进制显示没有字 母 o)

»> »>

X = OxFFFFFFFFFFFFFFFFFFFFFFFFFFFF X

5192296858534827628530496329220095

>» oct(X) '0017777777777777777777777777777777777777'

>» bin(X) 'ob111111111111111111111111111111111111111111111111111111111... and so on... 11111' 对于 二进制数字,下 一节将介绍处理单独的位的工具。

按位操作 除了 一般的数学运算(加法、减法等), Python 也支持 C 语言中的大多数数学表达式。这

包括那些把整数作为 二进制位串处理的运算,如果你的 Python 代码必须处理像网络数据包、 串行端口或 C 程序生产的打包 二 进制数据的这些内容,就会十分有用。

这里我们不再详细展开布尔数学的基本原理。那些必须使用它的人很可能已经知道它是如 何工作的,而其他人则通常可以完全地推后对这 一 主题的学习,但是其基础知识是非常简 单直接的。例如,下面是实际中 Python 的按位表达式运算符中的一 部分,能够实现整数的 按位移动及布尔运算: 一一>4>3>1 >>>> < 2

# 1 decimal is 0001 in bits # Shift left 2 bits: 0100 # Bitwise OR(either bit=]): 001 I

# Bitwise AND(both bits= I): 0001

在第一个表达式中, 二进制数 1 (以 2 为底数, 0001) 左移了两位,成为二进制的 4 (0100) 。 上面的最后两个运算实现了 一个二进制”或“来组合位 (000110010 = 0011) ,以及一 个“和“

来选择共同的位 (0001&0001 = 0001) 。这样的位掩码运算,使我们可以对一 个单独的整 数编码和提取多个标志位和值。 在该领域中,从 Python 3.0 和 Python 2.6 开始引人的二进制和十六进制数变得特别有用, 它们允许我们按照位 字 符串来编 写 和查看数字:

数值类型

I

163

» > X = Ob0001 »> X « 2

# Binary literals

# Shift left

4

»> bin(X « 2)

# Binary digits string

'ob100'

»> bin(X I obo10)

# Bitwise OR: either

'ob11' »> bin(X & Obl) 'Obl'

# Bitwise AND: both

这也适用于通过十六进制字面量创建的数字,以及改变过底数的数字:

>» X = oxFF >» bin(X)

# Hex literals

'ob11111111'

»> X" Ob10101010

# Bitwise XOR: either but not both

85

»> bin(X" ob10101010) 'ob1010101'

» > int ('01010101', 2)

# Digits=>number: string to int per base

85

>» hex(85)

#

Number=>digits: Hex digit string

'OX55' 同样在这一部分中, Python 3.1 和 2.7 引入了一个新的整数方法 bit_length, 它允许我们查 询以二进制表示一个数字的值时所需的位数。通过利用第 4 章介绍的内置函数 len, 从 bin 字符串的长度减 2 (代表字面量字符串开头的 “Ob") ,也可以得到同样的效果,但这种

方法效率较低:

»> X = 99 >» bin(X), X.bit_length(), len(bin(X)) - 2 ('ob1100011', 7, 7)

>» bin(256), (256).bit_length(), len(bin(256)) - 2 ('OblOOOOOOOO', 9, 9) 这里不会涉及更多关千"位运算"的细节。如果你需要, Python 是支持的,但是按位运算 在 Python 这样的高级语言中并不像在 C 这样的底层语言中那么重要。经验法则是,如果你 需要在 Python 中反转位,那么你应该考虑一下到底现在使用的是哪一门语言。我们将在接 下来的几章中看到, Python 的列表、字典以及其他数据结构提供了比位数字串更为丰富的(且

通常也更好的)编码信息的方式,而且这些方式可读性更强。

其他内置数值工具 除了核心对象类型以外, Python 还提供了用千数值处理的内置函数和内置模块。例如,内

置函数 pow 和 abs 用千分别计算幕和绝对值。下面是一 些内置模块 math (包含在 C 语言 math 库中的绝大多数工具)中的例子,以及一些在 Python 3.3 版本中引入的内置函数,如 前所述,一部分浮点数显示在 Python 2.7 和 3.1 之前的版本中可能展现出更多或更少的位数:

164

I

第5章

» > import math »> math.pi, math.e (3.141592653589793, 2.718281828459045)

# Common constants

»> math.sin(2 * math.pi / 180) 0.03489949670250097

# Sine, tangent, cosine

»> math.sqrt(144), math.sqrt(2) (12.0, 1.4142135623730951)

# Square root

»> pow(2, 4), 2 (16, 16, 16.0)

**

4, 2.0

**

#

4.0

Exponentiation (power)

»> abs(-42.0), sum((1, 2, 3, 4)) (42.0, 10)

# Absolute

value, summation

>» min(3, 1, 2, 4), max(3, 1, 2, 4) (1, 4)

# Minimum, maximum

这里展示的 sum 函数作用千一个数字的序列, min 和 max 函数接受一个参数序列或者多个单 独的参数。 Python 有多种方式可以去除一个浮点数的小数部分。我们前面介绍了截断和向 下取整;而我们也可以用四舍五入,其目的既可以是为了求值,也可以是为了显示:

»> math.floor(2,567), (2, -3)

math.floor(-2.567)

»> math.trunc(2.567), math.trunc(-2,567)

# Floor (next-lower integer)

# Truncate (drop decimal digits)

(2, -2) »> int(2.567), int(-2.567) (2, -2)

# Truncate (integer conversion)

>» round(2.567), round(2.467), round(2,567, 2) (3, 2, 2.57)

# Round (Python 3.X version)

»>'%.1f' 为 2. 567,'{o:.2f} •. format(2. 567)

# Round/or display (Chapter 7)

('2.6','2.57') 如前所述,上面的最后 一 个例子创建了通常用千打印的字符串,并且它支持多种格式化选 项。同样,如果我们把这里的倒数第二行测试包含到一个 print 调用中,那么它在 Python 2.7 和 3.1 之前的版本中输出 (3, 2, 2.57) ,从而得到一个更加用户友好的显示。然而,字符

串格式化仍旧略微不同,即使在 Python 3.X 中也是如此, round 会四舍五入并舍弃小数位数, 但仍会在内存中产生一个浮点数结果,然而字符串格式化将产生一个字符串,而不是数字:

»> (1 / 3.0), round(l / 3.0, 2), ('%.2f'% (1 / 3.0)) (0.3333333333333333, 0,33,'0,33') 有意思的是,在 Python 中有 3 种方式可以计算平方根:使用 一个模块函数、 一 个表达式或 者一个内置函数(如果你关心它们之间的性能差异,我们将在第四部分末尾的一个练习题

及其解答中回顾它们,从而了解哪种方式运行得更快)

>» import math >» math.sqrt(144)

#

Module

数值类型

I

16s

12.0

»> 144 **,5

#

Expression

12.0

>» pow(144, , 5)

# Built-in

12.0

»> math.sqrt(1234567890}

If Larger

numbers

35136,41828644462 »> 1234567890 **,5 35136,41828644462

»>

po树 (1234567890,.s)

35136.41828644462 要注意,内置模块 math 在使用前必须先导入,但像 abs 和 round 这样的内置函数则无须 导入就可以直接使用。换句话说,模块是外部的组件,而内置函数则位千 一 个隐含的命名

空间内,而 Python 会自动在这个命名空间中搜索程序中的名称。这个命名空间直接对应千

Python 3.X 中名为 builtins 的标准库模块(在 Python 2.X 中对应千_builtin—)。在本 书后面的函数和模块部分中,会有更多关千名称解析的介绍。从现在起,当你听到“模块“ 时,就要想到“导入“。

标准库中的 random 模块在使用时也必须导入。该模块提供了 一 系列工具,可以完成诸如在 0 和 1 之间挑选一个随机浮点数,在两个数字之间挑选一个随机整数的任务:

»> import rando111 »> random.random() o.5566014960423105 >» random.rando111() 0. 051308506597373515 »> random.randint(1, 10)

#

Random floats, i111egers, choices, shuffles

5

»> random.randint(1, 10) 9

random 模块也能够从一个序列中随机选取一项,并且随机地打乱列表中的元素:

>» random.choice(['Life of

B工ian','Holy Grail','Meaning of Life']) 'Holy Grail' »> random.choice(['Life of Brian','Holy Grail','Meaning of Life']) 'Life of Brian'

>» suits = ['hearts','clubs', •diamonds','spades'] »> random.shuffle(suits) »> suits ['spades','hearts','diamonds','clubs']

»> rando111.shuffle(suits) »> suits ['clubs','diamonds','hearts','spades'] 尽管我们需要额外的代码使该程序更加清晰明确,但 random 模块很实用,对千游戏中的洗

牌,在演示 GUI 中随机挑选图片,进行统计模拟等都很有用处。本书后面我们还将用到它(例 如,在第 20 章的全排列案例学习中),不过如果你想获取更多细节,请参考 Python 的库手册。

166

I

第5章

其他数值类型 目前,本章已经介绍了 Python 的核心数值类型:整数、浮点数和复数。对于绝大多数程序

员来说,这足以应对大部分的使用需求。然而, Python 还自带了 一 些更少见的数值类型, 值得让我们在这里简要浏览 一 下。

小数类型 Python 2.4 中引人了 一种新的核心数据类型:小数对象,其正式的名称是 Decimal 。从语法 上讲,你需要通过调用已导人模块中的函数来创建小数,而不是通过运行字面量表达式来 创建。从功能上讲,小数对象很像浮点数,但它们有固定的位数和小数点。因此,小数是

精度固定的浮点数。 例如,使用小数对象,我们可以得到 一 个只保留两位小数位精度的浮点数。此外,我们可 以定义如何省略和截断额外的小数数字。尽管这相对千一般的浮点数类型来说带来了性能

上的损失,但小数类型对表达固定精度的特性(例如货币的累加)以及对实现更好的数值 精度而 言 ,是 一 个理想的工具。

小数基础知识 下面是最后值得介绍的 一 点。正如我们在探索比较知识时简要预习过的,浮点数运算缺乏 棺确性,这是因为用来存储数值的空间有限。例如,下面的计算结果应该为零,但并非如此。

其结果很接近零,但却没有足够的位数来实现这样的精度:

»> 0.1 + 0.1 + 0.1 - 0.3

# Python 3.3

5,551115123125783e-17 在 Python 3.1 和 2.7 之前的版本上,打印结果将会产生一个用户友好的显示格式,但井不能 完全解决问题,因为与硬件相关的浮点数运算在准确度(又称精确度)方面有着内在的缺陷。

下面 Python 3.3 中的例子给出了与上一个输出相同的结果:

»> print(o.1 + 0.1 + 0.1 - 0.3)

#Pythons< 2.7, 3. /

s. 55111512313e-17 不过如果使用了小数对象,那么结果将更准确:

» > from decimal import Decimal »> Decimal('0.1') + Decimal('0.1') + Decimal('0.1') - Decimal('o. 3 •) Decimal('o.o') 如你所见,我们可以通过调用在 decimal 模块中的 Decimal 的构造函数来创建 一 个小数对象,

并传入 一 个表示结果中显示小数位数的字符串(如果需要,请使用 str 函数将浮点数转换 为 字 符串) 。 当你在表达式中混合使用不同精度的小数时, Python 会自动转换为最高的小 数位数:

数值类型

I

167

»> Decimal('o.1') + Decimal('o.10') + Decimal('o.10') - Decimal('o.30') Decimal(·o.oo·) 在 Python 2.7 、 3.1 及之后的版本中,你也可以从 一 个浮点数对象创建小数对象,即通过 decimal.Decimal.from_float(1.25) 形式的调用来实现,而最新的 Python 版本允许直接使

用浮点数来创建。这一 转换是精确的,但有时候会产生默认且庞大的小数位数,除非你按 照下一节所述的方式操作:

>» Decimal(o.1) + Decimal(o.1) + Decimal(o.1) - Decimal(0.3) Decimal('2.775557561565156540423631668E-17') 在 Python 3.3 及之后的版本中,小数模块的性能也得到了极大的提升:新版本宣称其速度 提升了 10 到 100 倍,不过这取决千基准测试的程序种类。

设置全局小数精度 decimal 模块中的其他 一 些工具可以用来设置所有小数数值的精度,安排错误处理等。例如,

该模块中的一个上下文对象可以指定精度(小数位数)和舍入模式(向下取整、向上取整等)。 该精度将全局性地应用到调用线程中创建的所有小数: >>>加port

decimal

>» decimal.Decimal(1) / decimal.Decimal(7)

#

Default: 28 digits

Decimal{'0.1428571428571428571428571429')

»> decimal.getcontext().prec = 4 »> decimal.Decimal(1) / decimal.Decimal{7)

#

Fixed precision

#

Closer ro 0

Decimal{'0.1429')

>» Decimal(0,1) + Decimal(0,1) + Decimal(o.1) - Decimal(0,3) Decimal{'1.110E-17')

这对于处理货币的应用程序尤其有用,其中美分表示为两位小数位数。在这个上下文中, 小数实际上是手动舍入和字符串格式化的一种替代方式:

»> 1999 + 1.33 2000.33

#

This has more digits in memory than displayed in 3.3

»> >» decimal. getcontext (). prec = 2 »> pay = decimal.Oecimal(str(1999 + 1.33)) »> pay Decimal ('2000. 33')

小数上下文管理器 在 Python 2.6 和 Python 3 .0 及之后的版本中,你可以使用 with 上下文管理器语句来临时重 置小数精度。在 with 语句退出后,精度又会重置为初始值 1 在一个新的 Python 3.3 会话中 (如第 3 章所述,这里的"…”是 Python 的交互提示符,用千在一些界面内表示行的连续,

并需要手动缩进 I IDLE 会省略这一提示符,并为你缩进) 168

I

第5章

C:\code> C:\Python33\python » > import decimal >» decimal.Decimal('1.00') / decimal.Decimal('3.00') Decimal ('o. 3333333333333333333333333333')

>> > >» with decimal.localcontext() as ctx: ctx.prec = 2 decimal.Decimal('1.oo') / decimal.Decimal('3.oo') Decimal('o. 33 ') »> >» decimal.Decimal('1.00') / decimal.Decimal('3.00') Decimal('o. 3333333333333333333333333333') 尽管 with 语句很有用,但它要求你掌握比这里所介绍的更多的背景知识,你可以参阅本书 第 34 章对 with 语句的介绍。 由千小数类型在实际中仍然较少用到,因此请参考 Python 的标准库手册和交互式帮助来了

解更多细节。而且由千小数和分数类型都解决了浮点数的某些精度问题,因此让我们继续 下一节,看看如何对比这两种类型。

分数类型 Python 2.6 和 Python 3.0 中首次引入了一种新的数值类型 Fraction (分数),它实现了一 个有理数对象。本质上,它显式地保持了一个分子和一个分母,从而避免了浮点数运算的 某些不精确性和局限性。与小数一样,分数的实现并不像浮点数靠近计算机的底层硬件。 这意味着它们的性能可能不会和浮点数一样优秀,但是这也允许它们在必要时作为一种有 用的标准工具。

分数基础知识 Fraction 与上 一小节介绍的 Decimal 固定精度类型十分相似,它们都可以用来处理浮点类

型的数值不精确性。分数的使用方式也和小数很像一一-就像 Decimal, Fraction 也位千模

块中;你需要导入其构造函数井传入一个分子和一个分母,从而产生一个分数(不过也存 在其他方式)。下面的交互式例子展示了分数对象的创建:

>>> from fractions import Fraction >» x = Fraction(1, 3) >» y = Fraction(4, 6)

# #

Numerator, denominator Simplified to 2, 3 by gcd

>» X Fraction(l, 3) >» y Fraction(2, 3) >» print(y) 2/3 一且创建了分数,它们就可以像平常一样用于数学表达式中:

数值类型

I

169

y Fraction(l, 1) »> X - y Fraction(-1, 3) >» X * y Fraction(2, 9) >>> x +

# Results are exact: numerator, denominator

分数对象也可以通过浮点数字符串来创建,这和小数很相似:

»> Fraction('.25') Fraction(1, 4)

»> Fraction('1.25') Fraction(S, 4)

>» »> Fraction('.25') + Fraction('1.25') Fraction(3, 2)

分数和小数中的数值精度 要注意,分数和小数中的数值精度与浮点数运算有所区别(浮点数运算受到浮点数硬件底

层限制的约束)。出于比较的目的,下面对浮点数对象进行相同的运算,注意到它们有限 的精度——最新的 Python 版本比之前的版本可能会显示更少的位数,但在内存中它们仍然 是不准确的:

>» a »> b »> a

= 1 / 3.0 = 4 / 6.o

# 011/y as accurate as floati11g-point hardware -II Can lose precision over ma11y calculations

o. 33333333333333333 >>> b 0.66666666666666666

»>

a + b

1.o

>» a - b -0. 33333333333333333 a * b 0.22222222222222222

»>

对千那些用内存中给定的有限位数无法精确表示的值,浮点数的局限性尤为明显。 Fraction 和 Decimal 都提供了得到精确结果的方式,但这需要付出 一 些速度和代码冗余性 的代价。例如,在下面的例子中(重复上一小节),浮点数并不能准确地给出期望的答案 0,

但分数和小数类型都做到了:

>» 0.1 + 0.1 + 0.1 - 0.3

# This should be zero

(close, 加t

1101 exact)

5,551115123125783e-17

»> from fractions import Fraction >» Fraction(1, 10) + Fraction(1, 10) + Fraction(1, 10) - Fraction(3, 10) Fraction(o, 1)

170

I

第5章

>>> from decimal import Decimal »> Decimal('0.1') + Decimal{ •o·.1 •) + Decimal{'0.1') - Decimal{'0.3') Decimal('o.o') 此外,分数和小数都能提供比浮点数更直观和准确的结果,它们以不同的方式做到这点一 使用有理数表示以及通过限制精度:

>» 1 / 3 0. 33333333333333333

# Use a ".0" in Python 2.Xfor rrne "!"

»> Fraction(l, 3)

# Numeric accuracy, two ways

Fraction(1, 3)

» > import decimal » > decimal. getcontext (). prec = 2 »> Decimal(t) / Decimal(3) Decimal('o . 33') 实际上,分数既保持了精确性,又自动简化了结果。继续前面的交互例子:

»> (1 / 3) + {6 / 12) 0. 83333333333333333

# Use a ".0" in Python 2.X for true "! "

>» Fraction(6, 12)

# Automatically simplified

Fraction(l, 2)

>» Fraction(1, 3) + Fraction(6, 12) Fraction(s, 6}

»> decimal.Decimal(str(l/3)) + decimal.Decimal(str(6/12}} Decimal('0.83') >» 1000.0 I 1234567890 8.100000073710001e-07 >» Fraction(1000, 1234567890) Fraction(100, 123456789)

# S11bsrantially simpler!

分数转换和混用类型 为了支持分数的转换,浮点数对象现在有 一 个方法,能够产生它们的分子和分母比,分数

有 一 个 from_float 方法,并且 float 函数可以接受 一 个 Fraction 对象作为参数。跟踪如下 的交互看看这是如何做到的(第 二 个测试中的*是 一 种特殊的语法,它可以把一个元组展

开成单独的参数 1 当我们在第 18 章中学习函数参数的时候,会再详细讨论这一点)

>» (2.5).as_integer_ratio()

# float object method

(5, 2)

>» f = 2.5 >» z = Fraction(*f.as_integer_ratio()) >» z

# Convert float -> fraction: two args # Same as Frac1io11(5. 2)

Fraction(s, 2)

»>

X

#./(from prior interaction

Fraction(l, 3)

数值类型

I

171

»>

X + Z Fraction(17, 6)

»> float(x) O. 3333333333333333 >» float(z)

# 5/2 + 1/3 = 15/6 + 2/6

# Convert fraction-> float

2. 5

»> float(x + z) 2. 8333333333333335

»> 17 /

6

2. 8333333333333335

>» Fraction.from_float(1.75)

#

Convert float-> fraction: other way

Fraction(7, 4) »> Fraction(*(1. 75).as_integer_ratio()) Fraction(7, 4) 最终,表达式中允许某些类型的混合,尽管 Fraction 有时必须手动地传递以确保精度。研 究如下的交互示例来看看这是如何做到的:

»>

X

Fraction(1, 3) »> X + 2 Fraction(7, 3) »> X + 2.0 2. 3333333333333335 >» X + (1.13} 0.6666666666666666 >» X + (4./3) 1.6666666666666665 >» x + Fraction(4, 3) Fraction(s, 3)

# Fraction + int -> Fraction

# Fraction +float-> float # Fraction + float -> float

# Fraction +Fraction-> Fraction

警告:尽管你可以把浮点数转换为分数,在某些情况下,这么做的时候会有不可避免的精 度损失,因为这个数字在其最初的浮点数形式下是不精确的。在必要时,我们可以通过限 制分母的最大值来简化这样的结果:

»> 4.0 I 3 1. 3333333333333333 »> (4.0 I 3).as_integer_ratio() (6004799503160661, 4503599627370496) »>

# Precision loss from float

X

Fraction(1, 3) >» a = x + Fraction(*(4.0 / 3).as_integer_ratio())

>» a Fraction(22517998136852479, 13510798882111488)

»> 22517998136852479 / 13510798882111488. 1.6666666666666667

# 5 I 3 (or close to it!)

>» a.limit_denominator(10)

# Simplify to closest jraczion

Fraction(5, 3)

172

I

第5章

要了解更多有关 Fraction 类型的细节,你可以自已进一步体验,并查询 Python 2. 6 、 Python 2 .7 和 Python 3.X 的库手册以及其他文档。

集合 除了小数外, Python 2.4 还引入了一种新的类型一集合 (set) ,这是一些唯一的、不可 变的对象的一个无序集合体 (collection) ,这些对象支持与数学集合理论相对应的操作。

按照定义, 一 个元素在集合中只能出现一 次,不管它被添人了多少次。因此,集合有着广 泛的应用,尤其是在涉及数值和数据库的工作中。

因为集合是其他对象的集合体,因此它具有列表和字典(列表和字典的讨论不在本章中) 这样的对象的某些共同行为。例如,集合是可迭代对象,可以按需增长或缩短,并且可以 包含多种对象类型。我们还将看到, 一 个集合的行为很像一个有键无值的字典,不过集合 还支持更多的操作。

然而,由于集合是无序的,而且不会把键映射到值,因此它们既不是序列也不是映射类型 1

它们是自成一体的类型。此外,由于集合本质上具有基本的数学特性(它对千很多读者来说, 集合可能更加学院派,并且比像字典这样更为普遍的对象用得要少很多),下面我们将介 绍 Python 中的集合对象基本工具。

Python 2.6 及之前版本中的集合基础知识 根据你所使用的 Python 版本,有几种不同方法可以创建集合。既然本书要顾及所有版本,

让我们先从 Python 2.6 及之前版本的情况开始,这种方式在之后的 Python 版本中也是可用 的(并且有时还是必需的) 1 稍后,我们将为 Python 2.7 和 3.X 再稍微增加一些细节。要 创建 一 个集合对象,你可以向内置的 set 函数传入一 个序列或其他可迭代对象:

»> x = set('abcde') » > y = set ('bdxyz') 现在你得到了 一 个集合对象,其中包含被传入对象内的所有元素(要注意集合并不包含位 置顺序一它们的顺序是任意的,且可能随 Python 的发行版本而变化) :

»> X set(['a','c','b','e','d'])

#

Pythons< =2.6 display format

集合通过表达式运算符支持 一 般的数学集合运算。要注意,我们不能对诸如字符串、列表 和元组的 一 般序列使用下面的运算一一我们必须将字符串、列表和元组传入 set 泊数,并 创建了相应 的集合后,才能使用这 些 工具:

»> X - y set(['a', ' c','e'])

#

Difference

数值类型

I

173

>» X I y set (['a','c','b','e','d','y','x','z'])

#

>» X & y set(['b','d']}

# / ntersectwn

>» x " y set (['a','c','e','y','x','z'])

# Symmetric difference (XOR)

>>> X > y, X < y (False, False)

#

Union

Superset, subset

该规则很明显的 一个例外就是集合成员测试 in 。 in 表达式也定义为可以在全部其他集合体

类型上工作,而其作用也是进行成员测试(或者搜索,如果你偏爱以过程化的方式思考)。 因此,我们不必将类似字符串和列表这样的数据类型转换成集合,就可以直接运行 in 测试:

»>'e'in x True

# Membership (sets)

>»'e'in'Camelot', 22 in [11, 22, 33] (True, True)

If

But works on other f)•pes too

除了表达式,集合对象还提供了与这些操作相对应的方法,从而支持集合的修改:集合的 add 方法插入一个项目, update 在原位置求并集, remove 根据值删除一个元素(在任何集

合实例或 set 类型名上运行 dir, 都可以查看到所有可用的方法)。假设 x 和 y 仍与之前的 会话相同:

»> z = x.].ntersection(y) >» z set(['b','d')) »> z.add('SP矶') »> z set(['b','d','SPAM')) >» z.update(set(['X','Y'])) »> z set (['Y','X','b','d','SPAM')) >» z.remove('b') »> z set(['Y','X','d','SPAM'))

# Same as x & y

# Insert one item

# Merge: in-place union

# Delete one item

作为可迭代的容器,集合也可以用千 len 、 for 循环和列表推导这样的操作中。然而,由千 集合是无序的,所以不支持像索引和分片这样的操作:

»> for item in set('abc'): print(item * 3) aaa CCC

bbb 最后,尽管前面介绍的集合表达式通常需要两个集合,它们基于方法的对应形式往往可以 对任何可迭代类型有效:

174

I

第 5章

»> S = set([1, 2, 3)) >» S I set([3, 4)) # Expressions require both to be sets set([1, 2, 3, 4]) »> s I [3, 4] TypeError: unsupported operand type(s) for I:'set'and'list' »> s.union([3, 4]) set([l, 2, 3, 4]) »> S.intersection((1, 3, 5)) set([l, 3]) »> S.issubset(range(-5, S)) True

# But their methods allow any irerable

关千更多集合操作的细节,请参阅 Python 的库参考手册或者其他参考书 。 尽管集合操作可

以在 Python 中通过其他类型(如列表和字典)手动实现,但 Python 的内置集合使用了更 高效的算法和实现技术来提供快速和标准的操作。

Python 3.X 和 Python 2.7 中的集合字面量 如果你认为集合很"酷",那么它们还可以变得更酷,这体现为从 Python 3.X 开始引入的 新式集合字面量以及集合推导语法,同时这也被向前移植到 Python 2.7 中。在这些 Python 版本中,我们仍然可以使用内置函数 set 来创建集合对象,不过也可以使用集合字面最形式, 该形式采用了原本为字典所保留的花括号。在 Python 3 . X 和 2.7 中,下面两种形式是等同的:

set([t, 2, 3, 4]) {1, 2, 3, 4}

# Built-in call(all) #

Newer set literals(2.7, 3.X)

该语法合乎常理,因为集合基本上就像是无值的字典一由千集合的项是无序的、唯 一 的、

不可改变的,因此集合中元素的行为与字典的键很像。这种相似性更为惊人的一点在千, 字典的键列表在 Python 3.X 中是视图对象,因而它能支持像交集和井集这样类似集合的行

为(参阅第 8 章了解关千字典视图对象的更多内容)。 不管一 个集合是如何被创建的, Python 3.X 都能使用新的字面员形式来显示它。 Python 2.7 接受新的字面量语法,但是仍然要使用上 一节中 Python 2.6 的显示形式来显示集合。在所 有 Python 版本中,要创建空的集合,或从巳有的可迭代对象创建集合(这不包括通过集合

推导的情况,本章稍后将介绍),都还是需要通过内置的 set 函数,但新的字面量语法便 千初始化具有已知结构的集合。 下面是 Python 3.X 中集合看起来的样子,它与 Python 2.7 中是一样的,只不过在 Python 2.X

中集合结果会采用 set( [...])记号来显示,以及元素的顺序可能会随版本而不同(不过这 与集合本身并不相关) :

C:\code> c:\python33\python >» set([1, 2, 3, 4]) {1, 2, 3, 4}

II Built-in: same as in 2.6

数值类型

1

175

,

»> set('spam') {'s','a','p','m'}

# Add all items in an iterable

»> {1, 2, 3, 4} {1, 2, 3, 4} >>> S = {'s','p','a','m'} »> s {'s','a','p','m'}

# Set literals: new in 3.X (and 2.7)

>» S.add('alot'} »> s {'s','a','p','a lot','m'}

# Methods work as before

上一 小节中所讨论的所有集合处理操作在 Python 3.X 中都同样有效,但是结果集合显示上

却有所不同:

»> S1 = {1, 2, 3, 4} >» S1 & {1, 3} {1, 3} >» {1, 5, 3, 6} I S1 {1, 2, 3, 4, 5, 6} »> S1 - {1, 3, 4} {2} »> S1 > {1, 3} True

# Intersection # Union #

Difference

# Superset

要注意,在所有 Python 版本中{}仍然是 一 个字典。因此空的集合必须通过内置函数 set 来创建,并且以同样方式显示:

>» S1 - {1, 2, 3, 4} set() >» type({})

# Empty sets print differently

»> S = set() >» S. add(1. 23) »> s { 1. 23}

# Initialize an empty set

#

Because {} is an empty dictionary

与 Python 2. 6 及之前版本中一样, Python 3.X 和 Python 2.7 中用字面最创建的集合支持相

同的方法,其中一些方法能支持表达式不支持的 一 般可迭代对象操作数:

>» {1, 2, 3} I {3, 4} {1, 2, 3, 4} >» {1, 2, 3} I [3, 4) TypeError: unsupported operand type(s) for I:'set'and'list' >» {1, >» {1, »> {1,

176

I

{1, 2, 3}.union([3, 41) 2, 3, 4} {1, 2, 3}.union({3, 4}) 2, 3, 4} {1, 2, 3}.union(set([3, 4))) 2, 3, 4}

第5章

3, s)) »> {1, 2,, 3}.intersection((1, 3} {1, 3} »> {1, 2, 3}.issubset(range(-5, s)) True

不可变性限制与冻结集合 集合是强大而灵活的对象,但它们在 Python 3.X 和 Python 2.X 中确实都有一个限制,需要 我们铭记(这很大程度上是由千集合的实现) :集合只能包含不可变的(可哈希化的)对 象类型。因此,列表和字典不能嵌入到集合中,但如果你需要存储复合对象的话,元组是 可以嵌入集合的。元组在集合操作中会比较其完整的值:

»> s {1. 23} »> S.add([1, 2, 3]) TypeError: unhashable type:'list' »> S.add({'a':1}) TypeError: unhashable type:'diet' »> S.add((1, 2, 3))

»>

s

# Only immutable objects work in a set

# No list or diet, but tuple ok

{1.23, (1, 2, 3)}

»> s I {(4, s, 6), (1, 2, 3)} {1.23, (4, s, 6), (1, 2, 3)} »> (1, 2, 3) in S True »> (1, 4, 3) in S False

# Union: same as S.union(...)

# Membership: by complete values

例如,集合中的元组可以用来表示日期、记录、 IP 地址等(本书本部分后面将更详细地介

绍元组)。集合也可以包含模块、类型对象等。集合本身也是可变的,因此,不能直接嵌 入到其他集合中 1 如果需要在另一个集合中存储一个集合,可以像调用 set 一样调用内置

函数 frozenset, 但 frozenset 会创建 一个不可变的集合,该集合不可修改,并且可以嵌套 到其他集合中。

Python 3.X 和 2.7 中的集合推导 除了字面量, Python 3.X 还引入了 一 种集合推导构造,同时也被向前移植到 Python 2.7 中。 正如 Python 3.X 的集合字面量, Python 2.7 虽然接受其语法,但却以 Python 2.X 的集合形

式来显示结果。集合推导表达式类似千第 4 章介绍的列表推导的形式,但它编写在花括号 中而不是方括号中,并且会创建一个集合而不是列表。集合推导会运行一个循环并在每次

迭代时收集一个表达式的结果,通过 一 个循环变扯来访问当前的迭代值以用千集合表达式

中。其结果就是通过运行代码创建了一个新的集合,它具备所有一般的集合行为。下面是

Python 3.3 中的集合推导(再次提醒,结果显示和顺序在 Python 2.7 中有所不同)

»> {x ** 2 for x in [1, 2, 3, 4]} {16, 1, 4, 9}

#

3.X/2.7 set comprehension

数值类型

1

177

在该表达式中,循环部分编写在右侧,而集合体表达式编写在左侧 (x**2) 。与列表推导一样,

我们可以很好地理解这个表达式的含义:

“对千列表中的每一个 X, 给出包含 X 平方的 一

个新集合。“推导也可以迭代其他类型的对象,例如字符串(下面的第 一 个例子展示了如 何从一 个已有的可迭代对象创建一 个集合)

» > { x for x in'spam'} {'m','s','p','a'}

#

>» {c * 4 for c in'spam'} {'pppp','aaaa','ssss','mmmm'} »> {c * 4 for c in'spamham'} {'pppp','aaaa','hhhh','ssss','mmmm'}

# Set of collected expression results

»> S

Same as: ser('spam')

= {c * 4 for c in'spam'}

» > S I { •mmmm •, •xxxx • } {'pppp •, • xxxx','mmmm','aaaa','ssss'} » > S & { •mmmm •, •xxxx {` m m m m ' } ' } 因为其他的关千推导的知识要依赖千我们现在还不准备介绍的底层概念,这些概念将推迟

到本书后面再详细介绍。在第 8 章中,我们将遇到 Python 3.X 和 Python 2.7 中的另一种推导, 即字典推导,并且我们随后将介绍关千所有推导(列表、集合、字典和生成器)的更多内容, 尤其是在第 14 章和第 20 章中。那里你将会了解到,所有的推导都支持这里没有展示的额 外语法,包括嵌套循环和 if 测试,在你有机会学习较大型语句之前,这些内容可能比较难

以理解。

为什么使用集合 集合操作有各种各样常见的用途,其中 一 些比其数学意义更加实用。例如,由千项在集合 中只能存储一 次,因此集合可以用千过滤其他集合体中的重复项,尽管元素可能会在该过

程中重新排序(因为集合通常是无序的)。你只需把集合体转换为一个集合,然后再转换 回来即可(集合可以在这里的 list 调用中工作,因为它们是可迭代对象。可迭代对象是稍 后将介绍的另 一种技术工具) :

»> L = (1, 2, 1, 3, 2, 4, 5] >» set(L) {1, 2, 3, 4, 5} »> L = list(set(L))

# Remove duplicates

»> L [1, 2, 3, 4, 5]

>» list (set (['yy','cc','aa','xx','dd','aa']))

#

But order may change

['cc','xx','yy','dd','aa'] 集合也可以用千提取列表、字符串以及其他可迭代对象中的差异(你只需转换为集合从而

获得差异)。当然同理,集合的无序本质意味着结果可能和原本不匹配。下面例子中的最 后两个比较了 Python 3.X 中字符串对象类型的属性列表(在 Python 2.7 中的结果有所不同):

178

1

第5章

~» s~t([1, 3, 5, 7]) - set([l, ~• 4, 5, 6]} {3, 7} >» set('abcdefg') - set('abdghij') {'c','e','f'} » > set ('spam') - set (['h','a','m']) {'p', ' s `}

#

Find list differences

#

Find string differences

# Find differences. mixed

»> set(dir(bytes)) - set(dir(bytearray)) # In bytes but not bytearray {'_getnewargs_'} »> set(dir(bytearray)) - set(dir(bytes)) {'append','copy','_alloc_','_imul , · remove ,'pop','insert',... more... } 你也可以通过转换成集合,借助集合进行顺序无关的等价性测试,这是因为顺序在集合中

并不重要。更正式地说,两个集合相等当且仅当两个集合中的每 一元素都被另一集合所包 含一一也就是说,撇开顺序,每一个集合都是另 一 个集合的子集。例如,你可能会借此比

较程序的输出,程序应当同样地工作但是也许会产生不同顺序的结果。在测试前进行排序 可以获得同样的等价性测试效果,但是集合不需要开销高昂的排序,而排序允许其结果支

持集合所不支持的额外的相对大小比较(大千、小千等)

»> Lt, L2 = [1, 3, 5, 2, 41, [2, 5, 3, 4, 1] # Order mafters in sequences »> Lt == L2 False # Order-neutral equality >» set(L1) == set(L2) True >» sorted(L1) == sorted(L2) fl Similar but results ordered True >»'spam'=='asmp', set('spam') == set('asmp'), sorted('spam') == sorted('asmp') (False, True, True) 当你遍历一 个图或其他有环结构时,集合可用千记录已访问的位置。例如,我们将分别在 第 25 章和第 31 章学习传递性模块重载和继承树打印程序示例,就必须记录访问过的项以

避免循环,正如第 19 章中所抽象地讨论的。在这一场景中使用列表是低效的,因为列表搜 索会进行线性扫描。尽管把访问状态作为键记录到字典中很高效,但集合提供了几乎等同 的一种替代方案(孰优孰劣,见仁见智)。

最后,当你在处理较大的数据集时(例如数据库查询结果),两个集合的交集包含了两个 种类中共有的对象,并集包含了两个集合中的所有元素。为了说明,这里给出集合操作的

一 些实际例子,这些操作应用千一个假想公司的人员名单,使用 Python 3.X 和 2.7 集合字 面量以及 3.X 的结果显示(在 Python 2.6 及更早版本中使用 set) , »>engineers= {'bob' , · sue · , · ann ,'vie'} >» managers = {'tom','sue'}

»>'bob'in engineers True

# Is bob an engineer?

»> engineers & managers {'sue'}

#

Who is both engineer and manager?

数值类型

1

179

»> engineers I managers {'bob','tom','sue','vie','ann'}

#

»> engineers - managers {'vie','ann','bob'}

# Engineers who are not managers

»> managers - engineers {'tom'}

#

Managers who are not engineers

»>engineers> managers False

#

Are all managers engineers? (superset)

»> {'bob','sue'}< engineers True

#

Are both engineers? (subset)

»> (managers True

I

engineers) > managers

»>managers" engineers {'tom','vie','ann','bob'} »> (managers {'sue'}

I

All people in either category

# ALL people is a superset of managers

#

Who is in one but not both?

engineers) - (managers" engineers)

# Intersection!

你可以在 Python 库手册以及关系数据库理论的一些相关资料中,找到关千集合操作的更多 细节。在第 8 章介绍 Python 3.X 中的字典视图对象时,我们将再次回顾这里所见到的一些

集合操作。

布尔型 有些人会认为 Python 的布尔类型 (bool) 本质上是数值的,因为它包含两个值 True 和 False, 而且就是整数 1 和 0 的定制版,只不过打印时有所不同。尽管这是大多数程序员所 需了解的全部,我们还是要稍深入地探索布尔类型。 更正式地说,今天的 Python 有一个名为 bool 的显式布尔数据类型,带有 True 和 False 作 为可用且预赋值的内置名称。在内部,名称 True 和 False 是 bool 的实例,而 bool 实际上

只是内置整数类型 int 的子类(从面向对象的角度来看)。 True 和 False 的行为与整数 1 和 0 是一样的,只不过它们有独特的显示逻辑:它们是作为关键字 True 和 False 显示的,

而不是数字 1 和 0 。 bool 为这两个对象重新定义了 str 和 repr 的字符串格式。 由千该定制,布尔表达式在交互式命令行模式的输出就作为关键字 True 和 False 来显示, 而不是曾经的 1 和 0。此外,布尔型让真值在你的代码中更加明显。例如,一个无限循环现 在可以写为 while True: 而不是更不直观的 while 1: 。类似地,通过使用 flag = False,

可以更清楚地设置标志位。我们将在第三部分深入讨论这些语句。 同样,对千大多数实际场景,你可以把 True 和 False 看作预定义的设置为整数 1 和 o 的名 称。不过很多程序员都曾把 True 和 False 预先赋值为 1 和 o, 所以新的 bool 类型直接让其

18O

I

第5章

成为一种标准。但它的实现会导致奇怪的结果。因为 True 仅仅是定制了显示格式的整数 1, 所以在 Python 中 True+4 得到了整数 5

»> type(True)

»> isinstance(True, int) True »> True == 1 True »> True is 1 False >>> True or False True »> True + 4 5

!

#

Same value

#

But a different object: see the next chapter

#

Same as: 1 or 0

#

(Hmmm)

因为你可能不会在真正的 Python 代码中遇到像上面例子中最后一个那样的表达式,所以你 可以完全忽略其任何更深奥的形而上的含义。 我们将在第 9 章回顾布尔类型来定义 Python 的真值的概念,并在第 12 章再次学习像 and 和 or 这样的布尔运算符是如何工作的。

数值扩展 最后,尽管 Python 的核心数值类型提供的功能对千大多数应用程序而言已经够用了,但还 是有大量的第 三 方开源扩展可以用来解决更加专门的需求。由千数值编程是 Python 的一个 很热门的领域,因此你将发现众多高级工具。 例如,如果你需要做一 些正式的数字计算, 一 个叫作 NumPy (Numeric Python) 的可选

Python 扩展提供了高级的数值编程工具,例如矩阵数据类型、向量处理和精密的计算库。 像洛斯阿拉莫斯国家实验室 (Los Alamos) 和美国国家航空航天局 (NASA) 这样的硬核科 学编程组织,都使用带有 NumPy 的 Python 来实现此前用 C++、 FORTRAN 、 Matlab 进行 的编程任务。 Python 和 NumPy 的组合往往可以比作 一款免费的、更加灵活的 Matlab, 可

以同时获得 NumPy 的性能以及 Python 语言 及其库。 由千 NumPy 较为高级,我们不打算在本书中进一步介绍它。你可以通过搜索 Web 获取高

级数值编程的更多资料,包括图形和绘制工具、扩展精度浮点数、统计库以及流行的 SciPy 包。另外还要注意, NumPy 目前是 一 个可选的扩展 1 NumPy 并没有纳入到 Python 核心中,

因此必须单独安装,不过你很可能很希望这么做。如果你十分关注该领域,请在网上查找 其他资料。

数值类型

1

181

本章小结 本章介绍了 Python 数值对象类型及其运算。在这个过程中,我们学习了标准的整数和浮点 数类型,以及一些更加奇特和少见的类型,例如复数、小数、分数和集合。我们也学习了 Python 的表达式语法、类型转换、按位运算以及各种在脚本中编写数字的字面量形式 。 接下来在本书的这一部分中,我们将继续更深入的类型旅程,井继续学习下一个对象类

型一字符串的一些细节。然而在下一章中,我们将花一些时间来探索这里用到的变批赋 值机制的更多细节。这也许是 Python 中最基本的概念,所以在继续学习之前,你要好好阅

读下一章。不过首先,让我们照例做一下章节测试。

本章习题 *

l.

Python 中表达式 2

(3 + 4) 的值是多少?

2.

Python 中表达式 2 * 3

3.

Python 中表达式 2 + 3 * 4 的值是多少?

4

你可以使用什么工具来计算一个数字的平方根以及它的平方?

5.

表达式 1 + 2.0 + 3 的结果是什么类型?

6.

如何截断或舍去浮点数的小数部分?

7.

如何将一个整数转换为浮点数?

8.

如何将一个整数显示成八进制、十六进制或二进制的形式?

9.

如何将 一 个八进制、十六进制或二进制数的字符串转换成一般 的整数?

+ 4 的值是多少?

习题解答 l

结果是 14, 即 2*7 的结果,因为括号强制让加法在乘法之前运算。

2.

结果是 10, 即 6+4 的结果。 Python 的运算符优先级法则适用于没有括号存在的场合, 按照表 5-2, 乘法的优先级要比加法的优先级高(先进行乘法运算)。

3.

结果是 14, 即 2

+ 12 的结果,与上一题一样是优先级 的原因。

4.

当你导人 math 模块后,即可求平方根、 pi 以及正切等丞数。为了获得一个数字的平方根,

import math 后调用 math.sqrt(N) 。为了得到一个数字的平方,使用指数表达式 X 2, 或者内置函数 pow(X,

X **.5) 。

182

I

第5章

**

2) 。上述两种方式都可以用来计算一个数的 o.s 次方(例如

5.

结果是一个浮点数:整数将转换升级成浮点数,也就是该表达式中最复杂的类型,然 后采用浮点数的运算法则进行计算。

6.

int(N) l:ll 数和 math.trunc(N) 泊数可以省略小数部分,而 round(N, digit) 函数会四 舍五人。我们可以使用 math.floor(N) 来计算向下取整,并且使用字符串格式化操作来 舍入以便千显示。

7.

float(!) 将整数转换为浮点数,在表达式中混合整数和浮点数也会实现转换。在某种意 义上, Python 3.X 的 I 除法也会转换,它总是返回一个包含小数部分的浮点数结果,即

便两个操作数都是整数。

8.

内置函数 oct(I) 和 hex(I) 会将整数以八进制数和十六进制数字符串的形式返回。在

Python 2.6 、 3.0 及后续版本中, bin(I) 也会返回一个数字的二进制数字字符串。%字 符串格式化表达式和字符串 format 方法也可以进行这样的转换。

9.

int(5, base) 函数可以用来将一个八进制和十六进制数的字符串转换为正常的整数(传 入 8 、 16 或 2 作为 base 的参数)。 eval(5) 函数也能够用作这个目的,但是运行起来 开销更大且可能导致安全问题。注意整数总是在计算机内存中以 二进制形式存储,这

些只不过是显示字符串格式的转换而已。

数值类型

1

183

第 6 章

动态类型

通过上一章学习的 Python 数值类型和操作,我们开始对 Python 的核心对象类型进行深入

探索。我们将会在下一章继续对象类型之旅,在继续学习之前,掌握 Python 编程中最基本 的概念是很重要的。动态类型以及由它提供的多态性,这些概念无疑是 Python 语言简洁性 和灵活性的基础。

贯穿全书,在 Python 中,我们并不会声明脚本中使用的对象的确切类型。事实上,大多数 程序甚至可以不在意特定的类型;相反地,它们能够自然地适用于更广泛的场景。由千动 态类型是 Python 语言灵活性的根源,同时也是困扰新手的一 个难题,让我们先简要地探索

一下这个模块。

缺少声明语句的情况 如果你有学习编译或静态类型语言 C. C++或 Java 的背景,学到这里,你也许会有些困惑。

到现在为止,我们使用变量时,都没有声明变量的存在和类型,但变裁还可以工作。例如, 在交互会话模式或程序文件中,当输入 a

=

3 时, Python 怎么知道那代表一个整数呢?在

这种情况下, Python 怎么知道 a 是什么? 一旦你开始问这样的问题,就已经进入了 Python 动态类型模型的领域。在 Python 中,类

型是在运行时自动决定的,而不是通过代码声明。这意味着没有必要事先声明变量(只要

记住,这个概念实质上对变量、对象和它们之间的关系都适用,那么这个概念也就很容易 理解并掌握了)。

变量耸对象和引用 就像本书已使用过的很多例子 一样,当在 Python 中运行赋值语句 a = 3 时,即使没有告诉

184

Python 将 a 作为 一个变最来使用,或者没有告诉它 a 应该作为 一 个整数类型对象, 一 样都 能工作。在 Python 语言中,这些都会以 一 种非常自然的方式完成,就像下边这样: 变最创建

一 个变批(也就是变批名),就像 a, 当代码第 一 次给它赋值时就创建了它。之后的赋 值将会改变己创建的变址名的值。从技术上来讲, Python 在代码运行之前先检测变扯名, 但你可以理解为最初的赋值操作在创建变量。

变昼类型 变批永远不会拥有任何和它关联的类型信息或约束。类型的概念存在千对象而不是变 批名中。变址原本是通用的,它只是在一个特定的时间点,简单地引用了 一 个特定的

对象而已。 变昼使用 当变黛出现在表达式中时,它会马上被当前引用的对象所代替,无论这个对象是什么 类型。此外,所有的变量必须在使用前被明确地赋值,使用未赋值的变 扯 会产生错误。 总而言之,变批在赋值的时候才被创建,它可以引用任何类型的对象,井且必须在引用之 前赋值。这意味着,不需要通过脚本声明所要使用的名字,但是,必须初始化名字然后才

能更新它们;例如,必须把计数器初始化为 0, 然后才能增加它。

这种动态类型模式与传统语言的类型模式相比有明显的不同。刚入门时,如果清楚地将变 量名和对象划分开来,动态类型是很容易理解的。例如,当我们用以下命令给一个变晕赋 值时:

»>

a = 3

# Assign a name to an object

至少从概念上来说, Python 将会执行 三 个不同的步骤去完成这个请求。这些步骤反映了 Python 语言中所有赋值的操作:

I.

创建 一 个对象来代表值 3 。

2.

创建 一 个变批 a, 如果它还没有创建的话。

3.

将变 址 与新的对象 3 相连接。

实际效果是如图 6-1 所示的一个在 Python 中的内部结构。如图中所示,变显和对象保存在

内存中的不同部分,并通过连接相关联(这个连接在图 6-1 中显示为 一 个箭头)。变量总 是连接到对象,并且绝不会连接到其他变量上,但是更大的对象可能连接到其他的对象(例 如, 一 个列表对象能够连接到它所包含的对象)。

动态类型

1

185

变量名

对象

引用

-

.....

爹二』 、、、一会乡

,多

图 6-1: 在运行 a=3 后的变量名和对象。变量 a 变成对象 3 的一个引用。在内部,变墨事实上 是到对象内存空间(通过运行常量表达式 3 而创建)的一个指针 在 Python 中从变址到对象的连接称作引用。也就是说,引用是一种关系,通过内存中的指

针的形式来实现注 J 。一且使用(引用)变量, Python 自动跟踪这个变量到对象的连接。这 实际上比术语所描述的要简单得多。以具体的术语来讲 :



变量是一个系统表的入口,包含了指向对象的连接。



对象是被分配到的一块内存,有足够的空间去表示它们所代表的值 。



引用是自动形成的从变址到对象的指针。

至少从概念上讲,在脚本中,每 一 次通过运行 一 个表达式生成 一 个新的值, Python 都创 建了 一 个新的对象(换言之, 一块内存)表示这个值。从内部来看,作为一种优化手段,

Python 缓存了这 一 类不可变的对象并对其进行复用,例如,小的整数和字符串(每一 个 0 都不是一块真正的、新的内存块,稍后会介绍这种缓存行为)。但是,从逻辑的角度来看,

这工作起来就像每一 个表达式结果的值都是 一个不同的对象,而每 一 个对象都是不同的内 存。

从技术上来讲,对象不仅仅有足够的空间表示它的值,还包含了更复杂的结构。每 一 个对 象都有两个标准的头部信息:类型标志符 (type designator) 标识这个对象的类型 1 引用的

计数器 (reference counter) 决定何时回收这个对象。要理解这两个头部信息对模型的影响, 我们需要继续学习下去。

类型属千对象,而不是变量 为了理解对象类型是如何使用的,请看当我们对一 个变量进行多次赋值后的结果:

注 I

有 C 语言编程背景的读者可能会发现 Python 的引用很像 C 的指针(内存地址) 。 事实上 ,

:

引用的实现非常像指针 , 而且它们通常扮演着和指针相同的角色 , 尤其是对于那些能够原 位置改变的对象(后面会介绍对象的可变性) 。 然而,因为引用在使用时总是被自动斛引 用,你永远不可能对引用本身做任何有用的操作;这是一个能够遇免一系列 C 语言中错

误的语言特性 。 不过 , 你可以把 Python 中的引用想象成 C 语言的 " void" 指针(这类指 针在被使用时都会自动斛引用) .

186

I

第6章

>» a = 3 , , >» a = spam >>> a = 1.23

# It's an inte~er # Now it's a string # Now it's a floating point

这不是典型的 Python 代码,但它是可行的。 a 刚开始是一个整数,然后变成一个字符 串,最后变成一个浮点数。这个例子对千 C 程序员来说可能特别奇怪,因为当我们说 a

=

'spam' 时, a 的类型似乎从整数变成了字符串。 事实并非如此。在 Python 中,情况很简单:变量名没有类型。就像前边说的,类型属千对象, 而不是变量名。就之前的例子而言,我们只是把 a 修改为对不同的对象的引用。因为变量

没有类型,我们实际上并没有改变变量 a 的类型,只是让变量引用了不同类型的对象而已。 实际上, Python 的变量就是在特定的时间引用了一个特定的对象。

从另一方面讲,对象知道自己的类型。每个对象都包含一个头部信息,其中标记了这个对 象的类型。例如,整数对象 3, 包含了值 3 以及一个标志符,告诉 Python 这是一个整数对

象(从严格意义上讲,一个指向 int (整数类型的名称)的对象的指针)。' spam' 字符串 的对象的标志符指向了一个字符串类型(叫作 str) 。因为对象记录了它们的类型,变最就 没有必要再记录了。 注意 Python 中的类型是与对象相关联的,而不是和变量关联。在典型的代码中, 一 个给 定的变量往往只会引用一种类型的对象。尽管这样,因为这并不是必需的,你将会发现 Python 代码比你通常惯用的代码更加灵活:如果正确使用 Python, 代码能够自动以多种类

型进行工作。 本书提到对象包含两个头部信息: 一 个是类型标志符,另 一 个是引用计数器。为了了解后者, 我们需要继续学习下面的内容,并简要地介绍对象生命结束时发生了什么变化。

对象的垃圾收集 在上一 节的例子中,我们把变量 a 赋值给了不同类型的对象。但是当重新给变量 a 赋值时, 它前一个引用值发生了什么变化?例如,在下边的语句中,对象 3 发生了什么变化?

»> a = 3 , , >» a = spam 答案是,在 Python 中,每当 一个变量名被赋予 一 个新的对象,如果原来的对象没有被其他 的变量名或对象所引用的话,那么之前的那个对象占用的空间就会被回收。这种自动回收 对象空间的技术叫作垃圾回收。也正是因为这种技术, Python 程序员的工作得到了很大的 简化。 为了讲清楚,考虑下面的例子,其中每个语句都把变量名 x 赋值给了不同的对象:

>» X = 42 >» x ='shrubbery'

#

Reclaim 42 now (unless referenced elsewhere)

动态类型

1

187

>» X = 3-1415 >» X = [1, 2, 3)

# Reclaim 'shrubbery' now # Reclaim 3.1415 now

首先注意 x 每次被设置为不同类型的对象。再者,尽管这并不是真正的情况,效果却是 x 的类型每次都在改变。在 Python 中,类型属千对象,而不是变最名 。 由千变量名只是引用

对象而已,这种代码自然行得通。 第 二 ,注意对象的引用值在此过程中会被逐个丢弃。当每一 次 x 被赋值给 一 个新的对象,

Python 都会回收之前对象的空间。例如,当它赋值为宇符串 'shrubbery' 时,对象 42 马上 被回收(假设没有被其他对象引用) :对象的空间自动放入自由内存 空 间池,等待后来的 对象使用。

在内部, Python 是这样来实现这一功能的:它在每个对象中保留了 一 个计数器,计数器记 录当前指向该对象的引用的数目。 一旦(并精确在同 一 时间)这个计数器被设置为零 , 这

个对象的内存空间就会自动回收。在前面的介绍中,假设每次 x 都被赋值给 一 个新的对象, 而前一 个对象的引用计数器变为零,就会导致它的空间被回收。

垃圾收集最直接的、可感受到的好处就是,这意味着可以在脚本中任意使用对象而不需要 考虑申请或释放内存空间。在程序运行时, Python 将会清理那些不再使用的空间。实际上, 与 C 和 C丑这样的底层语言相比,这样做省去了大址的基础代码 。

关千 Python 垃圾回收的更多讨论 实际上, Python 的垃圾收集主要基于引用计数器,正如前面所介绍的。然而`它也有

一部分组件可以及时地检浏并回收带有循环引用的对象 。 如果你确保自己的代码没有 产生徒环引用 , 可以关闭这部分功能,但该功能是默认可用的 。

循环引用是基于引用计数的垃圾回收器需要面对的经典问题 。 由于引用实现为指针 , 一个对象有可能会引用自身,或者引用另一个引用了自身的对象 。 例如 ,

笫一部分末

尾的练习 3 及其在附录 D 中的斛答.展示了如何把一个列表的引用嵌入其自身中,从

而简单地创这一个循环(例如 L.append(L)) 。 对来自用户定义的类的对象的属性赋 值的时候,会产生同样的现象。尽管相对很少 ,

由于这样的对象的引用计数器不会清

除为 O , 必须特别对待它们 。

要了轩 Python 的徙环检测器的更多细节,参见 Python 库手册中 gc 模块的文档 。 好消 息是 , 基于垃圾回收的内存管理已经在 Python 中为你实现好了 , 并且是由专人精心 设计过的 。 还要注意,这里对于 Python 的垃圾收集器的介绍只适用于标准的 Python (也 称为 CPython)

;笫 2 章中提到的诸如 Jython 、 IronPython 和 PyPy 的其他实现方案可

能采用不同的机制,但最后的效果都是类似的 , 未使用的内存空间都会被自动回收(尽

管不一定如标准 Python 中那般迅速) 。

188

I

第6章

共享引用 到现在为止,我们已经看到了单个变扯被赋值引用了多个对象的情况。现在,在交互式命 令行下,引入另 一 个变量,并观察变量名和对象如何变化:

3a ab >> >> >> =___

输人这两行命令后,生成如图 6-2 所示的结果。就像往常 一 样,第二行命令会使 Python 创 建变量 b 。变址 a 正在使用,并且它在这里没有被赋值,所以它被替换成其引用的对象 3,

从而 b 也成为这个对象的 一 个引用。实际的效果就是变量 a 和 b 都引用了相同的对象(也 就是说,指向了相同的内存空间)。

变量名

引用

』l 图 6-2: 运行赋值语句 b

对象

. 会- --

气」 、 ·-- - ·

= a 之后的变量名和对象。变量 b 成为对象 3 的一个引用。在内部,

变量实际上是一个指针指向了对象的内存空间,该内存空间是通过运行字面量表达式 3 创建的 这种情况在 Python 中称为共享引用(有时也称为共享对象),即多个变量名引用了同一个 对象。注意,名字 a 和 b 此时并没有彼此关联;实际上, Python 中不可能发生两个变量的 相互关联。真实情况是两个变量通过它们的引用指向了同 一 个对象。 下一步,假设运行另 一个语句扩展了这样的情况:

>» a = 3 »> b = a >» a = , spam , 对 千 所有的 Python 赋值语句,这条语句简单地创建了 一 个新的对象(代表 字 符串值 'spam') ,并设置 a 对这个新的对象进行引用。尽管这样,这并不会改变 b 的值, b 仍然引 用原始的对象一—整数 3 。最终的引用结构如图 6-3 所示。 如果我们把变最 b 改成 'spam' 的话,也会发生同样的事情:赋值只会改变 b ,不会对 a 有影响。 在 没有类型变化的时候这种现象同样会发生。例如,思考下面这三 条语句:

»> a >» b »> a

= 3 = a = a + 2

动态类型

1

189

对象

变量名

乡.

..、

-已J.,. 畴..::

.

乡.





、、

`、





,

....乡

图 6-3: 最终运行完赋值语句 a='spam' 后的变量名和对象。变量 a 引用了由字面量表达式 'spam' 所创建的新对象(也就是一片内存空间),但是变量 b 仍然引用原始的对象 3 。因为这

个赋值运算改变的不是对象 3, 所以仅仅改变了变量 a, 变量 b 并没有发生改变 在这里产生了同样的结果: Python 让变量 a 引用对象 3, 让 b 引用与 a 相同的对象,如图

6-2 所示。之前,最后的那个赋值将 a 设置为 一个完全不同的对象(这个例子中为整数 5, 即表达式”+”的计算结果)。这并不会连带着改变 b 。事实上,没有办法改变对象 3 的值 的:就像第 4 章所介绍过的,整数是不可变的,因此没有方法能够在原位置修改它。

认识这种现象的 一种方法就是,与其他一些语言不同,在 Python 中变最总是一个指向对象 的指针,而不是可改变的内存区域的标签给一个变最赋一个新值,并不是替换原始的对象, 而是让这个变量去引用完全不同的 一个对象。实际的效果就是对一 个变最赋值,仅仅会影

响那个被赋值的变量。然而当可变的对象以及原位置的改变进入这个场景时,这种情形会 有所改变。想知道是怎样一种变化的话,请继续学习。

共享引用和在原位置修改 正如你在这一部分后边的章节将会看到的那样,有一些对象和操作(包括列表、字典和集 合在内的 Python 可变类型)确实会在原位置改变对象。例如,在一个列表中对一个偏移进

行赋值确实会改变这个列表对象,而不是生成 一个全新的列表对象。 或许你必须相信,这种特殊性有时候会对你的代码造成很大影响。对千支持这种在原位置

修改的对象,共享引用时的确需要加倍小心,因为对一个变量名的修改会影响其他的变量。

否则,你的对象可能会莫名其妙地发生改变。考虑到所有的赋值都是基千引用(包括函数 传参)的,这种风险普遍存在。 为了探入理解,我们再看一 看在第 4 章介绍过的列表对象。回忆 一 下列表,它在方括号中 进行编写,是其他对象的简单集合,它支持在原位置的赋值:

»> ll = [2, 3, 4) »> l2 = ll

190

第6章

U 是一个包含了对象 2 、 3 和 4 的列表。在列表中的元素是通过它们的位置进行读取的,所 以 L1[0) 引用对象 2' 它是列表 L1 中的第一个元素。当然,列表自身也是对象,就像整数

和字符串一样。在运行之前的两个赋值后, L1 和 L2 引用了同 一个共享的对象,就像我们之 前例子中的 a 和 b 一样(如图 6-2 所示)。如果我们现在像下面这样去扩展这个交互:

»> Ll

= 24

Ll 直接设置为一个不同的对象, L2 仍是引用最初的列表。尽管这样,如果我们稍稍改变一 下这个语句的内容,就会有明显不同的效果。

>» ll = [2, 3, 4) »> l2 = l1 »> L1[0] = 24

# An in-place change

»> l1

# Li is different

[ 24, 3, 4] >» l2 [24, 3, 4)

# But so is L2!

# A mutable object # Make a reference to the same object

在这里我们并没有改变 L1, 而是改变了 L1 所引用的对象的一个元素。这类修改会在原位置 覆盖列表对象中的某部分值。因为这个列表对象是与其他对象共享的(被其他对象引用), 那么一个像这样在原位置的改变不仅仅会对且有影响。也就是说,你必须意识到当做了这

样的修改时,它会影响程序的其他部分。在这个例子中,这一改变也会对 L2 产生影响,因 为它与 U 都引用了相同的对象。另外,我们实际上井没有改变 L2, 但是它的值将发生变化, 因为它引用了一个在原位置改变的对象。 这种行为仅针对支持原位置改变的可变对象,井且通常来说就是你想要的效果,但你仍然

应该了解它是如何运作的,以让它按照预期去工作。这也是默认形式:如果你不想要这样 的现象发生,可以请求 Python 复制 (copy) 对象,而不是创建引用。复制 一个列表有很多 种方法,包括使用内置 list 函数或者标准库的 copy 模块。也许最常用的办法就是从头到 尾的分片(请查阅第 4 章和第 7 章有关分片的更多内容)。

»> ll = [2, 3, 4] >» L2 = L1[:] »> Ll[O] = 24

# Make a copy of Li (or list(Ll), copy.copy(Ll), etc.)

»> Ll [24, 3, 4]

>» L2

# L2 is not c加nged

[2, 3, 4] 这里,对 U 的修改不会影响 L2, 因为 L2 引用的是 Ll 所引用对象的 一 个副本,而不是那个 对象本身。也就是说,两个变量指向不同的内存区域。

注意这种分片技术不会应用在其他可变的核心类型(字典和集合,因为它们不是序列)上, 复制一 个字典或集合应该使用 X .copy( )方法调用 (列表在 Python 3.3 中也有这个方法),

动态类型

I

191

或者将原来的对象传入它们的类型的构造函数中,例如 diet 和 set 。而且,注意标准库中

的 copy 模块有 一个通用 的复制任意对象类型的调用,也有 一个复 制嵌套对象结构( 例如嵌 套了列表的一个字典)的调用 :

import copy X = copy.copy(Y) X = copy.deepcopy(V)

# Make top-level''shallow" copy of any object Y # Make deep copy of any object Y: copy all nested parts

我们将会在第 8 章和第 9 章更深入 f 了解列表和字典,并复习共享引用和复制的概念。这 里记住有些对象是可以在原位置改变的(即可变的对象),任何这些对象出现过的代码都

遵循这类规律。在 Python 中,这种对象包括了列表、字典、集合以及一些通过 class 语句 定义的对象。如果这不是你期望的现象,可以根据需要直接复制对象。

共享引用和相等 考虑到知识的完备性,应当指出本章前面提及的垃圾回收行为,对千特定的类型而言可能 更加概念化而不是完全遵照条规。请看下面的语句 :

»> >>>

X = 42 , x ='shrubbery'

# Reclaim 42 now?

因为 Python 缓存并复用了小的整数和小的字符串,就像前文提到的那样,这里的对象 42

也许并不 一定会被回收 1 相反 地,它将可能仍被保存在一 个系统表中,等待下一次你的代 码生成另一个 42 来重复利用。尽管这样,大多数种类的对象都会在不再被引用时马上回收, 对于那些不会被回收的对象,缓存机制并不影响你所编写的代码。 例如,基千 Python 的引用模型,在 Python 程序中有两种不同的方法去检查是否相等。让

我们创建一个共享引用来说明这一点:

>» L = [1, 2, 3] »> M = L »> L == M

II Mand L reference the same object

# Same values

True

»>

L is M

# Same objects

True 这里的第一种技术”==运算符",测试两个被引用的对象是否有相同的值。这种方法往往

在 Python 中用作相等的检查。第二种方法 ”is 运算符",是在检查对象的同一性。如果两 个变量名精确地指向同一个对象,它会返回 True 。这是一种更严格形式的相等测试,它在

大多数的程序中很少出现。 实际上, is 只是比较了实现引用的指针,所以如果有必要的话它是代码中检测共享引用的 一种办法。如果变址名引用值相等,但是对象不同,它的返回值将是 False, 正如当我们运 行两个不同的字面址表达式时:

192

I

第6章

»> L = [1, 2, 3] »> M = [1, 2, 3] »> L == M

# Mand l reference different objects # Same values

True

>» L is M

#

Difj灯enr

objects

False 看看当我们对小的数字采用同样的操作时的结果:

»> X = 42 »> Y = 42 »> X == y

# Should be two different objects

True

»> X is Y

# Same

ob丿ect

anyhow: caching at work!

True 在这次交互中, X 和 Y 应该是==的(具有相同的值),但不是 is 的(同一个对象),因为 我们运行了两个不同的字面量表达式 (42) 。不过,因为小的整数和字符串被缓存并复用了, 所以 is 告诉我们 X 和 Y 引用了一个相同的对象。

实际上,如果确实想创根问底的话,你总能向 Python 查询对一个对象引用的次数:在标准 的 sys 模块中的 get ref count 函数会返回对象的引用次数。例如,在 IDLE GUI 中查询整 数对象 1 时,它会报告这个对象有 647 次重复引用(绝大多数都是 IDLE 系统代码所使用的, 而非自己的代码,不过在 IDLE 外返回了 173 次,说明 Python 自己也私藏了很多 1)

»> import sys >» sys.getrefcount(1)

ft 647 pointers to this shared piece of memory

647 这种对象缓存和复用的机制与代码是没有关系的(除非你运行 is 检查)。因为不能在原位 置改变不可变的数字和字符串,所以无论对同一个对象有多少个引用都没有关系,所有的 引用都会看到同样的不可改变的值。然而,这种现象也反映了 Python 为了执行速度而采用 的优化其模式的众多方法中的 一种。

动态类型随处可见 在使用 Python 的过程中,你的确没有必要去用圆圈和箭头画变量名/对象的框图。尽管在

刚入门的时候,它会帮助你跟踪它们的引用结构,理解不常见的情况,就像我们在这里所 做的那样。例如,如果在程序中, 当传递过程中 一 个可变的对象发生了改变时,很有可能 你就是本章话题的第一现场目击者了。 此外,尽管目前来说动态类型看起来有些抽象,你最终还是需要关注它的。因为在 Python 中, 任何东西看起来都是通过赋值和引用工作的,对这个模型有基本了解在不同的场合都是很

有帮助的。就像你将会看到的那样,它也会在赋值语句、变量参数、 for 循环变址、模块导入、

动态类型

I

193

类属性等很多场合发挥作用。值得高兴的是,这是 Python 中唯 一 的赋值模型。 一 旦你对动

态类型上手了,将会发现它在这门语言中任何地方都有效。 从最实际的角度来说,动态类型意味着你将写更少的代码。尽管这样,同等重要的是,动 态类型也是 Python 中多态(我们在第 4 章介绍的一个概念,将会在本书后面再次见到)的 根本。因为我们在 Python 代码中没有对类型进行约束,它具备了精确性和高度的灵活性。 就像你将会看到的那样,如果使用正确的话,动态类型所蕴含的多态思想产生的代码,可 以自动地适应系统的新需求。

“弱”引用 你可能偶尔会在 Python 世界中看到“弱引用”的宇眼。这是一个相对高级的工具, 但也与我们这里讨论的引用模型有关系。就像 is 运算符,离开了它就没有办法理觥 . 简单来说,弱引用是通过 weakref 标准库来实现的一种用于防止对象被垃圾回收的引 用(这一引用并非来源于自身)。如果对象的最后一次引用是弱引用,那么这个对象

将被重新声明,而相应的弱引用会被自动删除(或被告知)。 例如,这对基于宇典的大对象缓存来说十分有用 。 否则,单靠缓存的引用并不能确保

对象存在于内存中。这种情况仍然可以视作引用模型的一种特例。关于史多细节,参

见 Python 库手册。

本章小结 本章对 Python 的动态类型模型(也就是 Python 自动为我们跟踪对象的类型,不需要我们

在脚本中编写声明语句)进行了深人的学习。在这个过程中,我们学会了 Python 中变量和 对象是如何通过引用关联在一起的,还探索了垃圾收集的概念,学到了对象共享引用是如 何影响多个变最的,并看到了 Python 中引用是如何影响相等的概念的。

因为在 Python 中只有一 个赋值模型,并且赋值在这门语言中的任意之处都能碰到,所以在 继续学习之前掌握这个模型是很有必要的。接下来的章节测试会帮助你复习这一章的概念。

在下一 章将会继续我们的核心对象之旅~习字符串。

本章习题 l.

思考下面 三 条语句。它们会改变 A 打印出的值吗?

A= " spam" B= A B = "shrubbery"

194

I

第6章

2.

思考下面三条语句。它们会改变 A 的值吗?

A= ["spam"] B= A B[o] = "shrubbery"

3.

这样如何, A 会改变吗?

A= ["spam"] B = A[:] B[o] = "shrubbery"

习题解答 I.

不: A 仍会作为 "spam" 进行打印。当 B 赋值为字符串 "shrubbery" 时,所发生的将是 变量 B 被重新设置为指向新的字符串对象。 A 和 B 最初共享(即引用或指向)了同 一 个 字符串对象 ”spam" ,但是在 Python 中这两个变量名从未连接在一起。因此,设置 B 为

另 一个不同的对象对 A 没有影响。如果这里最后的语句变为 B = B +'shrubbery' ,也 会发生同样的事情。顺便提一 句,合并操作创建了 一 个新的对象作为其结果,并将这

个值只赋值给了 B 。我们永远都不会在原位置覆盖一 个字符串(数字或元组),因为字 符串是不可变的。

2.

是: A 现在打印为 ["shrubbery”] 。从技术上讲,我们既没有改变 A 也没有改变 B, 我 们改变的是这两个变扯共同引用(指向)的对象的一部分,通过变量 B 在原位置覆盖 了这个对象的 一 部分内容。因为 A 像 B 一样引用了同 一个对象,这个改变也会对 A 产生 影响。

3.

不会: A 仍然会打印为 ["spam”] 。由千分片表达式语句会在 A 被赋值给 B 前创建一个副 本,这样对 B 在原位置赋值就不会有影响了。在第二个赋值语句后,就有了两个拥有

相同值的不同列表对象(在 Python 中,我们说它们是==的,却不是 is 的)。第三条 赋值语句会改变指向 B 的列表对象,而不会改变指向 A 的列表对象。

动态类型

I

19s

第 7 章

字符串基础

迄今为止,我们已经学习了数字,并探索了 Python 的动态类型模型。我们深度核心对象之 旅的下一个主要类型就是 Python 字符串一 - 个有序的字符集合,用来存储和表示基千文

本和字节的信息。我们曾在第 4 章对字符串进行过简单介绍。这里会再次更为深入地学习 它们,并补充 一些早先省略的细节。

本章范围 在开始之前,我想要澄清这里不会介绍什么。第 4 章简要地预览了 Unicode 字符串和文件一 用千处理非 ASCII 文本的工具。对千 一 些程序员,尤其是在互联网领域工作的那些而 言 , Unicode 是一个关键工具。例如,它可以在网页、邮件正文和头部、 FTP 传输、 GUI API 、

目录 工具,以及 HTML 、 XML 和 JSON 文本中弹出。 与此同时, Unicode 对千刚刚起步的程序员来讲,可能会是一个重批级主题,今天我遇见的 许多(或绝大多数) Python 程序员仍然做着他们的工作,并以愉悦的心态忽视掉整个主题。

鉴于此,本书把 Unicode 的大部分内容下放到第 37 章,其高级主题部分作为可选的阅读材 料,然后致力千这里的字符串基础知识。

也就是说,本章只是讲述 Python 中字符串的部分内容一大多数脚本使用的和大多数程序 员需要了解的那一 部分。本章探索基本的 str 字符串类型,它处理 ASCII 文本,不管使用

哪个 Python 版本它都同样地工作。尽管这是有意限定的范围,但由 于 str 在 Python 3 . X 中 也处理 Unicode, 而在 2.X 中另一种 unicode 类型几乎与 str 完全相同地工作,因此这里我

们学习的一切也都能直接应用千 Unicode 处理。

196

Unicode 简介 对千那些关心 Unicode 的读者,我还想提供其影响力的快速总结和一份进一步学习的指南。 更正式地说, ASCII 是 Unicode 文本的一种简单形式,但只是众多可能的编码和字母表中 的一种。来自非英语地区的文本可能会使用完全不同的文字,并且当在文件中存储时,可 能以完全不同的形式编码。 正如第 4 章所述的, Python 分别使用字符串对象类型和文件接口,用来区分文本和二进制

数据。对此的支持随 Python 系列而变化:



在 Python 3.X 中,有 三种字符串类型: str 用于 Unicode 文本(包括 ASCII), bytes 用千二进制数据(包括巳编码的文本),而 bytearray 是 bytes 的一个可修改的变体。 文件在两种模式下工作:文本,它将内容表示为 str 类型并实现 Unicode 编码;二进制,

它以原始 bytes 的形式处理,且不做任何数据转换。



在 Python 2.X 中, unicode 字符串表示 Unicode 文本, str 字符串同时处理 8 位文本和 二进制数据,而 bytearray 从 3.X 向后移植,在 Python 2.6 和随后版本中可用。普通文 件的内容是由 str 直接表示的字节,但是 codecs 模块打开 Unicode 文本文件,处理编码,

并将内容作为 unicode 对象表示。 尽管有这样的版本差异,但如果你确实需要了解 Unicode, 会发现它是一个相对小型的扩

展一一一且文本位干内存中就是 Python 字符序列,可支持我们将在本章学习的全部基本内 容。实际上, Unicode 的主要不同在千它在内存和文件之间来回移动所要求的转换(编码)

步骤。除此之外,它大体上只是字符串处理过程。 不过,因为大多数程序员不需要掌握最前沿的 Unicode 细节,所以我把这其中的大部分内 容移到了第 37 章。在阅读了这里的字符串基础知识的材料后,当你准备学习这些更高级的 字符串概念时,我鼓励你阅读第 4 章的预览,以及第 37 章中对 Unicode 和字节的完整介绍。 就本章而言,我们将聚焦千基本的字符串类型和它们的运算。你会发现,这里我们将要学

习的技巧也可直接应用于 Python 工具集中更加高级的字符串类型。

字符串基础 从实用的角度来看,字符串可以用来表示能够编码为文本或字节的任何事物。这样的文本

包含符号和词语(如你的名字)、载人到内存中的文本文件的内容、 Internet 网址和 Python 源代码等。字符串可以用来保持用千媒体文件和网络传输的原始字节,还有国际化程序中 使用的编码和解码形式的非 ASCII Unicode 文本。

你也许在其他语言中用过字符串。 Python 中的字符串与其他语言(如 C 语言)中的字符数 组扮演着同样的角色,然而从某种程度上来说,它们是比数组级别更高的工具。与 C 语言

字符串基础

I

197

不同的是, Python 中的字符串伴有 一 套强大的处理工具集。另外一处不同的是, Python 没

有为单个字符留有不同的类型,取而代之的是可以使用单字符的字符串。 严格地说, Python 的字符串被划分为不可变序列这一类别,意味着这些字符串所包含的字 符存在从左至右的位置顺序,并且它们不可以在原位置修改。实际上,字符串是我们将要 学习的名为序列的一大类对象的第一个代表。请格外留意本章所介绍的序列操作,因为它

在今后要学习的其他序列类型(如列表和元组)上同样也适用。 表 7-1 介绍了本章将要讨论到的常见的字符串字面量和操作。空字符串表示为 一 对其中什 么都没有的引号(单引号或双引号),另外还有许多编写字符串的方法。若要进行处理,

字符串支持表达式操作,例如拼接(组合字符串)、分片(抽取 一 部分)、索引(通过偏 移最获取)等。除表达式外, Python 还提供了 一 系列的字符串方法,可以执行常见的特定

于字符串的任务, Python 还提供了用千执行高级文本处理任务的模块,如模式匹配。我们 将会在本章学习全部这些内容。 表 7-1: 常见的字符串字面量和操作 , 一m

操作

勹:





一 一解释

5=''

空字符串

5= "spam's"

双引号,和单引号相同

S ='s\np\ta\xoom'

转义序列

S = """... multiline... """

三引号块字符串

5= r'\temp\spam'

原始字符串(不进行转义)

B = b'sp\xc4m'

Python 2.6 、 2.7 和 3.X 中的字节串(第 4 章,第 37 章)

U = u'sp\uOOc4m'

Python 2.X 和 3.3+ 中的 Unicode 字符串(第 4 章,第 37 章)

51 + 52

拼接

5 * 3

重复

5[i]

索引

5 [i: j]

分片

len(5}

长度

"a %s parrot"% kind

字符串格式化表达式

"a {o} parrot".format(kind)

Python 2.6 、 2.7 和 3.X 中的字符串格式化方法

5.find('pa')

字符串方法(查看接下来表 7-3 中全部的 43 种方法) : 搜索

5.rstrip()

移除右侧空白

5.replace('pa','xx')

替换

5.split(',')

用分隔符分组

5. isdigit()

内容测试

5.lower()

大小写转换

198

1

第7章

表 7-1: 常见的字符串字面量和操作(续) 一.....俸

操作

.

,.

解释

S. endswith ('spam')

尾部测试

'spam'.join(strlist)

分隔符连接

S. encode ('latin-1')

Unicode 编码

B.decode('utf8')

Unicode 解码等(参见表 7-3)

for x in S: print(x)

迭代

'spam'in S

成员关系

[ c * 2 for c in S]

成员关系

map(ord, S)

ord 返回单个字符的 ASCII 序号

re.match('sp(.*)am', line)

模式匹配:库模块

除了表 7-1 列出的核心字符串工具集之外, Python 还支持更高级的基千模式的字符串处理 过程,可使用在第 4 章和第 36 章介绍的标准库中的 re (用千“正则表达式”)模块来实现。

Python 甚至还支持更高级别的文本处理工具,如 XML 解析器(在第 37 章简单讨论)。然而, 本书主要关注表 7-1 介绍的基本原理。

为涵盖这些基础知识,本章将会以字符串字面量的形式以及基本的字符串操作作为开始, 之后将学习字符串方法和格式化等更高级的工具。 Python 带有多种字符串工具,而这里我 们不会介绍所有这些工具 1 完整的介绍可以在 Python 的库手册和参考书中找到。这里,我 们的目标是探索足够常用的工具,并给出有代表性的例子 1 在这里没有介绍的那些方法, 和在这里将要介绍的方法,基本上是类似的。

字符串字面量 从整体上来讲, Python 中的字符串用起来还是相当简单的。也许最复杂的事情就是有如此 多的方法可在代码中编写:



单引号:' spa"m'



双引号: “spa'm"



三 弓 1 号:','... spam... "',"""... spam... """



转义序列: “s\tp\na\om"



原始字符串: r"C:\new\test.spm"



Python 3 .X 和 2.6+ 中的字节字面址(参见第 4 章和第 37 章) :



Python 2. X 和 3.3+ 中的 Unicode 字面县(参见第 4 章和第 37 章) : u'eggs\uoo2ospam'

b'sp\xo1am'

字符串基础

I

199

单引号和双引号的形式是目前最常见的,其他的形式都是服务千特定功能的,并且我们将

最后两种高级形式的进一步讨论推迟到本书第 37 章。让我们先快速看看其他的形式。

单引号和双引号字符串是一样的 在 Python 字符串周围,单引号和双引号字符是可以互换的。也就是说,字符串字面最可以 包围在两个单引号或两个双引号之中——两种形式同样有效,并返回相同类型的对象。例如, 若像下面这样编写,则意味着两者是相同的:

»>'shrubbery', "shrubbery" ('shrubbery','shrubbery') 支持这两种形式的原因是,你不进行反斜杠转义就可以在一种引号的字符串中包含另一种

引号。可以在由双引号包围的字符串中嵌入一个单引号字符,反之亦然:

>»'knight"s', "knight's" ('knight"s', "knight's") 除了单引号内嵌到字符串中的情形外,本书通常倾向于在字符串周围使用单引号,只是因 为它们阅读起来稍稍简单。这是一项纯粹主观的风格选择,但是 Python 也以这种方式显示

字符串,并且大多数 Python 程序员今天也是这么做的,所以你应该尽扯也这么做。 注意这里的逗号很重要。若没有逗号, Python 会在表达式中自动拼接相邻的字符串字面扯, 尽管可以简单地在它们之间增加一个+运算符来明确地表示这是一个拼接操作(在第 12 章 中将会看到,把这种形式放到圆括号中,就可以允许它跨越多行)

»> title = "Meaning "'of'" Life" >» title 'Meaning of Life'

# Implici 1c1t concatenation

在这些字符串之间添加逗号会创建一个元组,而不是一个字符串。还要注意在全部这些输 出中, Python 用单引号打印所有这些字符串,除非字符串内嵌入了单引号。如果需要,你

也可以通过反斜杠转义来嵌入引号字符:

»>'knight\'s', "knight\ "s" ("knight's",'knight"s') 想要了解原因,你需要知道转义字符通常是如何工作的。

转义序列代表特殊字符 上一个例子通过在引号前增加一个反斜杠的方式,在字符串内部嵌人一个引号。这代表了

字符串中的一种通用模式:反斜杠用来引入特殊的字符编码,称为转义序列。 转义序列让我们能够在字符串中嵌入不容易通过键盘输入的字符。字符\以及字符串字面

200

I

第7章

批中在它后边的一个或多个字符,在生成的字符串对象中会被单个字符所替代,这个字符 拥有通过转义序列定义的二进制值。例如,这里有一个五字符的字符串,其中嵌入了一个 换行符和一个制表符:

»> s ='a\nb\tc' 其中两个字符\ n 表示单个字符一在字符集中换行字符的二进制值 (ASCII 中,字符编码 为 10) 。类似地,序列\t 替换为制表符。这个字符串打印时的格式取决千打印的方式。交 互模式下是以转义序列的形式回显的,但是 print 会将其解释出来:

>» s 'a\nb\tc'

»> print(s) a b

C

为了清楚地了解这个字符串中到底有多少个实际的字符,可使用内置的 len 函数一—它会

返回一个字符串中字符的实际数晁,无论该字符串是如何编码或显示的:

>» len(s) 5 这个字符串长度为五个字符,分别包含了一个 ASCII a 字符、一个换行字符,一个 ASCII b

字符等。

注意:如果你习惯千纯 ASCII 文本,可能倾向千认为这个字符串包含 5 个字节,但是最好不要 这么想。实际上,在 Unicode 世界中,

”字节“没有任何含义。首先, Python 中的字符

串对象在内存中可能会更大。 其次更为严格地讲,在 Unicode 语境下,字符串内容和长度都反映了码点(识别数字) 值;无论在文件中编码或在内存中存储, Unicode 中的单个字符不一定直接映射为单个 字节。一一映射对千简单的 7 位 ASCII 文本可能有效,但即便是 ASCII 也依赖千外部编

码类型和内部使用的存储方案。例如在 UTF-16 格式下, ASCII 字符在文件中是多字节, 而在内存中它们可能是 1 、 2 或 4 字节,这取决于 Python 是如何为它们分配空间的。而 对于非 ASCII 字符,字符的值可能太大了,不适合一个 8 位字节表示,这时字符到字节

的一一映射根本不再适用。 实际上, Python 3.X 形式上将 str 字符串定义为 Unicode 码点序列,而不是字节序列, 以明确这一点。想要了解的话,第 37 章中有关千字符串在内部是如何存储的更多内容。 从现在起,为了最安全起见,请在字符串中思考字符而不是字节。在这件事上请相信我, 作为一名前 C 语言程序员,我也得摒弃这一习惯!

注意在前面的结果中,最初的反斜杠字符并没有真正地和字符串一起存储在内存中,它们 只是用来描述要在字符串中存储的特殊字符的值。对于这些特殊字符的编写, Python 提供 了 一 整套转义字符序列,如表 7-2 所示。

字符串基础

I

201

表 7-2: 字符串反斜杠字符

:



转义

意义

\newline

被省略(行的延续)

\\

反斜杠(保留—个\)

\'

单引号(保留')

\"

双引号(保留”)

\a

响铃

\b

退格

\f

换页

\n

新行(换行)

\r

回车

\t

水平制表符

\v

垂直制表符

\xhh

十六进制值 hh 的字符(准确为 2 个数位)

\000

八进制值 000 的字符(可达 3 个数位)

\o

空字符:二进制的 0 字符(不是字符串结尾)

\N{ id}

Unicode 数据库 ID

\uhhhh

16 位十六进制值的 Unicode 字符

\Uhhhhhhhh

32 位十六进制值的 Unicode 字符 a

\other

不转义(保留\和 other)

a.

\Uhhhh.. .转义序列准确采用 8 个十六进制数位;在 Python 2.X 中,\ u 和\ U 只有在 Unicode 字符串字面量中才可被识别,但是在 Python 3 . X 中,它们可以在普通的 (Unicode) 宇符串中

使用。在 3.X 的宇节字面量中,十六进制和八进制转义表示有着给定值的字节 ; 而在宇符串宇 面量中,这些转义表示有着给定码点值的 Unicode 字符。在第 37 章有关于 Unicode 转义的更 多内容。

一 些转义序列允许在字符串的字符之间嵌入绝对 二进制数值 。 例如,这里有 一 个五字符的 字符串,其中嵌入了两个值为二进制 0 的字符(编写为一个数位的八进制转义)

»> s ='a\Ob\Oc' »> s 'a\xoob\xooc'

»> len(s) 5 在 Python 中,这样的零(空)字符不会像 C 语言的“空字节”那样去结束 一 个字符串。相反, Python 在内存中保存整个字符串的长度和文本。事实上, Python 中没有任何字符会结束 一

个字符串。下面是一个完全由绝对二进制转义字符组成的字符串 一二进制的 1 和 2 (以八 进制编写),以及 二进制的 3 (以十六进制编写)

202

I

第7章

» > s ='\001 \002\x03' »> s '\xo1\xo2\xo3'

>» len(s) 3

注意,不管如何指定不可打印字符, Python 都以十六进制显示。我们可以自由地组合表 7-2 中的绝对数值转义和更多的符号转义类型。下面的字符串包含了字符组 ”spam" 、一个制

表符和换行符,以及一个用十六进制编码的绝对 0 值字符:

»> S = "s\tp\na\xoom" »> s 's\tp\na\xoom'

»> len(S) 7

>» print(S) s a m

p

当在 P ython 中处理 二进制数据文件时,了解这些知识显得格外重要。由千它们的内容在脚

本中是以字符串的形式呈现的,因此处理包含各种二进制字节值的 二进制文件也是完全可 行的一当使用二进制模式打开时,文件对象从外部文件返回原始字节串(第 4 章、第 9 章和第 37 章有关千文件对象的更多内容)。 最后,如表 7-2 最后一条所提示的,如果 Python 认为“\”后的字符不是有效的转义编码, 那么它会直接在生成的字符串中保留反斜杠:

»> x = "C:\py\code" »> X

II Keeps\ literally (and displays it as\\)

'C: \\py\\code'

>» len(x) 10 然而,除非你能够记住表 7-2 中的所有内容(其实你也不应该费精力来记它们!),否则 你不应该依赖这种行为。如果希望在字符串中明确地编写字面量反斜杠,那么请重复两次

反斜杠(“\\”是“\"的转义字符),或者使用原始字符串,下一小节将介绍后者的使用。

原始字符串阻止转义 正如我们已经看到的,用转义序列在字符串中嵌入特殊字符编码是很方便的。然而有时

候,为了引人转义字符,而对反斜杠的特殊处理会带来 一些麻烦。其实这相当常见,例如, Python 新手会尝试使用下面这样的文件名参数来打开 一 个文件:

myfile =

open('C:\new\text.dat' ,飞')

他们认为这将会打开 一 个在 C:\new 目录下的名为 text.dat 的文件。问题是这里有 “\n",

字符串基础

I

203

它会识别为一个换行字符,并且 “\t" 会被一个制表符所替代。结果就是,这个调用会尝

试打开一个名为 C.(换行符)ew(制表符)ext.dat 的文件,这样的结果通常不尽如人意。 这正是原始字符串所要解决的问题。如果字母 r (大写或小写)出现在字符串的第一引号的 前面,它将会关闭转义机制。结果就是 Python 会将反斜杠作为字面量来保持,完全就像输 入的那样。因此,为了修复这样的文件名错误,记得在 Windows 系统上增加字母 r:

myfile

=

open(r'C:\new\text.dat','w')

还有一种办法,因为两个反斜杠是 一 个反斜杠的转义序列,所以可以直接编写两个反斜杠 来保留反斜杠:

myfile

=

open('C: \\new\\ text. dat','w')

实际上,当 Python 打印一个嵌入了反斜杠的字符串时,它自身也会使用这种双反斜杠的方 案:

>» path = r'C:\new\text.dat' >» path 'C:\\new\\text.dat' »> print(path) C:\new\text.dat >» len(path)

#

Show as Python code

# User-fi门endly format

#

String length

15 和数字的表示相同,交互提示打印结果的默认格式就像代码一样,并且在输出中有转义的

反斜杠。打印语句提供了一种对用户更友好的格式,而在每处实际上仅有一个反斜杠。为 了验证这种情况,你可以检查内置 len 函数的结果,它会返回这个字符串的字符数,并且

这与其显示的格式没有关系。如果计算整个 print(path) 输出中的字符数,你会发现每个 反斜杠只占一个字符,所以总计 15 个字符。 除了在 Windows 下的目录路径,原始字符串也在正则表达式(文本模式匹配,由第 4 章和 第 37 章介绍的 re 模块所支持)中常见。注意 Python 脚本会自动在 Windows 和 UNIX 的

路径中使用斜杠表示字符串路径,因为 Python 试图以可移植的方式解释路径(例如打开文 件的时候,' C:/new/text.dat' 也有效)。然而如果你编写的路径使用 Windows 的反斜杠,

那么原始字符串也是很有用处的。

注意

尽管能够阻止转义,但即便 一 个原始字符串也不能以单个反斜杠结尾,因为,反斜杠会 转义后面的引号字符一—你仍必须转义包围的引号字符以将其嵌入到字符串中。也就是 说, r”... \“不是一个有效的字符串常量,因此一个原始字符串不能以奇数个反斜杠结

束。如果需要用单个反斜杠结束 一 个原始字符串,可以使用两个反斜杠并分片切掉第 二 个 (r'l\nb\tc\\'[:-1] ),手动添加一 个反斜杠 (r ' l\nb\tc'+ '\\'),或者忽略原始字

符串语法并在普通字符串中把反斜杠改为双反斜杠 ('1\\nb\\tc\ \')。以上 三种形式都 会创建同样的 8 字符的字符串,其中包含 3 个反斜杠。

204

I

第7章

三引号编写多行块字符串 到现在为止,你已经在实践中看到了单引号、双引号、转义字符以及原始字符串。 Python 还有一种三引号内的字符串字面量格式,有时候称作块字符串,这是一种对编写多行文本 数据来说很便捷的语法。这种形式以三个引号开始(单引号和双引号都可以),并紧跟任 意行数的文本,并且以与开始时相同的三个引号结尾。嵌入在这个字符串文本中的单引号 和双引号可能会,但不是必须转义一直到 Python 看到和这个字面址开始时同样的三个 未转义的引号时,这个字符串才会结束。例如(这里的"...”是在 IDLE 外用千行延续的 Python 提示符) :

»>mantra= """Always look on the bright ... side of life.""" »> »> mantra 'Always look\n on the bright\nside of life. ' 这个字符串横跨三行。正如我们在第 3 章学过的,在一些界面中,对千像这样的连续行来

说,交互提示符会变成“... ",但是 IDLE 就会简单地开始下一行,本书以这两种格式显 示程序清单,所以如果需要请推断。无论采用哪种方法, Python 都会把所有在三引号之内

的文本收集到一个单独的多行字符串中,并在代码行转折处嵌入换行字符(\ n) 。注意, 就像在这个字面量中一样,结果中的第二行开头有空格,第三行却没有~入的是什么,

得到的就是什么。要查看换行符解释后的字符串,打印出它,而不是回显:

»> print(mantra) Always look on the bright side of life. 实际上,三引用字符串会保留所有包围的文本,包括位于代码最右边,你认为是注释的文本。 所以不要这么做一一把你的注释放在引用文本的上面或下面,或使用之前提到的相邻字符

串的自动拼接,如果愿意可以带有显式换行符,和周围的圆括号以允许行跨越(当我们在 第 10 章和第 12 章学习语法规则时,会了解关千后一形式的更多知识)

>>>menu= """spam .•• eggs

# comments here added to string! # ditto

>» menu 'spam # comments here added to string!\neggs »> menu = ( •.. "spam\n" ••• "eggs\n" .. . ) »> menu 'spam\neggs\n'

# ditto\n'

# comments here ignored # but newlines not automatic

字符串基础

I

20s

三引号字 符串在程序需要输入多行文本的任何时候都是很有用的。例如,在 Python 源文件

中嵌入多行出错消息,或 HTML 、 XML 和 JSON 代码。我们能够通过三重引用直接在脚本 中嵌人这样的文本块,而不需要求助千外部的文本文件,或者借助显式拼接和换行字符。

三引号字符串常用千文档字符串 ,当它出现在文件的特定地点时,会被当作注释(在本书 的后面会介绍更多细节)。这里井非只能使用三引号的文本块,但是它们往往是可以用作 多行注释的。 最后,三引号字符串经常在开发过程中作为一种“恐怖的黑客式”方法去废除一些代码(好 吧,这并不恐怖,并且这在今天实际上是 一种较为常见的做法,但是它曾经不是这个目的)。

如果希望让一 些行的代码不工作,之后再次运行脚本,可以简单地在这几行前、后加入 三 重引号,就像这样: X= 1 # Disable this code temporarily

import os print(os.getcwd()) '""'

y = 2

这有些黑客风格,因为 Python 实际上会从用这种方式废除的代码行中创建字符串,但是这 对性能来说也许没有什么显著影响。对千大段的代码,这也比手动在每一行之前加人井号,

之后再删除它们要容易得多。在使用对编辑 Python 代码没有特定支持的文本编辑器时,尤 为如此。在 Python 中,实用往往会胜过美观。

实际应用中的字符串 一 旦你使用我们刚刚见过的字面量表达式创建了 一 个字符串,必定会很想用它去做些什么。

这 一节以及后面的两节将会介绍字符串表达式、方法,以及格式化~些是 Python 语言 中文本处理的首要工具。

基本操作 打开 Python 解释器,开始学习在表 7-1 中列举的字符串基本操作。你可以使用+运算符拼 接字符串,或使用*运算符重复它们:

% python >» len('abc') 3 »>'abc'+'def' 'abcdef' »>'Ni!'* 4 'Ni!Ni!Ni!Ni!'

206

I

第7章

#

Length: number of items

# Concatenarion: a new string # Repetition: like "Ni!"+ "Ni!"+ …

这里的 len 内置函数返回字符串(或其他任何拥有长度的对象)的长度。从形式上讲,

使

用+相加两个字符串对象会创建一个新的字符串对象,其内容是两个被操作对象的内容相 连,使用*重复就像在字符串后再增加一定数摄的自身。无论是哪种情况, Python 都允许 你创建任意大小的字符串。在 Python 中没有必要去做任何预声明,包括数据结构的大小一一 需要的时候你简单地创建字符串对象,并让 Python 自动地管理底层的内存空间(参阅第 6 章, 获取关千 Python 的内存管理”垃圾回收器”的更多知识)。

重复最初看起来有些令人费解,然而它在相当多的场合使用起来十分顺手。例如,为了打 印包含 80 个横线的一行,你可以一个一个数到 80, 或者让 Python 去帮你数:

>» print('-------... more... ---') >» print('-'* 80}

# 80 dashes, the hard way # 80 dashes, the easy way

注意运算符重载已经发挥了作用:这里正在使用运算符+和*,它们与应用千数字时的加法 和乘法运算符+和*是相同的。 Python 执行了正确的操作,因为它知道被加和乘的对象的 类型。但是小心:这个规则并不和你预计的那样随意。例如, Python 不允许你在+表达式

中混合数字和字符串:' abc'+9 会抛出 一 个错误,而不会自动地将 9 转换为字符串。 正如表 7-1 最后一行所示,你也可以使用 for 语句在循环中对字符串进行迭代。 for 语句重

复动作,并使用 in 表达式运算符对字符和子字符串进行成员关系的测试,这本质上是一种 搜索。对于子字符串, in 很像是本章稍后介绍的 str .find( ) 方法,但是它返回一个布尔结果,

而不是子字符串的位置(下面的代码使用 3.X 的 print 调用,也许会让你的光标缩进一些; 在 2.X 中输入 print c) :

»> myjob = "hacker" >» for c in myjob: print(c, end='')

# Step through items, print each(3.Xform)

h a c k e r

»> "k" in myjob # Found True » > ”“. "z" in myjob # Not found False »>'spam'in'abcspamdef'# Substring search, no position returned True for 循环指派一个变扯去获取一个序列(这里是一个字符串)中的连续元素,并对每一个元

素执行一 条或多条语句。实际上,这里变量 c 成为了 一 个在这个字符串的字符中步进的光标。 我们将会在本书稍后讨论类似的迭代工具以及表 7-1 中列出的其他内容(特别是在本书第

14 章和第 20 章中涉及的内容)。

索引和分片 因为字符串被定义为字符的有序集合,所以我们能够通过位置访问它们的元素。在 Python 中,

字符串基础

I

207

字符串中的字符是通过索引来获取的一在字符串后面的方括号中提供所需要的元素的数

值偏移最。你就会得到在指定位置的单字符字符串。 就像在 C 语言中一样, Python 偏移量是从 0 开始的,并且以比字符串的长度小 1 的偏移撬 结束。与 C 语言不同, Python 还支持使用负偏移量,从类似字符串这样的序列中获取元素。 从技术上讲,一个负偏移最会与这个字符串的长度相加,从而得到 一 个正偏移址。可以把 负偏移量看作从结尾处反向计数。下面的交互例子对此进行了说明:

.

»> s ='spam »> S[O], S[-2]

# Indexing from front or end

('s','a')

»> 5[1:3], 5[1:], 5[:-1]

#Slicing: extract a section

('pa','pam','spa')

第一行定义了一个四字符的字符串,井将其赋予变量名 S 。下面一行用两种方式对其进行索 引: S[o] 获取从最左边开始偏移鼠为 0 的元素一单字符的字符串 's' ;而 S

[ -2]

则获取

从结尾开始偏移量为 2 的元素一或等效地,从头部开始偏移量为 (4 +(-2) )的元素。从更

加形象的角度来看,偏移量和分片对应千图 7-1 中所示的格子注 Io [开始结束] 索引值指的是刀要在哪里一切下 ”。

0

1

2

+ + + s |上Ul c 1

-2

•1

+ }

l

主且 1 s |!巨 I Ml

[:

:)

默认值为序列的开头和结尾 .

图 7-1 :偏移和分片。正偏移量从左端开始(偏移量 0 为第一个元素),而负偏移量从右端往 回计数(偏移量- 1 为最后一个元素)。这两种偏移量均可以用来在索引及分片运算中给出位 置 上边的例子中的最后 一 行对分片进行了演示,它是索引的 一 种扩展形式,返回的是 一 个完

整片段,而不是一个单独元素。也许理解分片最好的办法就是将其看作解析(对结构进行 分析)的一种形式,特别是当你对字符串应用分片时一它让我们能够在仅仅一步内就分 离提取出一个完整片段(子字符串)。分片可以用来提取数据列,丢掉前缀和后缀文本, 等等。实际上,我们将会在本章稍后探索文本解析上下文中的分片。 分片的基础知识非常简单。当你使用一对以冒号分隔的偏移址对字符串这样的序列对象进 注 1:

更加热哀数学的读者(还有我课上的学生)有时会发现这里的一处小小的不对称:最左边

的元素位于偏移量为 0 处,但是最右边的元素位于偏移量为 一 1 处 。 可惜 , Python 中并没 有像 一 0 这样能够和 0 区别的值 。

208

I

第7章

行索引时, Python 将返回一个新的对象,其中包含了由这对偏移址所标识的连续的内容。 左边的偏移量作为下边界(包含下边界在内),而右边的偏移量作为上边界(不包含上边

界在内)。即 Python 将获取从下边界直到但不包括上边界的所有元素,并返回一个包含所 获取元素的新对象。如果省略这两个偏移最,上、下边界的默认值分别为 0 和被分片对象

的长度 。 例如,在我们刚刚看到的例子中, S (1: 3] 提取出偏移量为 1 和 2 的元素。也就是说,它抓 取了第 二 个和第 三个元素,并在偏移量为 3 的第四个元素前停止。接下来, 5(1:] 得到除 第一 个元素外的所有元素——上边界在未给出的情况下,默认值为字符串的长度。最后,

S[: -1] 获取除最后 一个元素外的所有元素—一下边界默认值为 0, 且- 1 对应最后一项,但 不包含在内。

乍看起来这有些令人困惑,但是一旦你掌握诀窍,索引和分片就是简单易用的强大工具。

记住,如果你不确定分片的效果,可以先交互地试验一下。在下一章,我将会介绍如何通 过对一个分片进行赋值(尽管不可以赋值字符串这样的不可变对象),从而改变一个特定 对象的一部分内容。下面总结一些细节以供参考:

索引 (S[i]) 获取特定偏移量处的元素:



第 一个元素的偏移址为 0 。



负偏移量索引意味着从结尾或右端反向进行计数。



5(0] 获取第一 个元素。



5(-2] 获取倒数第二个元素(同 S[len(s)-2] 一样)。

分片 (S[i:j]) 提取序列的连续部分:



上边界并不包含在内。



分片的下边界和上边界,在缺省时默认为 0 和序列的长度。



5(1:3] 获取从偏移量为 1 直到但是不包括偏移量为 3 之间的所有元素。



5(1: ]获取从偏移最为 l 直到末尾(序列长度)之间的所有元素。



S[: 3] 获取从偏移量为 0 直到但是不包括偏移量为 3 之间的所有元素。



5(:-1] 获取从偏移址为 0 直到但是不包括最后一个元素之间的所有元素。



s[ :]获取从偏移量为 0 直到末尾之间的所有元素一实现了对 S 的顶层复制。

扩展的分片 (S[i:j:k] )接收一个步长(或步幅) k, 其默认值为+1:



允许跳过元素和反转顺序一参看下一 节。

上面列出的倒数第 二 项是 一 个非常常见的技巧:它实现了对 一 个序列对象的完全顶层复 制一个有着相同值,但是不同内存片区的对象(在第 9 章将介绍更多关千复制的内容)。

字符串基础

I

209

对千字符串这样的不可变对象来说,这并不是很有用。但对于可以在原位置修改的对象来说,

却很实用,比如列表。 在下一章,你将会看到通过偏移量(方括号)进行索引的语法,也可以用千对字典进行的 按键索扎操作看起来很相似,但是却有着不同的解释。

扩展分片:第三个限制值和分片对象 在 Python 2.3 及之后版本中,分片表达式增加了第三个可选的索引值,作为步长(有 时称为步幅)。步长会加到每个提取的元素的索引值上。分片的完整形式现在变成了 X[I:J:K] ,它表示“提取对象 X 中的全部元素,从偏移晕 I 直到偏移最]- l, 每隔 k 个元 素索引一次“。第三个限制 K, 默认值为+ l, 这就是通常在一个分片中从左至右提取每一

个元素的原因。如果你明确指定了一个值,那么你就能够使用第三个限制去跳过某些元素 或反转它们的顺序。 例如, X[l:10:2] 会取出 X 中,偏移最 l - 9 之间,每隔 一个元素的元素, 也就是收集位千 偏移量 1 、 3 、 5 、 7 和 9 处的元素。同样,第一个和第二个限制的默认值分别为 0 以及序 列的长度,因此 X[:: 2] 会取出序列中从头到尾每隔 一个元素的元素 :

>» S ='abcdefghijklmnop' >» S[l:10:2)

#Skipping items

'bdfhj' »> S[::2] 'acegikmo' 也可以使用负数作为步幅以相反的顺序收集元素。例如,分片表达式 ”hello" [:: -1] 返回

一个新的字符串 "olleh" 一就像之前一样,前两个边界的默认值分别为 0 和序列的长度, 步幅- l 表示分片将会从右至左进行,而不是通常的从左至右。因此,实际效果就是将序列 进行反转:

»> S ='hello' »> S[: : -1]

# Reversing items

' olleh' 使用一个负数步幅,前两个边界的意义实际上进行了反转。也就是说,分片 S[s:1:-1] 以

反转的顺序,获取从 2 到 5 的元素(结果包含偏移最为 5 、 4 、 3 和 2 的元素)

>» 5 "''abcedfg' >» s[s:1:-1)

# Bounds roles differ

'fdec' 像这样的跳跃或反转是三重限制分片最常见的使用情况,可以参见 Python 的标准库手册获 得更多细节(或者可以在交互模式下运行儿个实例)。我们将会在本书稍后再次学习这种 三重限制的分片,届时将与 for 循环配合使用。

210

I

第7章

之后我们将看到分片等同千使用一个分片对象进行索引,这对千那些寻求支持两种操作的 类编写者来说,是一 个重要的发现:

>»'spam'[1:3) pa »>'spam'[slice(l, 3)) pa >>> spam'[::-1) 'maps' >»'spam'[slice(None, None, -1)] maps

# Slicing syntax # Slice objects

with index syntax+object

请留意:分片 贯穿本书始末,我会包含常见的案例使用侧栏(就像现在这个),以便让你对正在 介绍的语言特性有个直观的认识 , 并了觥其在真实程序中的典型应用 。 因为在了斛 Python 的大部分内容之前,你不太能够抗清楚一些现实应用场景,所以这些侧栏中也 必要地提及了许多尚未介绍的话题 。 不过你应该将这些内容看作一种预览,使你就能 够发现这些抽象的语言概念在平常的编程任务中的用途 。

例如,稍后你将会看到,在一个系统命令行中启动 Python 程序时罗列出的参数,可 使用内置 sys 模块中的 argv 属性来荻取 . # File echo.py

import sys print(sys.argv) % python echo.py -a -b -c

['echo .py ','-a','-b','-c') 通常 , 你只对跟随在程序名后边的参数感兴趣 。 这引出了一个分片的典型应用:单个

分片表达式能够返回除笫一项之外的所有元素的列表 。 这里, sys.argv[l: ]返回所期 待的列表 ['-a','-b','-c' ] 。 之后 , 就能够对这个最前面不包含程序名的列表进行 处理 。

分片也常常用作清理轮入文件的内容 。 如果你知道一行将会以行终止字符 (\n 换行宇

符标识)结束 , 你可以使用像 line[:-1] 这样的单个表达式去掉它 , 它将这行除最后 一个字符之外的所有内容提取出来(下边界默认值为 O) 。 无论以上哪种情况,分片 都完成了需要底层语言显式实现的逻辑 。

话虽如此,要去除换行字符,最好调用 line.rstrip 方法,因为当一行的最后一个字 符不是换行符时(这在一些文本编辑器工具中是很常见的),这个调用会保留该行的

完整 。

当你确定每一行都是通过换行字符终止时 , 则可以使用分片 。

字符串基础

I

211

字符串转换工具 Python 的设计座右铭之一就是拒绝猜测。作为 一个基本的例子,在 Python 中不能够相加数 字和字符串,即使字符串看起来像是数字也不可以(例如一个全数字的字符串) # Python 3.X

>» "42" + 1 TypeError: Can't convert'int'object to str implicitly # Python 2.X

>>> "42" + 1 TypeError: cannot concatenate'str'and'int'objects 这是有意设计的,因为+既能够进行加法运算也能够进行拼接操作,这种转换的选择会变

得模棱两可。因此, Python 将其作为错误来处理。在 Python 中,通常会省略让程序变得更 复杂的语法。 那么,如果脚本从文件或用户界面得到了 一 个文本字符串形式的数字该怎么办?这里的技 巧就是,需要使用转换工具预先处理,把字符串转换为数字,或者把数字转换为字符串。 例如:

»> int("42"), str(42)

# Convert from/to string

(42,'42') >» repr(42) '42'

#

Convert to as-code string

int 函数将字符串转换为数字,而 str 函数将数字转换为其字符串表示(基本上,打印的效

果就是看起来的样子)。 repr 函数(和之前的反引号表达式,在 Python 3.X 中删除)也能 够将一个对象转换为其字符串表示,但是它返回可作为代码的字符串对象,可以运行该字

符串来重新创建被转换对象。对千字符串,如果使用 print 语句进行显示,其结果是周围 有引号,这在 Python 不同版本间形式是不同的:

»> print(str('spam'), repr('spam'))

# 2.X: print str('spam'), repr('spam')

spam'spam' » > str('spam'), repr('spam') ('spam', "'spam'")

# Raw interactive echo displays

参见第 5 章的侧栏 “str 和 repr 显示格式”部分了解关千这些主题的更多内容。其中, int

和 str 是通常规定的字符串到数字和数字到字符串的转换技巧。 尽管你不能在+这样的运算符两侧混用字符串和数字类型,但是你能够在进行这样的运算 之前手动转换:

>>> S = "42"

>» I = 1 >» S + I TypeError: Can't convert'int'object to str implicitly

212

I

第7章

»> int(S) + I

#

Force addition

43

»> S + str(I)

# Force concatenation

'421' 类似的内置函数可以在浮点数和字符串之间相互转换:

»> str(3,1415), float("1.5") ('3.1415', 1.5)

»> text = "1.234E-10" » > float (text)

# Shows more digits before 2.7 and 3.1

1. 234e-10 稍后,我们将会进一步学习内置 eval 函数,它运行一个包含 Python 表达式代码的字符串, 因此能够将一个字符串转换为任意种类的对象。函数 int 和 float 只能够将字符串转换为数 字,然而这样的限制意味着它们往往要运行更快一些(并且更安全,因为它们不接受那些 随意的表达式代码)。我们在第 5 章简略了解到,字符串格式化表达式也提供了 一 种将数

字转换为字符串的方法。本章稍后我们将进 一 步讨论格式化。

字符串代码转换 关千转换这个主题,还可以将单个字符转换为其底层的整数码(它的 ASCII 字节值),可

将其传递给内置的 ord 函数来实现一它返回用来表示内存中相应字符的实际二进制值。 而 chr I为数将会执行相反的操作,它获取整数码并将其转化为对应的字符:

»> ord('s') 115

>» chr{115) 's,

技术上讲,这两者都可以在字符和它们的 Unicode 序数或“码点”之间相互转换。 Unicode

序数只是它们在底层字符集中的识别数字。对千 ASCII 文本,码点是熟悉的 7 位整数,可 用内存中的单个字节存储,但是对千其他种类的 Unicode 文本,码点的范围可能更为广阔(第

37 章中有关千字符集和 Unicode 的更多内容)。如果需要,你可以通过循环对一 个字符串 中的全部字符使用这些函数。这些工具也可以用来执行一种基千字符串的数学运算。例如, 为了前进到下一字符,我们可以预先将当前字符转换为整型并进行如下的数学运算:

>>>5='5' = chr(ord(S) + 1)

>» S »> s '6'

»> S »> s

= chr(ord(S) + 1)

'7'

字符串基础

I

213

至少对于单个字符的字符串来说,这提供了一种使用内置函数 int 将字符串转换为整数的 替代方案(尽管这只在字符集中元素的顺序如你代码期望的那样才有意义!)

» > int ('5') 5

>» ord (• 5') - ord ('o') 5 这样的转换可以与循环语句配合使用(本书第 4 章介绍了循环语句,下一部分将更深入地

介绍它),将一个二进制数位的字符串转换为相应的整数值。每次循环时,都将当前值乘 以 2, 并加上下一位数字的整数值:

>» B ='1101' »> I = O » > while B ! ='':

# Convert binary digi1s to integer with ord

I= I* 2 + (ord(B[o]) - ord('o')) B = B[l:]

»> I 13 左移运算 (I

«

1) 与在这里乘 2 的运算是一样的。由千还没有详细地介绍循环,并且在

第 5 章遇到的内置函数 int 和 bin 自 Python 2.6 和 Python 3.0 起就用来处理二进制转换任务, 所以我们把这一修改留作建议的练习:

>» int('1101', 2)

# Convert binary to integer: built-in

13

»> bin(13)

# Convert integer to binary:built-in

'ob1101' 只要时间足够,未来的 Python 倾向千把大多数常规任务自动化!

修改字符串 l 还记得术语“不可变序列”吗?正如我们看到的,不可变的意思就是你不能在原位置修改

一个字符串,例如,给一个索引进行赋值 :

»> s ='spam >» S[o] = ' x ' # Raises an error! TypeError:'str'object does not support item assignment 那么,我们如何在 Python 中修改文本信息呢?若要改变一个字符串,通常需要利用拼接、 分片这样的工具来建立并赋值一个新字符串,倘若必要的话,可将结果赋值回字符串最初 的名称:

»> S = S »> s

+'SP矶!'

'spamSPAM !'

214

I

第7章

# To change a string, make a new one

>» S = S[:4] +'Burger'+ S[-1) >>>

S

'spamBurger !' 第一个例子通过拼接在 S 后面添加了一个子字符串。这的确创建了一个新字符串并赋值回 给 S, 然而你也可以把它看作对原字符串的“修改"。第二个例子通过分片、索引、拼接将

4 个字符替换为 6 个字符。下一节你会看到,这一结果同样可以通过调用 replace 这样的字 符串方法来实现:

» > S ='splot' »> S = S.replace('pl','pamal') »> s 'spamalot' 就像每次操作都生成新的字符串值,字符串方法也都生成新的字符串对象。如果愿意保留

那些对象,你可以将它们赋值给新的变量名。每修改一 次字符串就生成一个新的字符串对 象并不像听起来那么效率低下一记住,就像在前面的章节中我们讨论过的那样, Python

在运行的过程中对不再使用的字符串对象自动进行垃圾回收(回收再利用空间) , 所以新 的对象重用了之前的值所占用的空间。 Python 的效率往往超出了你的预期。

最后,可以通过字符串格式化表达式来创建新的文本值。下面两种方式都把对象替换为字 符串,在某种意义上,是把对象转换为字符串,并且根据指定的格式改变最初的字符串:

»>'That is %d %s bird!'% (1,'dead') That is 1 dead bird! »>'That is {o} {1} bird!'.format(l,'dead') 'That is 1 dead bird!'

# Format expression: all Pythons #

Format method in 2.6, 2.7, 3.X

尽管用替换这个词来比喻,但格式化的结果是一个新的字符串对象,而不是修改后的对象。 我们将在本章稍后学习格式化,那时候你会发现,格式化比这个例 子 所展示的更为通用和 有用。由千上述第 二 个调用作为 一 个方法提供,因此在深人介绍格式化之前,先来看看字

符串的方法调用。

注意:正如在第 4 章预习过的,以及将在第 37 章介绍的, Python 3.0 和 2.6 引人了一种叫作

bytearray 的新字符串类型,它是可变的,因此可以原位置修改其值。 bytearray 对象 并不是真正的文本 字 符串,它们是 8 位小整数的序列。然而,它们支持和普通 字 符串相 同的大多数操作,并且显示的时候打印为 ASCII 字符。相应地,它们为必须频繁修改的

大量 简单 8 位文本提供了另 一 个选项 (Unicode 文本的更为丰 富 的类型隐 含 不同的技巧)。 在第 37 章中,我们还将看到 ord 和 chr 也处理 Unicode 字符,这些字符也许不能用单 个 字 节存储 。

字符串基础

I

21s

字符串方法 除表达式运算符之外,字符串还提供了 一 系列实现更复杂的文本处理任务的方法。在 Python 中,表达式和内置函数可以在不同的类型之间工作,但是方法通常特定千对象类 型~只在字符串对象上起作用。尽管一些类型的方法集在 Python 3.X

中有交集(例如,许多类型都拥有 count 和 copy 方法),但是比起其他工具,它们与类型 的相关程度仍然更大一些。

方法调用语法 正如在第 4 章介绍的,方法是与特定对象相关联,并作用千特定对象的简单函数。从技术

的角度来讲,它们是附属于对象的属性,而这些属性碰巧引用了可调用泊数罢了,这些泊 数总是拥有一个隐含的主体。

从更精细的角度看,函数就是代码包,而方法调用同时结合

了两种操作: 一 次属性获取和一 次函数调用:

属性获取 具有 object.attribute 形式的表达式可以理解为”获取对象 object 中 attribute 属性 的值”。

调用表达式 具有 function(arguments) 形式的表达式可以理解为:

“调用函数 function 的代码,

向其传递零个或多个逗号分隔的参数 argument 对象,并且返回函数 function 的结果值。”

合并两者可以调用 一 个对象方法。方法调用表达式:

object.method(arguments) 从左至右进行求值一Python 首先读取对象 object 的方法 method, 然后调用它,传递进 对象 object 和参数 arguments 。或者深入浅出地说,方法调用表达式意味着: 使用参数调用方法来处理对象。

如果方法计算出 一 个结果,它还会作为整个方法调用表达式的结果返回。以下是 一 个更为

明确的示例:

, »> s ='spam' »>result= S.find('pa')

#

Call the find method to look for'pa ' in string S

这一映射对千内置类型以及我们之后将要学习的用户定义的类都是有效的。你会看到贯穿 本书这一 部分,绝大多数对象都拥有可调用的方法,而且所有对象都可以通过这一同样的

方法调用语法来访问。你将在下一节中看到,为了调用对象的方法,必须确保这个对象是 存在的,如果缺少主体,则方法不能够运行(井且几乎没有任何意义)。

216

I

第7章

字符串的方法 表 7-3 概括了 Python 3.3 中内置字符串对象的方法及调用模式 1 这些会经常发生变化,因此,

确保查看 Python 的标准库手册,或者在交互模式下在任意字符串(或 str 类型名称)上调 用 dir 或 help 以获取最新的清单。 Python 2.X 的字符串方法变化很小,例如,由千 2.X 对 Unicode 数据的处理有所不同(我们将在本书第 37 章中讨论 Unicode 数据),因此它包含 一个 decode 方法。在下面的表中, s 是一个字符串对象,而可选的参数则包含在方括号中。

表单中的字符串方法实现了诸如分隔和拼接、大小写转换、内容测试、子字符串搜索和替 换这样的高级别操作。 表 7-3: Python 3.3 中的字符串方法调用

S. capitalize()

S. ljust(width [, fill])

S. casefold()

S.lower()

S.center(width [, fill])

S.lstrip([chars])

S. count (sub [, start [, end]])

S.maketrans(x[, y[, z]])

S.encode([encoding [,errors]])

S.partition(sep)

S.endswith(suffix [, start [, end]])

S.replace(old, new[, count])

S.expandtabs([tabsize])

S.rfind(sub [,start [,end]])

S. find (sub [, start [, end]])

S.rindex(sub [, start [, end]])

S.format(fmtstr, *args, **kwargs)

S.rjust(width [, fill])

S.index(sub [, start [, end]])

S.rpartition(sep)

S. isalnum()

S.rsplit([sep[, maxsplit]])

S.isalpha()

S.rstrip([chars])

S.isdecimal()

S. split ([ sep [, maxsplit]])

5. isdigit()

S.splitlines([keepends])

S. isidentifier()

S.startswith(prefix [, start [, end]])

S.islower()

S.strip([chars])

s..isnumeric() j

S.swapcase()

S. isprintable ()

S. title()

S. isspace()

S.translate(map)

S.istitle()

S.upper()

S.isupper()

S.zfill(width)

S.join(iterable) 正如你所看到的,有很多的字符串方法,而我们没有足够的篇幅介绍全部,参见 Python 的 库手册或参考文献来了解所有的细节。为了帮助你入门,让我们来看 一 些实际中最常使用 的方法的演示代码,井随之介绍 Python 文本处理的 一 些基础知识。

字符串基础

1

217

字符串方法示例:修改字符串 II 正如我们所见,因为字符串是不可变的,所以不能在原位置直接对其进行修改。在 Python 2.6 、 3.0 和后续版本中, bytearray 支持原位置文本修改,但是只适用千简单的 8 位类型。先前 我们探索过对文本字符串的修改,但是这里,让我们在字符串方法的上下文中进行快速的 第二遍浏览。 通常,为了从已有字符串中创建新的文本值,我们可以通过分片和拼接这样的操作来建立

新的字符串。例如,为了替换一个字符串中的两个字符,你可以使用如下的代码来完成:

.

, >» s = ·spammy >» s = s[:3] +'xx'+ s[s:] >» s spaxxy

# Slice sections from S

.

.

但是,如果只是为了替换一个子字符串的话,那么可以使用字符串的 replace 方法来实现:

, »> s = · spammy ` »> S = S.replace('mm','xx') >» s 'spaxxy'

# Replace all mm

with 江 in

S

replace 方法比这一 段代码所表现的更具有普遍性。它的参数是最初子串(任意长度)和替 换最初子串的字符串(任意长度),之后进行全局搜索并替换:

>»'aa$bb$cc$dd'. replace('$','SP肌') 'aaSPAMbbSPAMccSPAMdd' 鉴于这样的角色, replace 可以当作工具用来实现模板替换(如一 定格式的信件)。注意这 次我们直接打印了结果,而不是赋值给一个变量名一只有当你想要保留结果为以后使用

的时候,才需要将它们赋值给变量名。 如果需要替换可能在任意偏移量处出现的 一 个固定长度的字符串,可以再做一次替换,或

者使用字符串方法 find 搜索子串,之后使用分片:

»> S ='xxxxSP加xxxSP矶xxxx' >» where = S.find('SP矶') »> where

#Search/or position # Occurs at offset 4

4

»> S = S[:where] +'EGGS'+ S[(where+4):] »> s 'xxxxEGGSxxxxSPAMxxxx' find 方法返回子串出现处的偏移量(默认从前向后开始搜索),或者未找到时返回- 1 。如

同我们在前面所见到的,子字符串查找操作就像是 in 表达式,但是 find 返回子串所处的 位置:

218

1

第7章

»> S ='xxxxSP肌xxxxSPAMxxxx' »> S.replace{'SP肌', `EGGS') 'xxxxEGGSxxxxEGGSxxxx' »> S.replace{'SP肌 ','EGGS', 1) 'xxxxEGGSxxxxSPAMxxxx'

# Replace all

# Replace one

注意 replace 每次返回一个新的字符串对象。由千字符串是不可变的,因此每一种方法并 没有真正在原处修改主体字符串,尽管 “replace" 就是"替换"的意思! 拼接操作和 replace 方法每次运行都会生成新的字符串对象,实际上是利用它们来修改字

符串的一个潜在缺陷。如果你不得不对一个超长字符串进行多处修改,为了优化脚本的性能, 你可以将字符串转换为一个支持原位置修改的对象:

, , »> s = ·spammy »> L = list(S) »> L ['s','p','a','m', ' m','y'] 内置的 list 函数(一个对象构造调用)从任意序列的元素中创建一个新的列表一在这个 例子中,它将字符串的字符“打散”为一个列表。一且字符串以这样的形式出现,你无需 在每次修改后生成新的副本就可以对其进行多处修改:

>» L[3] ='x' # Works for lists, not strings »> L[4] ='x' »> L ['s','p','a','x','x','y'] 修改之后,如果你需要将其转换回一个字符串(例如写入一个文件),可以使用字符串的 join 方法将列表“合成“为一个字符串 :

»> S =''.join(L) »> s , , spaxxy 可能第 一 眼看上去 join 方法有些落后。因为它是字符串的方法(而不是列表的),并通过 给定的分隔符来调用。 join 将列表(或其他可迭代对象)中的字符串连在一起,并在元素

间用分隔符隔开 。在这个例子中,它使用一个空字符串分隔符将列表转换回字符串 。 一 般地, 任何字符串分隔符和字符串的可迭代对象都可以运行: >»'SP肌 '.join(('eggs','sausage','ham','toast'])

'eggsSPAMsausageSPAMhamSPAMtoast' 实际上, 一 次性连接全部的子字符串,运行起来可能常常比单独地拼接它们的每一个要快 得多。请确保阅读前面关千自 Python 3 .0 和 2.6 起可用的可变 bytearray 字符串的提示,第 37 章还将完整介绍;由千它可以在原位置修改,因此针对某些种类必须经常修改的 8 位文本, 它还为这 一 list/join 组合操作提供了 一种替代方案。

字符串基础

I

219

字符串方法示例:解析文本 字符串方法的另外一个常规角色是以简单的文本解析的形式出现的一分析结构并提取子 串。为了提取位千固定偏移批处的子串,我们可以采用分片技术:

>» »> »> »>

line ='aaa bbb ccc' coll = line[0:3] col3 = line[8:] coll , aaa ' >» col3 , CCC `

这组数据出现在固定偏移量处,因此可以通过分片从最初的字符串中提取出来。

只要你所

需要的数据组件有固定的位置,这一技术就被当作解析。相反,如果是某种分隔符分开了

数据组件,那么你可以通过分割来拿出这些组件。即便数据出现在字符串中的任意位置, 这种方法都能够正常工作:

>» line ='aaa bbb ccc' >» cols = line.split{) >» cols ['aaa','bbb','ccc'] 字符串的 split 方法将一个字符串从分隔符处切成一系列子串。在上一个例子中,我们没

有传递分隔符,所以默认的分隔符为空白~制表 符或者换行符所分割,之后我们得到所生成的子串列表。在其他的应用中,可以使用更具

体的分隔符来分割数据。下面这个例子使用逗号分割(并因此解析)字符串,这在由某些 数据库工具返回的数据中很常见:

» > line ='bob, hacker, 40' »> line.split(',') ['bob','hacker','40'] 分隔符也可以比单个字符更长,比如:

»> line =''i'mSP矶aSP矶lumberjack" »> line.split("SP小”) ["i ' m",'a','lumberjack'] 尽管使用分片或分割方法做数据解析的潜力有限,但是这两种方法运行都很快,并且能够

胜任日常的基本文本提取操作。逗号分隔的文本数据是 CSV 文件格式的 一部分;想了解有 关这一前沿领域的更多高级工具,请参看 Python 标准库中的 CSV 模块。

实际应用中的其他常见字符串方法 其他的字符串方法都有着更为专一 的角色,例如,清除每行末尾的空白,执行大小写转换,

测试内容,以及检测末尾或起始的子字符串:

220

I

第7章

>» line = "The knights who say Ni I \n" »> line.rstrip() 'The knights who say Ni!' »> line.upper() 'THE KNIGHTS WHO SAY NI!\n' >» line. isalpha () False >» line.endswith('Ni! \n') True » > line. startswith ('The') True 替代的技巧有时也能够取得与字符串方法相同的结果~成员关系运算符 in 能够用 来检测一个子串是否存在,而长度和分片操作则能够用来模拟 endswith 方法:

>» line 'The knights who say Ni!\n' »> line.find{'Ni') != -1 True >»'Ni'in line True »> sub ='Ni!\n' »> line.endswith{sub) True

# Search via method c;all or expression

# End test via method call or slice

»> line[-len(sub):] == sub True 请参阅本章稍后介绍的字符串格式化方法 format, 它提供了更高级的替换工具,可在单个 步骤内组合多个操作。 此外,由千字符串有很多方法可以使用,因此我不会逐 一 介绍。你会在本书后边见到其他

的字符串示例,想要了解更多细节,可以在 Python 库手册以及其他文档中寻求帮助,或者 直接在交互模式下自己动手进行实验。你也可以查看 help(S.method) 的结果来得到关千

任意字符串对象 s 的方法 method 的更多提示。正如我们在第 4 章所见,类似地,在 str. method 上运行 help 函数会给出相同的细节。

注意没有字符串方法支持模式一—对于基千模式的文本处理,必须使用 Python 的 re 标准 库模块,这个模块是 一 个在第 4 章介绍过的高级工具,但是超出了本书的范围(第 37 章给

出了 一个更进一步的简洁示例)。虽然有这方面的限制,但字符串方法有时与 re 模块的工 具比较起来,还是有运行速度方面的优势的。

原始 string 模块的函数(在 Python 3.X 中删除) Python 的字符串方法历史有些曲折。大约在 Python 出现的最初 10 年, Python 只提供 一个 标准库模块,名为 string, 其 中包含的函数大致相当千目前的字符串对象方法集。由千火

字符串基础

I

221

热的需求,到 Python 2.0 时,这些函数就变成字符串对象的方法了。然而,因为有如此多的

人写了如此多的依赖原始 string 模块的代码,所以 string 模块被保留下来,以便向后兼容。

如今,你应该只使用字符串方法,而不是最初的 string 模块。事实上,如今字符串方法的 原始模块调用形式已经从 Python 3.X 中完全删除了,在 Python 2.X 或 3.X 编写的新代码中,

你不应该使用它们。然而,因为你还是会在老式的 Python 2.X 代码中看见这个模块,并且 本部分内容要涵盖 Python 2.X 和 3.X 的知识,所以在这里简单地介绍 一 下。 这种历史遗留问题的结果就是,在 Python 2.X 中,从技术上来说有两种方式可以启用高级 的字符串操作:调用对象方法,或者调用 string 模块函数,并把对象作为参数传递进去。

例如,将 X 赋值为字符串对象,并调用 一 个对象方法:

X.method(arguments) 这样通常等效千通过 string 模块调用相同的操作(如果已导人该模块)

string.method(X, arguments) 这里是一个在实际应用中使用方法调用的例子:

>» S ='a+b+c+'

>» x = S.replace('+','spam') »> X 'aspambspamcspam ' 在 Python 2.X 中,要通过 string 模块访问相同的操作,需要导入该模块(至少在这 一过程 中导入一次)并传入对象: >>>垃port string ' - ' ' - -' >» y = string.replace(S ,'+','spam') »> y 'aspambspamcspam' - -

因为通过模块的调用长久以来一直是标准,并且字符串是大多数程序的核心组件,所以你

可能会在以后遇到的 Python 2.X 程序中看到两种调用模式。 不过,现在你应该使用方法调用而不是老派的模块调用。

除了模块调用已经从 Python 3.X

版本移除以外,这样做还有着充分的理由。其中 一 个是,模块调用法需要你导入 string 模 块(而方法调用不需要导入)。另 一 个理由是,当你用 import 加载模块,而不是 from 时,

模块调用法在输入时需要多打几个字符。最后,模块运行速度比方法慢(模块会把大多数 调用映射回方法,因此会引发 一 次额外调用)。

最初的 string 模块在 Python 3.X 中被保存下来(而其等同的字符串方法没有保留),因为

它包含了其他的工具,包括预定义的字符串常数(如 string.digits) ,以及模板对象系统一— 一个相对晦涩的格式化工具,要早千字符串的 format 方法,这里将其大体略过(要了解更

222

I

第7章

多细节,参看接下来将它与其他格式化工具相比较的简短注解,以及 Python 的库手册)。 不过,除非你真的需要把代码从 Python 2.X 修改为使用 Python 3.X, 否则,你就应该将这

个模块中的任何基本字符串操作调用看作 Python 过去的遗物。

字符串格式化表达式 尽管已经掌握了介绍的字符串方法和序列操作, Python 还提供了一种更高级的方法来组合 字符串处理任务一字符串格式化允许在单个步骤中对一个字符串执行多个特定类型的替

换。严格地讲,它不是必需的,但是它很方便使用,特别是当格式化文本显示给程序用户 的时候。由千 Python 世界中充满了很多新思想,因此如今的 Python 中的字符串格式化可

以用两种形式实现(没有算上在上一节提到的很少使用的 string 模块的 Template 系统) : 字符串格式化表达式:'...%s ...'% (values) 这是从 Python 诞生的时候就有的最初的技术 1 这一形式是基于 C 语言的 “printf" 模型, 并且在大多数现有的代码中广泛地使用。

字符串格式化方法调用:'...{}...'. format(values) 这是 Python 2.6 和 Python 3.0 新增加的技术,这一形式部分地起源千 C#/.NET 中同名 的工具,并且和字符串格式化表达式的功能有很大重叠。 由于方法调用形式较新,其中的某些或另外一些可能会随着时间的推移而废弃或移除。当

Python 3.0 在 2008 年发行的时候,表达式似乎更有可能在以后的 Python 版本中废弃。确实, 3.0 的文档中威胁到要在 3.1 版本中废弃表达式并在这之后移除。从 2013 年和 Python 3.3 起,

这井没有发生,并且考虑到表达式的广泛使用,看起来现在也不太会发生一事实上,在 如今 Python 自己的标准库中,它仍然出现了数千次!

自然了,这一故事的发展取决千 Python 用户的未来实践。另一方面,因为表达式和方法在 今天使用都有效,并且两者都可能出现在你偶然遇到的代码中,因此本书涵盖了这两种技

巧的全部内容。正如你将看到,两者大体上是一个主旋律的变体,尽管方法有着一些额外 的特征(诸如千位分隔符),而表达式常常更加简洁,对干大多数 Python 程序员而言,表

达式更像是他们的习性。 为了说明性的目的,本书在后面的示例中使用这两种技巧。如果其作者有着偏好,他将大 致上保持归类,除了从 Python 的 import this 座右铭引用 : 理应只存在一种唯一的显然的觥决方法 。

根据这句话最初的和长久以来的意义来看,除非较新的字符串格式化方法压倒性地好于最 初的和广为使用的表达式,否则它对 Python 程序员在这一领域的知识库增倍的要求是毫无 根据的,甚至是不够 Python 的。如果两种复杂的工具大体重叠,程序员就不应该同时学习

字符串基础

I

223

它们。你得自己做出判断,格式化是否足以增加语言的分量,所以让我们平和地聆听这两

者的故事吧。

格式化表达式基础 字符串格式化表达式是本部分内容最初的方法,因此我们将从它们开始。 Python 在对字符

串操作的时候,定义了%二进制运算符(你可能还记得它在应用千数字时,是除法取余数、 或求模的运算符)。当应用在字符串上的时候,%运算符提供了根据格式定义,将不同类型

的值格式化为字符串的简单方法。简而言之,%运算符提供了一种一次性进行多个字符串替 换的简洁方法,而不是构建并拼接多个不同的部分。

格式化字符串:

1

在%运算符的左侧放置一个需要进行格式化的字符串,这个字符串带有一个或多个内 嵌的转换目标,都以%开头(如%d) 。

2

在%运算符右侧放置 一 个(或多个,内嵌在元组中的)对象,这些对象将会插入到你

想让 Python 进行格式化的左侧的字符串中,并替换一个(或多个)转换目标。 例如,在上一个格式化示例中,整数 1 替换格式化字符串左边的% d, 字符串 'dead' 替换 %s 。结果就得到了一个新的字符串。它是这两次替换的结果,并可能被打印或保存,以便 在其他情景中使用:

>»'That is %d %s bird!'% (1,'dead'} That is 1 dead bird!

#

Format expression

从技术上来讲,字符串的格式化表达式往往是可选的一通常你可以进行多次字符串拼接 和转换来达到类似的目的。然而,格式化表达式允许我们将多个步骤合并为一步简单的操作。 这一功能相当强大,我们多举几个例子来看一看:

» > exclamation ='Ni' »>'The knights who say %s!'% exclamation 'The knights who say Ni!' >»'%d %s %g you'% (1,'spam', 4.0) '1 spam 4 you'

#

String substitution

#

Type-specific substitutions

»>'%s -- %s -- %s'% (42, 3.14159, [1, 2, 3]) # All types match a o/os target '42 -- 3.14159 -- [1, 2, 3]' 在第 一个例子中,在左侧目标位置插入字符串 'Ni' ,替换标记% s 。在第二个例子中,在目 标字符串中插入三个值。需要注意的是,当不止一个值待插入的时候,应该在右侧用括号

把它们括起来(也就是说,把它们放到元组中去)。%格式化表达式运算符期待在其右边出 现单独的一项,或是拥有一个或多个元素的元组。

224

I

第 7章

第 三 个例子同样是插入 三 个值: 一 个整数、 一 个浮点数对象和一个列表对象。但是注意到

左侧的所有目标都是% s' 这就表示要把它们转换为字符串。由千每种类型的对象都可以转

换为字符串(打印时所使用的),因此每种对象类型都适用于% s 转换。正因如此,除非你 要进行某些特殊的格式化, 一 般你只需要记得在格式化表达式中使用讫转换。

另外,请记住格式化总是会创建 一 个新字符串,而不是对左仰l 的字符串进行修改;因为字 符串是不可变的,所以只能这样操作。如前所述,如果需要的话,你可以将新的字符串赋

给 一 个变量来保存结果。

高级格式化表达式语法 对于更高级的特定类型的格式化来说,你可以在格式化表达式中使用表 7-4 列出的任何一

种转换类型的代码,它们出现在替换目标中%字符的后面。它们中的大部分都是 C 语言程 序员所熟知的,因为 Python 字符串格式化支持 C 语言中所有常用的 printf 格式的代码(但 是并不像 printf 那样显示结果,而是返回结果)。表中的一 些格式化代码为同一类型的格 式化提供了不同的选择。例如,%e 、%f 和%g 都可以用千浮点数的格式化。

表 7-4: 字符串格式化的类型码 代码 __

_ _ _~

意义

s

字符串(或任何对象的 str(X) 字符串)

r

与 s 相同,但使用 repr, 而不是 str

C

字符 (int 或 str)

d

十进制数字(以 10 为底的整数)

i

整数

u

与 d 相同(已废弃:不再是无符号整数)



八进制整数(以 8 为底)

X

十六进制整数(以 16 为底)

X

与 x 相同,但是使用大写字母

e

带有指数的浮点数,小写

E

与 e 相同,但是使用大写字母

f

十进制浮点数

F

与 f 相同,但是使用大写字母

g

浮点数 e 或 f

G

浮点数 E 或 F

%

%字面量(编码为沈)

事实上,在格式化字符串中,表达式左侧的转换目标支持多种转换操作,这些操作自有 一 套相 当 严谨的语法。转换目标的 一般结构看上去是这样的 :

字符串基础

I

22s

%[(keyname)][f1ags] [width] [.precision]typecode 表 7-4 中第一列的类型码字符出现在目标字符串格式的结尾。在%和类型码字符之间,你 可以进行以下的任何操作:



为索引在表达式右侧使用的字典提供键名称



罗列出说明格式的标签,如左对齐(-)、数值符号(+)、正数前的空白以及负数前的-(空 格)和零填充 (o)



为被替换的文本给出总的最小字段宽度



为浮点数字设置小数点后显示的数位(精度)

width 和 percision 部分都可以编写为 一 个*,以指定它们应该从表达式右侧的输入值中的 下 一项取值(当在运行时才会得知这两个参数会很有用)。如果你不需要这些额外工具, 在格式化字符串中的一个简单的% s 会被对应值的默认打印字符串所替换,而不管其类型是 什么。

高级格式化表达式举例 有关格式化目标的语法在 Python 的标准手册和参考文献中都有完整的介绍,不过在这里, 我们还是针对一 般的用法举几个例子。下面这个例子首先是对整数进行默认格式化,随后 进行了 6 位的左对齐格式化,最后进行了 6 位补零的格式化:

»> X = 1234 »> res ='integers:... %d... %-6d,.. %06d'% (x, x, x) »> res 'integers:... 1234... 1234... 001234' %e 、 %f 和% g 格式以不同的方式显示浮点数,下面的交互示例对此进行说明_% E 和% e 是 相同的,但是前者的指数是大写的, g 根据数字内容选择格式(它正式地定义为:如果指数 小干- 4 或不小千精度,则使用指数格式 e, 其他情况则使用小数格式 f, 默认的总位数精

度为 6) :

»> X = 1.23456789 »> X 1. 23456789

# Shows more digits before 2.7 and 3.1

»>'%e I %f I %g'% (x, x, x) '1. 234568e+oo I 1. 234568 I 1. 23457' »>'%E'% x '1. 234568E+oo • 对浮点数来讲,通过指定左对齐、补零、数值符号、总的字段宽度和小数点后的位数,你 可以得到各种各样的格式化结果。对千较简单的任务来说,你可以利用格式化表达式% s 简

单地转换到字符串来完成,或者利用早先展示的 str 内置函数来完成:

226

I

第7章

»>'%:6.2f I %05.2f I _% +06.tf'~ (x, x, x) '1.23 I 01.23 I +001.2 ' >»'%s'% x, str(x) ('1. 23456789','1. 23456789') 如果在运行的时候才知道大小,你可以在格式化字符串中用一个*来指定计算得出宽度和 精度,从而迫使它们的值从%运算符右边的输入中的下一项获取,在这里,元组中的 4 指 定为精度:

»>'%f, %.2f, %.*f'% (1/3.0, 1/3,0, 4, 1/3,0) '0.333333, 0.33, 0.3333' 如果你对这一功能感兴趣,可以自行体验这些例子和操作以加深理解。

基千字典的格式化表达式 作为一种更加高级的扩展,字符串格式化同时也允许左边的转换目标引用右边编写的字典 中的键来提取对应的值。这打开了将格式化作为一种模板工具来使用的大门。迄今为止,

我们只在第 4 章简短地遇见过字典,但是这里举一个例子来说明其基本原理:

»>'%(qty)d more %(food)s'% {'qty': 1,'food':'spam'} '1 more spam'

上例中,左边的格式化字符串中的 (qty) 和 (food) 引用了右边字典中的键,并提取它们相 应的值。生成类似 HTML 或 XML 文本的程序往往利用这一技术。你可以建立一个值字典, 并利用单个基千键的引用的格式化表达式 一 次性替换它们(注意第 一 个注释在三重引用之 上,因此它不被添加到字符串,我正在 IDLE 中输入这个,并且没有用千延续行的"... " 提示符) : > >>

#

Template with substitution targets

#

Build up values to substitute

»> reply = ”“ Greetings... Hello %(name)s! Your age is %(age)s »>values= {'name':'Bob','age': 40} >» print(reply % values)

# Perform substitutions

Greetings... Hello Bob! Your age is 40 这样的小技巧也常与内置函数 vars 配合使用,这个函数返回的字典包含了在它被调用的地 方所有存在的变量:

»> food ='spam' »> qty = 10

字符串基础

I

227

»> vars() {'food':'spam','qty':

10,... plus

built-in names set by Python... }

当字典用在一个格式化操作的右边时,它会让格式化字符串通过名称来访问变量(也就是说, 通过字典中的键) :

>»'%(qty)d more %(food)s'% vars() '10 more spam'

# Variables are keys in vars()

我们将在第 8 章更深入地学习字典。第 5 章中也有几个使用% x 和% o 格式化表达式目标码

转换为十六进制和八进制的字符串的例子 ,这里我们不再重复。更多的格式 化表达式示例 也会在接下来出现,作为与格式化方法的比较一本章下一个和最后一个字 符串主题 。

字符串格式化方法调用 如前所述, Python 2.6 和 Python 3.0 引入了一种新的方式来格式化字符串,在某些人看来,

这种方式更特定千 Python 。与格式化表达式不同,格式化方法调用不是紧密地基于 C 语言

的 “printf'' 模型,井且它们的意图有时更加明确 。另一方面,新 的技术仍然依赖千核心的 "printf" 概念,例如,类型码和格式化声明。此外,它和格式化表达式有很大程度的重合,

并且有时比格式化表达式需要更多的代码,且在实践中扮演不同角色的时候会很复杂。正 因如此,目前在表达式和方法调用之间没有最佳使用建议,而大多数程序员对这两种方案

有粗略的理解就足够了。幸运的是,这两者非常相似,许多核心概念都是重叠的。

字符串格式化方法基础 Python 2.6 、 2.7 和 3.X 中可用的字符串对象的 format 方法,是基千正常的函数调用语法, 而不是表达式语法。特别地,它使用 主体字符串作为模板,井且接受任意多个参数,用来 表示将要根据模板替换的值。

它的使用要求具备函数和调用的知识,但其使用多半是清晰易懂的。在主体字符串中,花 括号通过位置(如 {1}) 、关键字(如 {food}) ,或 Python 2.7 、 3.1 以及之后版本中的相

对位置({})来指定替换目标及将要插入的参数。我们在第 18 章深入学习参数传递时会了 解到,函数和方法的参数可以使用位置或关键字名称来传递,并且 Python 收集任意多个位

置和关键字参数的能力允许这种通用的方法调用模式。例如:

» > template ='{ o} ,伈} and {2}' »> template.format('spam','ham','eggs') 'spam, ham and eggs'

# By position

»> template ='{motto}, {pork} and { f o o d } ' # By keyword >» template.format(motto='spam', pork='ham', food='eggs') 'spam, ham and eggs'

228

1

第7章

»>template='{motto}, {o} and {food}' >» template. format ('ham', motto='spam', food='eggs') 'spam, ham and eggs'

# By

» > template ='{}, {} and {}' » > template. format ('spam','ham','eggs') 'spam, ham and eggs'

# By relative

both

position

# New in 3.1 and 2.7

通过比较,上一 节的格式化表达式更加简洁,但是它使用字典而不是关键字参数,并且不 允许值的来源(这可能是优势或缺陷,取决千你的视角)拥有如此大的灵活性,接下来有 更多关千这两种技巧比较的内容:

»>template='%s, %sand %s' » > template % ('spam','ham','eggs') 'spam, ham and eggs'

#

Same via expression

>»template='%(motto)s, %(pork)s and %(food)s' >»template% dict(motto='spam', pork='ham', food='eggs') 'spam, ham and eggs' 注意这里使用 diet( )并通过关键字参数来建立一个字典,这在第 4 章介绍过,第 8 章将进

行全面讲述。它经常是{...}字面最的一种混乱度较轻的替代方案。本质上,格式化方法 调用中的主体字符串也可以是创建一 个临时字符串的字面最,并且任意的对象类型都可以 在目标上替换,非常像格式化表达式中的%s 目标码:

»> '{motto}, {o} and {food}'.format(42, motto=3.14, food=[1, 2]) '3.14, 42 and [1, 2]' 就像%表达式和其他字符串方法 一样, format 创建并返回 一 个新的字符串对象,可以立即 打印它或保存起来方便以后使用(别忘了,字符串是不可变的,因此, format 必须创建一 个新的对象)。字符串格式化不只是用来显示:

>» X ='{motto}, {o} and {food}'.format(42, motto=3,14, food=[1, 2]) »> X '3.14, 42 and [1, 2]' »> X.split('and') ['3. 14, 42','[ 1, 2l'l >» V = X.replace('and','but under no circumstances') >» V '3.14, 42 but under no circumstances (1, 2]'

添加键、属性和偏移量 像%格式化表达式一样,格式化调用可以变得更复杂以支持更多高级用途。例如,格式化

字符串可以指定对象属性和字典键一就像在常规的 Python 语法中一样,方括号指定字典 键,而点表示通过位置或关键字所引用的元素的对象属性。下面例子中的第 一 个,索引字

字符串基础

I

229

典上的键 “kind" ,然后从已经导入的 sys 模块对象获取 “platform“ 属性。第 二 个例子做

同样的事情,但是,通过关键字而不是位置指定对象:

»> import sys >»'My {1[kind]} runs {o..platform}'.format(sys, {'kind':'laptop'}) 'My laptop runs win32' »>'My {map[kind]} runs {sys.platform}•. format(sys=sys,map={'kind': •laptop'}) 'My laptop runs win32' 格式化字符串中的方括号可以指定列表(及其他序列)的偏移量来执行索引,但是,只有

单个正偏移批才能在格式化字符串的语法中有效,因此,这一功能并不是像你想的那样通用。 和%表达式一 样,要指定负的偏移批或分片,或者使用任意表达式的结果,必须在格式化

字符串自身之外运行表达式(注意这里使用*parts 解包 一 个元组的项作为单独的函数参数, 正如我们在第 5 音学习分数时所做的;关千这 一形式的更多内容请参见第 18 章)

>» somelist = list('SP矶') » > somelist ['S','P','A','M'] »>'first={O[o]}, third={0[2]}'.format(somelist) 'first=S, third=A' »>'first={O}, last={1}'.format(somelist[o], somelist[-1]) 'first=S, last=M'

# [-1) fails in fmt

>» parts = somelist[o], somelist[-1], somelist[1:3] >»'first={o}, last={1}, middle={2}'.format(*parts) "first=S, last=M, middle=['P','A']"

# [1:3] fails in fmt # Or '{}' in 2.7/3. l +

高级格式化方法语法 另 一 种和%表达式类似的是,你可以在格式化字符串中添加额外的语法来实现更具体的层 级。对千格式化方法,我们在可能为空的替换目标的标识码之后使用 一 个冒号,后面跟着

可以指定字段大小、对齐方式和特定类型编码的格式化说明符。如下是可以在 一 个格式字 符串中作为替代目标出现的形式化结构——它的四个部分全部都是可选的,中间必须不带

有空格:

{fieldname component !conversionflag :formatspec} 在这个替代目标语法中:



fieldname 是辨识参数的 一 个可选的数字 或关键字,在 Python 2.7 、 3.1 和后续版本中, 可以将其省略以使用相对参数编号。



component 是有着大千等于零个 “.name" 或 “[index]" 引用的字符串,它可以披省略 以使用完整的参数值。其中的引用用来获取参数的属性或索引值。

230

I

第7章



conversionflag 如果出现则以!开始,后面跟着 r 、 s 或者 a, 在这个值上分别调用 repr 、 str 或 ascii 内置函数。



formatspec 如果出现则以:开始,后面跟着文本,指定了如何表示该值,包括字段宽度、 对齐方式、补零、小数精度等细节,井且以一个可选的数据类型码结束。

冒号后面的 formatspee 组件本身也有着丰富的格式,形式上的描述如下(方括号表示可选 的组件,实际编写时不添加) :

[[fill ]align] [sign] [#] [ o] [width] [,] [.precision] [ typecode] 其中 fill 可以是除{或}之外的任意填充字符 ., align 可以是<、>、=或^,分别表示左对

齐、右对齐、符号字符后的填充,或居中对齐 I Sign 可以是+、-或空格 1 而,(逗号)选 项请求一个逗号表示从 Python 2.7 和 3.1 开始使用的千分位分隔符。 width 和 precision 与

在%表达式中时一样,而 formatspee 也可以包含嵌套的只有字段名称的{}格式化字符串, 它从参数列表动态地获取值(和格式化表达式中的*很相似)。 方法的 typecode 选项几乎完全与在%表达式中使用的,并在前面的表 7-4 里列出的那些功

能重叠,但是格式方法还允许使用以二进制格式显示整数的 b 类型码(它等同千使用 bin

内置函数调用),允许一个%类型码显示百分数,井且只使用 d 来表示以 10 为底的整数(这 里不使用 i 和 u) 。注意与表达式的%s 不同,这里的 s 类型码要求一个字符串对象作为参数,

通常可以省略类型码以接受任何类型。 参考 Python 的库手册以获取这里我们将要省略的替换语法的更多内容。除了字符串的 format 方法外,单独的对象也可以用 format(object, formatspec) 内控函数来格式化(这

是 format 方法内部使用的),在用户定义的类中,还可以使用_format_运算符重载方法 来定制。

高级格式化方法举例 你已经发现,格式化方法中的语法是复杂的。因此在这样的情形下你最好使用交互提示符, 让我们看一些示例吧。在下面的代码中,{ 0:10} 意味着一个 10 字符宽的字段中的第一个位

置参数,{ 1:10} 意味着第一个参数的 platform 属性在 10 字符宽度的字段中右对齐(再次注意使用 diet()

函数从关键字参数中创建字典,这在第 4 章和第 8 章中介绍) :

>»'{0:10} = {1:10}'.format('spam', 123,4567) 'spam = 123.4567'

#

In Python 3.3

>»'{0:>10} = {1:10} = {l[kind]:»'{:>10} = {:10} = {[kind] :'{o:. 2f}'. format(l / 3.o) ·0.33' >»'为 2f'% (1 / 3.0) '0.33'

# Parameters hardcoded

»>'{o:.{1}f}'.format(1 / 3.0, 4) ·0.3333' »>'%.*f'% (4, 1 / 3.0) '0.3333'

# Take value from arguments

I

第7章

# Ditto for expression

# Ditto for expression

最后, Python 2.6 和 Python 3.0 还引入了一种新的内置 format 函数,它可以用来格式化单

独的一项。它是字符串 format 方法的一种更简洁的替代方案,并且大致类似千使用%格式 化表达式来格式化单独一项 :

>»'{o:.2f}'.format(t.2345) '1.23' >» format(t.2345,'.2f') '1. 23' »>'%.2f'% 1.2345 '1. 23'

# String method #

Built-in function

ft Expression

从技术上讲, format 内置函数会运行主体对象的—format_方法。对千每个被格式化的元 素, str.format 方法都是在内部运行的。它仍然比具有相同效果的%表达式要冗长,然而

这引出了我们即将在下一节中讨论的话题。

与%格式化表达式比较 如果你仔细地学习了前面的小节,可能会注意到,至少对千位置的引用和字典键,字符串 格式化方法看上去和%格式化表达式很像,尤其是在类型代码和额外的格式化语法等高级

用法中。实际上,在通常的使用情况下,格式化表达式可能比格式化方法调用更容易编写。 当使用常见的%s 打印字符串替代目标时,以及在 Python 2.7 和 3.1 中添加自动编号的字段时, 尤为如此:

print( '%s=%s'% ('spam', 42))

#

Format expression: in all 2.X/3.X

print('{o}={l}'.format('spam', 42))

# Format method: in 3.0+ and 2.6+

print('{}={}'.format('spam', 42))

# With autonumbering: in 3.1+ and 2.7

稍后我们将看到,更复杂的格式化方式的复杂性是不可避免的(复杂的任务通常都很困难, 不管用什么方式)。考虑到表达式经常使用,某些人把格式化方法看作冗余的。

另一方面,格式化方法还提供了一些潜在的优点。例如,最初的%表达式无法处理关键字、

属性引用和二进制类型代码,尽管%格式化字符串中的字典键引用常常能够实现类似的目 标。要看看两种技术是如何重合的,可以把下面的%表达式与前面展示的等效的格式化方 法调用进行比较:

»>'%s, %sand %s'% (3,14, 42, [1, 2)) '3.14, 42 and [1, 2]'

# Arbitrary types

»>'My %(kind)s runs %{platform)s'% {'kind':'laptop','platform': sys.platform} 'My laptop runs win32' >»'My %{kind)s runs %{platform)s'% dict{kind='laptop', platform=sys.platform) 'My laptop runs win32'

字符串基础

I

233

» > somelist = list('SPAM') >» parts = somelist[o], somelist[-1], somelist[l:3] »>'first=%s, last=%s, middle=%s'% parts "first=S, last=M, middle= ['P','A']" 当进行更为复杂的格式化时,这两种技术方法的复杂性趋向等同。尽管把下面的代码与前 面列出的等效的 format 方法调用进行比较时,你仍将发现%表达式往往更简单更简练一些, 在 Python 3.3 中: #

Adding specific formatting

»>'%-10s = %10s'% ('spam', 123.4567) spam = 123.4567' >»'%10s = %-10s'% ('spam', 123.4567) ' s p a m = 123.4567' »>'%(plat)1os = %(kind)-10s'% dict(plat=sys.platform, kind='laptop') 'win32 = laptop' #

Floating-point numbers

>»'%e, %.3e, %g'% (3.14159, 3.14159, 3.14159) '3.14159oe+oo, 3.142e+oo, 3.14159' >»'%f, %.2f, %06.2f'% (3.14159, 3.14159, 3.14159) '3.141590, 3.14, 003.14' # Hex and octal, but not binary(see ahead)

»>'%x, %0'% (255, 255) 'ff, 377' format 方法拥有为表达式所没有的很多高级功能,但是更复杂的格式化过程本质上仍然会 增加代码的复杂性。例如,下面的代码展示了用两种技术运行所得到的相同结果,其中设

置了字段大小和对齐方式,并以各种方式引用参数:

Hardcoded references in both »> import sys

#

>»'My {1[kind]:8}'.format(sys, {'kind':'laptop'}) 'My laptop runs win32' »>'My %(kind)-8s runs %(plat)8s'% dict(kind='laptop', plat=sys.platform) 'My laptop runs win32' 在实践中,程序很少像这样将引用编写在代码中,而是会执行那些事先构建起 一 组替换数 据的代码(例如, 一次性收集好输入表单,或是用来在 HTML 模板中做替换的数据库数据)。

当我们考虑这种情形下的惯常做法时, format 方法和%表达式之间的比较更加直接:

234

I

第7章

H Building data ahead of time in both »>data= dict(platform=sys.platform, kind='laptop') »>'My {kind:8}'.format(**data) 'My laptop runs win32' >»'My %(kind)-8s runs %(platform)8s'% data 'My laptop runs win32' 正如我们将在第 18 章所见到的,这里方法调用中的** data 是特殊的语法,它把一 个由键 值对组成的字典解包为 一 组组像 “name=value" 的关键字参数,这样就可以在格式 字 符串 中通过名字来引用它们一~函数调用工具中另一个不可避免的、非常概念化的前向引用 。 这通常可能是 format 方法的另 一 个缺点,对新手们而 言尤为如此。

像往常 一 样, Python 社区将决定这 三种方式(%表达式、 format 方法调用,或同时拥有两 种技术的工具集)中的哪一 种随着时间的推移会更好。尝试在编写程序的过程中使用这些 技术,可以更好地体会到它们能够带来些什么。查看 Python 2.6 、 3.0 和后续版本的库参考 手册以便了解更多细节。

注意:

Python 3.1 和 2.7 中的字符串格式化方法升级: Python 3 . ] 和 2.7 为数字添加了 一 项千分 位分隔符语法,它在 3 位一 组的数字之间插入逗号。为了使用这项功能,可在类型码前 添加一个逗号,如果出现宽度和精度,则要在它们之间添加逗号,如下所示:

»>'{o:d}'.format(999999999999) '999999999999' »>'{o:,d}'.format(999999999999) '999,999,999,999' 如果没有显式地包含位置,这些版本的 Python 还将自动为替换目标分配位置索引。尽 管这 一 扩展功能不适用千所有的使用情形,并且可能会消除格式化方法的主要优点之 更为清晰的代码:

»>'{:,d}'.format(999999999999) '999,999,999,999' >»'{:,d} {:,d}'.format(9999999, 8888888) '9,999,999 8,888,888' »>'{:,.2f}'.format(296999.2567) '296,999.26' 参见 Python 3.1 发行版的注释来获取更多细节。也可以参考第 25 章中的 Jormars.py 逗 号

插人示例和货币格式化函数示例,以获取 一个可以在 Python 3.1 和 2.7 之前的版本中导 入和使用的简单手动解决方案。在编程中,往往自己编 写一 个可调用的、可重用的和可

定制化的函数,并实现一 项新功能是非常简单直接的。 一 般不要去依赖 一 个固定的内置 工 具莱:

»> from formats import commas, money »>'%s'% commas(999999999999) '999,999,999,999' 字符串基础

I

23s

»>'%s %s'% (commas(9999999), commas(8888888}} '9,999,999 8,888,888' >»'%s'% money(296999.2567} '$296,999.26' 同往常一样,像这样的简单闭数也可以在更为高级的上下文中应用,如我们在第 4 章所 见的,并且会在接下来的章节中完整学习的迭代工具:

»> [commas(x) for x in (9999999, 8888888)] ['9,999,999','8,888,888'] >»'%s %s'% tuple(commas(x) for x in (9999999, 8888888)) '9,999,999 8,888,888' »>''.join(commas(x) for x in (9999999, 8888888)) '9,999,9998,888,888' 无论好坏, Python 开发者似乎总是偏爱在通用开发技巧上添加特殊情况的内置工具一 在下一节将要探索的一种平衡之道。

为什么使用格式化方法 虽然我已经不惜 一 切代价来比较和对比两种格式化技巧,我还想要说明 一 下,为什么有时 你仍然想要考虑使用多样的 format 方法。简而言之,尽管格式化方法有时候需要更多的代 码,它还:



拥有%表达式自身所没有的少量额外功能(但是%能够使用替代方案)



拥有更为灵活的值引用语法(但是它可能杀鸡用牛刀,而%经常拥有等效的方法)



能够使得替换值的引用更加清楚明确(但是这现在是可选的)



牺牲一个运算符以换取更容易记忆的方法名称(但是这也更加冗余)



对千单个和多个值并不允许不同的语法(但是实践表明这是很中庸的)



作为一个函数可以在表达式不能使用的地方使用(但是 一个单行函数引发了这一争议)

尽管这两种技术现在都可用,并且格式化表达式的应用仍然很广泛,但是最终 format 方法 可能会变得更为流行,在将来也可能获得更多 Python 开发者的关注。进一步讲,在语言中

有了表达式和方法,二者中的任 一 个都可能出现在你将来遇到的代码中,因此你理应对这 两种方式都有所了解。但是因为当前仍然需要在新的代码中做出选择,所以在结束本书对

这一 主题的讲解之前,让我们简要地延伸讨论 一 下平衡之道。

额外特性:特殊情况的“电池 ”vs 通用技术 方法调用支持表达式所没有的 一 些额外功能,例如 二进制类型码和(从 Python 2.7 和 3.1 开

始的)千分位分组。正如我们已经见到的,格式化表达式通常可以以其他方式实现同样的 效果。这里是 二进制格式化的情形:

236

I

第 7章

#Expression(only)binaryformatcode »>'{o:b}'.format((2 ** 16) - 1) '1111111111111111' »>'%b'% ((2 ** 16) - 1) ValueError: unsupported format character'b'... »> bin((2 ** 16) - 1) '061111111111111111' >»'%s'% bin((2 ** 16) - 1) 'ob1111111111111111' »>'{}'. format(bin((2 ** 16) - 1)) 'Ob1111111111111111'

# But other more general options work too

»>'%s'% bin((2 ** 16) - 1)[2:] '1111111111111111'

#SliceoffObtogetexactequivalent

# Usable with both method and% expression .1 + relative numbering # With 2.713.1

上面的示例表明通常的函数可以类似地代替格式化方法的千分位分组选项,并且更为完整 地支持个性化定制。在这种情形下,一个简单的 8 行可重用函数为我们提供了相同的功能, 并且没有额外的特殊情况的语法:

» >'{:, d}'. format (999999999999) '999,999,999,999'

# New strformat method feature in 3.112.7

»>'%s'% commas(999999999999) '999,999,999,999'

#But% is same with simple 8-linefunction

参见前 一个“注意”获取更多关千逗号的比较。这在本质上与先前用千二进制格式化的 bin

情形是相同的,但是这里的 commas 函数是用户定义的,而不是内置的。就此而言,这一技 巧比预先编写的工具或为单一目的而添加的特殊语法更为通用目标化。

也许,这 一 示例似乎也表明 Python (以及通常意义下的脚本语言)中有一 股更多依赖于特 殊情况的“包含电池”的工具,而不是依赖千通用开发技巧的趋势。这是一种使代码依赖

千那些电池的心态,并且似乎很难去评判,除非将软件开发视为 一 项终端用户的事业。对 于 一 些人而言,程序员也许能够更好地适应学习如何去编写一段插入逗号的算法,而不是 适应 一个能够这样做的工具。

这里我们将会搁置那个哲学争论,但是在实践中,这 一 示例中的这种趋势的总效果就是你 得学习和记忆额外的语法。考虑到它们的替代方案,这些方法的额外特性是否足够强大到 使你做出决定,还不是很清楚。

灵活的引用语法:额外的复杂性和功能的重叠 方法调用也直接支持键和属性引用,有些人将其视为更有灵活性。我们在前面的示例中, 比较了在%表达式中基千字典的格式化和在 format 方法中对键和属性的引用。这两种方式

通常过千相似,以至千你常常不能决定自己更喜欢哪个。例如,这两种方式都能够多次引 用一个相同的值:

字符串基础

I

237

>»'{name} {job} {name}'.format(name='Bob', job='dev') 'Bob dev Bob' »>'%(name)s %(job}s %(name)s'% dict(name='Bob', job='dev') 'Bob dev Bob' 然而在惯常做法中,表达式似乎同样简单,甚至更简单:

>» D = dict(name='Bob', job='dev') »>'{o[name]} {o[job]} {o[name]}'.format(D) 'Bob dev Bob' »>'{name} {job} {name}'.format(**D) 'Bob dev Bob' >»'%(name)s %(job)s %(name)s'% D 'Bob dev Bob'

ff Method, key references If Me『hod, dio-ro-args

# Expressi()/1, key references

公平地讲,格式化方法拥有更加专门化的替换语法,而两种格式化技术在其他方面的比较 互有胜负。但是考虑到功能的重叠和额外的复杂性,有人可能争辩 :格式化 方法的功用要 么难以自明,要么很少能有合适的用例。至少, Python 程 序员现在需要同时了解这两种工具、 而添加在它们身上的概念重担似乎还没有清晰明确的理由。

显式的值引用:如今变得可选和不常用 format 方法存 在一种颇具争议的使用情况一—当有多个值需要替换到格式字符串中时。例 如,我们将在第 3] 章遇到的 lister.py 类示例,把 6 个项替换到单个字符串中。在这个例子中,

方法的 {i} 位置标签似乎比表达式的比略微地易读:

'\n%s\n'% (...)

If Expression

'\n{O}\n'.format(...)

If Method

另一方面,在%表达式中使用字典键可能会大大减少这一差异的程度。这是体现格式化复

杂性的 一 种最坏情况,但在实践中不是很常见;很多典型的用例经常如同投硬币,你永远 不知道哪种方法会更好。进一步地讲,从 Python 3 . 1 和 2.7 开始,相对位置的引入导致对替 换目标进行编号不再是必要的,这可能悄悄地颠授人们所认为的 format 方法的优点:

>»'The {o} side {1} {2}'.format('bright','of','life') 'The bright side of life'

#Python J.X, 2.6+

»>'The{} side{}{}'.format('bright','of','life') 'The bright side of life'

# Python 3.1+, 2.7+

>»'The %s side %s %s'% ('bright','of','life') 'The bright side of life'

# All Pythons

若考虑简洁性,上面第二个写法可能比第一个更受欢迎,但是似乎会抵消 format 方法的部

分优点。例如,比较在浮点数格式化上的效果一格式化表达式仍然更为简洁,并且似乎 更齐整一些:

238

1

第7章

»>'{o:f}, {1:.2f}, {2:05.2f}'.format(3.14159, 3.14159, 3.14159) '3.141590, 3.14, 03.14' >»'{:f}, {:.2f}, {:06.2f}'.format(3.14159, 3.14159, 3.14159) '3.141590, 3.14, 003.14' >»'%f, %.2f, %06.2f'% (3.14159, 3.14159, 3.14159) '3 .141590, 3.14, 003.14'

命名的方法与上下文中立的参数:美学 vs 实用 格式化方法还声称拥有两个优点,就是它用 一个更加便千记忆的 format 方法名替代了%运

算符,井且不区分单个和多个替代值。简单的名字可能会使得格式化方法对初学者来说乍

看上去更容易 ("format" 可能比多个“%”字符更容易理解),尽管不同的读者有不同的 看法,且似乎这么认为的只是少数派。

一 些人认为后 一项优点更加重要一使用格式化表达式,单个值可以独自给定,但多个值 必须放入一个元组中:

>>>' %.2f'% 1.2345 '1.23' >>>'为 2f %s I % (1.2345, 99) '1.23 99'

ff Single value ffMultiple values tuple

从技术上讲,格式化表达式接受单个替换值,也可接受一 个或多个替换值组成的元组。这 导致单个替换值既可以独自给定,也可以放在元组中,所以要格式化的元组必须嵌套在另 一个元组中一一种很罕见但是貌似有道理的情况 : »>'%s' 为 1.23

'1.23' >» '%s'% (1.23,) '1. 23' »>'%s'% ((t.23,),) '(1.23,)'

II Single value, by itself II Single value, in a tuple #

Single value 1ha1 is a 111ple

另 一方面,格式化方法无论在哪种情况下,都只接受通用的函数参数。这使得它的替换语 法更加严格,这样就不必将多个替换值和元组替换值放在一个元组中:

»>'{o:.2f}'.format(l.2345) '1.23' »>'{o:.2f} {1}'.format(l.2345, 99) '1.23 99'

# Single value

»>'{o}'.format(l.23) '1.23' »>'{o}'.format{{l.23,)) '(1.23,)'

#

# Multiple values

Single value, by itself

It Single value that is a tuple

因此,对千初学者来说,格式化方法不容易令人感到困惑,并且很少引发编程错误。然而 ,

字符串基础

I

239

这似乎不再是一个问题一如果你总是把替换值放在一个元组中,井且忽略不放在元组中

的选项,那么格式化表达式本质上就和这里的方法调用相同。此外,格式化方法需要付出 一 定的代价,你需要编写更多的代码来实现其严格的使用模式。考虑到格式化表达式在 Python 整个发展过程中的广泛使用,评判两种格式化方式孰优孰劣,更多地偏向千理论上 的探讨,而不是实际编程时必须做出的选择。我们也无法要求人们将已有的 Python 代码全

部用新的工具 (format 方法)来改写替换,更何况这一方法与它想要涵盖的表达式是如此 之像。

功能 vs 表达式:微小的便利 支持格式化方法的最后一个理由——它是一 个函数,能够在表达式不能出现的地方使用。

由千我们需要更多有关函数的知识才能理解这一点,因此这里我们不会详细论述它。简而

言之, str.format 方法和 format 内置函数都可以作为参数传递进其他函数,或存储于其他 对象中等。而一个像%这样的表达式则不能直接这样传递,但是这种观点也许过于偏激一

将表达式一次性包装在一行 def 函数定义或 lambda 表达式中是非常简单的,这样就可以将 其转换为可以作为参数传递的面数(尽管找到这样做的一 个理由可能更具挑战性)

def myformat(fmt, args): return fmt % args

#

See Part IV

myformat('%s %s', (88, 99)) str.format('{} {}', 88, 99)

# #

Call your function object Versus calling the built-in

otherfunction(myformat)

# Your function is an object too

最后,在格式化表达式和方法之间,不是一 个非此即彼的选择。尽管表达式在 Python 代码

中似乎仍然更为流行,但是今天你可以在 Python 代码中使用格式化表达式和方法中的任一 种,而大多数程序员由千熟悉这两种技巧,将会在未来的数年里受益。对千语言的人门者,

这可能会增加他们在这 一 部分的学习难度 , 但是在开源软件世界的奇思妙想中,总会有不

断改进完善的余地注 20 注意:还有 一点:从技术上讲,如果包括之前提到的 string 模块艰难晦涩的 Template 工具的话,

那么 Python 中就总共有 3 个(而不是 2 个)内置的格式化工具。既然我们已经见过了 其他两个工具,下面将会展示它们的比较结果。表达式和方法也能够作为模板化工具来 使用,可利用字典的键名称或关键字参数来引用替换值:

>»'%(num)i = %(title}s'% dict(num=7, title='Strings') '7 = Strings'

注 2:

还可以参考笫 31 章的关于一个在 Python 3 .2 和 3.3 中的 str.format 错误(或退化)的注 斛,其中讨论了通用的空替换目标,用于没有定义任何—format_处理函数的对象属性 。 这影响了本书上一个版本中的一个可使用的示例。尽管这可能是一个暂叶的退化,但是这

至少表明该方法目前仍然是不太稳定的 一质疑 Python 冗余特性的另一个理由 。

240

I

第7章

»>'{num:d} = {title:s}'.format(num=7, title='Strings') '7 = Strings' »>'{num} = {title}'.format{**dict(num=7, title='Strings')) '7 = Strings' string 模块的模板化系统也允许通过名称来引用替换值,无论是字典的键名称还是关键 字参数,加上$前缀即可。但是这个模板化工具并不支持其他两种格式化方式的全部功 能一~更少的功能促成更简洁的工具,这是编写这一工具的主要动机:

» > import string >» t = string.Template('$num = $title') »> t.substitute({'num': 7,'title':'Strings'}) '7 = Strings' »> t.substitute(num=7, title='Strings') '7 = Strings' »> t.substitute(dict(num=7, title='Strings')} '7 = Strings' 参阅 Python 的手册获取更多细节。你在 Python 代码中看到这 一 替代方案(以及第三方 产业里的额外工具)也是有可能的 l 多亏了这一技巧很简单,并且极少使用,我们只 要简单介绍就足够了。对干如今的初学者而言,最好的策略就是学习和使用%和 str.

format 中的 一种,或是两者都学。

通用类型分类 既然我们已经探索了第一个 Python 集合对象字符串,让我们定义 一 些通用类型概念来结束 本章,这些概念对于今后我们从本章往后学到的大多数类型来说都有效。对千内置类型,

对处千同一分类的类型的操作,运行起来都是一样的,所以大多数的概念我们只需要定义 一次。目前为止,我们只审视了数字和字符串,但是由千它们是 Python 三大类型分类之中

两个分类的代表,因此其实你已经了解了有关一些其他类型的很多内容。

同一分类中的类型共享同一个操作集 正像我们所学习的那样,字符串是不可变序列:它们不能在原位置进行修改(不可变部分), 并且它们是按位置顺序排序的集合,可以通过偏移量访问(序列部分)。在本书这一部分,

我们可以在将要学习的全部序列类型上,执行本章中在字符串上使用的相同序列操作。这 些序列操作包括——拼接、索引、迭代等。更正式地讲,在 Python 中有三大类型(操作) 分类拥有这种一 般性:

数字(整数、浮点数、小数、分数等) 支持加法和乘法等。

序列(字符串、列表、元组) 支持索引、分片和拼接等。

字符串基础

I

241

映射(字典)

支持桉键名称的索引等。

在这里的通用”字符串”标签下,我包含了在本章开始所提到的 Python 3.X 字节串和

Python 2.X Unicode 字符串(参见第 37 章)。集合是自成 一 派的分类(它们不会把键映射到值, 也不是按位置排列顺序的序列),我们还没有深入地学习映射(我们将会在下一章学习)。 但是,我们遇到的很多其他类型都与数字和字符串类似。例如,对千任意的序列对象 X 和 Y:



X+Y 会创建一 个包含了两个操作对象内容的新的序列对象。



X*N 会创建一 个包含操作对象 x 内容 N 份副本的新的序列对象。

换句话说,这些操作运行起来对于任意一种序列对象都一 样,包括字符串、列表、元组以 及用户定义的对象类型。唯一 的区别就是,你最终得到的新对象是根据操作对象 X 和 Y 的

类型来决定的一如果你拼接的是列表,那么你将得到 一 个新的列表而不是字符串。索引、 分片以及其他的序列操作,在所有类型的序列上执行的效果都是一 样的;对象的类型将会 告诉 Python 去执行什么样的任务。

可变类型能够在原位置修改 不可变的分类是需要特别注意的约束,尽管对千新用户来说,还是有可能在这里犯糊涂

的。如果一个对象类型是不可变的,你就不能在原位置修改它的值;如果你这么做的话, Python 将会报错。替代的办法就是,你必须运行代码来创建一 个新对象,包含这个新的值。 Python 中的主要核心类型划分为如下两类:

不可变类别(数字字符串、元组、不可变集合) 不可变类别中的对象类型都不支持原位置修改,尽管我们总是可以运行表达式来创建 新对象,井将返回的结果分配给变量。 可变类别(列表、字典、集合、字节数组)

相反,可变类型总是可以通过不生成新对象的操作在原位置修改,而不用创建新的 对象。尽管这样的对象可以复制,但是它们也支持在原位置处的直接修改。

一 般来说,不可变类型能够保证一个对象不会被程序的其他部分改变,因而具有一定程度 的完整性。对千新手来说,如果不知道这有什么要紧的话,请参见第 6 章关千共享对象引

用的讨论。要了解列表、字典和元组分别属于哪种类型类别的,我们需要继续学习下 一章。

本章小结 在本章中,我们深入学习了字符串对象类型。我们学习了如何编写字符串字面量,探索了

字符串的操作,包括序列表达式 、字符串方法调用,以及表达式和方法调用这两种 字符串

242

I

第7章

格式化方式。在这个过程中,我们探入学习了各种概念,例如分片、方法调用和三引用块

字符串。我们也定义了 一 些在不同类型中普遍适用的核心思想。例如,序列类别中的所有 类型都共享同 一 套完整的操作集。 在下一章,我们将继续学习类型,并集中学习 Python 中最通用的集合对象——列表和字典。

就像你发现的那样,这里你学到的很多东西在那两种类型中也大有用处。前面提到过,在 本书的最后部分,我们将回顾字符串模型,井充实 Unicode 文本和二进制数据的一些细节。 因为 一部分(但是不是全部) Python 程序员可能对这方面的内容很感兴趣,在继续学习之前, 先来看本章的习题以复习这里所介绍的内容。

本章习题 1

字符串的 find 方法可以用来搜索列表吗?

2

字符串分片表达式可以用千列表吗?

3

如何将一 个字符转换为 ASCII 整数码?如何进行反向转换,将一个整数转换为字符?

4

在 Python 中,如何修改一 个字符串?

5.

已知字符串 s 的值为 "s,pa,m", 说出两种取出中间两个字符的方式。

6

字符串 “a\nb\x1f\oood" 中有多少个字符?

7

为什么有时要使用 string 模块,而不是字符串方法调用?

习题解答 I.

不可以。因为方法是特定千类型的,也就是说,它们只能用千单 一数据类型上。然而, 像 X+Y 这样的表达式和 len(X) 这样的内置函数是具有 一般性的,它们可以用千多种类

型上。在本例中, in 成员关系表达式和字符串的 find 方法具有类似的效果,但它不仅 可以用来查找字符串,还可以查找列表。在 Python 3.X 中,人们尝试按类别对方法进

行分组(例如,可变序列类型 list 和 bytearray 具有类似的方法集合),但方法仍然 比其他的操作集更加特定于类型。

2

可以。与方法不同,表达式具有 一 般性,可用千多种类型。在本例中,分片表达式其 实是一个序列操作,它可在任何类型的序列对象上使用,包括字符串、列表以及元组。 唯 一 的差别就是,当你对列表进行分片时,你得到 一 个新列表,而这里你得到的是 一 个新字符串。

3

内置的 ord(S) 函数可将单字符的字符串转换成整数字符编码; chr(I) 则可以将一个整

数码转换回字符串。但是,请牢记,这些整数只是用千文本的 ASCII 编码,它们的字

符只能从 ASCII 字符集中获取。在 Unicode 模型中,文本字符串实际上是用来识别整

字符串基础

I

243

数的 Unicode 码点序列,它们可能超出了 ASCII 保留的 7 位(比特)数字范围(了解

关于 Unicode 的更多知识,请参见第 4 章和第 37 章)。

4.

字符串无法被修改,它们是不可变对象。尽管如此,你可以通过拼接、分片 、 执行格 式化表达式、调用 replace 这样的方法来创建一个新字符串,再将结果赋值给最初的 变最名,从而达到相似的效果。

5

你可以使用 5[2:4] 对字符串进行分片,或者使用 S.split(',')[1] 用逗号分隔字符串, 再进行索引。在交互命令行下亲自尝试一下,看看运行结果吧。

6.

6 个。字符串 "a\nb\x1f\oood" 中包含字符 a 、换行符(\ n) 、 b 、 二进制的 31 (十六 进制转义为\ x计)、 二 进制的 0 (八进制转义为\000) 以及 d 。把字符串传给内置的

len 函数可以验证这一 点。打印出每个字符的 ord 结果,可查看实际的码点(识别整数) 值。参看表 7-2 获取关千转义字符的更多细节。

7.

如今,无论何时你都不应该使用 string 模块代替字符串对象方法调用。 string 模块已

经弃用,并且 Python 3..X 完全移除了它的调用。如今使用 string 模块的唯 一 正当理由 就是可以使用它的其他工具,例如预定义的常数。你也许会在一些现在看来非常陈旧 的 Python 代码(以及 20 世纪 90 年代的书)中,窥见它的身影。

244

I

第7章

第8 章

列表与字典

现在我们已经学习了数字和字符串类型,本章我们将继续讲述 Python 中列表和字典的故 事一它们都是其他对象的集合,也效力千几乎所有的 Python 脚本。如你所见,这两种类 型都相当灵活:它们都可以在原位置进行修改,也可以按需求增长或缩短,而且可以包含 任何种类的对象或者被嵌套。借助这些类型,你可以在脚本中创建并处理各式各样丰富的 信息结构。

列表 我们的 Python 内置对象之旅的这一站是列表 (list) ,列表是 Python 中最具灵活性的有序

集合对象类型。与字符串不同的是,列表可以包含任何种类的对象:数字、字符串甚至其 他列表。同样,与字符串不同,列表都是可变对象,它们都支持在原位置修改的操作,可

以通过指定的偏移量和分片、列表方法调用、删除语句等方法来实现。

Python 中的列表可以完成许多集合体数据结构的工作,而这些在稍底层 一 些的语言中(如 C 语言 )你不得不手动去实现 。 让我们快速地看一下它们的主要属性, Python 列表是:

任意对象的有序集合 从功能上看,列表就是收集其他对象的地方,你可以把它们看作组。同时列表维护了

其中每一项从左到右的位置顺序(也就是说,它们是序列)。 通过偏移访问 就像字符串 一 样,你可以通过列表对象的偏移对其进行索引,从而读取对象的某一部

分内容。由千列表的每一项都是有序的,你也可以执行诸如分片和拼接之类的任务。 可变长度、异构以及任意嵌套 与字符串不同的是,列表可以原位置增长或者缩短(长度可变),并且可以包含任何

245

类型的对象,而不仅仅是包含单个 字 符的字符串(列表和 字 符串是异构的)。因为列

表能够包含其他复杂的对象,又能够支持任意的嵌套,因此你可以创建列表的子列表 的子列表等 。 属于“可变序列”的分类

就类型分类而 言 ,列表是可变对象(它们可以在原位罚被修改),也可以响应所有针 对字符串的序列操作,例如索引、分片以及拼接。实际上,序列操作在列表与 字 符串 中的工作方式相同。唯 一 的区别是: 当 应用千字符串上的拼接和分片这样的操作应用 干列表时`返回的是新列表。然而列表是可变的,因此它们也支持字符串不能支持的 其他橾作,例如,删除和索引赋值操作,它们会在原位置修改列表 。 对象引用数组 从技术上讲, Python 列表包含了零个或多个其他对象的引用。列表也许会让你想起其

他语 言 中的指针(地址)数组,从 Python 的列表中读取 一 个项的速度基本上 与 索引 一 个 C 语 言数组差 不多。实际上,在标准 Python 解释器内部,列表就是 C 数组而不是链

接结构。我们曾在第 6 章学过,每当用到引用时, Python 总是会将这个引用指向 一 个对象, 所以程序只需处理对象。每当你把 一 个对象赋给 一 个数据结构组件或变显名时, Python

总是会存储同 一 个对象的引用,而不是对象的 一 个副本(除非你显式地要求保存副本)。 作为预习和参考,表 8-1 总结了常见的和具有代表性的列表对象操作。该表对 Python 3.3

来说已经相当完整,但若想查看更完整的内容,可以参考 Python 的标准库手册,或者运行 help(list) 或 dir(list) 查看 list 方法的完整列表清单一一你可以传人一 个真正的列表, 或者列表数据类型的名称一也就是 list 这个单词 。 这里列出的方法尤其针对 Python 3.3 版本的更新,实际上,它们中的两个是 Python 3.3 中新加入的。 表 8-1: 常用列表字面曼和操作 操作

解释

L= []

一个空的列表

L= (123,'abc', 1.23, {}]

四项;索引为 0 到 3

L= ['Bob', 40 . o, ['dev','mgr']]

嵌套的子列表

L= list('spam')

一 个可迭代对象元素的列表

L= list(range(-4, 4))

连续整数的列表

L[i]

索引

L[i](j]

索引的索引

L[ i:j]

分片

len(L)

求长度

Ll + L2

拼接

L * 3

重复

246

I

第 8章

表 8-1 :常用列表字面量和操作(续) 操作

解释

for x in L: print(x)

迭代

3 in L

成员关系

L.append(4)

尾部添加

L.extend([S,6, 7])

尾部扩展

L.insert(i, X)

插入

L.index(X)

索引

L.count(X)

统计元素出现次数

L. sort()

排序

L.reverse()

反转

L.copy()

复制 (Python 3 .3 及以上)

L.clear()

清除 (Python 3.3 及以上)

L. pop(i)

删除 i 处元素,并将其返回

L. remove(X)

删除元素 X

del L[i]

删除 i 处元素

del L[i:j]

删除 i 到 j 处的片段

L[i:j] = []

删除 i 到 j 处的片段

=

3

L[i:j]

=

L[i]

索引赋值

[4, 5, 6)

L = [x**2 for x in range(S)]

分片赋值

列表推导和映射(见第 4 章、第 14 章和第 20 章)

list(map(ord,'spam')) 当写成字面扯表达式时,列表会被写成一系列 的对象(实际上是返回对象的表达式),这 些对象括在方括号中并用逗号隔开。例如,表 8-1 的第 2 行将变量 L 赋给一个四项的列表。 嵌套的列表写成一串嵌套的方括号(第 3 行)。空列表就是一对内部为空的方括号(第 l

行)注 Io 表 8-L 中的大多数操作看上去应该很熟悉,因为它们都与我们先前在字符串上使用的序列 操作相同,例如,索引、拼接和迭代等。列表除了支持在原位置的修改操作(删除项、赋

值给索引和分片等)之外,还可以进行特定的列表方法调用(它们可用于排序、反转操作 以及在结尾添加元素等任务),列表可以使用这些工具进行修改操作是因为列表是可变的 对象类型。

汪 I:

在实际中 、 你不会在处理列表的程序中见到很多写成这样的列表。通常,数组都是(在运 行时)被动态构造出来的,来自用户的轮入、文件内容等。事实上,尽管精通字面量语法

十分重要,但大部分 Python 的数据结构都是在程序运行时动态构达出来的。

列表与字典

I

247

列表的实际应用 理解列表最好的方法可能还是要在实践中体会它们是如何运作的。让我们再看几个简单的 解释器交互的例子来说明表 8-1 中的操作。

基本列表操作 由千列表是序列,它支持很多与字符串相同的操作。例如,列表对“+”和“*“操作的响

应与字符串很相似,两个操作的意思也是拼接和重复,只不过结果是一个新的列表,而不 是一个字符串: % python

»> len([1, 2, 3])

#

Length

3

+ (4, s, 6] (1, 2, 3, 4, 5, 6] » > ['Ni !'] * 4 ('Ni!','Ni!','Ni!','Ni!']

»> (1, 2, 3]

# Concatenation #

Repetition

尽管列表的“+"操作和字符串中的一样,然而值得重视的是,“+”两边必须是相同类型的序列,

否则运行时会出现类型错误。例如,不能将一个列表和一个字符串拼接到一起,除非你先

把列表转换为字符串(使用例如 str 或%格式这样的工具),或者把字符串转换为列表 (list 内置函数能完成这一 转换)

»> str([1, 2]) + "34"

# Same as "[J, 2]"

'(1, 2)34' >» [1, 2] + list("34") (1, 2,'3 ' ,'4']

# Same as fl, 2) + ["3", "4"]

+ "34"

列表迭代和推导 更广泛地说,列表对于我们在上一章对字符串使用的所有序列操作都能做出响应,包括迭 代工具:

»> 3 in [1, 2, 3)

# Membership

True

»> for x in [1, 2, 3): print(x, end='') 1 2

# Iteration (2.X uses: print x,)

3

我们将在第 13 章更正式地讨论 for 迭代和 range 内置函数,因为它们都与语句语法有关。 简而言之, for 循环会从左到右地遍历任何序列中的项,对每一项执行一条或多条语句; range 会产生连续的整数。

表 8-1 中的最后一项,列表推导和 map 调用在本书第 14 章中会有更详细的介绍,并且在本

248

I

第8章

书第 20 章还会展开介绍。正如第 4 章所提到的,它们的基本操作是很简单的,列表推导只

不过是通过对序列(事实上,可以推广到所有的可迭代对象)中的每 一项应用一个表达式 来构建 一 个新的列表的方式,它与 for 循环密切相关:

»>res= [c * 4 for c in'SP矶'] »> res ['SSSS','PPPP','AAAA','MMMM']

#

List comprehensions

这个表达式功能上等同于手动构建一个结果的列表的 for 循环,但是,正如我们在后面的 章节中将要了解到的,列表推导的编码更简单,而且在今天的 Python 中似乎运行起来更快:

»> res = [] »> for c in'SP店': res.append(c * 4)

# List comprehension equivalent

»> res ['SSSS','PPPP','AAAA','MMMM'] 正如第 4 章简要介绍的,内置函数 map 能实现类似的效果,但它对序列中的各项应用 一 个 函数并把结果收集到一个新的列表中:

>» list(map(abs, [-1, -2, o, 1, 2])) [1, 2, o, 1, 2]

#

Map a function across a sequence

由于我们还没有准备好完整地介绍迭代,我们将推迟更多的细节,但是,在本章稍后可以

看到关千字典的 一 个类似的推导表达式。

索引、分片和矩阵 由千列表都是序列,对千列表而言,索引和分片操作与字符串中的操作基本相同。然而对 列表进行索引的结果就是你指定的偏移处的对象(不管是什么类型),而对列表进行分片 时往往返回一个新的列表:

» > L = ['spam','Spam','SPAM!'] »> l[2] 'SPAM!' >» l[-2] 'Spam' >» l[t:] ['Spam','SPAM!']

# Offsets start at zero #

Negative: count from the right

# Slicing fetches sections

注意:由千可以在列表中嵌套列表(和其他对象类型),有时需要将几次索引操作连在 一

起使用来深人到数据结构中去。举个例子,最简单的 一个办法是将其表示为矩阵(多维数组), 在 Python 中相当于嵌套了子列表的列表。在这里我们看一个基于列表的 3X3 的二维数组:

» > matrix = [[ 1, 2, 3], [ 4, 5, 6], [ 7, 8,

911

列表与字典

I

249

如果使用一次索引,会得到 一 整行(实际上,也就是嵌套的子列表),如果使用两次索引,

你会得到某一 行里的其中一项:

>» matrix[l] (4,

s,

6)

»> matrix[1][1] 5

>» matrix[2][0] 7

»> matrix = [ [ 1, 2, 3], [4, s, 6], [7, 8, 9]] »> matrix[1][1] 5 在之前的交互式命令行下可以让列表自然地横跨很多行,因为列表是以一对方括号栝起来 的;这里的"…”是 Python 命令行中的行间连续符(参见第 4 章中不带”噜 .. "的对照代码, 本书下一部分也会对语法做更多的介绍)。 关千矩阵的更多内容,本章稍后会介绍基千字典的矩阵表达形式,这是一种在矩阵稀疏时

效率会特别高的形式。我们将在第 20 章中继续这段线索,在那里我们会介绍其他的矩阵编

码,尤其是基千列表推导。就高性能数值运算的工作来说,第 4 和第 5 章所提到的 NumPy 扩展提供了处理矩阵的其他方式。

原位置修改列表 由千列表是可变的,它们支持原位置改变列表对象的操作。也就是说,本节中的操作都可 以直接修改列表对象(覆盖它原本的值),而不会像字符串那样强迫你建立一 个新的副本。 因为 Python 只处理对象引用,所以你需要着重区分原位置修改一个对象以及生成一个新对 象区的不同。正如在第 6 章已经讨论过的,如果你在原位置修改 一 个对象时,可能同时会

影响一个以上指向它的引用。

索引与分片的赋值 当使用列表的时候,可以将它赋值给一个特定项(偏移)或整个片段(分片)来改变它的内容

» > L = ['spam','Spam','SPAM I'] »> L(1] ='eggs' >» L

#

Index assignment

#

Slice assignmenr: delete+insert

['spam','eggs','SPAM!']

» > L[ o: 2] = ['eat','more'] >» L

# Replaces items 0,1

['eat','more','SPAM I'] 索引和分片的赋值都是原位置修改,它们对主体列表进行直接修改,而不是生成 一个 新的

2so

I

第8章

列表作为结果。 Python 中的索引赋值 (index assignment) 与 C 及大多数其他语言极为相 似一—

Python 用 一 个新值取代单个指定偏移的对象引用。 上 一 个例子的最后一个操作是分片赋值 (slice assignment) ,它仅仅用 一 步操作就能够将 列表的整个片段替换掉。因为这可能有点复杂,所以分片赋值最好分成两步来理解:

].

删除。删除等号左边指定的分片。

2

插入。将包含在等号右边可迭代对象中的片段插入旧分片被删除的位置 注 20

实际情况并非如此,但这有助下你理解为什么插入元素的数目不需要与删除的数目相匹配。 例如,已知一 个列表 L 包含两个或多个元素.赋值操作 L [1:2] = [4,5] 会把 L 一 个元素替换

成两个一一Python 会先删除 [1:2] 处的切片(从偏移扯 1 开始,直到但不包括偏移量 2)' 然后在删除切片的地方插入 4 和 5 。

这也解释了为什么下面的第 二 个切片赋值的例子实际上是一 个插入一Python 将 [1:1] 之 间的空白切片替换为两个元素,这也是为什么下面的第三个例子实际上是删除操作一一 Pytl1on 删除分片(位千偏移为 l 的项)之后什么也不插入:

»> L = [1, 2, 3] >» L[l:2] = [4, 5] »> L [1, 4,

s,

Replacement/insertion

3]

»> L[1:1] = [6, 7] »> L [1, 6, 7, 4,

s,

# Insertion (replace nothing)

3]

»> l[l:2] = (] »> L [1, 7, 4,

#

#

Deletion (insert nothing)

s, 3]

实际上,即使要被替换的"栏”或者替换它的内容是空值,分片赋值也是一次性替换整个

片段或“栏"。因为被赋值的序列长度不 一 定要与被赋值的分片的长度相匹配,所以分片

赋值能够用来替换(搅盖)、增长(插入)、缩短(删除)主体列表。这是 一 种功能强大 的操作,然而坦率地说,它也是在实践中井不常见的操作。 Python 通常还有更简单直接方

便记忆的方式实现替换、插入以及删除(例如拼接、 insert 、 pop 以及 remove 列表方法), 实际上那些才是 Python 程序员比较喜欢用的工具。 另 一 方面,这种操作可以被用作在列表头部原位置的 一 种拼接方式-~按照下 一 节的方法

概要,列表的 extend 方法在列表末尾所做的更加容易记忆:

»> L = [1] »> L[:o] = [2, 3, 4] >» L 注 2:

# Insert all at :0, an empty slice at front

这个揣述需要在赋值语句的左右侧分片出现交叠时被详细地肝释:例如 L[2:5]=L[3:6] 可

以正常工作是因为右侧被插入到左侧的值.在左侧被执行删除操作前就被取出来了 .

列表与字典

I

2s1

[2, 3, 4, 1)

>» L[len{L):] = [5, 6, 7] »> L [2, 3, 4, 1,

s,

6, 7]

>» L.extend([8, 9, 10]) >» L [2, 3, 4, 1,

s,

# Insert all at len(L):, an empty slice at end

#

Insert all at end, named method

6, 7, 8, 9, 10]

列表方法调用 与字符串相同, Python 列表对象也支持特定类型方法调用,其中很多调用可以在原位置修 改主体列表:

>>> L = ['eat','more','SP矶!'] » > L. append ('please')

»>

# Append method call: add item at end

L

['eat','more','SPAM!','please']

»> L.sort() »> L

# Sort lisr items ('S' L = ['abc','ABO','a Be']

>» sorted{L, key=str.lower, reverse=True)

II Sorting built-in

['a Be','ABD','abc']

» > L = ['abc','ABO','aBe') »> sorted{[x.lower() for x in L), reverse=True)

HPretransform items: differs!

['abe','abd','abc'] 注意这里的最后一个例子一一我们可以用 一 个列表推导在排序之前将字母转换为小写,但 是结果不包含最初的列表的值,因为这是用 key 参数来实现的。后者在排序中临时应用, 而不会改变排序的值。随养我们进 一 步学习,将会看到这样的情况,内翌函数 sorted 有时

候比 sort 方法更有用。

其他常见列表方法 与字符串相同,列表有其他方法可执行其他特定的操作。例如, reverse 可原位罚反转列表, extend 和 pop 方法分别能够在末端插入多个元素,删除一 个元素。也有 一 个 reversed 内隍 函数,像 sorted 一样工作并返回一个新的结果对象,但是 ,它 在 2.X 和 3.X 中都必须包装 在一个 list 调用中, 因为它 的 结果是 一个能够按需产生值的迭代器(后面更详细地讨论迭 代器) :

>» L = [1, 2] »> L.extend([3, 4, SJ) »> L

HAdd many items ar end (like in-place +)

[1, 2, 3, 4, 5]

»> L.pop()

#

Delete and return last item (by default: - 1)

5

»> L [1, 2, 3, 4]

» > L. reverse() >» L

# /11-place reversal method

[4, 3, 2, 1]

»> list(reversed(L))

# Reversal built-in wirh a result (iterator)

[1, 2, 3, 4] 从技术上讲, extend 方法总是循环访问传入的可迭代对象,井逐个把产生的元素添加到列 表尾部 ,然而 append 会直接把这个传入的可迭代对象添加到尾部而不会遍历它一一~是 一 个我们会在第 l4 章中深入了解的差异。就目前而言,我们只需要知道 extend 会添加多个

元素,而 append 只能添加 一 个。在某些类型的应用程序中,往往会把这里用到的列表 pop 方法和 append 方法联用,以 实现快速的后进先出 ( last-in-first-out , UFO ) 栈结构。列表

的末端作为栈的顶端:

>» L = [) >» L.append(1) »> L.append(2) >» L

#

Push onto stack

[ 1, 2]

»> L.pop() 2s4

I

第8章

HPop off stack



L

[1] pop 方法也能够接受某一个即将被删除并返回的元素的偏移量(默认值为最后一个元素的偏

移量,也就是- l )。其他列表方法可以通过值删除 (remove) 某元素,在偏移处插入 (insert) 某元素,计算某元素的出现次数 (count) ,查找某元素的偏移 (index——注意它是查找一

个元素的索引值,不要跟索引操作混淆)等:

>» L = ['spam','eggs','ham') >» L. index('eggs')

# Index of an object (search/ftnd)

1

>» L.insert(1,'toast') >» L

#

Insert at position

#

Delete by value

#

Delete by position

#

Number of occurrences

['spam','toast','eggs','ham')

»> L.remove('eggs') »> L ['spam','toast','ham')

>» L.pop(1) 'toast'



L

['spam','ham') » > L. count('spam') 1

注意,与其他列表方法不同, count 和 index 不会改变列表本身,不过能返回列表内容相关 的信息。可以参考其他说明源文件或者多练习这些调用进行更探入的学习。

其他常见列表操作 由千列表是可变的,你可以用 del 语句在原位置删除某项或某片段:

>>> L = ['spam','eggs','ham','toast']

>» del L[o] >» L

#

Delete one item

#

Delete an entire section

['eggs','ham','toast']

>» del l[l:] »> L

# Same as L[ I:} = [ I

['eggs'] 如前所述,因为分片赋值是删除外加插入操作,你也可以通过将空列表赋值给分片来删除

列表片段 (L[i:j]=[]) 。 Python 会删除左侧的分片,然后什么也不插入。另 一 方面,如果 将空列表赋值给 一 个索引,则会在指定的位置存储空列表对象的引用,而不是删除该位置

上的元素:

»> L = ['Already','got','one'] >» l[l:] = [] >» L ['Already']

»> L[o]

= []

列表与字典

I

255

»> L [ []] 虽然刚才讨论的所有操作都很典型,但是还有其他列表方法和操作并没有在这里列出。因 为可用的方法总是在不断地变化,而且实际上在 Python 3.3 中,新增的 L.copy( )方法能够

实现一个列表的顶层复制,这跟 L[ :]和 list(L) 很像,不过也和集合与字典的 copy 很像。 为了得到更全面的最新类型工具清单,你应该时常参考 Python 手册、 Python 的 dir 和 help

函数(我们在第 4 章首次提到过),以及其他在前言中所提到的参考书籍。 由千这是一个常见易错点,因此我还想再次提醒你,我们这里讨论的原位置修改操作都只

适用千可变对象 : 无论你怎样绞尽脑汁,都不能用在字符串上(以及第 9 章中我们将要讨 论的元组)。可变性是每个对象类型的固有属性。

字典 除了列表以外,字典 (dictionary) 也许是 Python 中最灵活的内置数据结构类型。如果把列 表看作有序的对象集合,那么就可以把字典当作无序的集合。它们主要的差别在干:字典

当中的元素是通过键来存取的,而不是通过偏移存取的。 Python 中的列表能扮演其他语言 中数组的角色,而字典则能扮演记录、搜索表,以及任何其他元素的键比索引重要的数据

结构。 例如,字典可以帮你代劳许多要在其他低级语言中手动实现的搜索算法及数据结构一作

为一种被高度优化的内置类型,字典的索引是一种非常快速的搜索操作。字典有时也能执 行其他语言中的记录、结构体、符号表的功能,也可以表示稀疏(多数为空)数据结构等。 Python 字典的主要属性如下:

通过键而不是偏移最来读取 字典有时又叫作关联数组 (associative array) 或散列表 (hash, 尤其是被其他脚本语言

的用户这么称呼)。它们通过键将一 系列值联系起来,这样你就可以使用键从字典中

取出初始存储千该键下的一项。像列表一样,你也可以使用索引操作从字典中获取内容。 但是索引采取键的形式,而不是相对偏移。 任意对象的无序集合 与列表不同,保存在字典中的项并没有特定的顺序;实际上, Python 将各项伪随机地 从左到右随机排序,以便快速查找。键提供了字典中项的象征性(而非物理性的)位置。 长度可变、异构、任意嵌套 与列表相似,字典可以在原位置增长或缩短(无需另外生成 一 份副本)。它们可以包 含任何类型的对象,而且它们支持任意深度的嵌套(可以包含列表和其他字典等)。 每个键只有 一 个与之相关联的值,但是这个值可以是 一系 列多个所需对象的集合,而

一 个值也可以同时存储在多个键下。

2s6

I

第8章

属千”可变映射”类型

通过给索引赋值,字典可以在原位置修改(字典是可变的),但不支持用千字符串和

列表中的序列操作。实际上,因为字典是无序集合,所以根据固定顺序进行操作(如 拼接和分片操作)是行不通的。相反,字典是唯一内置的、核心的映射类型一也就是

把键映射到值的对象。 Python 中其他的映射需要通过导入模块来创建。

对象引用表(散列表) 如果说列表是支持位置读取的对象引用数组,那么字典就是支持键读取的无序对象引 用表。从本质上讲,字典是作为散列表(支持快速检索的数据结构)来实现的,一开 始很小,并根据要求而增长。此外, Python 采用优化过的散列算法来寻找键,因此搜

索是很快速的。和列表一样,字典存储的是对象引用(不是副本,除非你显式地要求 它们这样做)。 照例作为查阅和预习,表 8-2 总结了一些最为普通并具有代表性的字典操作,而且就 Python 3.3 而言已经相对完整。和往常一样查看库手册或者运行 dir(dict) 或 help(dict) 可以得 到完整的清单一字典的类型名是 diet 。当写成字面量表达式时,字典以一系列”键.值”

(key: value) 对形式写出,每一对之间用逗号隔开,最外面用大括号括起来注 4 。一个空 字典就是一对空的大括号,而字典可以作为另一个字典(或者列表、元组)中的某一个值 被嵌套。

表 8-2 : 常见字典字面量和操作 操作

贯释

D = {}

空字典

D = {'name':'Bob','age': 40}

有两个元素的字典

E = {'cto': {'name':'Bob','age': 40}}

嵌套

D = dict(name='Bob', age=40)

其他构造方法:关键字

D = diet ([('name','Bob'), ('age', 40)))

键值对

D = dict(zip(keyslist, valslist))

拉链式键值对

D = dict.fromkeys(['a','b'))

键列表

D['name')

通过键索引

E['cto'] ['age']

嵌套索引

'age'in D

成员关系:键存在测试

D. keys()

方法:所有键

注4

和列表一样,你通常可能不会看到字典编写为字面量的形式一大部分程序很少会在运行 一开始就知道它们所有的数据,而且普遍会从用户轮入、文件等处动态地抽取 。然而,列

表和宇典采取不同的方式增长。在下一节中,你将看到你常常通过赋值给新的键的形式来 构建字典,这种方法却不能用于列表,而后者通常采用 append 和 extend 来增长。

列表与字典

1

257

表 8-2: 常见字典字面量和操作(续) 操作

解释

D.values()

所有值

D.items()

所有"键+值“元组

D.copy()

复制(顶层的)

D.clear()

清除(删除所有项目)

D.update(D2)

通过键合并

D.get(key, default?)

通过键获取,如果不存在默认返回 None

D.pop(key, default?)

通过键删除,如果不存在返回错误

D. setdefault(key, default?)

通过键获取,如果不存在默认设置为 None

D.popitem()

删除/返回所有的(键,值)对

len(D)

长度(储存的键值对的对数)

D[key]

42

=

新增/修改键,删除键

del D [key]

根据键删除条目

list(D. keys())

查看字典键 (Python

3.X)

D1. keys() & D2. keys() Dictionary views (Python 3.X)

查看字典键 (Python 2.7)

D = {x: x*2 for x in range(10)}

字典推导 (Python

3.X, 2.7)

字典的实际应用 如表 8-2 所示,字典通过键进行索引,嵌套的字典项是由一系列索引(方括号中的键)来 访问的。当 Python 创建字典时,会按照任意所选从左到右的顺序来存储项。为了取回 一 个值,

你锯要提供相应的键而不是相对位置。让我们回到解释器中,感受一下表 8-2 列出的 一 些 字典的操作。

字典的基本操作 通常情况下,创建字典并且通过键来存储、访问其中的某项:

% python

»> D = {'spam': 2,'ham': 1,'eggs': 3} >» D['spam']

#

Make a dictionary

# Fetch a value by key

2

»> D

#

Order is "scrambled"

{'eggs': 3,'spam': 2,'ham': 1} 在这里,字典被赋值给一个变最 D ,键' spam' 的值为整数 2 等。像利用偏移索引列表一样, 我们使用相同的方括号语法,用键对字典进行索引操作,只不过这里意味着用键来读取, 而不是用位置来读取。

258

1

第8章

注意这个例子的结尾和集合很像,字典 内 部键由左至右的次序几乎总是和原先输 人 的顺序 不同。这样设计的目的是为了快速执行键查找(也就是散列查找),键会在内存中重新排序。

这就是为什么假设固定从左至右的顺序操作(如分片和拼接)不适用千字典,你只能用键 进行取值,而不是用位置来取值。从技术上讲,该顺序是伪随机的一它不是真正意义上 的随机(如果时间充足,你能通过 Python 的源代码破解该顺序),不过它是任意的,而且 会随着版本与平台而不同,甚至在 Python 3.3 中随着不同的交互式会话而不同。

内咒 le n 函数也可用千字典,它能够返回存储在字典里的元素的数目,或者说是其 keys 列 表的长度,这两者是等价的。字典的 in 成员关系运算符提供了键存在与否的测试方法,

keys 方法则能够返回字典中所有的键。后者对千按顺序处理字典是非常有用的,但是你不 应该依赖 keys 列表的次序。然而 , 因为 keys 的结果可以作为 一 个普通列表来使用,如果 次序要紧,你随时都可以对这个列表进行排序(之后会介绍更多关千排序和字典的内容)

>» len(D) 3 » >' ham' in D True »> list(D.keys () ) ['eggs','spam','ham')

# Number of entries in dictionary #

Key membership test alternative

#

Create a new list of D's keys

注意这段代码中的第二个表达式。之前我们提到过,用千字符串和列表的 i n 成员关系测试

同样适用千字典一一它能够检查某个键是否存储在字典内。从技术上来讲,这样做能行得 通是因为字典定义了自动遍历键值列表的迭代器。其他类型也提供了反映它们共同用法的

迭代器,例如,文件有逐行读取的迭代器。我们将会在第 14 章和第 20 章更加正式地讨论 迭代器。

还要注意上面列出的最后 一 个示例的语法。由千类似的原因,在 P yt h on 3.X 中,我们必须 将其放到 一个 l i st 调用中一Pyth on 3.X 中的 key s 返回 一个可迭代对象,而不是一 个物 理的列表。 list 调用强制它一次生成所有的值,以便我们可以交互式地将其打印出来,尽 管这 一 调用在一些其他的上下文中井不需要。在 Python 2 . X 中, keys 构建井返回一个真正

的列表,因此, list 调用不需要显示结果。更多细节将在本章稍后介绍。

原 位 置修改字典 让我们继续介绍交互式命令行会话。与列表相同,字典也是可变的,因此你可以在原位置

对它们进行修改、增大以及缩短,而不需要生成新字典。只需给 一 个键赋值就可以改变或 者生成元素。 del 语句在这里也适用;它用千删除作为索引的键相关联的元素。此外,注意 这个例子中字典所嵌套的列表(键 ' h a m' 的值)。 Pyt hon 中,所有集合数据类型都可以彼

此任意嵌套:

»> D {'eggs': 3, ' spam': 2, ' ham ' : 1}

列表与字典

I

2s9

» > D['ham'] = ['grill','bake','fry'] »> D

# Change entry (value=list)

{'eggs': 3,'spam': 2,'ham': ['grill','bake','fry']}

>» del D['eggs'] »> D

# Delete entry

{'spam': 2, ' ham': ['grill','bake','fry']}

» > D['brunch'] = ' B a c o n ' # Add new entry >» D {'brunch':'Bacon','spam': 2,'ham': ['grill ' ,'bake ' ,'fry')} 与列表相同,向字典中已存在的键索引赋值会改变与索引相关联的值。然而,与列表不同 的是,每当对新字典键进行赋值(之前没有被赋值的键),就会在字典内生成一个新的元素, 就像前一个例子里对 'brunch' 所做的那样。在列表中情况不同,因为仅仅可以赋值给已存 在的列表偏移量, Python 会将超出列表末尾的偏移视为越界并报错。要想扩充列表,你需

要使用 append 方法或分片赋值来实现。

其他字典方法 字典方法提供了多种类型特定的工具。例如,字典 values 和 items 方法分别返回字典中所 有的值列表和 (key,value) 对元组。和 keys 一样,这些方法在循环中很有用,而且需要逐 个遍历字典项(我们从下一小节开始将编写这样的循环的例子)。与 keys 一样,这两个方

法在 3.X 中同样返回可迭代对象,所以你要把它们包在一 个 list 调用里面,以便一次收集 它们所有的值用千显示:

»> D ={'spam': 2,'ham': 1,'eggs': 3} »> list(D.values()) [3, 2, 1)

»> list(D.items()) [('eggs', 3), ('spam', 2), ('ham', 1)) 在那些运行时收集数据的实际程序中,你通常不能在执行前预计将出现在一个字典中的 数据。读取不存在的键往往都会出错,然而键不存在时通过 get 方法能够返回默认值一

None 或者用户定义的默认值。这是在 当键不存在时填入默认值的简单方法,井在你的程序 无法提前预测内容时避免了 missing-key 的错误:

>» D.get('spam')

#Akey /hat is there

2

»> print(D.get('toast'))

# A key that is missing

None

>» D.get('toast', 88) 88 字典的 update 方法有点类似千拼接,但是,它和从左到右的顺序无关(再 一 次强调,字典 中没有这样的事情)。它把一个字典的键和值拼接到另一个字典中,当遇到键冲突时盲目 地覆盖相同键的值:

260

I

第8章

»> D {'eggs': 3,'spam': 2,'ham': 1} >» 02 = {'toast':4,'muffin':s} >» D.update(D2)

# Lots of delicious scrambled order here

>» D {'eggs': 3,'muffin': S,'toast': 4,'spam': 2,'ham': 1} 注意最后的结果中键的顺序是如何被混合的 1 再次,这就是字典的工作方式。最后,字典 pop 方法能够从字典中删除一个键并返回它的值。这类似千列表的 pop 方法,只不过删除的 是一个键而不是一个可选的位置:

# pop a dictionary by key

»> D {'eggs': 3,'muffin': s,'toast': 4,'spam': 2,'ham': 1} »> D.pop('muffin') 5 , >>> D. pop('toast') # Delete and return from a key 4

»>

D

{'eggs': 3,'spam': 2,'ham': 1} # pop a list by position >>> L = ['aa','bb','cc','dd']

» > L.pop()

# Delete and return from the end

'dd'

»> L ['aa','bb','cc']

»> L.pop(1)

#

Delete from a specific position

'bb'

»> L ['aa','cc'] 字典也能够提供 copy 方法。我们会在第 9 章进行讨论,因为它是避免相同字典共享引用潜 在的副作用的一种方式。实际上,字典还有很多其他方法并没有在表 8 -2 中列出,可以参

考 Python 库手册、 dir 和 help, 或者其他参考来源查看完整清单。

注意:你的字典顺序可能不同:当你的字典打印顺序和这里的不一样时不必惊讶。如前所述, 键的顺序是任意的,而且可能随着 Python 的版本、使用平台以及 Python 3.3 中不同的交 互式会话而不同(而且很有可能随着今天是星期几还有今天的月亮的形状而改变!)。

本书中大部分的字典例子反映了 Python 3.3 的键顺序,但这从 3.0 版本前后就已经开始 发生改变。你的 Python 键顺序可能不同,但你不必在意这个细节:字典通过键来处理, 而不是位置。程序不应该依赖字典中键的任意顺序,即便这些顺序在书中已给出。 在 Python 标准库中有可以维护键插人顺序的扩展类型(参见 collections 模块中的

OrderedDict) 不过它们是通过使用额外空间并降低速度来实现其功能的混合产物,而 且不是真正意义上的字典。简而言之,在 OrderedDict 中,键被保存在一个冗余的链表 中用来支持序列操作。

列表与字典

I

261

正如我们将在第 9 章中看到的,这一 模块也实现了 一 个 name dt up le 工具,从而使元组

中的元素能够同时被属性名和位置顺序访问~它 不是 Python 的核心类型,但是能增加额外的处理步骤。 Python 的库手册有这些以及其 他工具的详细说明 。

示例:电影数据库 我们来看 一个更实际的字典的例子。为了纪念 Python 名称的由来,下面的例子创建了 一 个 简单的内存中的 Monty Python 电影数据库,这个数据库是一 张从电影发行年份(键)映射

到电影名称(值)的表。正如下面所编写的,你可以通过发行年限字符串获取电影名称:

»> table= {'1975':'Holy Grail',

# Key: Value

'1979':'Life of Brian', '1983':'The Meaning of Life'}

>» »> year ='1983' »> movie = table[year] »> movie

If dictionary/Key}=>

Value

'The Meaning of Life'

>» for year in table:

# Same as: for year in rable.keys()

print(year +'\t'+ table[year]) 1979 1975 1983

Life of Brian Holy Grail The Meaning of Life

最后的命令使用了 一条 for 循环,我们在第 4 章中进行了预习,但还没有讨论过它的细节。 如果你对 for 循环不熟悉,这条命令只不过是通过迭代表中的每 一 个键,打印出用制表符

单位长度空格译注 1 分开的键及其值的表单而已。在第 l3 章我们将对 for 循环做更多的介绍。 字典与列表和字符串这样的序列不 一 样,不过你却可以很容易地遍历字典的各项:调用字

典的 keys 方法会返回包含所有键的列表,然后就可以用 for 循环进行迭代。如果有需要, 你可以像上面的代码中所做的那样在 for 循环中进行从键到值的索引。

实际上, Python 也能够让你遍历字典的键列表,而不必在多数 for 循环中调用 keys 方法。 就任何字典 D 而言,写成 “for key in D” 和写成完整的 “key in D.keys( )”效果是 一 样的。 这其实只是先前所提到的迭代器能够允许 in 成员关系运算符用千字典的另 一个实例 。 有关 迭代器更多内容将在本书后面进行介绍。

译注 I :

原文为 “tab-separated " 。 在 Python 编码中键盘上的 tab 制表符会祁据设置换算成一定数 目的空格(空格的数目 一般是代码编辑器中可调的) 。 这里的意思是打印出来的年份和名 称采用一个 tab 制表符单位对应的空格分开 。

262

I

第8章

预习:将值映射为键

.

注意前面的表将年份映射到电影名,而不是反过来。如果你想要得到从电影名映射到年份 的效果,你既可以换一 种方式编写字典,也可以使用像 items 这样能够返回可搜索序列的

方法,为了最大限度地使用它们,需要用到我们现在还没学到的知识:

>»table= {'Holy G r a i l ' : ' 1 9 7 5 ' , 'Life of Brian':'1979', 'The Meaning of Life':'1983'}

# Key=>Value (title=>year)

>>>

>» table['Holy Grail'] '1975' >» list(table.items()) # Value=>Key (year=>title) [ ('The Meaning of Life','1983'), ('Holy Grail','1975'), ('Life of Brian','1979')] » > [title for (title, year) in table.items() if year =='1975'] ['Holy Grail'] 这里的最后 一 行命令和第 4 章中的推导语法相关内容 一 样是 一 种预习,关千推导语法的

详细介绍将在第 14 章中展开。简而言之,推导语法将扫描 items 方法返回的字典 (key, value) (键/值)元组对,并选择一个拥有特定值的键。最终的效果就是反向索引(从值到键, 而非从键到值),如果你只希望存储 一 次数据或者只是偶尔进行反向索引,这是 一个不错 的选择(像这样的序列搜索通常会比一个直接的键索引慢很多)。

事实上,尽管字典的本质是单方向地将键映射到值,你也有很多方式能把值映射回键,这 只需要一 点额外的通用化编码:

»> K ='Holy Grail' >» table[K] '1975'

# Key=> Value (normal usage)

>» V ='1975' >» [key for (key, value) in table.items() if value == V] ['Holy Grail'] >» [key for key in table.keys() if table[key] == V] [ ' Holy Grail']

# Value=>Key # Ditto

注意,最后两行命令都返回了 一 个电影名的列表:在字典中,每个键只有 一 个值,但是 一

个值可以有多个键与之对应。一个给定的值可以存储在多个键中(键值间遵循多对一的关 系),井且 一 个值本身可以是一 个集合(通过这点支持单键多值)。关千这一部分的更多 内容,也请参考第 32 章中 mapattrs.py 的字典反向函数示例

那段代码将扩展这里的预习,

同时延续这里的讲述。就本章的目的而 言 ,让我们继续探索更多字典的基础知识。

字典用法注意事项 一 且你熟练掌握了 字 典,它将成为相当直接的工具,但是在使用字典时,有几点需要注意:

列表与字典

I

263



序列运算无效。字典是映射机制,不是序列 。因为字典元素间没有顺序的概念,类似

拼接(有序合并)和分片(提取相邻片段)这样的运算是不能用的。实际上,如果你

试着这样做, Python 会在你的程序运行时报错。



对新索引赋值会添加项。当你编写字典字面最时(此时的键是嵌套干字面量本身的), 或者单独向现有字典对象的新键赋值时,都会生成键。最终的结果是一样的。



键不一定总是字符串。我们的例子中都使用字符串作为键,但任何不可变对象都是可 以的。例如,你可以用整数作为键,这样让字典看起来很像列表(至少进行索引时很像)。 元组也可以用作字典键,从而实现复合的键(如日期或 IP 地址的映射)与值的关联。

用户定义类的实例对象(在本书第六部分中讨论)也可以用作键,只要存在一个合理 的协议方法,大体上来讲,它需要告诉 Python 其值是可散列的同时是不变的,否则作

为固定键将会毫无用处。像列表、集合以及其他的字典,这样的可变对象不能当作键, 但却可以作为值。

用字典模拟灵活的列表:整数键 前面的表中的最后一点非常重要,因此我们这里举些例子来说明一下。当使用列表的时候, 对在列表末尾范围外的偏移赋值是非法的:

»>L=[] »> L[99] ='spam' Traceback (most recent call last): File "", line 1, in ? IndexError: list assignment index out of range 虽然你可以使用重复来预先分配一个足够大的列表(如

[O] *100) ,但你也可以用字典来

做类似的事情,这样就不需要这样的空间分配了。使用整数键时,字典可以模拟实现列表 在偏移赋值时增长:

»> D = {} »> D[99] ='spam' »> D[99] , spam ' »> D {99:'spam'} 在这里, D 看起来就像是一个有 100 项的列表,但其实是一 个有单个元素的字典;键 99 的 值是字符串 'spam' 。你可以像列表那样用偏移访问这一结构,如果需要的话,可以使用 get 或 in 测试方法来获取不存在的键,但你不需要为将来可能会用到的会被赋值的所有位 置都分配空间。在这种用法中,字典表现为一个更具灵活性的列表。 作为另 一 个例子,我们也可以在前面的第一个电影数据库代码中使用整数键来避免引述年

份,虽然这种方式的表现力并不那么好(键中不能包含非数字的字符)

264

I

第8章

»> table = {1975:'Holy Grail', 1979:'Life of Brian', # Keys are integers, not strings 1983:'The Meaning of Life'} »> table[1975] 'Holy Grail' »> list(table.items()) [(1979,'Life of Brian'), (1983,'The Meaning of Life'), (1975,'Holy Grail')]

对稀疏数据结构使用字典:用元组作键 类似地,字典键也常用千实现稀疏数据结构,例如,对千那些只有少数位置上有存储值的 多维数组:

>» Matrix = {} »> Matrix[ (2, 3, 4)] = 88 » > Matrix [ (7, 8, 9)] = 99 »> »> X = 2; Y = 3; Z = 4 » > Matrix[ {X, Y, Z)] 88 »> Matrix { (2, 3, 4) : 88, (7, 8, 9) : 99}

#; separates statements: see Chapter 10

在这里,我们用字典表示一个三维数组,这个数组中只有两个位置 (2,3,4) 和 (7,8,9) 有值,

其他位置都为空。键是元组,它们记录非空存储点的坐标。我们并不是分配一个庞大而几 乎为空的 三 维矩阵,而是用一个简单的两个元素的字典。通过这一方式读取空存储点时, 会触发键不存在的异常,因为这些元素实质上并没有存储:

»> Matrix[(2,3,6)] Traceback (most recent call last): File "", line 1, in ? KeyError: (2, 3, 6)

避免键不存在错误 读取不存在的键的错误在稀疏矩阵中很常见,然而我们可能并不希望程序因为这一 错误被

关闭。在这里至少有 三种方式可以让我们填入默认值而不会出现这样的错误提示:你可以

在 if 语句中预先对键进行测试,也可以使用 try 语句明确地捕获并修复这一异常,还可以 用我们前面介绍的 get 方法为不存在的键提供一个默认值。我们将在第 IO 章中学习前面提 到的两种方式的相关语法:

» > if (2, 3, 6) in Matrix: print(Matrix[(2,3,6)]) ••• else: print(o)

# #

Check for key before fetch See Chapter JO and Chapter 12 for if/else



»> try: print(Matrix[(2,3,6)]) ... except KeyError:

# Try 10 index #

Catch and recover

列表与字典

I

26s

print(o)

#

See Chapter 10 and Chapter 34 for try/except



>» Matrix.get((2,3,4), o)

# Exists: fetch and return

88

»> Matrix.get((2,3,6), o)

#

Doesn'I exist: use default arg



就编码角度而言 , get 方法是这三者中最简洁的,不过 if 和 try 语句往往更普遍,我们会 在第 10 章中进一 步讨论。

字典的嵌套 正如你能看到的,字典在 Python 中能够扮演多种角色。 一般来说,字典可以取代搜索数据

结构(因为用键进行索引是一 种搜索操作),并且可以表示多种结构化信息。例如,字典 是在程序范围中给出 一 个元素多项描述的方式。也就是说,它们能够扮演与其他语 言 中“记 录”和“结构体”相同的角色。 这是 一 个随时间通过向新键赋值,来填写 一 个描述虚构人的字典的例 子 (如果你名叫

Bob, 我可得向你道个歉一—谁让这个名字那么好写呢?)

»> >» »> »> »> » > Bob

rec = {} rec['name') ='Bob' rec['age') = 40.5»> rec['job'] ='developer/manager , print (rec ['name'])

特别是在嵌套的时候, Python 的内置数据类型可以很轻松地表达结构化信息 。 下面的代码 再 一次使用字典来捕获对象的属性,但它是一 次性写好(而不是分开对每一 个键进行赋值) , 同时嵌套了 一 个列表和 一 个字典来表达结构化属性的值:

» > rec = {'name':'Bob', 'jobs': ['developer','manager'], 飞eb':'www.bobs.org/?Bob',

'home': {'state':'Overworked','zip': 12345}} 当访问嵌套对象的元素时,只要简单地把连续的索引操作串起来就可以了:

>» rec['name'] 'Bob' »> rec['jobs'] ['developer','manager'] »> rec['jobs')[1] 'manager' »> rec['home')['zip'] 12345

266

I

第8章

尽管我们将会在本书第六部分学习类(类是 一 种能够将数据和逻辑组织到一起的工具), 它能更好地实现这里的记录功能,但字典是满足简单需求的一种易用的工具。关千更多记

录表示的选择,也请参考后面的边栏“请留意 : 字典 vs 列表”,以及字典在第 9 章中对元 组的扩展和第 27 章中对类的扩展。 同样要注意,尽管我们现在只是关注于单一的带有嵌套数据的"记录”,我们也未尝不可

将这条记录本身嵌套在一个更大的、外围的数据库集合(可以用列表或字典实现),不过 在实际程序中,外部文件以及更加正式的数据库接口通常扮演若这种顶层容器的角色:

db=[) db.append(rec) db.append(other) db[o]['jobs'] db = {} db['bob'] = rec db['sue'] = other db ['bob'] ['jobs']

# A list "database"

# A dictionary "database"

在本书的后面,我们将看到 Python 中类似 shelve 这样的工具,它有着类似的工作方式,

但会自动地在对象和外部文件之间进行映射,从而使对象持久化(更多内容参考本章边栏“请 留意 : 字典接口”)。

创建字典的其他方式 最终,注意因为字典非常有用, Python 中不断涌现出许多创建字典的方法。例如,在 Python 2 .3 和后续的版本中,下面代码中的最后两次对 diet 构造函数(实际是类型名称)的调用与之 前字面量和键赋值的形式有着相同的效果:

{'name':'Bob','age': 40}

# Traditional literal expression

D = {} D['name'] ='Bob' D['age'] = 40

II Assign by keys dynamically

dict(name='Bob', age=40)

# diet keyword argument form

diet([ ('name','Bob'), ('age', 40)])

# diet keyfioalue tuples form

这四种形式都会建立相同的两键字典,但它们有着不同的适用条件:



如果你可以事先拼出整个字典,那么第一种是很方便的。



如果你需要一 次动态地建立字典的一个字段,第 二种比较合适 。



第 三种关键字形式所需的代码比字面量少,但是键必须都是字符串才行 。



如果你需要在程序运行时通过序列构建字典,那么最后 一种形式比较有用。

列表与字典

I

267

在前面排序的时候,我们见过了关键字参数,这段代码中展示的第 三 种形式在如今的

Python 代码中尤其流行,因为它语法简单(因此,也不太容易出错)。如前面的表 8-2 所示, 最后一种形式通常也会与 zip 函数一起使用,把程序运行时动态获取的单独键列表和单独 值列表 一一对应地拼接在一起(如分析数据文件的列)。

dict(zip(keyslist, valueslist))

# Zipped key/value tuples form (ahead)

下 一 小节将更详细地介绍拼接字典键。如果所有键的值 一 开始都相同,你也可以用这个特 殊的形式对字典进行初始化一直接传人 一 个键列表,以及所有这些键初始的值(默认值 为 None) :

» > diet. fromkeys (['a','b'], o) {'a': o,'b': o} 虽然在你目前的 Python 生涯中可以只使用字面最以及键赋值,但是当开始将字典用在实际 的、灵活的以及动态的 Python 程序中时,你可能会发现所有这些字典创建形式的用处 。 本节中的清单列举了在 Python 2.X 和 Python 3.X 中创建字典的各种方式。然而,还有创建

字典的另一种方式,仅在 Python 3.X 和 2.7 中可用:字典推导表达式 。 要了解如何使用这 最后一种形式,我们需要继续学习下面也是本章最后的 一 小节。

请留意:字典 vs 列表 在介绍过所有 Python 中的核心类型工具集后,一些读者可能会对列表与字典之间的 选择感到困惑 。

简而言之,尽管两者都是能灵活管理其他对象的集合工具,但是列表

将元素赋值给位置,而字典却将元素赋值给史加便于记忆的键 。

因此,字典数据通常

为人类读者提供史多的信息 。 例如,表 8-1 中笫 3 行嵌套的列表数据也可以用于表示 一个记录 :

>» L = ['Bob', 40. 5, ['dev','mgr']] >» L[o]

# List-based "record"

'Bob'

»> L[1]

# Positions/numbers for f ields

40.5

»> L[2] [1] mgr 对于一些数据类型而言 , 列表的按位置访问是有意义的,例如一个公司中员工的列表、

一个目录中的文件,或者数值矩阵 。 但像这样一个史符号化的记录也许能更有意义地 编写成表 8-2 笫二行中的一个字典 , 使用带标签的字段来替代字段位置 ( 这和笫 4 章

中我们编写的记录很相似 ) :

»> D = {'name':'Bob','age': 40.s,'jobs': ['dev','mgr']} »> D['name'] 'Bob'

268

I

第 8章

»> D['age']

# Dictionary-based "record"

40.s

»> D['jobs'][1] 'mgr'

#

Names mean more than numbers

出于多样性,这里将相同的数据采用关键宇形式进行编写 , 对人来说史具可读性:

>» D = dict(name='Bob', age=40.5, jobs=['dev','mgr']) >» D['name'] 'Bob' »> D['jobs'].remove('mgr') »> D {'jobs': ['dev'],'age': 40. 5,'name':'Bob'} 在实际中,字典适用于存储带有标签的数据,以及需要通过名称直接快速查询的结构, 而非缓慢的线性搜索 。 正如我们已经看到的,它们也能很好地实现稀抗集合和在任意

位置增长的集合。 Python 程序员也可以使用我们在笫 5 章中介绍过的集合,它史像是没有键的字典 ;他 们不会把键映射到值,但通常可以像字典一样快速查看一个元素是不是在其中,尤其 是在搜索程序中:

»> D = {} >» D['statel'] = True >»'statel'in D True »> S = set() »> S.add('statel') >»'statel'in S True

# A visited-state dictionary

HSame, but with sets

诗看下一章中对这一记录表示的一种重新演绎,在那里我们会看到元组与有名元组同 字典在这一角色上的比较,以及在笫 27 章中,我们将学习用户定义类是如何步入这

一领域并带上相应的处理数据和逻辑的 。

Python 3.X 和 2.7 中的字典变化 本章到目前为止还是关注于在不同版本中字典的基本知识,但是,字典的功能在 Python 3.X 中已经有所变化。如果你使用 Python 2.X 代码,可能会遇到不同行为的字典工具,或者干 脆在 Python 3.X 中取消的字典工具。此外, Python 3.X 代码也可能使用 Python 2.X 中没有 的工具,不过 2.7 版本可能例外(因为 2.7 中含有两处来自 3.X 的向下移植)。

具体来说, Python 3.X 中的字典:



支持 一种新的字典推导表达式,这是列表和集合推导的“近亲”



对千 D.key 、 D.values 和 D.items 方法,返回类似集合的可迭代的视图,而不是列表。

列表与字典

I

269



由于前面一 点,需要新的编码方式来实现通过排序键的遍历。



不再直接支持相对大小比较,取而代之的是手动比较。



不再有 D.has_key 方法,相反,使用 in 成员关系测试。

作为之后从 3.X 版本中的向下移植, Python 2. 7 中的字典(但不包括之前的 2.X)



支持前面的第一 条(字典推导)作为从 3.X 中的向下移植。



支持前面的第 二 条(集合式的可迭代视图),但需要通过特殊的 D.viewkeys 、

D.viewvalues 、 D.viewitems 方法名来实现它们的非视图方法将返回跟前面一 样的列表 。 因为这种重叠,本章中的部分内容适用千 3.X 和 2.7, 不过却在 3.X 的上下文中进行展示。 记住了这点后,让我们再来看看在 Python 3.X 和 2.7 中的字典有什么新特性 。

3.X 和 2.7 中的字典推导 正如上一小节末尾提到的, Python 3.X 和 2.7 中的字典也可以用字典推导来创建。就像我们 在第 5 章中遇到的集合推导 一样,字典推导也只在 Python 3 . X 和 2.7 中可用(在 Python 2.6

和更早的版本中不可用)。就像较早的时候我们在第 4 章中以及在本章开始简单介绍过的 列表推导一样,字典推导也隐式地运行 一个循环,根据每次迭代收集表达式的键 / 值结果,

并且使用它们来填充一个新的字典。 一 个循环变最允许推导在过程中使用循环迭代值。 例如,在 Python 2.X 和 Python 3.X 中,动态初始化 一个字典的标准方式都是:使用 zip 调

用将其键和值对应起来再把结果传递给 diet 调用。 zip 内置函数允许我们通过这种从键和 值列表中构造字典一如果你不能在代码中预计键和值的集合,你总是可以将它们构建为

列表,然后再对应起来。我们将在第 13 章和第 14 章中深入学习 zip, 它是 3.X 中的 一 个 可迭代对象,因此我们必须在那里把它的结果包在 一 个 list 调用中,不过它的基础用法却

非常直接:

»> list(zip(['a','b','c'], [1, 2, 3]))

#Zip10gerherkeysandva/11es

[('a', 1), ('b', 2), ('c', 3))

>» D = dict(zip(['a','b','c'], [1, 2, 3])) >» D

#Makeadictfrom zipresult

{'b': 2,'c': 3,'a': 1} 而在 Python 3 . X 和 2.7 中,你可以用 一 个 字 典推导表达式来实现同样的效果。如下代码使

用 zip 对应结果(它的样子和在 Python 代码中儿乎相同,只不过更正式一 些)中的每 一个 键/值对构建了 一个新的字典:

>» D = {k: v for (k, v) in zip(['a','b','c'], [1, 2, 3])} »> D {'b': 2,'c': 3,'a': 1}

270

I

第8章

这个例子中,推导实际上需要更多的代码,但是,它们也比这个例子所展示的要更为通用一一 我们可以使用它们把单独的一串值映射到字典,并且键和值一样,也可以用表达式来计算:

» > D = {x: x >» D

**

2 for x in [ 1, 2, 3, 4]}

#

Or: range(}, 5)

{1: 1, 2: 4, 3: 9, 4: 16}

>» D = { c: c >» D

*4

for c

in'SP矶'}

# Loop over any irerable

,'M':'MMMM'} {'5':'5555','P':'PPPP','A':'AAAA','M': »> D = {c.lower(): c +'!'for c in ['SPAM','EGGS','H矶']} >» D {'eggs':'EGG5!','spam':'5PAM!','ham':'HAM!'} 字典推导对干从键列表来初始化字典也很有用,这和我们在前一小节末尾遇到的 fromkeys 方法很相似:

>» D = diet. fromkeys (['a','b','c'], o) »> D

# lnitialize diet from keys

{'b': o,'c': o,'a': o}

»> D = {k:o fork in ['a','b','c']} »> D

# Same, but with a comprehension

{'b': o,'c': o,'a': o}

» > D = diet. fromkeys('spam') »> D

# Other iterables, default

val邸

{'s': None,'p': None,'a': None,'m': None}

»> D = {k: None fork in'spam'} »> D {'s': None,'p': None,'a': None,'m': None} 和相关的工具 一 样,字典推导支持这里没有介绍的其他语法,包括嵌套循环和 if 分句。遗 憾的是,要真正理解字典推导,我们还需要了解 Python 中有关迭代语句和概念的更多知识,

并且,我们目前还没有足够的知识储备来介绍这些内容。我们将在第 14 章和第 20 章学习

有关各种推导(列表推导、集合推导、字典推导和生成器)的更多知识,因此,稍后再介 绍更多细节。我们还将在第 13 章中介绍 for 循环的时候,回顾本节中用到的 zip 内置函数。

3.X 中的字典视图(在 2.7 中要通过新的方法) 在 Python 3 . X 中,字典的 keys 、 values 和 items 都返回视图对象,而在 Python 2.X 中, 它们返回实际的结果列表。这一 功能在 2.7 中也是可用的,不过需要通过这 一 小节 一 开始

列出的特殊方法名 (2.7 的普通方法仍将返回简单列表,从而能避免破坏现有的 2.X 代码), 因此,我将在本节中将其称为一个 3.X 功能。 视图对象是可迭代对象,也就是每次只产生 一 个结果项的对象,而不是在内存中立即产生

结果列表。除了是可迭代的,字典视图还保持了字典成分的最初的顺序,反映字典未来的 修改,并且能够支持集合操作。另一方面,因为它们不是列表,所以不能直接支持像索引

列表与字典

1

271

和列表 sort 方法这样的操作,打印的时候它们也不能像一个普通的列表那样显示自己的项

(它们确实在 Python 3.1 中能打印它们的组件,却不是像一个列表,而且在这一点上与 2.X 不同)。 我们将在第 14 章更正式地讨论可迭代对象,但是,现在只要知道这一点就够了:如果想要

应用列表操作或者显示它们的值,我们必须通过内置函数 list 来运行这 3 个方法的结果。 例如,在 Python 3.3 中(其他版本的输出可能略有不同)

>» D = dict(a=1, b=2, c=3) »> D {'b': 2,'c': 3,'a': 1}

»> K = D.keys() »> K

# Makes a view object in 3.X, not a list

diet_ keys (['b','c','a'])

>» list(K)

#

Force a real list in 3.X if needed

('b','c','a']

»> V = D.values() »> V

# Diuo for values and items views

dict_values([2, 3, 1))

»> list(V) [2, 3, 1]

»> D.items() diet_items ([ ('b', 2), ('c', 3), ('a', 1)])

»> list(D.items()) [('b', 2), ('c', 3), ('a', 1)]

>» K[O] # List operations fail unless converted TypeError:'dict_keys'object does not support indexing »> list(K)[o] 'b' 除非在交互式命令行显示结果,我们可能很少会注意到这 一 改变,因为 Python 中的循环结 构会自动强制可迭代的对象在每次迭代上产生 一 个结果:

»> for k in D.keys(): print(k)

#

Iterators used automatically in loops

b C

a 此外, Python 3.X 的字典自己仍然拥有迭代器,它返回连续键一—就像在 Python 2.X 中一样, 它往往仍然没有必要直接调用 Keys:

»> for key in D: print(key) b C

a

272

1

第8章

# Still no need to call keys() to iterate

然而,和 Python 2.X 的列表结果不同, Python 3.X 中的字典视图并非创建后不能改变一一

它们可以动态地反映在视图对象创建之后对字典所做的修改:

>» D = {'a': 1,'b': 2,'c': 3} >» D {'b': 2,'c': 3,'a': 1} »> K = D.keys() »> V = D.values() >» list(K) ['b','c','a'] »> list{V) [2, 3, 1)

# Views maintain same order as dictionary

»>delD['b'] »> D {'c': 3,'a': 1}

# Change the dictionary in place

»> list{K) ['C','a'] »> list{V) [3, 1)

# Reflected in any currelll view objects # Not true in 2.X! - lists detached from diet

字典视图和集合 与 Python 2.X 中的列表结果不同, keys 方法所返回的 Python 3.X 的视图对象类似千集合, 并且支持交集和井集等常见的集合操作 1 values 视图则不像集合,因为它们不是唯一的;

但 items 结果是的,如果 (key,

value) 对是唯一的,并且是可散列的(具有不变性的)。

由千集合的行为很像是无值的字典(甚至在 Python 3.X 和 2.7 中与字典一样编写在花括号

中),这是一 种符合逻辑的对称。正如第 5 章中说的,集合的项是无序的、唯 一 的、不可变的, 就跟字典中的键一样。 如下是 keys 视图用千集合操作中的样子(接着前一 小节中的交互式命令行会话) 1 字典的 值视图从来都不像集合,因为它们的元素不 一 定是唯一 或不可变的:

>» K, V (dict_keys(['c','a']), dict_values([3, 1))) >» K I {'x': 4} {'c', ' x','a'}

# Keys (and some items) views are set-like

»> V & {'x': 4} TypeError: unsupported operand type(s) for&:'diet values'and'diet' »> V & {'x': 4}.values() TypeError: unsupported operand type(s) for&:'dict_values'and'diet_values' 在集合操作中,视图可能与其他的视图、集合和字典混合。在这 一 上下文中,字典与它们

的 keys 视图被一 样地对待:

»> D = {'a': 1,'b': 2,'c': 3} >» D.keys() & D.keys()

# Intersect keys views

列表与字典

I

273

{'b','c','a'} »> D.keys() & {'b'} {'b'} »> D.keys() & {'b': 1} {'b'} >» D.keys() I {'b','c','d'} {'b','c','a','d'}

# Intersect keys and set # lnterJ·ect keys and diet # Union keys and set

如果字典项视图是可散列的话(也就是说,如果它们只包含不可变的对象的话),它们是 类似千集合的:

»> D = {'a': 1} »> list(D.items()) [('a', 1)] >» D.items() I D.keys() {('a', 1),'a'} >» D.items() I D {('a', 1),'a'}

# Items set-like ifhashable # Uni nion view and view # diict treated same as its keys

»> D.items() I {('c', 3), ('d', 4)} {('d', 4), ('a', 1), ('c', 3)}

#Setofkey!va/uepairs

»> dict(D.items() I {('c', 3), ('d', 4)})

# diet accepts iterab/e sets too

{'c': 3,'a': 1,'d': 4} 如果你需要复习 一 下这些集合相关的操作,请参阅第 5 章。现在,让我们看看 Python 3 .X

中字典的另外 3 个快速编码注意事项。

3.X 中字典键排序 首先,由千 keys 在 3.X 中不会返回 一个列表,在 Python 2.X 中传统的通过排序键来浏览 一 个字典的编码模式,在 Python 3.X 中并不适用:

>» D = {'a':1,'b':2,'c':3} »> D {'b': 2,'c': 3,'a': 1}

»> Ks = D.keys() »> Ks.sort()

# Sorting a view object doesn't work!

AttributeError:'dict_keys'object has no attribute'sort' 为了解决这个问题,你在 3.X 中必须要么手动地使用 sorted 调用转换为 一 个列表,要么在 一个键视图或字典自身上使用第 4 章和本章前面介绍的 sorted 调用:

»> Ks = list(Ks) »> Ks.sort() >» for k in Ks: print(k, D[k]) a 1

b

2

C

3

»> D 274

I

第8章

# Force it to be a list and then sorr # 2.X: omit outer parens in prints

{'b': 2,'c': 3,'a': 1} »> Ks = D.keys() »>fork in sorted(Ks): print(k, D[k])

# Or you can use sorted() on the keys #

sorted() accepts any iterable

# sorted() returns its result

123 abc

在这些方式中,对于 3.X 更推荐使用字典的键迭代器,同样也适用千 2.X:

>» D # Better yet, sort the diet directly # diet iterators relllrn keys

{'b': 2,'c': 3,'a': 1}

»> for k in sorted{D): print{k, D[k]) 123 abc

3.X 中字典大小比较不再有效 其次,尽管在 Python 2.X 中可以直接用<、>等比较字典的相对大小,但在 Python 3 . X 中 这不再有效。然而,可以通过手动地比较排序后的键列表来模拟:

sorted (01. items()) < sorted (02. i terns()) 然而, Python 3.X 中字典相等性测试(如 “D1

II Like 2.X DJ < D2

==

D2") 仍然有效。由千我们将在下一章

末尾详细讨论的时候再回顾这一点,我们将在那里介绍更多细节。

在 3.X 中 has_key 方法已死: in 方法万岁 最后,广为使用的字典 has_key 键存在测试方法在 Python 3.X 中袚取消了。相反,使用 in

成员关系表达式,或者带有默认测试的 一 个 get (两者中, in 通常是首选的)

>» D {'b': 2,'c': 3,'a': 1} # 2.X only: True/False »> D.has_key('c' ) AttributeError:'diet'object has no attribute'has_key'

>»'c'in D True »>'x'in D False »> if'c'in D: print('present', D['c']) ... present 3 >» print(D.get('c')) 3 »> print(D.get('x')) None »> if D.get('c') !:: None: print('present', D['c'])

# Required in 3.X #

Preferred in 2.X today

# Branch on result

# Ferch with default

# Another option

present 3 列表与字典

1

275

作为总结,字典在 3.X 中发生了很多重要的改变。如果你使用 Python 2.X 并且关心 Python 3.X

的兼容性(或者为某天会遇到这个问题而未雨绸缪),这里列出了一些要点。在本节中我 们所遇到的 3.X 变化中:



第一个变化(字典推导)只能在 3.X 和 2.7 中编写。



第二个变化(字典视图)只能在 3.X 中编写,而且在 2.7 中使用的话需要通过特殊的方 法名。

然而后 3 种技术(排序、手动比较和 in) 如今可以在 Python 2.X 中编写,并且未来很容易 迁移到 Python 3 . X 中。

请留意:字典接口 宇典不只是一种在程序中通过键值对存储信息的方便方式一一一一些 Python 的扩展工具 也提供了从外观到实现上都与字典一样的接口。例如, Python 向 DBM (数据库管理 软件)的键访问文件暴露的接口就很像一个需要被打开的字典。你使用键索引存储并

访问宇符串:

# Named anydbm in Python 2.X import dbm file = dbm.open("filename") # Link to file file['key'] = ' d a t a ' # Store data by key # Fetch data by key data = file ['key'] 在笫 28 章中,你将看到你也可以用这种方式存储一整个 Python 对象,你只需把前面 代码中的 dbm 换成 shelve (shelve 是持久化 Python 对象的键访问数据库,而不仅仅是

字符串),对于网络访问, Python 的 CGI 脚本支持也提供了一个字典一样的接口。 一个对 cgi.FieldStorage 的调用能产生一个宇典一样的对象,该对象对每一个客户 端网页的轮入字段都带有一条记录:

import cgi form = cgi.FieldStorage() # Parse form data if'name'in form: showReply('Hello,'+ form['name'].value) 尽管字典是唯一的核心映射类型,所有这些其他的扩展都是映射的实例,同时支持大

多数相同的操作。一旦你学会了字典接口,你将发现它们适用于一系列 Python 的内 置工具。

关于其他宇典的使用案例,也讨参见接下来笫 9 章中对 JSON 的概览——-这是一个语 言无关的数据格式,应用于数据库以及数据传轮。 Python 的字典、列表以及它们相嵌 套的组合基本上能够符合这套格式的规范,而且能够很容易地从 JSON 文本字符串转

换到 Python 中使用 Python 的 json 标准库模块。

276

I

第8章

本章小结 本章中我们探讨了列表和字典类型一一这可能是在 Python 程序中所见到并使用的两种最常 见、最具有灵活性且功能最为强大的集合类型。本章介绍的列表类型支持任意对象以位置 排序的集合体,而且可以任意嵌套,按需要增长和缩短。字典类型也是如此,不过它是以

键来存储元素而不是位置,并且不会保持元素之间任何可靠的由左至右的顺序。列表和字 典都是可变的,所以它们支持各种不适用于字符串的原位置修改操作。例如,列表可以通

过 append 调用来进行增长,而字典则是通过赋值给新键的方法来实现。 下 一 章我们将要介绍元组和文件,同时结束我们的探入核心对象类型之旅。随后我们将会

介绍编写处理对象的逻辑语句,让我们向着编写完整程序的目标再前进 一 步。在那之前, 还是让我们先做 一 些习题巩固一下本章所学的知识。

本章习题 1

举出两种方式来创建内含 5 个整数零的列表。

2.

举出两种方式来创建一个字典,有两个键 'a' 和 'b' ,而每个键相关联的值都是 o 。

3

举出四种在原位置修改列表对象的运算。

4.

举出四种在原位置修改字典对象的运算。

5.

为什么在一些情况下你要使用字典而不是列表?

习题解答 1.

像[ o, o, o, o, 0] 这种字面量表达式以及 [o] * s 这种重复表达式,都能创建 5 个零 的列表。在实际应用中,你可能会通过循环创建这种列表。 一开始是空列表,在每次迭

代中通过 L.append(o) 附加 o 。这里也可以使用列表推导 ([o for i in range(S)]), 不过,这种方法比较费工夫。

2

像 {'a': o,'b': o} 这种字面量表达式,或者像 D = {}, D['a'] = o, D['b'] = 0 这 种一系列的赋值运算,都会创建所需要的字典。你也可以使用较新并且编写起来更简 单的关键字形式 dict(a=O, b=O) ,或者更有灵活的 diet ([ ('a', o), ('b', o) ])键/值

序列形式。因为所有键的值都相同,你也可以使用特殊形式 dict.fromkeys('ab', o) 。 在 Python 3.X 和 2.7 中,还可以使用一个字典推导:

{ k: o for k

in'ab' },虽然在这

里可能有些超纲。

3.

append 和 extend 方法可在原位置增长列表, sort 和 reverse 方法可以对列表进行排序 或者翻转, insert 方法可以在一 个偏移量处插入一个元素, remove 和 pop 方法会按照

列表与字典

1

277

值和位置从列表中删除元素, del 语句会删除一个元素或一段切片,而索引以及切片赋

值语句则会取代一个元素或整个片段。本题可任意挑选其中 4 个。

4.

字典的修改主要是给新的键或已存在的键赋值,从而建立或修改键在表中的项目。此外, del 语句会删除一个键的元素,字典 u pdate 方法会把一个字典合并到另 一个字典的适 当的地方,而 D.pop(key) 则会移除一个键并返回它的值。字典也有其他更古怪的方法 可以在原位置进行修改,但在这一章中没有列出,如 setdefaul t 。查看参考资源来了 解更多的细节。

5.

如果数据带有标签的话,字典通常是更好的选择(例如带有字段名的一条记录),列 表最适合千无标签项目的集合(例如 一个目录中的文件)。字典查找通常也比列表中 的搜索更快,不过这可能随着不同的程序而变化。

278

I

第8章

第 9 章

元组、文件与其他核心类型

这 一 章我们将要探讨元组( 一 种无法修改的其他对象的集合)以及文件( 一种 计算机上外

部文件的接口),这也为我们深入了解 Python 核心对象类型的部分画上 一 个圆满的句号。 我们将看到,元组是一个相对简单的对象,其实它执行的大部分操作在我们介绍字符串和 列表的时候就已经学过了。文件对象是处理计算机上文件的 一种常用且功能完整的工具。 文件在编程中儿乎无处不在,因而本章对文件的基本介绍会辅以章中大型示例进行说明。

本章也将通过介绍核心对象类型的共有特性来总结本部分内容,包括等价性、比较、对 象复制等概念。我们也将简要学习 Python 工具箱中的其他对象类型,包括 None 占位符和 namedtuple 混合类型。正如你会看到的,尽管我们已经涵盖了所有 主要的内置类型,但 Python 中的对象远比我们到目前为止所介绍的要多得多。最后,我们将学习几个有关对象 类型的常见错误,并给出 一 些能巩固所学知识的练习题来结束本章。

注意:本 章的范围一文件 :正如第 7 章对字符串的讨论,本章对文件的讨论将局限在大多数(包 括新手在内的) Python 程序员都需要了解的文件基础知识。同理,在第 4 章中我们预习 过 Unicode 文本文件,完整介绍放到第 37 章,并作为本书可选的高级话题。

就本章而言,我们假设所有用到的文本文件都使用你所在操作系统的默认编码和解码模 式。这意味着在 Windows 上使用 UTF-8, 在其他平台上使用 ASCII 码或其他方式(如

果你尚未认识到它的重要性,你大可不必提前准备)。

我们同时假设文件名在各个平台

中能够被正确地编码,尽管这里将出千可移植性的考虑而使用 ASCil 码文件名。 如果 Unicode 文本和文件对你而言是一个重要的主题,我建议你先阅读第 4 章来快速预 习 一遍,然后在你掌握本章的文件基础内容之后,再继续阅读第 37 章。对千其他的读者,

这里所涉及的文件内容同时适用千普通文本和二进制文件,并且对其他高级文件处理模

式也适用。

279

元组 我们要介绍的最后 一 个 Python 集合体类型是元组 (tuple) 。元组构建了对象的简单组合。 元组与列表非常相似,只不过元组不能在原位置修改(它们是不可变的),并且通常写成 包在圆括号(而不是方括号)中的一系列项。虽然元组不能支持列表的所有操作,但元组 具有列表的大多数属性。以下是 一 些基础内容的快速浏览。元组是: 任意对象的有序集合

与字符串和列表类似,元组是 一 个基千位置的有序对象集合(其内容维持从左到右的 顺序) 。 与列表相同,它们可以嵌进任何类型的对象。 通过偏移最存取

同字符串、列 表一 样,元组中的元素通过偏移量(而不是键)来访问 。 它们支持所有 基千偏移篮的操作,例如索引和分片。 属于“不可变序列”

与字符串和列表一样,元组是序列,它们支持许多同样的操作。然而,与字符串相同, 元组是不可变的,也就是说元组不支持适用千列表的任何原位置修改操作。

固定长度、多样性、任意嵌套

因为元组是不可变的,如果你不创造 一 个新的副本,就不能增长或缩短元组。另 一 方面, 元组可以包含其他的复合对象(如列表、字典和其他元组等),因此支持任意嵌套。 对象引用的数组

与列表相似,元组应当被看作对象引用的数组。元组存储了指向其他对象的存取点(引 用),而且对元组进行索引操作的速度相对较快。 表 9-1 中列出了常见的元组操作。元组编写为一系列对象(实际上是生成对象的表达式),

用逗号隔开,并且通常用圆括号括起来。一个空元组就是一对内部为空的圆括号。 表 9-1: 常见元组字面量和运算

运算 一

解释

()

空元组

T

=

单个元素的元组(非表达式)

(o,)

T = (o,'Ni', 1.2, 3)

四个元素的元组

T = o,'Ni', 1.2, 3

另—种四个元素的元组(与上一行相同)

T = ('Bob', ('dev','mgr'))

嵌套元组

T = tuple('spam')

一个可迭代对象的元素组成的元组

T[i]

索引

T[i][j]

索引的索引

T[i:j]

分片

280

I

第9章

表 9-1 :常见元组字面量和运算(续) 运算

解释

len(T)

长度

T1 + T2

拼接

T

*

重复

3

迭代

for x in T: print(x) spam [x

**

成员关系测试

in T 2 for x in T]

T.index('Ni')

Python 2.6/2.7/3.X 中的 search 方法

T. count ('Ni')

Python 2.6/2.7/3.x 中的 count 方法

namedtuple('Emp', ['name','jobs'])

有名元组扩展类型

元组的实际应用 照例,让我们开始用交互式会话的方式来探索实际应用中的元组。在表 9-1 中,元组并不

拥有列表的所有方法(例如, append 调用在这是不可用的)。然而,元组的确支持 字符串 和列表的一般序列操作:

»> (1, 2) + (3, 4)

# Concatenation

(1, 2, 3, 4)

>» (1, 2) * 4

# Repetition

(1, 2, 1, 2, 1, 2, 1, 2)

>» T = (1, 2, 3, 4) »> T[o], T[l:3]

#

Indexing, slicing

(1, (2, 3))

元组的特殊语法:逗号和圆括号 表 9-1 中的第二行和第四行值得做进一步说明。因为圆括号也可以把表达式括起来(见第 5

章),所以如果想让圆括号里的单一对象是元组对象而不是一个简单的表达式,就需要对 Python 进行特别说明。如果你确实想得到 一 个元组,那么只需在这一单个元素之后、关闭

圆括号之前加一 个逗号即可:

»> »>

X

= (40)

# An integer!

= (40,)

# A tuple containing an integer

X

40

>» y >» y (40,)

作为 一种特例,在不会引起二 义性的情况下, Python 允许你忽略元组的圆括号。例如,表

元组、文件与其他核心类型

I

281

9-1 中第四行简单列出了四个由逗号隔开的项。在赋值语句的上下文中,即使没有圆括号, Python 也能够识别出这是一个元组。

现在,有些人会告诉你,元组中 一 定要使用圆括号,而有些人会告诉你 一 定不要用(其他 人井不会告诉你该怎样编写元组)。 一 般来说,在以下的情况中,必须对元组字面址使用 圆括号:



圆括号不可省略一—出现在 一 个函数调用中,或是嵌套在 一个更大的表达式内。



逗号不可省略~字典,或是在 Python 2.X 的 print 语句中。

在大多数其他上下文中,外围的圆括号是可选的。对初学者而 言 ,最好的建议是 一 直使用

圆括号,这可能会比弄明白什么时候可以省略圆括号要更简单 一 些。许多程序员(包栝我 自己在内)也发现圆括号有助于增加脚本的可读性,因为这样可以使元组更加明确,尽管

你的使用经验可能有所不同。

转换、方法和不可变性 除了字面且语法不同之外,元组的操作(见表 9-1 的中间各行)和字符串及列表是 一致的。 值得注意的区别在于,当“+”

“*”以及分片操作应用于元组时,会返回新的元组,井且

元组不提供字符串、列表和字典中的方法。例如,如果你想对元组进行排序,通常先得将

它转换为列表从而使其成为一个可变对象,才能使用排序方法调用。或者使用新的内置函 数 sorted, 它接受任何序列对象(以及其他的可迭代对象-—-第 4 章已介绍过,本书的后 一部分将进行更加详细的介绍) :

>>> T = ('cc','aa','dd','bb') >» tmp = list(T)

# Make a list from a tuple 's items

» > tmp. sort()

# Sort the list

»>

t叩

['aa','bb','cc','dd']

»> T = tuple(tmp) »> T

# Make a tuple from the list's items

('aa','bb','cc','dd')

»> sorted(T)

# Or use the sorted built-in, and save two steps

['aa','bb','cc','dd'] 这里的 list 和 tuple 内置函数用来将对象转换成列表,或转换回元组。实际上,这两个调 用都会生成新的对象,但总结果等效千转换。

列表推导也可以用来转换元组。例如,下面的代码从元组产生列表,并在过程中将每 一项 都加上 20:

>» T = (1, 2, 3, 4, 5)

»>

L = [ x + 20 for x in T]

»> L [21, 22, 23, 24, 25]

282

I

第9章

列表推导本质上是序列操作一一它们总会创建新的列表,但它们也可以用千遍历包括元组、

字符串以及其他列表在内的任何序列对象。我们将会看到,列表推导甚至可以用在某些并 非实际储存的序列上--ff 何可迭代对象都可以,包括可自动逐行读取的文件。考虑到这 一点 ,列表推导实际上可以被视作一种迭代工具。 尽管元组的方法与列表和字符串不同,但它们在 Python 2.6 和 Python 3.0 中的确有两个自 己的方法-index 和 count 借鉴列表的工作方式,但它们却属于元组对象:

>» T = (1, 21 3, 2, 4, 2) >» T.index(2)

# Tuple methods in 2.6 and 3.0 # Offset offirsc appearance of 2

1

»> T.index(2, 2)

# Offset of appearance after offset 2

3

>» T.count(2)

# How many 2s are there?

3

在 Python 2.6 和 Python 3.0 之前,元组根本没有方法~是 Python 对千不可变对象的一

种古老惯例,但在多年前出千字符串的实用性而打破了这一惯例,近来对于数字和元组也 都突破了这一传统。

同样,要注意元组的不可变性只适用千元组本身顶层而并非其内容。例如,元组内部的列 表是可以像往常那样修改的:

>» T = (1, [2, 3), 4) »> T[1] = ' s p a m ' # This 如ls: can't change tuple itself TypeError: object doesn't support item assignment

>» T[1][0] ='spam' >» T

# This works: can change mutables inside

(1, ['spam', 3), 4) 对多数程序而言,这种单层深度的不可变性对一般元组所要充当的角色来说已经足够了。

这碰巧把我们引人下一节的内容。

为什么有了列表还要元组 初学者学习元组的时候,这似乎总是第一个会被提出的问题:既然已经有列表了,为什么 还需要元组?其中的某些原因可能是历史性的。 Python 的创造者是一个训练有素的数学家, 他曾说过元组是一种对象的简单组合,而列表是一种随时间改变的数据结构。实际上,单 词“元组”就借用自数学领域,同时它也被用来指代关系数据库表的一行。 然而,最佳答案似乎是元组的不可变性提供了某种一致性。这样你可以确保元组在程序中

不会披另一个引用修改,而列表就没有这样的保证了。因此,元组和其他不可变对象的角 色类似于其他语言中的“常量”声明,然而这种常量概念在 Python 中是与对象相结合的, 而不是变最。

元组、文件与其他核心类型

1

283

元组也可以用在列表无法使用的地方。例如,作为字典的键(参考第 8 章稀疏矩阵的例子)。

一些 内置操作可能也要求或暗示要使用元组而不是列表,尽管近年来这样的操作往往已经 通用化而变得更加灵活了。凭经验来说,列表是适用千可能需要进行修改的有序集合,而 元组能够处理其他固定关系的情况。

重访记录:有名元组 事实上,数据类型的选择比前一节所展示的要更加丰富一今天的 Python 程序员可以从各

式各样的内置核心类型和这些内置类型之上的扩展类型中做选择。例如,在前一章的边栏“请 留意:字典 vs 列表”中,我们看到如何使用列表和字典来表示记录型的信息,也注意到字

典的优势在千提供了更加便千记忆的键来标记数据。只要我们不要求可变性,那么元组可 以胜任列表在表示记录方面的角色:

>» bob = ('Bob', 40. 5, ['dev','mgr']) >» bob ('Bob', 40. 5, ['dev','mgr'])

#

Tuple record

>» bob[o], bob[2] ('Bob', ['dev','mgr'])

# Access by position

就像列表一样,字段的序号并不能跟字典中的键一样携带关千这个字段的信息。以下是相 同记录的字典表示:

»>bob= dict(name='Bob', age=40.5, jobs=['dev','mgr']) »> bob {'jobs': ['dev','mgr'],'name':'Bob','age': 40.5}

# Dictionary record

»> bob['name'], bob['jobs'] ('Bob', ['dev','mgr'])

ft Access

by key

事实上,我们可以在需要时将字典的 一部分转换成元组:

>» tuple(bob.values()) # Values to tuple (['dev','mgr'],'Bob', 40. 5) »> list(bob.items()) # Items 10 ruple list [ ('jobs', [ ' dev', ' mgr']), ('name','Bob'), ('age', 40. 5)] 但是这需要一些额外的工作,我们可以实现一个同时提供序号和键两种访问方式的对象。

例如, namedtuple 工具(可以在第 8 章中提到的标准库的 collections 模块中被使用)实 现了一个增加了逻辑的元组扩展类型,能同时支持使用序号和属性名访问组件,也可以披 转换成使用键的类字典形式的访问。有名元组的属性名来自类,因此井不与字典的键完全 一 样,但它们都很方便记忆:

»> from collections import namedtuple »> Rec = namedtuple('Rec', ['name','age','jobs']) »>bob= Rec('Bob', age=40,5, jobs=['dev','mgr']) »> bob Rec(name='Bob', age=40 . 5, jobs=[' dev','mgr'])

284

I

第9章

#

Import extension type

# Make a generated class # A named-tuple record

# Access by position

»> bob[o], bob[2] ('Bob', ['dev','mgr']) »> bob.name, bob.jobs ('Bob', ['dev','mgr'])

# Access by attribute

在需要时,可以转换成一个类字典的基于键的形式: # Dicrionary-likeform >» 0 = bob._asdict() # Access by key too >» O[ •name'], O[ •jobs•] (•Bob•, ['dev •, •mgr']) >» 0 OrderedDict([('name','Bob'), ('age', 40.5), ('jobs', ['dev','mgr'])])

如你所见,有名元组是 一 个元组、类、字典的混合体。它们也代表了一种经典的取舍。为 了获得它们的额外功能,它们需要额外的代码(如上一个例子中的前两行就需要导人类型 并创建类的实例)和一些性能上的损失来实现这种魔法。

(简而言之,有名元组建立了新

的扩展了元组类型的类,对每个有名字段插入了 property 访问器方法,将它们的名字映射

到相应的序号~使用了格式化 代码字符串而不是像装饰器和元类的类注解工具。)然而,它们给我们提供了一个很好的

例子,让我们知道可以在像元组这样的内置类型需要额外使用特性时,基千内置类型建立 个性化定制的数据类型。

有名元组可以在 Python 3.X 、 2.7 、 2. 6 (_asdict 方法返回一个字典)以及可能更早的版本

中使用,尽管它们依赖千相对现代的 Python 标准的功能。它们也是扩展,而非核心类型(它 们位千标准库中,与第 5 章中的 Fraction 和 Decimal 属千同 一类),因 此我们把更详细的 介绍留给 Python 标准库手册。 作为一 个快速预习,元组与有名元组都支持解包元组赋值,我们将在第 13 章中正式学习解

包元组赋值,并在第 14 章与第 20 章中学习迭代上下文(注意这里与位置相关的初值:有

名元组接受名称、序号,或两者) :

For both tuples and named tuples Tuple assignment (Chapter I I)

>» bob = Rec ('Bob', 40. 5, ['dev','mgr']) »> name, age, jobs = bob »> name, jobs ('Bob', ['dev','mgr'])

# #

»> for x in bob: print(x) ... prints Bob, 40. 5, ['dev','mgr']...

# Iteration context (Chapters 14, 20)

元组解包赋值并不能用千字典,因为缺少了获取并转换键值对,为键值对添加序号(字典

没有顺序),以及对键而不是值进行迭代(注意这里的字典字面批形式,它是 diet 的另 一 种写法) :

»>bob= {'name':'Bob','age': 40.5,'jobs': ['dev','mgr']} >» job, name, age = bob.values() # Dier equivalent (bur order may vary) »> name, job

元组 沦文件与 其他核心类型

1

285

('Bob', ['dev','mgr']) »> for x ... prints »> for x . .. prints

in bob: print{bob[x]) values... in bob.values(): print(x) values...

# Step though keys, index values #

Step through values view

我们将在第 27 章介绍井对比用户定义类时,再重温并最终结束这里关千记录表示的内容。 正如我们将看到的,类也能为各个字段命名,但是类同时提供了处理记录数据的程序逻辑。

文件 想必大多数读者都熟悉文件的概念,也就是计算机中由操作系统管理的具有名称的存储区域 。

我们最后要讲的这个主要内置对象类型提供了 一种可以在 Python 程序内存取文件的方式。 简而言之,内置函数 open 能够创建一 个 Python 文件对象作为到计算机上 一 个文件的链接。 在调用 open 之后,你可以通过返回的文件对象的方法,在程序与相应文件之间来回传递串 形式的数据。

与我们目前见过的类型相比,文件对象多少有些不同。作为内置函数创建的对象,它们被 视作核心类型,但它们不是数字、序列或映射,它们也不支持表达式运算符 1 它们只支持 与文件处理任务相关的方法。大部分文件方法都与执行外部文件相关的文件对象的输入和

输出有关,但其他文件方法可用千查找文件中的新位置,刷新愉出缓冲等。表 9-2 总结了 常见的文件操作。 表 9-2: 常见的文件操作 操作

解释

output = open(r'C:\spam','w')

创建输出文件 ('w' 代表写入)

input = open('data','r')

创建输入文件 ( , r' 代表读入)

input= open('data')

与上—行相同 ('r' 是默认值)

a String = input. read()

把整个文件读进—个字符串

aString = input.read(N)

读取接下来的 N 个字符(一个或多个)到—个字符串

a String = input. readline()

读取下—行(包含\ n 换行符)到 —个字符串

alist = input.readlines()

读取整个文件到—个字符串列表(包含\ n 换行符)

output.write(aString)

把字符串(或字节串)写入文件

output.writelines(alist)

把列表内所有字符串写入文件

output. close ()

手动关闭(当文件收集完成时会替你关闭文件)

output. flush()

把输出缓冲区刷入硬盘中,但不关闭文件

anyFile.seek(N)

把文件位置移动到偏移量 N 处以便进行下—个操作

for line in open('data'): use line

文件迭代器逐行读取

286

I

第 9章

表 9-2: 常见的文件操作(续)

.

操作

解释

open('f.txt', encoding='latin-1')

Python 3.X Unicode 文本文件 (str 字符串)

open('f.bin','rb')

Python 3.X 字节码文件 (bytes 字节串)

codecs.open('f.txt', encoding='utf8')

Python 2.X Unicode 文本文件 (unicode 字符串)

open('f.bin','rb')

Python 2.X 字节码文件 (str 字符串)

打开文件 为了打开一 个文件、程序会调用内置函数 open, 传入的参数首先是外部文件名,接着是文

件的处理模式。 open 函数返回 一 个文件对象,这个文件对象带有可以传输数据的方法:

afile = open(filename, mode) afile.method() open 函数的第一个参数是外部文件名,它可能含有一 个与平台相关的绝对或相对目录路径

前缀。如果没有这个目录路径,文件被默认为存在当前工作目录下(脚本所运行的目录)。 正如我们将在第 37 章中看到的扩展文件,文件名也可以包含非 ASCII 码的 Unicode 字符, Python 将自动根据所在平台的编码设置进行转换,或作为一个未编码的字节串进行处理。

open 函数的第二个参数是处理模式,通常用字符串 'r' 表示以输入模式打开文件(默认值), 飞'表示以输出模式生成并打开文件,' a '表示在文件尾部追加内容并打开文件(例如,向 日志文件中添加内容)。处理模式参数也可以指定为其他选项:



在模式字符串中加上 b 可以进行二进制数据处理(换行符转换和 Python 3.X Unicode 编 码被关闭了)。



加上“+”意味着被打开文件同时支持输入输出(也就是说,我们可以同时对该文件对 象进行读写,往往与对文件中的修改的查找操作配合使用)。

open 函数的前两个参数必须都是 Python 的字符串。第 三个是可选参数,它能够用来控制输 出缓冲:传人 0 意味着输出无缓冲(写入方法调用时立即传给外部文件), open 函数的其

他参数可用于特殊种类的文件(例如,在 Python 3.X 中对千 Unicode 文本文件的 encoding

参数译注 I) 。 这里我们将介绍文件的基础知识并探讨一 些基础的示例,但是,我们不会介绍所有的文件

处理模式选项,与往常 一 样,请查看 Python 库手册以了解其他详细信息。

译汪 I:

Python 中向函数传入参数时可以指定关键宇,这里 encoding 的使用就需要写出其关键字 。

例如, open ('test',

mode='w+', encoding='utf-8' )中指定了以 UTF-8 编码格式打开

text 文件 。 关于函数传参的详细内容,计查阅笫 18 章 。

元组 ` 文件与其他核心类型

1

287

使用文件 一且你用 open 函数产生了一个文件对象,就可以调用它的方法来读取或写入对应的外部文 件。在任何情况下,文件的文本在 Python 程序中都采用字符串的形式。读取文件时会返回

字符串形式的文本,写入文件时的文本作为字符串被传入 write 方法。读和写的方法有许 多种,表 9-2 列出的方法是最常用的。如下是 一 些基础用法的提示: 文件迭代器最适合逐行读取 虽然表中的读写方法都是常用的,但是要记住,今天从文本文件逐行读取的最佳方式

就是根本不要读取该文件。我们在第 14 章将会看到,文件也有 一 个迭代器,可以在 for 循环、列表推导或者其他迭代上下文中自动逐行读取文件。

内容是字符串,不是对象 要注意,从文件读取的数据回到脚本时是 一 个字符串。所以如果字符串不是你所需要的, 就得将其转换成其他类型的 Python 对象。同样,当你把数据写入文件时, Python 不会

自动把对象转换为字符串,这与 print 语句不同 --➔- 必须传入一 个已经格式化的字符 串。因此,我们之前见过的处理文件时可以来回转换字符串和数字的工具迟早会派上 用场(例如, int 、 float 、 str 以及字符串格式化表达式)。 Python 也包括一些高级标准库工具,它用来处理一般的对象存储(如 pickle 模块)、 文件中的打包二进制数据(如 struct 模块)以及如 JSON 、 XML 和 CSV 等特殊类型 的内容。本章稍后部分我们会对它们进行介绍,更详细的内容参阅 Python 手册。 文件是被缓冲的以及可定位的

默认情况下,输出文件总是被缓冲的,这意味着写入的文本可能不会立即自动从内存

转移到硬盘~或者运行其 flush 方法,才能强制被缓冲的数据进 入硬盘。 Python 文件也是在字节偏移址的基础上随机访问的,它们的 seek 方法允许脚

本跳转到指定的位置读取或写人。 close 通常是可选的:回收时自动关闭

调用文件 close 方法将会终止与外部文件的连接、释放操作系统的资源,以及将内存中 的缓冲内容输出到磁盘并清空缓冲区 。 我们在第 6 章介绍过,在 Python 中, 一 且对象 不再被引用,那么这个对象的内存空间就会自动被收回。当文件对象被收回时, Python

也会自动关闭该文件(通常在一个程序关闭的时候发生)。这就意味着在标准 Python

环境下你不需要总是手动去关闭文件,尤其是对千不会运行很长时间的简单脚本和被 单行命令或表达式使用的临时文件。 另 一 方面,手动关闭调用并没有任何坏处,而且在长时间运行的大型程序中通常是个 很不错的习惯。此外,严格地讲,文件的这种回收后自动关闭的特性不是语言定义的

一部分,而且可能随时间而改变,在交互式命令行中可能不会按预期发生,在其他与 CPython 垃圾回收及文件关闭机制不同的 Python 实现中也不一定能正常被回收。实际上,

当循环中有许多文件被打开时, CPython 以外的其他 Python 实现可能需要在垃圾回收

288

I

第9章

机制发挥作用前,就立即调用 close 函数释放操作系统资源。此外, close 函数调用在 需要清空还未释放的文件对象的输出缓冲时是必须使用的。要了解确保自动文件关闭

的另 一 种替代方案,请参阅本节后面对文件对象的上下文管理器的讨论,它在 Python 2.6 、 2.7 和 3.X 中与新的 with/as 语句一起使用。

文件的实际应用 让我们看一 个能够说明文件处理原理的简单例子。下面的代码首先为输出打开一个新文件,

写 入两行文本(以换行符\ n 终止的字符串),之后关闭文件。接下来,我们将会在输人模 式下再一 次打开同 一文件,并使用 readline 函数逐行读取。注意第 三个 readline 调用会

返回 一个空字符串。这是 Python 文件方法告诉我们已经到达文件末尾的方式(文件的空行 是含有换行符的字符串 , 而不是空字符串)。下面是完整的交互式会话:

» > myf ile = open('myf ile. txt' ,飞') »> myfile.write{'hello text file\n')

# #

Open for text output: crearelempty Write a line of text: string

16

»> myfile.write('goodbye text file\n') 18

»> myfile.close()

# Flush output buffers to disk

»> myfile = open('myfile.txt') »> myfile.readline()

# Read the lines back

# Open for text input: 'r'is default

'hello text file\n'

»> myfile.readline() 'goodbye text file\n'

»> myfile.readline()

# Empty string: end offile

',

注意,文件 write 调用返回了在 Python 3.X 中写入的字符数;而在 Python 2.X 中,它们却 不会,因此,在 2.X 中这些数字不会被交互式地显示出来。这个例子把一 行文本写成字符串,

并包含了换行符\ n 。由千写入方法不会为我们添加换行符,因此程序必须包含它来恰当地 终止行(否则,下次写人时会直接延长文件的 当 前行)。

如果你想在显示文件内容时展示换行符,可以用文件对象的 read 方法把整个文件读入到 一 个 字 符串中,并打印它:

»> open('myfile. txt').read()

# Read all at once into string

'hello text file\ngoodbye text file\n'

» > print (open('myf ile. txt'). read()) hello text file goodbye text file

# User-friendly display

如果你想逐行扫描 一个文本文件,文件迭代器往往是最佳选择:

»> for line in open('myfile'.txt'):

# Use file iterators, not reads

print(line, end='')

元组.文件与其他核心类型

1

289

hello text file goodbye text file 以这种方式编程时, open 创建的临时文件对象将自动在每次循环迭代时读入并返回一行。 这种形式通常最容易编写,有更高的内存使用效率,并且比其他方式都更快(当然,这取 决千多种因素)。由千我们还没有介绍语句和迭代器,你必须等到第 14 章才能完全理解这 段代码。

注意:对千 Windows 用户:正如第 7 章所述, open 函数接受 UNIX 风格的斜杠路径,而在

Windows 中则是反斜杠,因此下面的所有形式都是合法的文件路径

原始字符串、斜

杠或双反斜杠:

»> open(r'C:\Python33\Lib\pdb.py').readline() '#! /usr/bin/env python3\n'

>» open('C: /Python33/Lib/pdb.py').readline() '#! /usr/bin/env python3\n'

»> open('C:\\Python33\ \Lib\ \pdb.py').readline() '#! /usr/bin/env python3\n' 第二条指令中的原始字符串形式,能够通过关闭转义字符来帮助你应对无法预料字符串 内容等情况。

文本和二进制文件:一个简要的故事 严格地讲,前面小节中的示例使用了文本文件。在 Python 3.X 和 Python 2.X 中,文件类型

都由 open 的第二个参数(模式字符串)决定一模式字符串中包含一个 “b" 表示二进制。 Python 总是支持文本和二进制文件,但是在 Python 3.X 中,两者之间有明显的区别:



文本文件把内容表示为常规的 str 字符串,自动执行 Unicode 编码和解码,并且默认执 行末行转换。



二进制文件把内容表示为一个特殊的 bytes 字节串类型,并且允许程序不修改地访问 文件内容。

相反, Python 2.X 文本文件处理 8 位文本和 二进制数据,并且有一种特殊的字符串类型和 文件接口 (unicode 字符串和 codecs.open) 来处理 Unicode 文本。 Python 3.X 中的区别源

自千简单文本和 Unicode 文本都合并为同一种常规字符串类型这一事实~是有意义的, 因为所有的文本都是 Unicode, 包括 ASCII 和其他的 8 位编码。 由千大多数程序员只是处理 ASCII 文本,因此 ASCII 文本可以用前面的示例中所使用的

基本文本文件接口和常规的字符串来获取。在 Python 3.X 中,所有的字符串事实上都是 Unicode, 但是 ASCII 用户通常不会在意 。实际上,如果你的脚本仅限千处理如 此简单形式

的文本,那么文本文件和字符串在 Python 3.X 和 Py~hon 2.X 中能够同样地工作。

290

I

第9章

如果需要处理国际化应用程序或者面向字节的数据,那么 Python 3.X 中的区别将会影响到

代码(通常是更好的影响)。通常在 Python 3.X 中,你必须使用 bytes 字符串处理二进制文件, 并且用常规的 str 字符串处理文本文件。此外,由于文本文件实现了 Unicode 编码,因此不

能以文本模式打开一个二进制数据文件一因为将其内容解码为 Unicode 文本可能会失败。 让我们来看一个示例。当你读取一 个二进制数据文件的时候,会得到 一个 bytes 对象一一 表示绝对字节值的较小整数的 一 个序列(可能会也可能不会对应为字符),其外观几乎与

常规的字符串完全相同。在 Python 3.X 中,假设存在一 个二进制文件:

»>data= open('data.bin','rb').read(} »> data b'\xoo\xoo\xOO\x07spam\xOO\xo8' »> data[4:8] b'spam' >» data[o] 115 »> bin(data[4:8][o]) 'ob1110011'

# Open binary file: rb=read binary # bytes string holds binary data

# Act like strings

# But really are small 8-bit integers #

Python 3.X/2.6 bin() junction

此外,二进制文件不会对数据执行任何换行符转换;在 Python 3.X 中当写入和读取时,文

本文件会默认执行对\ n 的来回转换,并采用 Unicode 编码。与本例相同的二进制文件在

Python 2.X 中能够同样地工作,但是字节串就是简单的 一般字符串,而且在显示的时候没 有一个打头的 b 字母,同时文本文件必须使用 codecs 模块添加对 Unicode 的处理。 如本章开头所述,这就是我们要介绍的所有关于 Unicode 和 二进制文件的内容,这些知识

足以让你理解本章后面的例子。由千 Unicode 和 二进制数据对很多 Python 程序员来说并不 直接相关,因此我们仅在第 4 章中快速预习一遍,并将完整的介绍推迟到第 37 章。现在, 让我们继续看一些更为实际的文件示例。

在文件中存储 Python 对象:转换 下面这个例子把多种 Python 对象写入一个文本文件的各行。要注意,我们必须使用转换工

具把对象转成字符串。同样,文件数据在脚本中 一 定是字符串,而写入方法不会自动地替 我们做任何到字符串的格式转换工作(为了节省篇幅,这里我省略了 write 方法中的字节 计数返回值) :

»> >» »> >» >» >» >» »> >» >»

X, Y, Z = 43, 44, 4S S ='Spam' D = {'a': 1,'b': 2} L = [1, 2, 3]

# Native Python objects # Must be strings to store in file

F = open('datafile. txt' ,飞') F.write{S +'\n') F.write('%s,%s,%s\n'% {X, Y, Z)) F.write(str(L) +'$'+ str{D) +'\n') F. close()

II Create output file II Terminate lines with\n # Convert numbers to strings # Convert and separate with $

元组、文件与其他核心类型

I

291

一且我们创建了文件,就可以通过打开和读取字符串来查看文件的内容(这里将打开和读

取合并成了一步操作)。要注意,交互式显示给出了直接的字节内容,而 print 操作则会 解释内嵌的换行符,从而按照用户友好的方式展示结果:

>» chars = open('datafile. txt').read() »> chars

#

Raw string display

"Spam\n43,44,45\n[1, 2, 3]${'a': 1,'b': 2}\n"

»> print(chars)

#

User-friendly display

Spam 43,44,45 [1, 2, 3]${'a': 1,'b': 2} 现在我们不得不使用其他转换工具,把文本文件中的字符串转换成真正的 Python 对象。鉴

千 Python 不会自动把字符串转换为数字(或其他类型的对象),因此如果我们需要使用诸 如索引、加法等普通对象工具,就需要这么做:

>» F = open{'datafile.txt') >» line = F.readline() »> line

# Open again # Read one line

'Spam\n'

»> line.rstrip{)

#

Remove end-of-line

'Spam' 对千第 一 行,我们使用字符串的 rstrip 方法去掉多余的换行符,也可以使用 line[: -1] 分片, 但只有确保所有行都含有 “\n" 的时候才行(文件中最后一 行有时候会没有)。

到目前为止,我们读取了包含字符串的行。现读取包含数字的下一行,并解析出(抽取出) 该行中的对象:

»> line = F.readline() »> line

# Next line from file # It's a string here

'43,44,45\n'

»> parts = line.split(',') »> parts

# Split (parse) on commas

['43','44','45\n']

我们在这里使用字符串的 split 方法,从逗号分隔符的地方将整行断开,得到的结果是含 有各个数字的子字符串列表。如果我们想对这些数字做数学运算,那么还得把字符串转换 为整数:

>» int(parts[l])

#

Convert from string to int

#

Convert all in list at once

44

»> numbers »> numbers

= [int(P) for P in parts]

[43, 44, 45] 如前所述, int 能够把数字字符串转换为整数对象,我们在第 4 章所介绍的列表推导表达式

也可以 一 次性对列表中的每 一项应用函数调用(你会在本书后面看到更多关于列表推导的

292

I

第9章

介绍)。要注意,我们不一定非要运行 rstrip 来删除最后部分的 “\n", int 和一些其他 转换方法会忽略数字旁边的空白 c 最后,要转换文件第三行所存储的列表和字典,我们可以运行内置函数 eval, eval 能把字

符串当作可执行程序代码(从技术上讲,就是 一 个含有 Python 表达式的字符串)

>» line = F.readline() >» line "[1, 2, 3]${'a': 1,'b': 2}\n"

»> parts= line.split('$') »> parts

# Split (parse) on $

['[1, 2, 31', "{'a': 1,'b': 2}\n"]

»> eval{parts[o]) (1, 2, 3] »> objects = [eval(P) for P in parts] >» objects ((1, 2, 3], {'a': 1,'b': 2}]

# Convert ro any object type # Do same for all in list

因为所有这些推导和转换的最终结果是一个普通的 Python 对象列表,而不是字符串,所以 我们现在就可以在脚本内应用列表和字典操作了。

存储 Python 原生对象: pickle 如前面程序所示,使用 eval 可以把字符串转换成对象,因此它是一个功能强大的工具。事

实上,它有时太过千强大。 eval 会高高兴兴地执行 Python 的任何表达式,只要给予必要的 权限,甚至有可能会删除计算机上所有文件的表达式。如果你真的想存储 Python 原生对象, 但又无法信赖文件的数据来源,那么 Python 标准库 pickle 模块将是个理想的选择。 pickle 模块是能够让我们直接在文件中存储几乎任何 Python 对象的高级工具,同时并不需 要我们对字符串进行来回转换。它就像是超级通用的数据格式化和解析工具。例如,想要 在文件中存储字典,你可以直接使用 pickle:

»> »> »> »> >»

D = {'a': 1,'b': 2} F = open('datafile.pkl','wb') import pickle pickle.dump(D, F) F.close()

# Pickle any object co file

之后,在想要取回字典时,只需直接再次使用 pickle 重建即可:

>» F = open('datafile.pkl','rb') >» E = pickle.load(F) »> E

#

Load any object from file

{'a': 1,'b': 2}

我们取回相同的字典对象,无需手动切分或转换。 pickle 模块会执行所谓的对象序列化

(object serialization) ,也就是对象与字节字符串之间的相互转换,而我们亲自要做的工

元组、文件与其他核心类型

I

293

作却很少。事实上, pickle 内部将字典转成一 种串形式,不过这其实没什么可看的(如果 我们在其他数据协议模式下使用 p ickle 将各不相同)

:

»> open('datafile.pkl','rb').read() # Formal is prone 10 change! b'\x8o\x03}q\xoo(X\xo1\xoo\xoo\xoobq\xo1K\xo2X\xo1\xoo\xoo\xooaq\xo2K\xo1u.' 因为 pickle 能够从这一格式中重建对象,所以我们不必手动来处理。有关 pickle 模块的 更多内容可以参考 Python 标准库手册,或者在交互式命令行下将 pickle 传入 help 来查阅

相关信息。与此同时,你也可以顺便看一看 shelve 模块。 shelve 用 pickle 把 Python 对象 存放到 一 种按键访问的文件系统中,不过这并不在我们讨论的范围之内(尽管你可以在本

书第 28 章中看到应用 shelve 的 一 个示例,并且在第 31 章和第 37 章看到其他的 pickle 示

例)。

注意:这里用 二 进制模式打开了存储 pickle 化对象的文件; 二 进制模式在 Python 3.X 中必

须总是显式给出的,因为 pickle 程序创建和使用 一 个 bytes 字符串对象,并且这些对 象意味着 二 进制模式文件(文本模式文件暗含 Python 3.X 中的 str 字符串)。在早期 Python 中,对千协议 o 使用文本模式文件是没有问题的(默认情况下,它创建 ASCII 文 本),只要文本模式能被一 致地使用;较高的协议要求采用二进制模式文件。 Python

3.X

的默认协议是 3 ( 二进制),但是 Python 3.X 即便对于协议 0 也创建 bytes 。参阅第 28

章、第 31 章和第 37 章 Python 的库手册或其他参考书,了解关千这 一 点的更多细节以

及 pickle 化数据的更多示例。

Python 2.X 也有 一个 cPickle 模块,它是 pickle 的 一 个优化版本,可以直接快速导人。 Python 3.X 把这模块改名为_ pickle, 井且在 pickle 中自动使用它一一脚本直接导入 pickle 并且让 Python 自己来优化它。

用 JSON 格式存储 Python 对象 上一节中的 pickle 模块将几乎任意的 Python 对象转换成 一 个 Python 独有的格式,而且在 多年的使用中性能得到了很大的提升。 JSON 是 一 种新兴的数据交换格式,它既是语言无关

的,也支持多种系统。例如 MongoDB 就将数据存储在一个 JSON 文件的数据库中(它使 用 二 进制 JSON 格式)。

虽然 JSON 并不能像 pickle 一样支持种类繁多的 Python 对象类型,但是它的可移植性在 一些场景中会带来很大优势,同时 JSON 也提供了将一类特定的 Python 对象序列化,从而

实现存储和传输的方式。此外,由于 JSON 与 Python 中的字典和列表在语法上的相似性, 因此 Python 的 json 标准库模块能够很容易地在 Python 对象与 JSON 格式之间来回转换。 例如, 一 个带有嵌套结构的 Python 字典与 JSON 数据十分相似,尽管 Python 的变显和表达

式支持更丰富的结构选项(下面例子中的赋值语句右侧可以使用任意的 Python 表达式)

294

I

第9章

» > name = diet (first='Bob•, last=• Smith') >» rec = dict(name=name, job=['dev','mgr'], age=40.5) »> rec {'job': ['dev','mgr'],'name': {'last':'Smith','first':'Bob'},'age': 40.s} 这里展示的最后一种字典格式是 一 个合法的 Python 字面量,而且儿乎以它所显示的形式被 原封不动地传给 JSON 文件,但是 json 模块则能更正式地转换-~下面的例子将 Python 对 象转换成内存中 JSON 序列化的字符串形式:

>» import json >» json.dumps(rec) '{"job": ["dev", "mgr"], "name": {"last": "Smith", "first": "Bob"}, "age": 40.S}'

»> S = json.dumps(rec) >» s '{"job": ["dev", "mgr"], "name": {"last": "Smith", "first": "Bob"}, "age": 40,5}'

»> »>

O 0

= json.loads(S)

{'job': ['dev','mgr'],'name': {'last':'Smith','first':'Bob'},'age': 40.5} >>> O == rec True 在 Python 对象和文件中, ISON 数据字符串之间的相互转换都是非常直接的。在被存入

文件之前,你的数据是简单的 Python 对象,当从文件中读取对象时, json 模块将它们从 JSON 表示重建成 Python 对象:

»> json.dump{rec, fp=open('testjson.txt' ,飞 '),indent=4) »> print(open('testjson. txt').read()) { "job": [ "dev", mgr

~.

-" name": { "last": "Smith", "first": "Bob" }, "age": 40.5 -

}

»> P = json.load{open('testjson.txt')) »> p {'job': ('dev','mgr'],'name': {'last':'Smith','first':'Bob'},'age': 40.s} 一且你将 JSON 文本转换成 Python 对象之后,你就可以用 一般的 Python 对象操作来处理数

据。关千更多与 JSON 相关的话题,请参阅 Python 的库手册。 要注意, JSON 文件中的所有字符串都是 Unicode 编码的国际化字符集,因而在 Python 2.X 中的那些从 JSON 数据转换过来的字符串都带有 一 个 u 打头的字母;这只是 2.X 版本中 Unicode 对象的语法而已,我们在第 4 章、第 7 章和第 37 章中有相应介绍。由于 Unicode

元组、文件与其他核心类型

I

29s

格式字符串支持所有一般字符串的操作,因此当你的 Unicode 字符串位千内存中时,代码

的形式几乎与一般字符串形式一样,而两者最大的差异体现在与文件进行交互,通常在非 ASCII 类型的文本才需要考虑编码格式。

注意:在 Python 中也同样有支持 Python 对象与 XML 格式的相互转换,后者将在第 37 章中进 行介绍,详情请查阅互联网。关千另外一种相关的格式化数据文件转换工具,请参阅标 准库中的 CSV 模块。 CSV 模块能在文件和字符串中解析和创建 CSV

(comma-separated

value, 逗号分隔值)格式的数据。 CSV 虽然不能直接映射成 Python 对象,但它是另外 一种常见的数据交换格式: >>>扣port csv »> rdr = csv.reader(open('csvdata.txt')) ) ) ) for ro树 in rdr: print(row)

['a','bbb','cc','dddd'] ['11','22','33','44']

存储打包二进制数据: struct 在继续学习下面的内容之前,还有一个与文件相关的细节需要注意:有些高级应用程序也 需要处理打包二进制数据,这些数据可能是 C 语言程序或者网络连接产生的。 Python 标准

库中包含一 个能够应用在这 一领域的工具: struct 模块能够构造并解析打包二进制数据。 从某种意义上讲,它是另 一种数据转换工具,能够把文件中的字符串转换为 二进制数据。

我们在第 4 章中曾接触过这个工具,现在让我们从一个新的角度来快速了解一下。例如 , 要生成一 个打包二进制数据文件,你可以用 'wb' (写入二进制)模式打开它,并将 一 个格

式化字符串和几个 Python 对象传给 struct 。下面使用的格式化字符串是指将一个 4 字节整 数、一个包含 4 个字符的字符串(从 Python 3.2 起必须是一个 bytes 字节串)以及 一个 2 字节整数的内容,都按照高位在前 (big-endian) 的形式(其他格式的代码能处理补位字节、 浮点数等)打包:

>» F = open('data.bin' ,飞b') >» import struct » > data = struct. pack('>i4sh', 7, b'spam', 8) >» data b'\xOO\xOO\XOO\X07spam\xOO\x08' »> F.write(data) >» F.close()

#

Open binary output file

#

Make packed binary data

#

Write byte string

Python 会创建一个我们通常写入文件的二进制 bytes 数据字节串,它主要由不可打印字

符的十六进制转义组成,而且就是我们前面遇到过的二进制文件。要把值解析成 一 般的 Python 对象,你可以直接读取字节串,井使用相同的格式字节串解压即可。 Python 能够把

值提取出来转换为普通的 Python 对象,下面以整数和字符串为例:

296

I

第9章

»> F = open('data.bin','rb') »> data = F.read() >» data b'\xOO\xOO\xOO\x07spam\xoo\xo8' »> values = struct.unpack('> 坏sh', data) »> values (7, b'spam', 8)

#

Get packed binary data

#

Convert to Python objects

二进制数据文件是高级且底层的工具,我们这里不再展开介绍更多的细节。如果需要更多 帮助,你可以参考 Python 库手册,或在交互式命令行下将 struct 传入 help 函数来查阅相 关信息。同时要注意,你可以用二进制文件的处理模式 'wb `和` rb' 整体地处理更简单的

二进制文件(如图片或音频文件),而无需将它们解压 ;这样,你的代码就可以将它们 原 封不动地传给其他文件或工具。

文件上下文管理器 关千对文件上下文管理器的支持,你也可以阅读第 34 章的讨论,它是一种从 Python 3.0 与 2.6 起被引入的 一种语言新动能。尽管它更像是异常处理的一种功能而不是文件本身,但它

让我们可以把文件处理代码包装到一个逻辑层中,以确保在退出后一定会自动关闭文件(同 时可以满足将其输出缓冲区内容写入磁盘),而不是依赖千垃圾回收时的自动关闭 :

with open(r'C:\code\data.txt') as myfile: for line in myf ile: ... use line here...

#

See Chapter 34 for details

我们在第 34 章将学习的 try /finally 语句也可以提供类似的功能,但这需要一些额外代码

的成本一一准确来说,是 3 行额外的代码(尽管我们通常可以选用第三种备选方案,即让 Python 自动关闭文件) :

myfile = open(r'C:\code\data.txt') try: for line in myfile: ... use line here... finally : myfile. close() with 上下文管理器方案在所有 Python 版本中都能保证操作系统资源的释放,而且对千确保 刷新输出文件缓冲区更加有用,与更常用的 try 语句不同, with 则只局限千能够支持它的 协议的对象。由千 try 和 with 都要求我们掌握更多的背景知识,因此我们将推迟到本书的 后面再详细介绍。

其他文件工具 表 9-2 中列出了其他一些更专用的文件方法,而且还有很多井没有在表中列出。例如,

元组、文件与其他核心类型

1

297

seek 函数能重置你在文件中的当前位置(下次读 写将从重置后的新位置上开始), flush 能

够在不断开连接的情况下强制将输出缓冲写入磁盘(文件总会默认进行缓冲)等。 Python 标准库手册及前言中提到的参考书提供了更完整的文件方法清单,想要快速地浏览 一下 的话,可以在交互式命令行下运行 dir 或 help, 向它们传入 一个打开后 的文件对象(在

Python 2.X 中,可以传人名称 file, 但在 Python 3.X 中不能这么做)。更多有关文件处理 的例子请参考第 13 章的边栏“请留意:文件扫描器”。它给出了常见的文件扫描循环代码 模式,而其中用到的一些语句我们在这里还尚未涉及。 同样需要注意的是,虽然 open 函数及其返回的文件对象是 Python 脚本中通向外部文件的

主要 接口,但 Python 工具集中还有其他类似可用的文件 工具, 例如:

标准流 在 sys 模块中预先打开的文件对象,例如 sys.stdout (参见第 II 章的“打印操作“一节)。 OS 模块中的描述文件

文件的整数参数,能够支持如文件锁定之类的较低级工具(也请参阅 Python 3.3 的 open 函数的 "x" 模式下的互斥创建)。 套接字、管道和 FiFO 文件

用千同步进程或者通过网络进行通信的类文件对象。 通过键来存取的文件,如 shelve 模块

通过键直接存储经过 pickle 模块序列化的不可变的 Python 对象(第 28 章中用到)。 Shell 命令流

像 os.popen 和 subprocess.Papen 这样的工具,支持 shell 命令处理,并读取和写入到 标准流(参见第 13 章和第 21 章中的例子)。 第 三 方开源领域提供了更多的类文件工具,包括 Py Serial 中支持串口通信的扩展,以及

pexpect 系统中的交互程序。请参阅更多面向应用的 Python 资料和网页来了解关千类文件

工具的更多信息。

注意:版本差异提示:在 Python 2.X 中,内置名称 open 基本上是名称 file 的同义词,并且

文件既可以通过 open, 也可以通过 file 来打开(通常更倾向千使用 open 打开)。在

Python 3 .X 中,名称 file 不再可用,因为它和 open 相冗余。 Python

2.X 用户也可以使用名称 file 作为 一 种文件对象类型,以便用面向对象编程来

定制文件(本书后面介绍)。在 Python 3 . X 中,文件已袚彻底改变了。用来实现文件对 象的类位于标准库模块 io 中。你可以参阅这个模块的文档,或者编写可以使用它来定

制的类,并且在打开的文件 F 上运行一条 type(F) 调用,来得到相关提示 。

298

I

第9章

核心类型复习与总结 现在我们已经学过所有 Python 核心内臂类型以及它们的实际应用,让我们再看一 看它们所 共有的 一 些属性.以此来结束我们的对象类型之旅。表 9-3 根据前面介绍的类型分类,对

所有类型加以分类。下面是需要记住的 一些要点:

按照分类, 一 些对象拥有共同的操作;例如,字符串 、 列表和元组都拥有如拼接、长



度和索引等序列操作。 只有可变对象(列表 、字 典和集合)可以在原位置修改;我们不能在原位罚修改数 字 、



字符串或元组。

文件只导出方法,因此可变性并不真的适用干它们—一当处理文件的时候,它们的状态



可能会改变,但这与 Python 核心类型所描述的可变性完全是两种含义 。



表 9-3 中的“数字“包含了所有数字类型:整数(以及 Python 2.X 中独有的长整数)

`

浮点数、复数、小数和分数。



表 9-3 中的宇符串包括 str, 以及 Python 3.X 中的 bytes 和 Python 2.X 中的 unicode;

Python 3.X 、 2. 6 和 2.7 中的 bytearray 字符串类型是可变的。 •

集合很像 一 个没有值只有键的字典,但它们不能进行映射,也没有顺序 1 因此,集合 不是一 个映射类型或者序列类型, frozenset 是集合的 一种拥有不可变性的变体。



除了类型分类操作,表 9-3 中的 Python 2.6 和 Python 3.0 的所有类型都有可调用的方法, 这些方法通常属于每种特定的类型。

表 9-3: 对象分类 对象类型

分类

是否可变

数字(所有)

数值



字符串(所有)

序列



列表

序列



字典

映射



元组

序列



文件

扩展

N/A

集合

集合



frozenset

集合



bytearray

序列



元组 、 文件与其他核心类型

I

299

请留意:运算符重载 本书笫五部分会介绍我们自己实现的类对象,可以任意地在这些核心类型中选择 。 例 如,如果你想提供一种新的特殊序列对象,它与内置序列一致,那么就可以写一个类, 并重载索引和拼接等操作:

class MySequence: def __getitem_(self, index): # Called on self[index], others def _add (self, other): # Called on self+ other def _iter_(self): # Preferred in iterations



你还可以选择性地实现一些原位置修改操作的方法调用,来创造新的可变或不可变对

象(例如, self[index]=value 索引赋值操作中调用了_setitem_函数)。尽管这 不在本书的内容范围之内,但你确实也可以使用 C 语言这样的外部语言来创造新的对

象。就此而言 , 我们需要埃上 C 函数指针接槽,从而在数宇、序列和映射操作之间做 出选择。

对象灵活性 本书这一部分介绍了一些复合对象类型(带有组件的集合)。一般来说:



列表、字典和元组可以包含任何种类的对象。



集合可以包含任意的不可变类型对象译注 20



列表、字典和元组可以任意嵌套。



列表、字典和集合可以动态地扩大和缩小 。

由千 Python 的复合对象类型支持任意的结构,因此它们非常适用千表示程序中的复杂数据。 例如,字典的值可以是列表,这 一 列表又可能包含了元组,而元组又可能包含了字典,诸 如此类。只要能够满足创建待处理数据的结构的需要,嵌套多少层都是可以的。 让我们来看 一 个嵌套的例 子。 下面的交互式会话定义了 一 个嵌套复合序列对象的树,其结 构如图 9-1 所示。要存 取它的内容时,我们要按需连续运行多个索引操作。 Python 会从 左

到右计算这些索引,每 一 步取出 一 个更深层嵌套对象的引用。图 9-1 也许是 一 个过千复杂 又莫名其妙的数据结构,但它描述了一般情况下用千存取嵌套对象的语法:

译注 2

读者可以试着在交互式命 令行内轮入 a

=

{1,3, {1: "123”} } `其 运行结果是 得到一个

"TypeError: unhashable type:'diet' " 。 也就是说,可变对象因为不能进行哈希运算` 所以不能置于集合中 。

300

I

第9章

»> L = ['abc', [(1, 2), ([3], 4)], _S] » > L[1] [(1 , 2), ([3], 4)]

>» L[l] [1] ([3], 4)

»> L[1][1][0] [3]

»> L[1][1][0][0] 3

图 9-1: 一 个通过运行字面量 表达式 ['abc ', [(1 , 2), ([3], 4)], 5] 生成的嵌套对象树(及其元素的

偏移量 ) 。 从语法上来说 , 嵌套对象在内部被表示为对不同内存区域的引用(指针)

引用 vs 复制 我们在第 6 章曾经提到过,赋值操作总是存储对象的引用,而不是这些对象的副本 。 在实 际应用中,这往往就是你想要的。不过 , 由于赋值操作会产生相同对象的多个引用,因 此 你要意识到在原位置修改可变对象时, 可能会影响程序中其 他地方对同一对象的其 他 引用, 这点很重要。如果你不想这么做,就需要显式地告诉 Python 复制该对象。 我们在第 6 章中曾经研究过这种现象,但是当涉及较大的对象时,这就变得更为微妙。例

如,下面这个例子生成一 个列表并赋值为 x , 另 一个列表赋值为 L, L 嵌套有对列表 X 的引用。 这一例子中还生成了 一个字典 D , 含有另一个对列表 x 的引用。

»> X = [1, 2, 3] » > L = ['a', X,'b'] »> D = { ' x ' :X, ' y':2}

# Embed references to X's object

本例中我们为第 一 行中的列表创建了 三个引用,分别是:名称 X 、名称为 L 的列表内部,以 及名称为 D 的字典内部。关系如图 9-2 所示。

元组、文件与其他核心类型

I

301

名称

i

对象



伸三



•I

巨l

! I!I!

.工田口田

图 9-2: 共享对象引用:因为变晨 X 引用的列表也同肘在被 L 和 D 引用的对象内引用,所以修 改 X 的列表会导致 L 和 D 内对同一列表的引用发生相应的改变 由千列表是可变的,修改这 三 个引用中的任意 一 个共享列表对象,也会改变另外两个引用 的对象:

>» X[1] ='surprise , »> L

# Changes all three references!

['a', [ 1,'surprise', 3],'b']



0

{'x': [1,'surprise', 3),'y': 2} 引用是其他语言中指针的更高级模拟,这些语言在使用引用的时候不断地跟踪它们的情况。

虽然你不能获得引用本身,但你可以在不止 一 个地方存储相同的引用(变晁、列表等)。 这是 Python 的一大特征:你可以在程序范围内任何地方传递大型对象而不必在途中进行开

销巨大的复制操作。然而,如果确实需要复制,那么你可以明确要求:



没有参数的分片表达式 (L[ :])可以复制序列。



字典、集合或列表(列表的 copy 方法是从 3.3 版本开始新增的)的 copy 方法(如 X.copy()) 可以复制一个字典、集合或列表。



一 些诸如 list 和 diet 的内置函数,可以进行复制 (list(L) 、 dict(D) 、 set(S)) 。



copy 标准库模块能够在需要时创建完整副本 。

举个例子,假设有一个列表和 一 个字典,你又不想通过其他变扯修改它们的值:

>» L = (1,2,3] »> D = {'a':1,'b':2} 为了避免这一情况,你可以简单地把一份副本赋值给其他变址,而不是同一对象的引用:

»> A= L[:] »> B = D.copy() 302

I

第9章

# #

Instead of A = L (or list(L)) Instead of B = D (ditto for sets)

这样 一 来,由其他变晁引发的改变将修改新产生的副本,而不是原有对象:

»> A[l] ='Ni' »> B['c'] ='spam' >» »> L, D ([1, 2, 3], {'a': 1,'b': 2})

>» A, B ([1,'Ni', 3], {'a': 1,'c':'spam','b': 2}) 就我们最开始的例子而言,你可以通过对原有列表进行分片而不是使用简单的命名操作来

避免共同引用的副作用:

>» X = (1, 2, 3] »> L = ['a', X[ : ],'b'] >» D = {'x':X(:],'y':2}

# Embed copies of X's objecr

这样做将改变图 9-2一一L 和 D 现在会指向不同的列表而不再是 X 。最终结果是,通过 X 所 做的修改只会影响 x 而不会再影响 L 和 D 。同理,修改 L 或 D 也不会影响 X 。

复制需要注意的最后一点是:无参数的分片以及字典的 copy 方法只能进行顶层复制。也就

是说,它们不能复制嵌套的数据结构(如果有的话)。如果你需要创建一个探层嵌套的数 据结构(就像我们之前编写的那些复杂结构 一 样)完整的、完全独立的复制,那么就要使 用在第 6 章中介绍过的标准库 copy 模块:

import copy X = copy.deepcopy(Y)

II Fully copy an arbitrarily nested object Y

这一调用语句能够递归地遍历对象来复制它们所有的组件。然而这是相当罕见的情况,你 也需要在使用这种方案之前慎重考虑。通常情况下,使用引用就可以了,如果使用引用还 无法实现,那么你可以再考虑使用分片和 copy 方法。

比较、等价性和真值 所有的 Python 对象也都可以支持比较操作:测试等价性、相对大小等。 Python 的比较总是

检查复合对象的所有组件,直到可以得出结果为止。事实上,当嵌套对象存在时, Python 能够自动遍历数据结构,并从左到右地应用比较,要多深就走多深。在该过程中首次发现 的区别将决定比较的结果。

这有时候也称为递归比较一对顶层对象的比较会被应用到每一 个嵌套的下 一 层对象中, 直到最底层的对象并最终得到结果。在第 19 章中,我们将看到如何为嵌套结构编写我们自 己的递归函数。就目前而言,你可以用网站中相互链接的网页来形象地类比这种嵌套结构,

而编写这种递归函数的目的在千处理这些网页。

元组、文件与其他核心类型

I

303

就核心类型而言,递归功能是默认实现的。例如,在比较列表对象时将自动比较它的所有 内容,直至找到一个不匹配或完成所有的比较:

»> Ll = [1, ('a', 3)) >» L2 = [1, ('a', 3)) >» Ll == L2, Ll is L2

It Same value, unique objects

II Equivalent? Same object?

(True, False) 在上面的例子中,且和 L2 被赋值为列表,虽然它们相等但不是同 一 个对象。回顾我们在第

6 章中所学到的, Python 的引用本质(我们在第 6 章曾经学过)有两种测试等价性的方式:



“==”运算符测试值的等价性。 Python 会运行等价性测试,并递归地比较所有内嵌对象。



"is” 表达式测试对象的同一性。 Python 测试两者是否是同一 个对象(也就是说,在存 储器中的相同地址).

在上例中,且和 L2 通过了“=="测试(它们的值相等,因为它们的所有内容都是相等的) , 但是 is 测试却失败了(它们是两个不同的对象,因此位千不同的内存区域)。我们要注意 对短字符串的运行结果:

»> S1 = , spam , »> S2 = , spam , >» S1 == s2, S1 is S2 (True, True) 在这里,我们本应又 一 次得到两个截然不同的对象碰巧有着相同的值:

"==“应该为真,

而 is 应该为假。但是因为在 Python 内部会对临时存储并重复使用短字符串做优化,所以 事实上内存中只有 一个字符串 'spam' 供 51 和 S2 共用。因此,

“is" 一致性测试结果为真。

为了得到更 一 般的结果,我们需要使用更长的字符串:

>» S1 ='a longer string' >>> S2 ='a longer string' »> S1 == s2, S1 is S2 (True, False) 当然,由千字符串是不可变的,对象缓存机制与程序代码无关一—无论有多少变量引用它们, 字符串都是无法在原位置修改的。如果一 致性测试令你感到困惑,可以回头再看 一 看第 6 章的相关内容并回忆一 下对象引用的概念。

经验法则是,"=="儿乎是所有等值检验时都会用到的运算符;而 is 则保留了极为特殊的角色 。 你会在本书后面看到一些同时用到这些运算符的情况。

相对大小比较也能递归地应用千嵌套的数据结构: 、,、,

、r>

304

'"'

-l', 32 ', 12 aa >>LL __ 11 ,'(( ,' ))

I

__

第9章

-

»> L1 < L2, L1 == L2, L1 > L2 (False, False, True)

# Less, equal, greater: tuple of results

因为内嵌的 3 大千 2 ,所以这里的 L1 大千 L2 。现在你可以明白上面最后一行的结果的确是 一 个含有三 个对象的元组-我们输入的 三 个表达式的结果(这是 一 个不带圆括号的元组 的例子)。 更确切地说, Python 按照下面的方式来比较不同的类型:



数字在转换成必要的公共最高级类型后,比较数值的相对大小。



字符串按照字母字典顺序比较(按照 ord 函数返回的字符集编码顺序), 一 个字符接 一 个字符地比对直至末尾或发现第 一 处区别 ("abc"




[1,2]) 。



集合是相等的,当且仅当它们含有相同的元素(更正式地说,如果两者互为子集)。 集合的相对大小比较采取子集和超集的检验标准。



字典通过比较排序之后的 (key,

value) 列表来判断是否相同。 Python 3.X 中不支持字

典的相对大小比较,但 Python 2.X 及更早的版本中却能支持它们,并且是通过比较排

序之后的 (key, value) 列表。



非数字混合类型的相对大小比较(如 1

< 'spam' )在 Python 3.X 中会出错。但在

Python 2.X 中却允许这样的比较,不过要使用 一种基千类型名字符串的固定且任意的排 序规则。通过代理,这也适用千排序(排序会在内部使用比较) :非数字的混合类型 集合体不能在 Python 3.X 中排序。

一 般来说,结构化对象的比较就像是你把对象写成字面最,并从左到右一次一 个地比较所 有组件。在后面的章节中,我们将会看到其他可以改变比较方法的对象类型。

Python 2.X 和 3.X 混合类型比较和排序 接着上一节对列表的介绍, Python 3.X 中对干非数值类型的比较使用了相对大小测试标准, 虽然不是等价性,但它能通过代理来排序,而代理在内部进行了相对大小检测。在 Python 2.X

中这些都能工作,但混合类型通过一个任意的顺序进行比较:

c:\code> c:\python27\python >>> 11 =='11' False >» 11 >='11' False » > ['11','22']. sort() »> [11,'11'].sort()

#

Equal切 does

not convert non-numbers

/12.X compares by type name string: int, str II Ditto for sorts

但 Python 3 . X 除了数值类型和可手动转换的类型以外,均不允许混合类型的相对大小比较:

元组.文件与其他核心类型

I

30s

c:\code> c:\python33\python >>> 11 =='11' False >» 11 >='11' TypeError: unorderable types: int() > str() » > ['11','22']. sort() »> [11,'11'].sort() TypeError: unorderable types: str() < int() »> 11 > 9.123 True >» str(11) >='11', 11 >= int('11') (True, True)

# 3.X: equality works but magnitude does not

# Ditro for sorts

# Mixed numbers convert to highest

八'pe

#Ma1111alconversionsforcetheiss11e

Python 2.X 和 3.X 中的字典比较 再来看一下前面小节介绍的优点中的倒数第二点。在 Python 2.X 中,字典支持相对大小比较,

就等效千比较排序的键/值列表:

C:\code> c:\python27\python » > D1 = {'a': 1,'b': 2} »> D2 = {'a':1,'b':3} »> D1 == D2 False »> D1 < D2 True

# Dictionary equality: 2.X + 3.X # Dictionary magnitude: 2.X only

正如第 8 章中所提到的,在 Python 3.X 中,字典的相对大小比较被移除了,因为在进行等 价性判断的时候,相对大小比较会导致很大的计算开销(在 Python 3.X 中,等价性使用了 一种优化的实现方案,而不是直接比较排序后的键/值列表)

C:\code> c:\python30\python »> D1 = {'a':1,'b':2} >» D2 = {'a': 1,'b': 3} »> D1 == D2 False >» D1 < D2 Type Error: unorderable types: diet() < diet() Python 3 .X 中的替代方式是 ,要么编 写循环根据键 比较值,要么手动比较排序的键/值列 表一一-items 字典方法和内置函数 sorted 就足够了:

>» list(D1.items()) [('b', 1), ('a', 2)) »> sorted(D1.items()) [('a', 1), ('b', 2)) >>>

>» sorted(D1.items()) < sorted(D2.items()) True »> sorted(D1.items()) > sorted(D2.items()) False

306

I

第9章

# Magnitude test in 3.X

手动实现相对大小比较需要编写更多的代码,但在实际应用中,许多需要用到字典相对大 小比较的程序,都发展出了比这里的替代方案以及 Python 2.X 中的自带功能都要高效的方

式,用来比较字典中的数据。

Python 中 True 和 False 的含义 要注意,上 一节最后两个例子的元组返回的测试结果代表着真和假。它们被打印成 True 和

False, 但现在我们要认真地使用这种逻辑测试,这里开始正式讲述 True 和 False 的真正 含义。

在 Python 中(与大多数程序设计语言 一样),整数 o 代表假,整数 1 代表真。不过,除此之外, Python 也把任何空数据结构视为假,把任何非空数据结构视为真。更一般地,真和假的概 念是 Python 中每个对象的固有属性:每个对象非真即假,如下所示 :



数字如果等千零则为假,反之则为真。



其他对象如果为空则为假,反之则为真。

表 9-4 给出了 Python 中对象的真、假值的例子。 表 9-4: 对象真值的例子 对象



" spam "

True False

[1, 2]

True

[]

False

{'a': 1}

True

{}

False

1

True

0.0

False

None

False

作为一个应用,因为对象的真假是客观存在的,所以通常会看到 Python 程序员编写像 “if X:“ 这样的测试,其中,假设 X 是一个字符串,那么 ,等同千 "ifX != ":"。换句话说, 你可以测试对象自身看它是否包含任何内容,而不是将它们与 一 个空的同类型对象作比较 (下一章将更详细地介绍 if 语句)。

None 对象 Python 还有一 个特殊对象: None (表 9-4 中最后 一 行),总被认为是假。我们曾经在第 4

元组、文件与其他核心类型

I

307

章介绍过 None, 这是 Python 中 一 种特殊数据类型的唯 一值, 一 般都起到一个空占位符的作

用(与 C 语言中的 NULL 指针类似)。 例如,回顾一 下,对千列表来说,你是无法为偏移批赋值的,除非这个偏移量已经存在(如 果你进行越界的赋值操作,列表并不会自动变长)。要想预先分配一 个含 100 项的列表, 你可以在 100 个偏移猛的每一个上赋值 None 对象:

>» L = [None] *

100

>>>

»> L [None, None, None, None, None, None, None,... ] 这样做并不会限制列表的大小(它随后仍然可以增长或缩短),而只是直接预先设置了 一

个初始大小,从而允许之后的索引赋值。当然,你也可以用同样的方式以 0 来初始化一 个 列表,但最佳实践是如果还不知道列表的内容则使用 None 。 记住, None 不是意味着“未定义”

(undefined) 。也就是说, None 是某些内容,而不是没

有内容(不要从 “None" 的字面意思上理解)一一-None 是 一个真正的对象,并且有 一块真

实的内存, None 是由 Python 给定的 一 个内置名称。在本书随后的内容中可以看到这一 特殊 对象的其他用法;比如我们将在本书第四部分看到,它同时还是函数的默认返回值(如果

函数没有运行到 return 语句就结束的话)。

bool 类型 当我们讨论真值的话题时, Python 的布尔类型 bool (在第 5 章中曾介绍过)只不过是扩展 了 Python 中真、假的概念。正如第 5 章所述,内置的 True 和 False 关键字只是整数 1 和 o 的定制版本而已。按照这个新的类型的实现方式,这其实只是先前所说的真、假概念的较小

扩展而已,这样的设计只是为了让真值更加显式:



当显式地用在真值测试时, True 和 False 关键字就变成了 1 和 o. 但它们使得程序员的 意图更明确。



交互式命令行下运行的布尔测试的结果被打印成单词 True 和 False, 而不是 1 和 0, 使 得程序的结果更明确。

像 if 这样的逻辑语句中,井不是要求只能用布尔类型。所有对象本质上依然可以为真或假,

对千其他类型而言,本章提到的所有布尔概念都仍然是可用的。 Python 还提供了 一 个内登 函数 bool, 它可以用来显式地测试一 个对象的布尔值(例如,如果结果为 True, 就说明该 对象是非零或非空的)

»> bool{l) True

»> bool('spam') True

308

I

第9章

>» bool({}) False 在实际中,我们很少真正注意到逻辑测试所产生的布尔类型,因为 if 语句和其他的选择工 具都会自动地使用布尔结果。我们将在本书第 12 章学习逻辑语句的时候再进一步介绍布尔

类型。

Python 的类型层次 作为总结和参考,图 9-3 描绘了 Python 中所有可用的内置对象类型,以及它们之间的关系。 我们已经讨论了它们中最主要的,图 9-3 中多数其他种类的对象都相当千程序单元(例如,

函数和模块),或者暴露出的解释器内部组件(例如,栈帧和编译码)。 这里最后需要注意的一点是, Python 系统中的一切东西都是对象类型,而且可以由 Python

程序来处理。例如,可以把一个类传入函数,将其赋值给一个变址,或者将其放人一个列 表或字典中等。

类型的对象 事实上 ,即使是类型本身在 Python 中也是 一种类型 的对象:对象的类型本身,也属于 type 类型的对象(重要的事情要说三遍!)。严格地说,调用内置函数 type(X) 会返回对象 X

的类型对象。这在实际中的应用是,类型对象可以用在 Python 的 if 语句中,用来手动进 行类型比较。然而,如第 4 章所述,手动类型检测在 Python 中通常井不是明智的做法,因 为它限制了代码的灵活性。 关千类型名称需要提醒一下:从 Python 2.2 开始,每个核心类型都有一个新的内置名,

用来支持通过面向对象编写子类的类型定制: diet 、 list 、 str 、 tuple 、 int 、 float 、

complex 、 bytes 、 type 、 set 等。在 Python 3.X 中这些名称都代指类。在 Python 2.X 中, file 既是一个类型名称又是 open 的同义词,但在 Python 3.X 中并非如此。调用这些名称 其实就是调用这些对象的构造函数,而不是简单的转换函数,不过对千基本的使用来说, 你还是可以把它们当作简单函数。 此外, Python 3.X 中的 types 标准库模块提供了其他不能作为内置类型使用的类型的名称

(例如,一个函数的类型;在 Python 2.X 中, types 模块也包含了内置类型名称的同义词, 但在 Python 3.X 中并非如此),同时你也可以使用 is instance 函数来进行类型测试 。例如 , 下面所有的类型测试都为真:

type ([ 1]) == type ([ J) type([1]) == list isinstance([1], list)

# # #

Compare to type of another list Compare to list type name Test if list or customization thereof

import types def f(): pass type(f) == types.FunctionType

#

types has names for other types

元组、文件与其他核心类型

I

309

主』

』罕主主

主二主主二三



声 巳 亡

卢对象勹启 玉 一

图 9-3: 按分类组织的 Python 的主要内置对象类型 。 Python 中所有一切都是某种类型的对象,

即便是某个对象的类型!一些诸如有名元组的扩展类型可能也包含在本图中,但是更正式地说 它们并不属千核心类型

310

第 9章

因为目前你也可以为 Python 的类型编写子类,所以一般都建议采用 is instance 技术。参 考第 32 章来学习更多有关 Python 2.2 及之后版本中的内置类型子类化内容。

注意:在第 32 章中,我们将介绍 type(X) 类型测试如何更 一般地应用千用户定义的类。简而 言之,在 Python 3.X 和 Python 2.X 的新式类中,一个类实例的类型就是该实例产生自的 那个类。对千 Python 2.X 及其以前的版本中的经典类,所有的类实例都是“实例”类型

的对象,因此如果需要有意义地比较实例的类型,就应当比较实例的—class —属性。 由千我们目前还不具备类的背景知识,因此我们将推迟到第 32 章再详细介绍其他内容。

Python 中的其他类型 一个典型 的 Python 安装后的类包括:本书这一部分中我们所学的核心对象 1 我们在后面将 要遇到的函数、模块和类;以及允许作为 C 语言的扩展程序或 Python 的类一正则表达式 对象、 DBM 文件、 GUI 组件、网络套接字等。本章前面提到的有名元组从某种意义上也属

千这一 类别(第 5 章的 Decimal 和 Fraction 的界定则更为模糊)。 这些附带工具与我们至今所见到的内置类型之间的主要区别在于 : 内置类型有针对它们的

对象的特殊语言创建语法(例如, 4 用千整数,[ 1,2] 用千列表, open 函数用千文件, def 和 lambda 用千函数)。而其他在标准库中通过 import 导人来使用的模块,则通常不被认为

是核心类型。例如,为了创建一个正则表达式对象,你需要导入 re 并调用 re.compile( )。 参考 Python 的标准库手册来了解 Python 程序中可用的所有工具的完全指南。

内置类型陷阱 这将是我们核心数据类型之旅的最后 一 站。我们将一起讨论可能会困扰新手的(有时也会 让专家感到头疼的)一些常见问题和相应的解决办法,以此来结束这一章的内容。其中有 些是我们已经讨论过的内容,但它们的确非常重要,值得我们再 一 次提醒读者。

赋值创建引用,而不是复制 由于引用是非常核心的概念,因此我在这里重申一次:可变对象的共享引用在你的程序中 至关重要。例如,下面的例子中赋值给 L 的列表对象不但被 L 所引用,也被赋值给 M 的内部 列表所引用。如果在原位置修改 L, 也会同时修改 M 的引用:

>» L = [1, 2, 3] >» M = ['X', L,'Y'] »> "'

['X', [1, 2, 3],'Y'] = 0

»> l[1] >» "'

# Embed a reference to L

#

Changes M too

['X', [1, o, 3],'V']

元组、文件与其他核心类型

I

311

这种影响通常只是在大型程序中才显得重要,而共享引用往往就是你真正想要的。如果这 种联动改变的效果不是你想要的,那么你可以显式地对它们进行复制来避免对象共享。就 列表而言,你总能通过使用无参数的分片来创建一个顶层复制:

>» »> >» >»

L = (1, 2, 3) M = ['X', L(:],'Y'] L[1] = o L

# Embed a copy of L (or list(L), or L.copy()) # Changes only L, not M

[1, o, 3]

»> M ['X', [1, 2, 31,'V'] 记住,分片参数默认为 0 以及被分片序列的长度一如果两者都省略,分片就会抽取序列

中每一 项,并由此创建一个顶层复制( 一个新的、非共享的对象)。

重复会增加层次深度 序列重复就好像是多次将一个序列加到自己身上。然而,当可变序列进行嵌套时,最终的 效果可能并不总是如你所愿。例如,下面这个例子中的 X 赋值了重复四次的 L, 而 Y 赋值了 包含重复四次的 L 的列表:

>» L = [4, 5, 6] >» X = L * 4 >» Y = [ L] * 4

#

Like /4, 5, 6] + /4, 5, 6] + ...

# [LJ + [L] + ... = [L, L, …]

>» X [4,

s,

6, 4,

s,

6, 4,

s,

6, 4,

s,

6]

>» V

s,

[[4,

6], [4,

s,

6], [4, 5, 6], [4,

s,

6]]

由千 L 在赋值给 Y 时是被嵌套的,因此 Y 中包含了指回原本 L 的列表的引用,因此出现了与 上一节一样的联动修改副作用:

»> L[1] = 0 »> X [4,

s,

6, 4,

# Impacts Y but not X

s,

6, 4, 5, 6, 4,

s,

6]

»> V [[4, O, 6], [4, O, 6], [4, O, 6], [4, O, 6]] 这种看上去咬文嚼字的刻意而为,却真的有可能发生在你的代码中。解决这一问题的方法 与前面所述一样,因为这就是另一种创建共享可变对象引用的方式一如果你不想共享引

用,那么就要进行复制:

»> >» »> »>

L = (4, 5, 6)

Y = [list(L}] • 4 L[1] = o

# Embed a (shared) copy of L

y

[[4, 5, 6], [4, 5, 6], [4, 5, 6], [4, 5, 6]]

312

I

第9章

更为微妙的是,尽管 Y 不再跟 L 共用同一个列表对象了,但 Y 对应的列表对象所嵌套的引用

却又指向了同一个对象。如果你同时想避免这种共享,你必须保证每一个嵌套都得到一份 单独的副本:

>» Y[O)[l] >» V

= 99

#

All four copies are still the same

[ (4, 99, 61, [4, 99, 6), [4, 99, 6), [4, 99, 6))

»> L = [4, 5, 6] >» V = [list(L) for i in range(4)] >» V [ [4, s, 61, (4, s, 61, [4, s, 6], (4, s, 6))

>» Y[o][1] = 99 »> V [ [ 4, 99, 6), [ 4, 5, 6], [ 4, 5, 6], [ 4, 5, 6]] 如果你还记得重复、拼接以及分片只是在复制操作数对象的顶层的话,那么这些情况就比 较好理解了。

注意循环数据结构 在前面的练习中我们遇到过这个问题:如果一个复合对象包含指向自身的引用,就称之为 循环对象。无论何时 Python 在对象中检测到循环,都会打印成[...],而不会陷人无限循 环(而在较早版本中则会出现这种情况) :

»> L = ['grail') »> L.append(L) »> L

# Append reference to same object # Generates cycle in object: [… j

['grail', [...)] 除了要知道方括号中的三个点号代表对象中带有循环之外,你还应当知道另一种情况,因 为这会造成错误一一如果不小心,循环结构可能导致程序代码陷人无法预期的循环当中。

例如,有些需要遍历结构化数据的程序将已经被访问过的元素保存在列表、字典或者集合中,

并通过检测来确定它们是否陷人循环。你可以参考附录 D 中的“第一部分练习题”来了解 有关这一问题的更多信息。也可以从第 19 章中对递归的通用化讨论、第 25 章的 reloadall. PY 程序和第 31 章的 ListTree 类中找到相关的解决方案。 经验法则就是:除非你真的需要,否则不要使用循环引用,同时确保你在相应的程序中考

虑到了这种情况。虽然存在很多创建循环引用的不错的理由,但你的代码应清楚如何处理 它们,否则将会招来不必要的麻烦。

不可变类型不可以在原位置改变 最后,你不能在原位置改变不可变对象。如果需要的话,你必须通过分片、拼接等操作来 创建一 个新的对象,再赋值回原本的引用:

元组`文件与其他核心类型

I

313

T = (1, 2, 3)

T[2] T

=

=

4

T[:2) + (4,)

# Error!

#OK:(], 2, 4)

这看起来可能像是多余的代码编写工作,但是这种做法带来的优势是,当你使用元组和字

符串这样的不可变对象的时候,可以避免前面提到的可变性带来的陷阱。因为无法在原位 置修改,就不会产生列表的联动修改副作用。

本章小结 我们在这一章学习了最后两种主要的核心对象类型:元组和文件。我们看到,元组支持所 有 一 般的序列操作,它们拥有一些方法,由千不可变性而不能进行任何在原位置的修改, 以及通过有名元组类型进行扩展。我们也看到,文件是由内置 open 函数返回的对象,井且 提供读写数据的方法。

同时,我们探讨了如果要存储到文件当中,应当如何在 Python 对象和文件中字符串之间进 行来回转换,而我们也了解了 pickle 、 json 和 struct 模块的高级用法(对象序列化和二

进制数据)。最后,我们复习了一些所有对象类型共有的特性(如共享引用),并总结了 对象类型领域内常见的错误("陷阱")来结束这一章的内容。

本书接下来的部分要转向讨论 Python 的语句语法~下 一章是这一 部分的开头,我们将会介绍 Python 适用千所有语句类型的通用语法模型。不过, 继续学习下面的内容之前,还是让我们先来做一做本章的习题,然后做一下本部分结尾的 实验练习,复习 一 下与类型相关的概念。语句大体上就是创建并处理对象,所以在继续学 习之前,你需要做这些练习,确保你已经熟练掌握本部分的内容。

本章习题 I.

如何确定元组的大小?为什么这个工具是内置工具?

2.

编写 一个修改元组中第一个元素的表达式。在此过程中,( 4, 5, 6) 应该变成 (1, 5, 6) 。

3.

文件的 open 调用中 ,处理模式参数的默认值是什么?

4.

你需要使用什么模块把 Python 对象存储到文件中,而不需要亲自将它们转换成字符串?

5.

如何一次性递归地复制嵌套结构的所有组成部分?

6.

Python 在什么情况下会判断一个对象为真?

7.

我们的目标是什么?

314

I

第9章

习题解答 I.

内置函数 len 会返回 Python 中任何容器对象的长度(即所含 元素的数目),这也包括 元组。 len 是内悝泊数而非类型方法,因为它适用干多种对象。通常,内置函数和表达

式可以适用千多种对象类型;而方法只特定于某 一 种对象类型.尽管有些方法可以在 多种类型上使用(例如, index 适用千列表和元组)。

2.

由干元组是不可变的,因此你无法在原位罚修改元组,但是你总可以创建 一 个满足需

求的新元组。已知 T = (4, 5, 6) , 要修改第一个元素时,你可以借助分片和拼接来创

建新元组: T = (1,) + T[l:] (回顾一下,单个元素的元组需要额外的逗号)。你也可 以把元组转成列表,在原位置进行修改,再转换成元组,但这样做太麻烦了,在实际

中也很少用到。如果你知道对象需要在原位置进行修改,就应当选用列表。

3.

文件的 open 调用中处理模式参数默认值为 'r' :读取输入。对千读取文本文件、只需

传人外部文件名即可。

4.

pie kle 模块可用于把 Python 对象存储到文件中,而无需将其显式地转成字符串。 struct 模块功能相近,但 struct 假定数据在文件中被打包成二进制格式 1 json 模块同

理在 一组受限的 Python 对象与 JSON 格式之间进行来回转换。

5.

如果要递归地复制嵌 套结构 X 的所有组成部分 ,就需要导入 copy 模块,然后调用

copy.deepcopy(X) 。在实际中,这也很 罕见;引用往往就是你所需要的行为,而浅层复

制(如 alist[ :]、 aDict.copy( )、 set(aSet)) 通常就能足够满足绝大多数的复制。

6.

如果对象是非零数字或非空集合体对象,就被认为是真。内置的 True 和 False 关键字,

从本质上讲,就是预先定义的整数 1 和 o 。

7.

“学习 Python"

“进人本书下一部分”或者”寻找圣杯”译注 3 都是不错的回答。

第二部分练习题 这一部分涉及内仅对象的基础知识。与往常 一 样,我们介绍了 一些新的概念,所以当你完

成阅读时(或者还未读完), 一 定要翻到附录 D 看 一 看答案。如果你时间有限,建议你从 练习题 10 和练习题 11 开始(这部分习题中最实际的),然后在时间允许的情况下,再从

头做到尾。不过这全都是基础知识,所以尽品多完成一些;由干编程是 一 门需要动手的学科, 因此不存在能够替代练习的捷径。

l.

基础。以交 互式命令行重现第二部分各章表格中的常见类型的表达式。首先,打开 Python 交 互式命令行解释器,输入下列表达式,然后试着解释每种情况所产生的结果。

注意,在这些情况中 . 分号用作语句分隔符,从而把多条语句压缩成单独的 一 行:例如,

译,主 3

作者在这里使用了《 Monty

Python: Holy

Grail 》的 " 梗" 。

元组 , 文件与其他核心类型

I

31s

“X=1;X” 会赋值并打印出 一个变量(本 书下一部分将介绍更多关千语句语法 的知识)。 还要记住表达式之间的逗号通常会创建元组,即便没有包在外围的圆括号:例如, X,Y,Z

是一个含三个元素的元组,当 Python 打印它的时候会带上圆括号。

2 ** 16 2/5,2/5.0 "spam" + "eggs" S = "ham" eggs "+ S S * 5 S[ :o]

"green %sand %s" % ("eggs", 5) 'green {o} and {1}'. format('eggs', S) ('x',)[o] ('x','y')[1] L = [1,2,3] + [4,5,6] L, L[:], L[:o], L[-2], L[-2:] ([1,2,3] + [4,5,6])[2:4] [L[2], L[3]] L. reverse(); L L.sort(); L L. index(4) {'a':1,'b':2}['b'] D = {'x':1,'y':2,'z':3} D['w']=O D['x'] + D['w'] 0[(1,2,3)] = 4 list(D.keys()), list(D.values()), (1,2,3) in D [ []], [" ", [ ], (), {},None]

2.

索引运算和分片运算。在交互式命令行下,定义一个名为 L 的列表,其中包含四个字符 串或数字(如 L=[0,1,2,3]) 。然后,实验一下下面所示的边界情况。你可能不会在真

实的程序中看到这些例子(尤其不会看到这里所使用的奇特的形式),但它们可以促 使你思考底层的模式,并且其中一些可能在不那么刻意的情况下有用一一例如,越界分

片可以工作,只要这样分片结果能满足你的需求的话:

a.

索引越界时会发生什么(如 L[4]) ?

b.

分片越界时又会发生什么(如 L[-1000:100])?

C.

最后,如果你试着反向抽取序列,也就是较低边界值大千较高边界值(如

L[ 3 : 1]), Python 会怎么处理?提示:试着对此分片 (L[3:1] =['?'])赋值,看看 此值置千何处。你觉得这与分片越界属千相同的现象吗?

3

索引、分片以及 del 。定义另 一个列表 L ,包含四个元素,然后赋值一个空列表给其中

一个偏移量(如 L[2]=[]) 。发生了什么?然后,赋值空列表给分片 (l[2:3]=[]) 。现在,

316

I

第 9章

又发生了什么?回顾一下,分片赋值运算会删除分片,并将新值插入分片原先的位置。 del 语句会删除偏移量、键、属性以及名称 。将 del 用在列表上来删除一个元素(如

del L[O]) 。如果你删除整个分片,会发生什么 (del L[l:]) ?当你把非序列对象赋 值给分片时,会发生什么变化 (L[l:2)=1)?

4.

元组赋值运算。输人下列代码:

>» X = , spam , »> Y ='eggs , »> X, Y = Y, X 当输入这段代码后,你觉得 X 和 Y 会发生什么变化?

5

字典键。考虑下列代码片段:

>» 0 = {} >» D[l] ='a' »> 0[2] ='b' 我们学过字典不是按偏移址读取的,那么这里是在做什么?你能从下面的代码中找到

答案吗?

(提示:字符串、整数以及元组同属千哪种类型分类?)

»> D[(l,

2, 3)] ='c'

>>> D

{1:'a', 2:'b', (1, 2, 3):'c'}

6.

字典索引运算。创建一个字典,名为 D, 包含三个元素,而键为 'a' 、

'b' 和 'c' 。如果

你试着以不存在的键进行索引运算 (D['d']) ,会发生什么?如果你试着赋值到不存在

的键 'd'

(如 D['d']='spam'), Python 会怎么做?这一点和列表越界的赋值运算以及

列表引用有何联系?这看上去是否像是变量名的规则?

7.

通用操作。执行交互式命令行测试来回答下列问题:

a

在不同或混合的类型之间使用“+”运算符(例如,字符串+列表、列表+元组), 会发生什么?

b

当操作数之一是字典时,

“+”运算符还能工作吗?

c.

append 方法能用于列表和字符串吗?可以对列表使用 keys 方法吗?

(提示:

append 对其主调对象做了什么假设吗?)

d

8

最后,当你对两个列表或两个字符串做分片或拼接时,会得到什么类型的对象?

字符串索引运算。定义 一个有四个字符 的字符串 5: 5 = "spam” 。然后输入下列表达式:

s[o][o][o][o][o] 。你对此所发生的事有任何线索吗?

(提示:回顾一 下,字符串是字

符的集合体,但 Python 中的字符是单字符的字符串)。如果你将此索引表达式作用千

['s",'p','a','m'] 这样的列表,还能运行吗?为什么?

元组、文件与其他核心类型

I

317

9.

不可变类型。再次定义一个四字符的 S 字符串: S = "spam” 。编写一 个赋值运算,把字 符串改成 “slam", 只使用分片和拼接运算。你可以只用索引运算和串接进行相同运算

吗?索引赋值运算可以吗? 10. 嵌套。编写 一 个表示你个人信息的数据结构,其中包括:姓名(名、中间名、姓)、年龄、 职业、住址、电子邮箱以及电话号码。你可以采用任何喜欢的内置对象类型(列表、元组、

字典、字符串、数字)的组合来创建这个数据结构。然后,通过索引运算读取数据结 构的各个元素。就该对象而言,某些结构会不会比其他结构更合适?

l I. 文件。编写一个脚本,能够创建名为 myfile.txt 的新输出文件,并把字符串 “Hello file world!" 写入该文件中。然后编写另 一 个脚本,打开 myfile.txt, 把读人其内容并 打印出来。从系统命令行执行你的两个脚本。新文件是否出现在执行脚本所在的目录

中?如果将传入 open 的文件名添加了其他目录路径,又会怎样?注意:文件 write 方 法并不会向你的字符串添入换行符;如果你想在文件中换行,就要在字符串末尾显式地 增加\n 。

3181

第9章

第三部分

语句和语法

第 10 章

Python 语句简介

现在我们已经熟悉了 Python 核心内置对象类型,在这一章里,我们将探讨它的基本语句类型。

照例,我们先在这里大概介绍一下语句的语法,在接下来的几章中,我们将深人介绍每种 语句的细节。

简单地说,语句就是你编写来告诉 Python 程序要处理哪些事务的句子。如果程序诚如第 4

章所言”使用`材料'来处理`事务'",那么语句便是你指定程序处理事务的方式。或 者更正式地说, Python 是一门过程化的、基千语句的语言。你可以组合这些语句来指定一 个由 Python 实现的过程 (procedure) ,从而达到程序的目标。

重温 Python 的知识结构 另 一 种理解语句角色的方式就是再回顾 一 下我们在第 4 章介绍的概念层次,在那里我们 曾讨论了内置对象以及相应的操作这些对象的表达式。本章将在更高 一 级的层次上讨论 Python 程序的结构:

I.

程序由模块构成。

2.

模块包含语句。

3

语句包含表达式。

4

表达式创建并处理对象。

从基础上看, Python 编写的程序实质上是由语句和表达式构成的。表达式用千处理对象,

并被嵌人到语句中。语句编写实现了程序操作中更大的逻辑,也就是说,语句使用并引导

表达式处理我们前几章所学的对象。此外,语句还是对象被创建的地方(例如,赋值语旬

321

中的表达式),而有些语句会创建全新的对象(函数、类等)。从宏观上看,语句总是存 在千模块中,而模块本身又是由语句来管理的。

Python 的语句 表 10-1 总结了 Python 的语句集。 Python 中的每种语句都有它特有的目标和语法(语法定 义了这种语句的结构),不过在后面的章节我们会发现,许多语句有着相似的语法形式,

同时某些语句的作用又相互重叠。表 10-1 同时给出了每种语句符合其语法规范的例子。在 你的程序中,这些代码单元可以执行操作、重复任务、做出选择,以及参与构成更大的程

序结构等。 本书这一部分将按照表格中从上到下的顺序进行讨论,并一直讲到 break 和 continue 。我 们曾经非正式地接触过表 10-1 的一部分语句。本书这一部分将会补充我们之前略过的细节, 同时介绍剩下的 Python 基本过程语句并涵盖整体语法模型。表 10-1 中位置靠后部分的语

句和更大的程序单元有关(函数、类、模块以及异常),这些语句会引人更大、更复杂的 程序设计思路,所以我们将用一章的篇幅对每个概念进行阐述。更多相关的语句(例如删

除各种组件的 del 语句)会在本书的其他部分进行讨论,你也可以参考 Python 标准手册进 行学习。 表 10-1

:

Python 语句

语句

功能

示例

赋值

创建引用值

a, b ='good','bad'

调用与其他表达式

运行函数

log.write("spam, ham")

print 调用

打印对象

print('The Killer', joke)

if/elif/else

选择动作

if "python" in text: print(text)

for/else

序列迭代

for x in my list: print(x)

while/else

通用循环

while X > Y: print ('hello')

pass

空占位符

while True: pass

break

循环退出

while True: if exittest(): break

continue

循环继续

while True: if skiptest(): continue

def

函数与方法

def f(a, b, c=1, *d): print(a+b+c+d[o])

322

I

第 10 章

表 10-1 : Python 语句(续) 语句

功能

示例

return

函数结果

def f(a, b, c=1, *d): return a+b+c+d[o]

yield

生成器函数

def gen(n): for i inn: yield i*2

global

命名空间

x ='old' def function(): global x, y; x ='new'

nonlocal

命名空间 (3.X)

def outer(): x ='old' def function(): nonlocal x; x ='new'

import

获取模块

import sys

from

获取属性

from sys import stdin

class

构建对象

class Subclass(Superclass): staticData = [] def method(self): pass

try/except/finally

捕捉异常

try: action() except: print ('action error')

raise

触发异常

raise EndSearch(location)

assert

调试检查

assert X > Y,'X too small'

with/as

上下文管理器 (3.X 、 2.6+)

with open('data') as myfile: process(myfile)

del

删除引用

del del del del

data[k] data[i:j] obj.attr variable

从技术上讲,表 10-1 中包含了 Python 3.X 中的语句形式。你可以把它看作是 一 次快速的预 习,但它还不算完整。下面是关于表 10-1 内容的一些说明:



赋值语句有很多种语法形式,如第 11 章所介绍的:基本的、序列的、扩展的等等。



从技术上讲, print 在 Python 3.X 中既不是一个保留字,也不是一条语句,而是一个内 置函数调用;由于它几乎总是作为一条表达式语句被执行(而且经常独占一行),因 此通常将它看作是一种语句类型。我们将在第 11 章学习打印操作。

Python 语句简介

I

323



从 Python 2.5 开始,

yield 同样也是一个表达式,而不是一条语句 1 如同 print, 它通

常作为一条表达式语旬被使用,因此它被包含在本表中。正如我们将在第 20 章中见到

的那样,脚本偶尔也会赋值或使用它的运行结果 。与 print 不同,作为 一个表达式 的 yield 同时也是一个保留字。 表中的大多数语句也适用千 Python 2.X 版本,除了个别情况一如果你使用 Python 2.X, 下面是对 2.X 版本的一些注意事项:



在 Python 2.X 中, nonlocal 不可用;正如我们将在第 17 章看到的,有其他替代方式 可以在 2.X 中实现 nonlocal 语句的可写状态保持效果。



在 Python 2.X 中, print 是一条语句,而不是一个内置函数调用,其具体语法将在第 11 章中介绍。



在 Python 3.X 中, exec 是一个代码执行内置函数;而在 Python 2.X 中, exec 则是一条 语句,并有着特定的语法,因为 exec 支持带有圆括号的形式,所以你可以在 Python 2.X 代码中通用地使用其 Python 3.X 的调用形式。



在 Python 2.5 中, try/except 和 try/finally 语句合井了:这两种形式曾经代表不同的

语句,但是现在我们可以在同一条 try 语句中使用 except 和 finally 。



在 Python 2.5 中 with/as 是一个可选的扩展,并且它默认是不可用的,除非你通过运

行 from _future

—import

with_statement 语句(参见第 34 章)来显式地启用它。

两种不同的 if 在深入介绍表 10-1 中的任何一条具体语句之前,我们应首先了解在 Python 代码中不能编

写什么,之后再开始学习 Python 语句语法。只有弄明白这些,你才能将 Python 与你之前 见过的其他编程语言的语法模型进行比较和区分。 考虑下面这个 if 语句,它采用类 C 语言的语法来编写:

if (x > y) { x = 1; y = 2;

这有可能是 C 、 C++、 Java 、 JavaScript 或类似语言的语句。现在,让我们看一看 Python 语

言中与之等价的语句:

if

> y:

X

X = 1

y

= 2

你的第一感觉可能是等价的 Python 语旬更加简洁、易懂和紧凑,也就是说它所包含的语法

324

I

第 10 章

成分更少。这是刻意设计的——作为脚本语言, Pytbon 的目标之一就是通过让程序员少打 些字来使生活轻松一些。 更具体地讲,当对比两种语法模型时,你会注意到 Python 增加了一项新元素,而类 C 语言 中的 三项元素在 Python 程序中消失了。

Python 增加的元素 Python 中新的语法成分是冒号(:)。所有 Python 的复合语句(即内嵌了其他语句的语句) 都有相同的 一 般形式,也就是首行以冒号结尾,首行下 一 行嵌套的代码往往按缩进的格式

书写,如下所示:

Header line: Nested statement block 冒号是不可或缺的,遗漏掉冒号可能是 Python 新手最常犯的错误之一—一这绝对是在我教 过的 Python 培训课上见过无数次的错误。事实上,如果你刚刚开始学习 Python, 几乎很快

就会忘记这个冒号。如果忘了,你就会得到 一 个错误信息。不过大多数 Python 友好的编辑

器都很容易发现这一个错误。最终,输入冒号将变成潜意识里的一种习惯(习惯到你也许 会在类 C 程序代码中输入冒号使那些语言的编译器产生很多有趣的错误信息)。

Python 删除的元素 虽然 Python 需要额外的冒号,但是在类 C 语言程序中加入的三项语法成分通常不需要在 Python 中加入。

括号是可选的 首先是语句顶端位千检测表达式两侧的一对括号:

if (x < y) 许多类 C 语言的语法都需要这里的栝号。而在 Python 中并非如此,我们可以省略括号但语

句依然会正常工作:

if

X

y) { X = 1;

y

=

2;

} 取而代之的是,在 Python 中,我们统一把嵌套代码块里所有的语句向右缩进相同的距离, Python 能够 使用语句的实际缩进来确定代码块的开始与结束 :

if

X

> y: X = 1

y = 2

所谓缩进,是指这里的两条嵌套语句左侧的所有空白。 Python 并不在乎怎么缩进(你可以

326

I

第 10 章

使用空格或制表符)或者缩进多少(你可以使用任意多个空格或是制表符)。实际上,两 个嵌套代码块的缩进可以完全不同。语法中仅仅规定对千一个给定的嵌套语句块,在它之

中的所有语句都必须向右缩进相同的距离。如果不这么做就会出现语法错误,在你修改缩 进之前程序将无法运行。

为什么采用缩进语法 对千习惯了类 C 语言的程序员而言,缩进规则乍一看可能会有点特别,但这正是 Python 精 心设计的特点,也是 Python 迫使程序员写出统一 、整齐并具有可读性代码的主要方式之一 。

这就意味着你必须根据程序的逻辑结构,以垂直对齐的方式来组织程序代码。其结果是让 代码更 一 致井具有可读性(不像类 C 语言所写的多数程序那样)。 更明确地讲,根据逻辑结构将代码对齐是使程序具有可读性的重要步骤,程序因此具备了 可重用性和可维护性,对自己和他人都是如此。实际上,即使你在看过本书之后不使用

Python, 也应该在任何块结构的语言中对齐代码让程序更具可读性。 Python 将其设计为语 法的 一 部分来强制程序的书写,但这也是在任何程序语言中都非常重要的 一 点,并对代码

的可用性起着重要的作用。

大家的经历可能不同,但当我还在做全职开发的时候,大部分的工作都是在处理许多程序 员做过很多年的大而老的 C+ +程序。几乎不可避免的是,每位程序员都有自己的缩进代码 的风格。例如,别人常常让我把 C++的 while 循环开头写成下面这样 :

while (x > o) { 在我们深人研究缩进之前,有 三 四种供程序员在类 C 语言程序中安排大括号的方式,而软 件公司或组织时常饱受这种官方争斗的影响,通常也只能通过制定代码风格标准手册来解

决这些潜在选项(对干如何用编程解决问题的主题而言,这似乎有点跑题了)。尽管如此, 还是先看一下我时常在 C++代码中碰到的情况。第一个写代码的人的缩进为 4 个空格:

while (x > o) {

这个人后来挤进管理层,只能由某个喜欢再往右缩进 一 点的人来接替他的位置:

while (x > o) {

那个人后来又遇到了其他的机会(终结了他对代码的暴政),而某个接手这段代码的人喜 欢少缩进一些:

Python 语句简介

I

327

while (x > o) {

凡此种种。最后,这个代码块由闭大括号(})终止,大括号当然能让它变成“块结构的代码”

(他讽刺地说)。不!在任何有块结构的语言中(无论是 Python 还是其他语言),如果嵌 套代码块缩进的不一致,它们都将很难被读者解释、修改或者重用,因为代码不再能形象 地反应其逻辑含义。可读性至关重要,而缩进又是可读性的主要构成。 如果你用类 C 语言写过很多程序的话,也许你曾经因为下面的例子而头疼过。考虑下面这 个 C 语言的语句:

if (x) if (y)

statement1; else statement2; 这个 else 是属千哪个 if 的呢?令许多人惊讶的是,这个 else 在 C 语言中属千嵌套的 if

语句 (if (y)) ,即使它看上去很像是属于外层 if

(x) 的。这是 C 语言中经典的陷阱.

而且可能导致读者完全误解代码并用不正确的方式进行修改还一直找不出原因,直到产生

巨大的错误为止! 这种事在 Python 中是不可能发生的,因为缩进很重要,程序看上去是什么样就意味着它将 如何运行。考虑一个等价的 Python 语句:

if x: if y:

statementl else: statement2 这个例子里, else 垂直对齐的 if 就是其逻辑上的 if (外层的 if x) 。从某种意义上讲, Python 是一种 WYSIWYG 语言-~所见即所得 (what you see is what you get) 。因为不管 是谁写的,程序看上去的样子就是其运行的方式。 如果这样还不足以突显 Python 语法的优越性,那么来听听下面这个故事。在我职业生涯的

早期,我在一家成功地用 C 语言开发系统软件的公司工作,而 C 语言并不要求一致的缩进。 即使是这样,当我们在下班之前把程序代码上传到源代码控制系统的时候,公司会运行一

个自动化的脚本来分析代码中的缩进。如果这个脚本检查到我们没有一致的缩进程序代码,

我们将在第二天早上收到自动发出的关千此事的电子邮件,同时我们的老板们也会收到!

328

1

第 10 章

我的意思是,即使是某个不要求这样做的语言,优秀的程序员也都知道一致地使用缩进对 千程序代码的可读性和质星有着至关重要的作用。 Python 将它升级到语法层次的事实,被

绝大多数人视为是语言的一种鲜明特点。 最后,请记住一点,目前几乎每个对程序员友好的文本编辑器都有对 Python 语法模型的

内置支持。例如,在 IDLE Python GUI 中,当输人嵌套代码块时代码行会自动缩进,按下 Backspace 键就会回到上一层的缩进,井且你可以自定义 IDLE 在嵌套块里面将语句往右缩 进多少。缩进没有绝对的标准:常见的是每层四个空格或一个制表符,但是你想怎么缩进 以及缩进多少通常由你自己决定(除非你在一个拥有政策和手册规定这一标准的公司工作)。

嵌套越探的代码块向右缩进得越厉害,越浅就越靠近前一个块。

经验法则是,不应该在同一段 Python 代码中混合使用制表符和空格,除非你能保证一致性 I 在一段给定的代码中,使用制表符或空格,但不要二者都用(实际上, Python 3.X 现在会 对使用制表符和空格的不一致性引发一个语法错误,参阅本书第 12 章的介绍)。其次,不 应该在任何结构化语言中混合使用制表符和空格来缩进—一如果下一位程序员使用与你不 同的编辑器来显示制表符,那么这样的代码可能会引发较大的可读性问题。类 C 语言可能

会帮程序员绕过这样的问题,但它们也不应该这么做:结果总会被搞得乱糟糟的。

不管用哪种语言编程,都应该缩进一致以保证可读性。实际上,如果你在此前的职业生涯 中没有学习如何做到这点,很可能是你的老师给你留下了伤害。大多数程序员,尤其是那 些必须阅读其他人的代码的程序员,都认为 Python 将这一点上升到语法层次是一笔巨大的 财富。此外,自动产生缩进而非大括号对于实际中必须产出 Python 代码的编辑器而言,井 不是什么难题。总的来说,还是按照你在类 C 语言中该做的那样去做,不过无论如何都要

去掉大括号,这样你的程序代码就满足 Python 的语法规则了。

几种特殊情况 正如我们曾经提到的,在 Python 的语法模型中:



一行的结束就终止了该行语旬(没有分号)。



嵌套语句通过它们的缩进,来划分代码块(没有大括号)。

这两条规则几乎涵盖了实际中你会写出或看到的所有 Python 程序。然而, Python 也提供了 一些特殊用途的规 则来定制语句和嵌套语句的代码块。这些规则不是必须的而且应当被谨

慎使用,然而程序员在实践中发现了它们的作用。

语句规则的特殊情况 虽然语句一般都是一行一条,但是 Python 中也可能出现某一行挤进多条语句的情况,这时 它们由分号隔开:

Python 语句简介

I

329

a= 1; b = 2; print(a + b)

# Three statements on one line

这是 Python 中唯一需要分号的地方:作为语句分隔符 。 不过,只有当摆到 一起的语句本身 不是复合语句时才能这么做。换句话说,你只能把简单语句放在一 起,例如赋值操作、打 印操作,和函数调用。类似 if 检测和 while 循环这样的复合语句还是必须出现在自己的行

里(否则,你就可以把整个程序挤在同 一 行上,这样很有可能让你在团队里不受欢迎!)。 语句的另 一 个特殊规则在功能上是相反的:你可以让一 条语句的范围横跨多行。为了能实

现这一 操作,你只需要用 一 对括号把语句括起来就可以了,这包括圆括号(())、方括号([]) 或者字典的大括号({})。任何括在这些符号里的程序代码都可横跨好儿行:直到 Python

遇到包含闭合括号的那一行你的语句才结束 。 例如,下面是一 个横跨几行列表的字面 撬 :

mylist = [1111, 2222,

3333] 由于程序被栝在一对方括号里,因此 Python 就会接着运行下 一 行,直到遇见闭合的方括号

为止。大括号包含的字典(以及 Python 3.X 和 2.7 中的集合字面址、字典解析以及集合解析) 也可以使用该方法横跨数行,并且圆栝号可以处理元组、函数调用和表达式。分行书 写 的 后续行的缩进是随意的,不过常识告诉我们为了让程序具有可读性,那几行也应该对齐。 括号是可以包含 一切的一因为任何表达式都可以被包含在内,所以只要插人一个 左 括号 , 你就可以到下 一 行接着写你的语句: X =(A+ B + C + D)

顺便说 一 句,这种技巧也适用于复合语句。在任何你常要编写 一 个大型表达式的地方,只 要把它括在括号里,就可以在下一行接着写:

if (A== 1 and B == 2 and C == 3): print('spam'* 3) 有 一 条比较老的规则也允许我们跨越数行一—当上一行以反斜线结束时,可以在下 一 行继 续: X = A + B+ \ C + D

# An error-prone older alternative

但是这种方法已经过时了,目前不再提倡使用这种方法,因为要关注并维护反斜线比较困 难,而且这种做法相 当 脆 弱 和易错一反斜线后面 一 定不能有 空 格,如果不小心加上了 空

330

I

第 10 章

格,那么将会引发一 个 SyntaxError 语法错误译让 l 。另外这也是另 一个倒退回 C 语言的例子, 因为反斜线时常用千 "#define" 的宏定义。再强调一 次,在 Python 的世界中,做 Python

程序员该做的事,不要做 C 程序员做的事。

代码块规则的特殊情况 如前所述,嵌套代码块中的语句 一 般通过向右缩进相同的量来相互关联。这里给出 一 个特 殊情况,说明复合语句的主体可以出现在 Python 的首行冒号之后:

if x > y: print(x) 这样我们就能够编写单行 if 语句、单行 while 和 for 循环等。不过,只有当复合语句本身

不包含任何其他复合语句的时候,才能这么做。也就是说,只有简单语句才可以跟在冒号 后面,比如赋值 、 print 、函数调用等 。较复杂的语句必须独占一行。复合语句的附带部分 (例如下 一 节中 if 的 else 部分)也必须独占 一 行。复合语句体也可以由几个简单语句组

成并用分号隔开,但这种做法已经越来越不受欢迎了。 一 般来说,虽然井不总是这样做,但如果你将所有语句都分别放在不同的行内并总是将嵌 套代吗块缩进,那么程序代码会更容易读懂井且便千后期的修改。此外, 一 些代码分析和 投盖工具井不能把压缩到同 一 行中的多条语句,以及 一 条单行复合语句的头部和主体区分

开来。在 Python 中,保持代码简单对你来说总是有利的。对千一 些难以阅读的 Python 代码,

你可以使用 一 些特殊的方式来编写。由千这么做需要花费大量的工作,因此你或许可以把 时间用在更有价值的地方。

不过,在实际应用中这些规则有 一 个非常重要而且很常见的特例(中断循环的单行 if 语句 加 break 的情况),而为了介绍更多的 Python 语法,让我们继续学习下 一 节并编写 一 些实 际的代码吧。

简短示例:交互式循环 我们在后续几章学习 Python 具体的复合语句时,会看到所有这些语法规则的 实际应 用,但 它们在 Python 语言中的工作方式几乎是相同的。首先,让我们用 一 个简单的例子来说明在

实际应用中组合使用语句语法和语句嵌套的方式.并顺便介绍一 些语句 。

一个简单的交互式循环 假设有人要你写 一 个 Python 程序,要求在控制窗口与用户交互。也许你要把输入的数据传

送到数据库,或者读取将参与计算的数字。不管出干什么目的,你需要 写 一 个能够读取用 译注 I : 此处如果在反针线后面加上任何字符、都会得到一个 “SyntaxError : unexpected

character

after line continuation character" 的错误提示、因此译者在这里修正了原书中的说明。 Python 语句简介

I

331

户键盘输入数据的循环并打印每次读取的结果。换句话说,你需要写一个标准的“读取 I 计算/打印"的循环程序。 在 Python 中,这种交互式循环的典型模板代码可能如下所示:

while True: reply = input('Enter text:') if reply=='stop': break print (reply. upper()) 这段代码使用了 一 些新的理念,而另 一 些则是我们已经见过的:



这段代码利用了 Python 的 while 循环,它是 Python 中最通用的循环语句。我们稍后会 介绍 while 语句的更多细节,但简单来说 while 的组成为: while 这个单词之后跟一 个

其结果为真或假的表达式,再接一个当顶端测试为真(这里的 True 看作是永远为真) 时不停地迭代的嵌套代码块。



我们之前曾在本书中见到过的 input 内置函数,在这里用千获得通用控制台输入:它 会打印可选的参数字符串作为提示,并返回用户输入的字符串。按照下面的注意边栏 所述,在 Python 2.X 中你需使用 raw_input 来替代 input 。



利用嵌套代码块特殊规则的单行 if 语句也在这里出现: if 语句体出现在冒号之后的首 行,而不是在首行的下一行缩进。这两种方式哪一种都可以,但在这里我们就省了一行。



最后, Python 的 break 语句用于立即退出循环一在完全跳出循环语句之后,程序会 从循环之后的部分继续执行。如果没有这个退出语句, while 循环会因为测试总是真值 而永远循环下去。

事实上,这样的语句组合实质上是指:从用户那里读取一行输入井转换成大写字母打印, 直到用户输入 “stop" 为止。还有一些其他的方式可以编写这样的循环,但这里我们采用的

是在 Python 程序中很常见的一种形式。

需要注意的是,在 while 首行下面嵌套的 三行代码的缩进是相同的。由千它们是以垂直的 方式对齐的,因此它们是与 while 测试相关联的、重复运行的代码块。源文件的末尾或者

另 一 条缩进较少的语句都足以终止这个循环体块。 当这段代码运行时,无论是交互式地运行还是作为脚本文件运行,我们都能从中得到的某 种程度上的交互~个例子中的所有代码都在本书示例包中的文件 interact.py 内:

Enter text:spam SPAM

Enter text:42 42 Enter text:stop

332

I

第 10 章

注意;版本差异提示:上面这个例子是针对 Python 3.X 编写的。如果你使用 Python 2.X 或之前

的版本,这段代码也能工作,但你在所有的例子中应该使用 raw_input 来代替 input, 并且你可以在 print 语句中省略外围的圆括号(尽管它们并没有威胁)。事实上,如果 你学习了示例包中的 interact.py, 你将看到这是自动生成的一为了支持对 Python 2.X 的兼容性, interact.py 会在运行时 Python 主版本是 2 的情况下对 input 进行重置 ("input~

最终以 raw_input 运行) :

import sys if sys.version[o] =='2': input= raw_input

#

2.X compatible

Python 3.X 中的 raw_input 被重新命名为 input, 并且 print 成为了一个内置函数而不 是 一 条语句(下一 章将对 print 进行更多的介绍)。在 Python 2.X 中也有 input, 但它

会尝试把一 个输入字符串作为 Python 代码进行求值,这也许不符合此处的使用场景。

Python 3.X 中的 eval(input( ))能发挥与 Python 2.X 中 input 同样的作用。

对用户输入做数学运算 我们的脚本能够运行,但假设现在的需求不是把文本字符串转换为大写字母,而是想对数

值的输入做些数学运算。例如,求平方,也许是用来误导用户的 一 个输入年龄的程序。我 们首先可能会尝试使用下面的语句来达到想要的效果:

>» reply ='20' »> reply ** 2 .. . error text omitted... TypeError: unsupported operand

type(s) 于or

**

or pow():'str ' and'int'

不过这并不奏效,因为(我们在本书前一 部分讨论过)除非表达式里的对象类型都是数字, 否则 Python 不会在表达式中自动转换对象类型。而用户的输入返回给我们脚本的结果一定 是 一 个字符串,因此我们无法使用数字的字符串求幕,除非我们手动地把它转换为整数:

»> int(reply) •• 2 400 有了这个概念之后,我们现在可以重新编写循环来执行必要的数学运算。将以下代码输人

到 一 个文件中井测试它:

while True: reply = input('Enter text:') if reply=='stop': break print(int(reply) ** 2) print('Bye') 像以前一样,这个脚本用了 一 条单行 if 语句在 “stop" 处退出,但是它也能转换输入来进

行必要的数学运箕。这个版本也在底端加了 一 条结束信息。因为最后一 行的 print 语句没

Python 语句简介

I

333

有像嵌套代码块那样缩进,所以也就不会被看作是循环体的一部分,因此只能在退出循环

之后运行一次:

Enter text:2 4

Enter text:40 1600 Enter text:stop Bye 注意:使用提示:从现在开始我假设这段代码存储在一个脚本文件中井通过命令行、 IDLE 菜

单选项,或其他任何我们在第 3 章中见到的文件加载工具运行。再次提醒,它在本书的 示例中称为 inreract.py 。然而,如果你在交互式命令行下输入这段代码,请确保在最后

的 print 语句之前包含一个空行(例如,按下 Enter 键两次),以终止循环。这暗示你 不能完全复制并粘贴代码到一个交互式命令行中:在交互式命令行下必须有一 个额外的 空行,在脚本文件中则不需要。在交互式命令行下,最后的 print 意义并不大,毕竟你 需要在与循环交互之后才能编写它!

通过测试输入数据来处理错误 到目前为止一切都好,但仍需注意当输入无效时会发生什么现象:

Enter text:xxx ... error text omitted... Value Error: invalid literal for int() with base 10:'xxx' 面对错误输入时,内置的 int 函数会引发 一 个异常。如果要使我们的脚本足够健全,你可

以用字符串对象的 isdigit 方法提前检查字符串的内容:

»> S ='123• , >» T ='xxx »> S.isdigit(), T.isdigit() (True, False) 这样就给了我们一个在本例中进一步嵌套语句的理由。下面这个新版本的交互式脚本使用 一组完整的 if 语句来解决错误引发的异常:

while True: reply= input('Enter text:') if reply == •stop• : break elif not reply. isdigit(): print (• Bad! • * 8) else: print(int(reply) ** 2) print('Bye•)

334

I

第 10 章

我们会在第 12 章进一步研究 if 语句,这是在脚本中相当轻批级的逻辑编写工具。』 f 语句 完整形式的构成是:

if 这个关键字后面接测试以及相对应的代码块,一个或多个可选的

eli f (else if) 测试以及代码块,以及一个可选的 else 部分和末尾的 一 个相对应的代码块 作为默认行为。 Python 会按照由上至下的顺序,执行第 一 个测试为真所对应的代码块。如 果所有测试都为假,就执行 else 部分。

上面例子中的 if 、 elif 以及 else 部分都是同一个语句的组成部分,因为它们都垂直对齐(也 就是共享相同层次的缩进)。 if 语句从 if 这个单词横跨至最后一行的 print 语句之前。接着,

整个 if 区块是 while 循环的一部分,因为它们全部缩进在该循环首行下。 一 旦你理解这点, 像这样的语句嵌套就显得很自然了。

当我们运行新脚本时,程序会在错误发生前捕捉它,然后在继续运行前打印错误消息(你 可能期望这 一 点在下一个版本中能有所改进),但输人 “stop" 仍然会跳出程序,输入合法 数字仍然会披平方:

Enter text:5 25

Enter text:xyz Bad!Bad!Bad!Bad!Bad!Bad!Bad!Bad! Enter text:10 100 Enter text:stop

用 try 语句处理错误 上面的解法能够工作,但当你阅读到本书后面就会知道,在 Python 中,处理错误最常用的 方式是使用 try 语句来捕捉错误并完全修复。本书的第七部分将探入探索 try 语句,作为 预习,使用 try 语句要比上一个版本看上去更简洁 一 些:

while True: reply= input('Enter text:') if reply=='stop': break try: num = int(reply) except: print ('Bad!'* 8) else: print(num ** 2) print('Bye') 这个版本的工作效果和上一 版相同,但作为显式错误检查的替代,我们转而将常规代码包 装到 try 代码块中,并通过异常处理的方式来解决出错情况。换句话说,当 一 个异常发生时,

我们会直接对它进行响应,而不是预先检测一 个错误。

Python 语句简介

I

33s

try 语句是另一种复合语句,遵循与 if 、 while 相同的模式。 try 语句的组成是: try 关键 字后面跟着主要代码块(我们尝试运行的代码),再跟上提供异常处理代码的 except 部分,

以及当 try 部分没有引发异常时执行的 else 部分。 Python 会先执行 try 部分,然后运行 except 部分(如果有异常发生)或 else 部分(如果没有异常发生)。

从语句嵌套的角度来看,因为 try 、 except 以及 else 这些关键字全都缩进在同一层次上, 所以它们全都被视为同一条 try 语句的一部分。要注意,这里的 else 部分是与 try 相关联, 而不是与 if 相关联。正如我们之前见过的,在 Python 中, else 既可出现在 if 语句中,也 可以出现在 try 语句以及循环中,而 else 的缩进会告诉你它属千哪个语旬。在这个例子中, 由千 else 和 try 为相同的缩进层级,因此 try 语句从单词 try 开始,一直到 else 语句下 面缩进的代码结束。这段代码中的 if 语句是一个单行语句,并且在 break 之后结束。

对浮点数的支持 我们在本书后面会重新介绍 try 语句。就目前而言,你只需知道,因为 try 可用于拦截任 何错误,所以它减少了你必须编写的错误检查代码的数量,而且这也是处理不常见的情况

的通用办法。如果我们很肯定 print 不会发生错误,那么这个例子还可以更简短:

while True: reply = input('Enter text:') if reply=='stop': break try: print(int(reply) ** 2) except: print('Bad!'* B) print ('Bye') 再比如,如果我们想支持输入浮点数而不只是整数,那么使用 try 语句将比手动的错误检

测要容易得多

我们可以直接运行一个 float 调用并捕获其异常:

while True: reply = input('Enter text:') if reply=='stop': break try: print(float(reply) ** 2) except: print('Bad!'* 8) print ('Bye') 现在已经没有针对字符串的 isfloat 方法了,所以这一基千异常的方法可以使我们避免在一 个显式的错误检查中分析所有可能的浮点语法错误。当以这种方式编写程序时,我们可以

输人一个更大范围的数字,但错误和退出仍然与之前的工作效果相同:

Enter text:so 2500.0 Enter text:40.5 1640.25 336

I

第 10 章

Enter text:1.23E-100 1.5129e-200 Enter text:spam Bad!Bad!Bad!Bad!BadlBad!Bad!Bad! Enter text:stop Bye 注意:我们在第 5 章和第 9 章中使用过 Python 的 eval 调用,它能够在字符串或文件中转换数 据类型,这对此处的 float 同样适用,并且允许输入任意表达式 ("2** 100" 也是合法 的输入,不过这对我们假设程序是在处理年龄的情况来说,好像有点奇怪!)。 eval 是 一个很强大的概念,同时也会引发前面章节中提到的安全问题。如果你不能信任一 个代

码字符串的来源,你可以使用类似 int 或者 float 这种更加严谨保守的方式。

Python 中的 exec 在第 3 章中被用来运行从文件中读入的代码,它和 eval 很相似(但 exec 会将字符串视作 一条语句而不是表达式 ,并且没有返回值),而它的 compile 调

用会把常用的代码字符串预编译成字节码对象,从而加快运行速度。运行 help 命令可 以帮助你查看这些方法的细节。如前所述,在 Python 2.X 中 exec 是 一 条语句,而在

Python 3.X 中 exec 是一 个函数,所以最好在 2.X 的手册中查阅它的条目。在第 25 章中, 我们也会使用 exec 来通过名称字符串载入模块,那个例子将展现它更加动态的角色。

嵌套三层深的代码 现在,我们来看看脚本的最后一 次改进。如果有必要的话,嵌套甚至可以让我们再深人一步。 例如,我们可以根据有效输入的相对大小,来扩展我们前面仅用千整数的脚本,将其分流 到另 一 组备选分支上:

while True: reply= input('Enter text:') if reply =='stop': break elif not reply. isdigit(): print('Bad!'* 8) else: num = int(reply) if num < 20: print('low ') else: print(num ** 2) print('Bye') 这个版本加入了 一 条 if 语句,将其嵌套在另 一 条 if 语句(嵌套在 while 循环中)的 else 分句中。当 代码像这样是条件化的或重复化的时候,我们只 需再 向右缩进即可。结果就像 前几个版本那样,不同的是我们现在可以为小千 20 的数字打印 ”low"

Enter text:19 low Python 语句简介

I

337

Enter text:20 400 Enter text:spam Bad!Bad!Bad!Bad!Bad!Bad!Bad!Bad! Enter text:stop Bye

本章小结 本章我们快速浏览了 Python 语句的语法,介绍了编写语句和代码块的通用规则。如你所学,

Python 中 一般情况下是每行编写一条语句,而嵌套代码块中的所有语句都要缩进相同的址 (缩进是 Python 语法的 一 部分)。然而,我们也看到这些规则的 一 些特例,包括连续行以

及单行测试和单行循环。最后,我们把这些想法落实到 一 个交互式命令行下的脚本中,示 范了 一些语句并在实际中展示了语句的语法。 在下 一章 中,我们要开始逐一深入探讨 Python 基础的过程化语句。不过,所有语句都遵循 本章介绍的通用规则。

本章习题 l.

Python 中省略了类 C 语言中的哪三项必备语法成分?

2.

Python 中的语句一般是怎样终止的?

3

在 Python 中,嵌套代码块内的语句 一 般是如何相互关联在一起的?

4

如何让一条语句横跨多行?

5.

如何在 一 行上编写复合语句?

6.

Python 中是否有正当的理由在语句的末尾使用分号呢?

7.

try 语句的作用是什么?

8.

Python 初学者最常犯的编程错误是什么?

习题解答 1.

类 C 语言需要在一 些语句的测试表达式两侧使用圆括号,需要在每条语句末尾使用分 号,以及在嵌套代码块周围使用大括号。

2.

Python 中 一行的结尾就是该行语句的终止。此外,如果一条以上的语句出现在同 一行上, 则可以使用分号分隔 1 同样,如果 一 条语句跨过数行,可以用语法上的闭合括号终止

该语句。

3

338

嵌套代码块中的语句都必须缩进相同数目的制表符或空格。

I

第 10 章

4

你可以编写一个横跨多行的语句,只要将其封闭在圆括号、方括号或大括号内即可。 当 Python 遇到一行含有一对括号中的闭合栝号,语句就会结束。

5.

复合语句的主体可以移到开头行的冒号后面,但前提是主体仅由非复合语句构成。

6.

只有当你需要把多条语句挤进一行代码时。即使在这种情况下,也只有当所有语句都

是非复合语句时才行得通。此外,因为这样会让程序代码难以阅读,所以通常不建议 这么做。

7.

try 语句是用于在 Python 脚本中捕捉和恢复异常(错误)的。这通常是取代你自行手

动检查错误的一种替代方案。

8.

忘记在复合语句首行末尾输人冒号,是初学者最常犯的错误。如果你是一个 Python 新手,

并且还没有犯过这样的错误,你可能很快就会遇到。

Python 语句简介

I

339

第 11 章

赋值、表达式和打印

现在,我们已经快速地介绍了 Python 语句的语法,这一章要开始探入具体地学习 Python 语句。本章从基础开始介绍,内容包括赋值语句、表达式语句和打印。我们之前已经接触

过这些语句的用法,不过本章要详细介绍之前漏掉的重要细节。尽管它们都相对简单,但 这些语句的类型都有可选的多种形式,一且你开始编写实际的 Python 程序,就能体会到它

们的便捷之处。

赋值语句 我们已经使用 Python 的赋值语句把对象赋给一 个变晨。其基本形式是在等号左侧写赋值语

句的目标,而要赋值的对象则位于等号右侧。左侧的目标可以是变量或对象组件,而右侧 的对象可以是任何通过计算能得到对象的表达式。在绝大多数情况下,赋值语句都很简单, 但下面的这些特性需要你用心记住:



赋值语句创建对象引用。如第 6 章所述, Python 赋值语句会把对象引用存储在变量或

数据结构的组件中。赋值语句总是创建对象的引用,而不是复制对象。因此, Python 变量更像是指针,而不是数据存储区域。



变量在首次赋值时会被创建。 Python 会在首次将值(即对象引用)赋给变 量时创建其 变量名,所以你不必提前预声明变最名。有些(井非全部)数据结构组件也会在赋值 时被创建(例如,字典的键值对,一些对象的属性)。 一且赋值后,每当这个变量出 现在表达式中时,就会替换成其引用的值。



变量在引用前必须先赋值。使用尚未被赋值的变址是一种错误。如果你试图这么做,

那么 Python 将引发异常,而不是返回某种模糊的默认值。这点在 Python 中十分重要, 因为变最不是预声明的。假设 Python 对程序中使用的未赋值变垦提供了默认值而不是

把它们当作错误来对待,那么你就很难在程序中找出变量名的拼写错误。

340



某些操作会隐式地进行赋值。在本节中,我们关心的是“="语句,但赋值出现在

Python 的许多上下文中。例如我们后面会学到,模块导入、函数和类的定义、 for 循 环变量,以及函数参数等全都是隐式赋值运算。因为所有赋值语句的工作原理都相同, 所以这些上下文都会在运行时把名称和对象引用相互绑定。

赋值语句形式 尽管赋值是 Python 中通用且无处不在的概念,但本章中我们主要讨论赋值语句。表 11-1 展

示了 Python 中不同赋值语句的形式,以及它们的语法含义。 表 11-1 :赋值语句形式 运算

解释

spam='Spam'

基础形式

spam, ham ='yum','YUM'

元组赋值(基于位置)

[spam, ham] = ['yum','YUM']

列表赋值(基于位置)

a, b, c, d ='spam'

推广的序列赋值

a, *b ='spam '

扩展序列解包 (Python 3.X)

spam= ham='lunch'

多目标赋值

spams += 42

增量赋值(等价于 spams = spams + 42)

表 11-1 中的第 一种形式是目前为止最常见的:它将 一 个名称(或数据结构组件)绑定到单 个对象上。实际上,仅仅使用这种基本的形式就可以处理所有的 工 作了。表中其他各行都

是可选的,但在实际中程序员常常会觉得它们很便千使用: 元组及列表解包赋值

表中第二和第 三 行形式是相关的。当你在“="左侧编写元组或列表时, Python 会按照

位置把右侧的对象和左侧的目标从左至右相配对。例如,表中第二 行,字符串 'yum' 被赋值给名称 spam ,而' YUM' 则被赋值给名称 ham 。在这种情况下, Python 在内部会 为右侧的各项创建 一 个元组,这也是“元组解包赋值”名字的由来。

序列赋值

在较新的 Python 版本中,元组和列表赋值语句得到了推广,并成为现在所谓的序列 赋值的 一 种特殊情况 。 也就是说,任何值的序列都可以赋值给任何名称的序列,而

Python 会按照位置 一 次性完成所有的赋值。实际上,我们可以混用不同种类的序列来 匹配赋值。例如,表 11-1 的第四行,把名称的元组和字符的字符串对应起来: a 赋值 为 's I'b 赋值为 'p' 等。

扩展的序列解包 仅在 Python 3.X 中, 一种新形式的序列赋值允许我们更灵活地选择待赋值序列的 一部

赋值表达式和打印

I

341

分。例如,表 11-1 中的第五行,用右侧的字符串的第一个字母来匹配 a' 用剩下的部 分来匹配 b: a 赋值为 's I'b 赋值为 'pam' 。这为原本只能通过手动分片来得到赋值结

果的方式,提供了另一种简单的替代方案。 多目标赋值 表 11-1 的第六行指的是多重目标形式的赋值语句。在这种形式中, Python 赋值相同对

象的引用值(最右侧的对象)给左侧的所有目标。表 11-1 中,名称 spam 和 ham 两者都 赋值了同 一 个字符串对象 'lunch' 的引用。效果与我们写成 ham ='lunch' 以及 spam

= ham 是 一 样的,这是因为 ham 得到了原始的字符串对象(换句话说, spam 和 ham 并 没有得到独立的两份副本,而是得到了同 一 个对象)。 堵址赋值

表] 1-J 的最后一 行是增昼赋值 (augmented assignment) 语句的例子。它是 一种将表达 式和赋值语句结合到 一 起的简写形式。例如 spam+= 42 等价千 spam= spam+ 42, 但 增篮形式的输入较少,而且通常执行得更快。此外,如果操作主体是可变的井且支持

这 一 操作,那么增盘赋值会选择原位置更新而不是复制对象,从而加快运行速度。在 Python 中,每一 种二 元表达式运算符都有对应的增掀赋值语句。

序列赋值 我们已在本书中使用和探索过基础的赋值语句,所以这里就不再展开。下面是 一 些序列解 包赋值语句的简单例子:

% python >» nudge = 1 >» wink = 2 »> A, B = nudge ,正nk »> A, B

# Basic assignment #九1p/e assignment # Like A= nudge; 8 = wink

(1, 2)

»> (c, D] = (nudge, wink] »> C, D

# List assignment

(1, 2) 要注意在该交互式命令行下,我们实际上在第 三 行编写了两个元组。其实,我们只是省略 了它们的括号。 Python 会把等号右侧元组内的值和左侧元组内的名称互相匹配,然后一 次

性完成所有的赋值。 元组赋值代表着 Python 中 一种很常见的编程技巧,我们在第 二部分练习题的解答中曾有所 介绍。因为在语句执行时, Python 会为右侧变最原本的值创建 一 个临时元组,所以解包赋 值也能用千交换两个变批的值,而不需要你另外创建一 个临时变批。也就是说,右侧的元 组会自动记住变屈原先的值:

»> nudge = »> wink = 342

I

1 2

第 11 章

»> nudge, wink = wink, nudge »> nudge, wink

# Tuples: swaps values # Like T = nudge; nudge = wink; wink= T

(2, 1) 事实上, Python 中原本的元组和列表赋值形式已经得到了推广,从而能在右侧接受任意类

型的序列(实际上,也可以是可迭代对象),只要长度等千左侧序列即可。你可以将值元 组赋给变量列表,字符串中字符赋给变量元组,等等。在所有这些情况下, Python 都将按

照位置由左至右,把右侧序列中的元素赋值给左侧序列中的变量:

>» [a, b, c] >» a, C

= (1, 2, 3)

# Assign tuple of values to list of names

= "ABC"

# Assign string of characters to tuple

(1, 3)

»> (a, b, c) »> a, C ('A','C')

从技术角度讲,序列赋值语句实际上在右侧支持任意的可迭代对象,而不仅局限千任何序列。

可迭代对象是更通用的一种分类,包括物理的(如列表)和虚拟的(如一个文件的行)两 种集合,我们在第 4 章中给出过它简单的定义,而它在之后将不断地出现。我们会在第 14 章和第 20 章学习可迭代对象的时候再巩固这一知识点。

高级序列赋值语句模式 虽然我们可以在“=”两侧混用相匹配的序列类型,但右侧元素通常要与左侧变最保持数目 相同,否则会产生错误。 Python 3.X 允许我们使用更为通用的扩展解包*语法,我们会在 下一 节详细介绍。但通常在 Python 3 . X 中(在 Python 2 . X 中也是如此),赋值的目标数目 和 主 体对象数目必须一致:

>» string ='SP肌' >» a, b, c, d = string »> a, d

# Same number on both sides

('S','M') # Error if not •.. error text omitted... ValueError: too many values to unpack(expected 3)

>» a, b, c = string

要想更灵活的话,我们可以在 2.X 和 3.X 中使用分片 。 下面给出几种不同的分片运算方式, 来实现上面最后的情况:

»> a, b, c = string[o], string[l], string[2:] »> a, b, c

#lndexandslice

('S','P','AM')

»> a, b, c = list(string[:2]) + [string[2:]] »> a, b, c

#Slice and concatenate

('S','P','AM')

»> a, b = string[:2)

# Same, but simpler

赋值表达式和打印

I

343

»> c = string[2:] >» a, b, c ('S','P','AM')

»> (a, b), c = string[:2], string[2:] »> a, b, c

#

Nested sequences

('S','P','AM') 如这里最后的例子所示,我们甚至可以赋值内嵌的序列,而 Python 按照预期会根据其形状 解包组成部分。在本例中,我们赋值了 一个两元素元组,其中的第 一 个元素是 一 个内嵌的

序列(字符串),这与下面的写法 一模一样:

»> ((a, b), c) = ('SP','AM') »> a, b, c

# Paired by shape and position

('S','P','AM') Python 先把等号右侧的第一个字符串 ( 'SP') 和左侧的第一 个元组 ((a,

b) )配对,然后

一 次赋值一个字符,接着把整个第 二 个字符串 ('AM' )一次性赋值给变量 c 。在这个过程中, 左侧对象的序列嵌套的形状必须与右侧对象的形状相同。这样的嵌套序列赋值运算是比较 少见的,但利用已知的形状条件来取出数据结构的各个组件是非常方便的。

例如我们将在第 13 章中看到该技术也能用于 for 循环,因为在循环头部,循环项袚赋给了 指定的目标:

for (a, b, c) in ((1, 2, 3), (4,

s,

6)]:

for ((a, b), c) in [ ((1, 2), 3), ((4, 5), 6)):

#

Simple tuple assignment

# Nested tuple assignment

在第 18 章的 一 个提示中,我们还将看到这种嵌套元组(实际上是序列)解包赋值形式在

Python 2.X (但不是在 Python 3.X 中)中用千函数参数列表,因为函数参数也是通过赋值 来传递的:

def f(((a, b), c)): ··· f(((l, 2), 3))

# For arguments too in Python 2.X, but not 3.X

序列解包赋值也催生了另一种 Python 中常见的用法,也就是将一系列整数赋给一组变最:

>» red, green, blue= range(3) »> red, blue (o, 2) 这将把 三个名称分别初始化为整数 0 、 l 、 2 (这相当千其他语言中的枚举数据类型)。要 理解它,你需要知道 range 内置函数会产生连续整数的列表(仅在 3.X 版本中,如果你想

一次显示它所有的值,就需要额外包裹一个 list 调用) :

»> list(range(3))

[o, 1, 2)

344

I

第 11 章

# list() required in Python 3.X only

range 调用在第 4 章简单介绍过;因为 range 一般用千 for 循环中,所以我们会在到第 13 章再详细介绍它。

另 一个元组赋值语句的应用场景,就是在循环中把序列分割为开头和剩余两部分,如下所示:

»> L = (1, 2, 3, 4] »> while L: front, L = L(o], L[1:] print(front, L)

#

See next section for 3.X* alternative

1 (2, 3, 4]

2 [3, 4] 3 [4] 4 []

上述循环的元组赋值也可以编写成下面的两行,但写在一 行内常常更方便一些:

front = l[o] L = l[1:] 要注意,该程序将列表用作 一 种名为"栈"的数据结构。通常,我们也可以用列表对象的

append 和 pop 方法来实现“栈"的功能。在这里,另 一种 front

=

L.pop(o) 的写法和元

组赋值有着一样的效果,但这是在原位置进行的修改。我们将在第 13 章中学到更多关于 while 循环的内容,以及其他更好的通过 for 循环访问序列的方式。

Python 3.X 中的扩展序列解包 前一小节展示了如何通过手动分片让序列赋值更为通用的方式。在 Python 3.X (而不是

Python 2.X) 中,推广后的序列赋值让一切变得更方便。简而言之, 一个带有单个星号的名 称* X ,可以用在赋值目标中,来指定对千序列的一个更为通用的匹配方式:带星号的名称 会被赋值一个列表,该列表收集了序列中剩下的没被赋值给其他名称的所有项。这对千上

述那种将一个序列划分为其“前面”和“剩余”部分的常见编码模式而言,尤其有用。

扩展的解包的实际应用 让我们来看一 个示例。如前所述,序列赋值通常要求左侧的目标名称数目与右侧的主体对 象数目完全一致。如果长度不同,则在 2.X 和 3.X 中都将得到一个错误(除非像前面小节 介绍的那样手动地在右侧使用分片)

C:\code> c:\python33\python >» seq = [1, 2, 3, 4] »> a, b, c, d = seq »> print(a, b, c, d) 1 2 3 4

赋值表达式和打印

I

345

»> a, b = seq ValueError: too many values to unpack (expected 2) 然而在 Python 3.X 中,我们可以在目标中使用带单个星号的名称来更通用地匹配。在如下 的后续交互会话中, a 匹配序列的第一项, b 匹配剩下的内容 :

»> a, *b = seq »> a 1



b

(2, 3, 4] 当使用一个带星号的名称时,左侧的目标数目不必等千右侧主体对象数目。实际上,带星 号的名称可以出现在目标中的任何地方。例如,在下面的交互中, b 匹配序列中的最后一项,

a 匹配最后一项之前的所有内容:

»> *a, b = seq »> a (1, 2, 3)

»> b 4

当带星号的名称出现在中间时,它将收集其他列出的名称之间的所有内容。因此,在下面 的交互中,第一项和最后 一 项分别赋给了 a 和 C, 而 b 获取了二者之间的所有内容:

»> a, *b, c = seq »> a 1

»> b (2, 3]

»>

C

4 更一般的是,无论带星号的名称出现在哪里,这个名称都会被赋值一个列表,而这个列表

会收集起所有在这个位置上没有被分配给其他名称的待分配对象:

»> a, b, *c = seq >>>

a

»> b »>

C

[3, 4] 当然,与正常的序列赋值一样,扩展的序列解包语法对千任何序列类型(再次强调,可以

是任意的可迭代对象)都有效,而不仅限千列表。下面,序列解包赋值解包了一个字符串 的字符和 一个 range (在 3.X 中返回 一个可迭代对象)

»> a, *b ='spam' »> a, b 346

I

第 11 章

('s', ['p','a','m'])

» > a, *b, c ='spam , »> a, b, c ('s', ['p','a'],'m')

»> a, *b, c = range(4) »> a, b, c (o, [1, 2], 3) 这与分片在思路上是 一 致的,但不完全相同一一一个序列解包赋值总是返回一个包含多个

匹配项的列表,而分片则返回与被分片对象相同类型的对象:

, >» s ='spam'

»> S[o], 5(1:)

# Slices are type-specific, * assignment always returns a list

('s','pam')

»> 5(0), S[l:3], 5(3) ('s','pa','m') 由于考虑到 3.X 中引入了序列解包赋值,我们处理上一 小节最后 一 个例子的列表将变得更 加简洁,因此,我们不必再手动分片来获取第一项和剩余项:

>» L = [1, 2, 3, 4] >» while L: front, *L = L print(front, L)

#Get/irsf, rest without slicing

1 [2, 3, 4) 2 [3, 4) 3 [ 4l 4 [)

边界情况 尽管扩展序列解包十分灵活,但一些边界情况还需要注意。首先,带星号的名称有可能只 匹配到单个的项,但总会向其赋值一 个列表:

»> seq=[l, 2, 3, 4] >» a, b, c, *d = seq »> print(a, b, c, d) 1 2 3 [4] 其次,如果剩下的内容不能匹配带星号的名称,那么它将赋值 一 个空的列表,不论该名称 出现在哪里。如下所示, a 、 b 、 c 和 d 已经匹配了列表中的每 一 项,因此 Python 会给 e 赋

值 一 个空的列表,而不是将其作为错误情况对待:

>» a, b, c, d, *e = seq »> print(a, b, c, d, e) 1 2 3 4 []

赋值表达式和打印

I

347

»> a, b, *e, c, d = seq »> print(a, b, c, d, e) 1 2 3 4 []

最后,如果使用了多个带星号的名称或是名称数目少千值序列长度,同时没有带星号的名 称(像前面一样),亦或是带星号的名称自身没有被编写到一个列表中,都会引发错误:

»> a, *b, c, *d = seq SyntaxError: two starred expressions in assignment »> a, b = seq ValueError: too many values to unpack (expected 2) »> *a = seq SyntaxError: starred assignment target must be in a list or tuple

>» *a, = seq »> a [1, 2, 3, 4]

一种有用的便捷形式 记住,扩展序列解包赋值只是 一种便捷形 式。我们通常可以用显式的索引和分片实现同样

的效果(并且在 Python 2.X 中必须这么做),但是扩展序列解包赋值更容易编写。例如, 这两种方式都可以满足常见的“第一项,剩余项“分隔编程需求,但是分片方式需要更多 的工作量:

»> seq [1, 2, 3, 4] »> a, *b >» a, b

=

seq

#

First, rest

#

First, rest: traditional

(1, [2, 3, 4))

»> a, b = seq[o], seq[t:] »> a, b (1, [2, 3, 4))

同理,两种方式也能处理常见的"剩余项,最后项“分隔需求,但是新的扩展解包语法明 显可以节省很多代码输入:

»> *a, b = seq »> a, b

# Rest, last

([1, 2, 3], 4)

»> a, b = seq[:-1], seq[-1] >» a, b

#

Rest, last: lraditional

([1, 2, 3], 4) 由千扩展序列解包语法更加简单自然,因此该语法很可能随时间的推移,在 Python 代码中 得到更广泛的使用。

348

I

第 11 章

for 循环中的应用 由千 for 循环语句中的循环变最可以是任意的赋值目标,因此扩展序列赋值也可以用在这里。 我们在第 4 章中简单介绍了 for 循环迭代工具,并将在第 13 章中正式地学习它。在 Python

3.X 中,序列扩展赋值可以出现在单词 for 之后,而此时也一般会使用较简单的变量名:

for (a, *b, c) in [(1, 2, 3, 4), (5, 6, 7, 8)]:

在这种情况下, Python 会在每次迭代中直接把下一个值元组赋给名称元组。例如,第一次 的循环执行等效千下面的赋值语句:

a, *b, c

=

(1, 2, 3, 4)

# b gets [2, 3]

名称 a 、 b 和 c 在循环体代码中,可以引用从值元组中提取的内容。其实,这并不是一种特 例,只能说是赋值用法的一种一般应用。如本章之前所述,在 Python 2.X 和 Python 3.X 中, 我们都可以借助简单元组赋值实现相同的效果:

for (a, b, c) in [(1, 2, 3), (4, 5, 6)]:

# a, b, c

= (1, 2, 3),...

我们总是可以在 Python 2.X 中使用手动分片来模拟 Python 3.X 的扩展赋值行为:

for all in [(1, 2, 3, 4), (5, 6, 7, 8)]: a, b, c = all[o], al1[1:3], all[3] 由干目前我们还没详细介绍 for 循环语句的语法,因此我们会留到第 13 章中再重拾这里的 话题。

多目标赋值 多目标赋值是将最右侧的对象依次赋值给左侧所有的名称。例如,下面给三个变扯 a 、 b 和

c 赋值了字符串 'spam': ', >» a = b = c ='spam »> a, b, c ('spam','spam','spam')

该形式等效千下面的三条赋值语句(但却更为简洁)

spam »> c = '------• »> b = C »> a = b

多目标赋值以及共享引用 要记住,在这里只有一个对象,但由三个变批共享(全部指向内存中的同一个对象)。这

赋值表达式和打印

I

349

种行为对千不可变类型而言井没问题。例如,把一组计数器的初始值设为零(回想 一下 变 址在 Python 中必须先赋值再使用,因此你在累加之前,必须先把计数器初始化为零) :

»> »> »> (o,

a = b = o b = b + 1

a, b 1)

上面对 b 的修改不会影响到 a, 原因是数字不支持原位控的修改。也就是说,只要赋值的 对象是不可变的,即使有一个以上的名称引用该对象也不会相互影响。

不过和往常 一 样,当我们需要把变址初始化为空的可变对象时(例如列表或字典),就要 格外小心了:

>» a = b = [] >» b.append(42) >» a, b ([ 42), [ 42]) 这一 次,因为 a 和 b 引用了同一个对象,所以通过 b 在原位置添加值,会直接影响到通过 a 引用的对象。这其实就是第 6 章介绍的共 享引用值现象的另一个例子而已。为了避免这

个问题,你需要在不同的语句中初始化不同的可变对象,这样每条语句就会通过运行一个 单独的表达式来创建一个独立的空间对象:

»> »> »> »>

a = []

b = [] b.append(42) a, b ([], [ 42])

#a and b do not share the same objecl

下面的元组赋值有着与上面例子相同的效果,通过运行两个列表表达式,它创建了两个不 同的对象:

»> a, b = [], []

# a and b do nor share the same objecr

增量赋值 从 Python 2.0 起,表 ll-2 列出的 一组新增的赋值语句形式开始投入使用。它们被称为增量

赋值 (augmented assignment) 是借鉴千 C 语言的一种表示形式,同时它们也只是一种简写 形式。增量赋值包含了一个二元表达式以及一个赋值表达式。例如,下面的两种格式在效 果上是相同的:

X= X+ Y X += y

3so

I

第 11 章

# Traditional form # Newer augmented form

表 11-2: 增量赋值语句

X += y

X &= y

X -= y

X I= y

X *= Y

X "= Y

X /= Y

X »= Y

X %= Y

X «= Y

X **= Y

X //= Y

增扯赋值适用千任何支持了相应 二元表达式的对象类型。例如,下面是两种让一个变址增 加 1 的方式:

>» »> >»

X

X

= 1 = X +

1

#

Traditional

#

Augmented

X

2

>» »>

X

+= 1

X

3 当作用千 一 个诸如字符串的序列时,增截形式会改为执行拼接运箕。千是,下面的第 二 行 就相当千输入较长的 S = S + "SPAM":

>» s = "spam " >» S += "SPAM" »> s

If Implied concatenation

'spamSPAM'

如表 11-2 所示,每个 Python 二元表达式运算符(即运算符的左右两侧都有值),都有相应 的增益赋值形式。例如, X *= y 执行乘法井赋值, X »= y 执行向右位移并赋值。 XII= Y (向 下取整除法)则是从 2 .2 版本开始新增的形式。

增址赋值语句有以下三个优点注 I: •

能够减少程序员的输入。



左俐只需计算 一 次。在 X += y 中, X 可以是 一 个复杂的对象表达式。在增最形式中, 代码只需运行 一 次。然而,在完整形式 X = X + Y 中, X 会出现两次,所以也必须执行 两次。因此,增量赋值语句通常执行得更快。



增址赋值有着自动选择的优化技术。对千支持原位置修改的对象而言,增量形式会自 动选择执行原位置修改运算,而不是更慢的复制运算。

上面的第 三 点需要更详细的说明。对千增址赋值,原位置运算能作为可变对象的一种优化。

注 I:

CIC++程序员要注意:尽管现在 Python 支持如 X

+= y 的语句`但它仍不支持像 C 语言

的自增和自戏运算符(如 X++ 、 x -- ) 。 这些运算符并不能很好地扯入 Python 的对象模型 , 因为 Python 对数字这类不可变对象并没有原位置改变的概念 。

赋值表达式和打印

I

351

回想一下,列表可以用各种方式扩展。要在列表的尾部添加单个元素,我们可以采用拼接 或调用列表的 append 方法:

»> L »> L »> L

= [1, 2) = L + [3]

# Concatenate: slower

[1, 2, 3]

»> L.append(4) >» L

# Faster, but in place

[1, 2, 3, 4)

而要把一组元素增加到末尾,我们可以再次使用拼接或调用列表的 extend 方法注 2: »> L = L + [5, 6] »> L [1, 2, 3, 4,

s,

6]

»> L.extend([7, 8]} >» L [1, 2, 3, 4,

s,

# Concatenate: slower

# Faster, but in-place

6, 7, 8]

在这两种情况下,拼接更不容易被共享对象引用的副作用影响,但是通常会比等价的原位 置形式运行得更慢。拼接操作必然会创建一个新对象,把加号左侧和右侧的列表都复制到

其中。相反,原位置方法调用直接在一个内存块末尾添加项(在 Python 内部的实现会更复 杂一些,但这里的描述足够说明问题)。 当我们使用增最赋值来扩展列表时,大可忽略这些细节: Python 会自动调用较快的 extend 方法,而不是使用较慢的“+"拼接运算:

>» L += [9, 10] »> L [1, 2, 3, 4,

s,

H Mapped ro

L.exrend([9, JO})

6, 7, 8, 9, 10]

然而要注意,因为列表的“+=”并不在所有的情况下都精确地等同于“+”和“="。也就是说, 对千列表,

“+=“接受任意的序列(就像 extend) ,但是拼接一 般情况下不接受:

»>L=[]

, »> L +='spam'

#+=and extend allow any sequence, but+ does not! »> L ['s','p','a','m'] »> L = L + spam TypeError: can only concatenate list (not "str") to list

增量赋值与共享引用 这种行为通常就是我们想要的,但注意这隐含了“+=”对千列表而言是原位置修改。因此,

注 2:

如笫 6 章所述 , 我们也可以使用分片赋值方法(如 L[len(L):] 色于效果一样且史加简单易记的列表 extend 方法 。

3s2

I

第 11 章

= [11,12,13]) ,但这逊

这与总是产生新对象的“+"拼接并不完全相同。对千所有的共享引用情况,当其他名称引

用了被改变的对象时,就会体现出这一区别: >» »> >» »>

L = [1, 2] #Land M reference the same object # Concatenation makes a new object # Changes L but not M

M= L L = L + [3, 4) L, M

([1, 2, 3, 41, [1, 2))

>» L = [1, 2) >» M = L >» L += [3, 4] >» L, M ([1, 2, 3, 4), [1, 2, 3, 4))

# But += really means extend # M sees the in-place change too!

这只对千列表和字典这类可变对象才有影响,而且是相当隐晦的情况(至少,直到影响你 的代码!)。与以往一样,如果你需要打破共享引用的结构,就要对可变对象进行赋值。

变量命名规则 在介绍了赋值语句后,让我们对变量名的使用做更正式的介绍。在 Python 中,当给名称赋 值后,名称就会存在。但是,当你为程序中的对象挑选名称时,需要遵循如下规则: 语法:

(下划线或字母)

+

(任意数目的字母、数字或下划线)

变量名必须以下划线或字母开头,后面接任意数目的字母、数字或下划线。_ spam 、 spam 以及 Spam_l 都是合法的变扯名,但 l_Spam 、 spam$以及@#!则不是。 区分大小写: SPAM 和 spam 并不同 Python 程序中区分大小写,包括创建的名称以及保留字。例如,名称 X 和 x 指的是两 个不同的变量。就可移植性而言,大小写在导入的模块文件名中也很重要,即使是在

文件系统不分大小写的操作系统上也是如此。这样的话,当你的程序复制到其他不同 的操作系统上时,导入的文件仍然可以工作。

禁止使用保留字 你自己定义的名称不能和 Python 语言中有特殊意义的名称相同。比如,如果使用像 class 这样的名称, Python 就会引发语法错误,但允许使用 klass 和 Class 作为变量名。 表 11-3 列出当前 Python 中的保留字(因此你不能把它们用作名称)。 表 11-3: Python 3 . X 的保留字

False

class

finally

is

return

None

continue

for

lambda

try

True

def

from

nonlocal

while

and

del

global

not

with

赋值、表达式和打印

I

353

as elif 1x t se sc ab ea ee ee sr rk

pt

if

or

import

pass

in

raise

yield

表 11-3 针对 Python 3.X 。在 Python 2.X 中,保留字的范围略有不同:



print 是一个保留字,因为打印是一条语句,而不是一个内置函数(本章稍后会更详细

地介绍)。



exec 是一 个保留字,因为它是 一 条语句,而不是一个内置函数。



nonlocal 不是一个保留字,因为 2.X 中这条语句不存在。

在更早的 Python 中,情况或多或少也有些不同,有以下 一 些变化:



with 和 as 在 Python 2.6 之前都不是保留字,只有在开启上下文管理器之后,它们才是。



yield 在 Python 2.3 之前都不是保留字,只有在生成器函数可用后它才是。



yield 自 Python 2 .5 开始从语句变为表达式,但它仍然是一个保留字,而不是一个内置 函数名。

如你所见,大多数 Python 保留字都是小写的。而且它们确实是保留字,不像本书下一部分

介绍的内置作用域中的名称,你无法通过赋值来重新定义保留字(例如, a 刓:: 1 会导致

语法错误)注 30 除了大小写是混合的,表 11-3 中的前 三项 True 、 False 和 None 的含义稍微有些特殊- 它们也出现在第 17 章将介绍的 Python 的内置作用域中,并且从技术上讲,在 2.X 中它们 是可以赋值其他对象的名称。而在 3.X 中它们则成为了完全意义上的保留字,也就是说在 脚本中它们只能代表相应的对象,并且彻底不能再赋值其他对象。所有其他的保留字都与 Python 的语法紧密相关,因此只能用在其应属的特定上下文。

此外,因为 import 语句中的模块名会成为脚本中的变 量,变量命名规 则也会限制模块的文 件名:你可以编写 and.py 和 my-code.py 这样的文件,并作为顶层脚本运行。但是你将无法

导入它们:因为文件名在没有 “py" 扩展名时,就会成为代码中的变最,所以也就必须遵 循刚才提到的所有变晕命名规则。保留字是禁用的,减号也不行,不过下划线却可以。我 们将在第五部分深入介绍模块的概念。

注 3:

至少在标准 CPython 中是如此。其他的 Python 实现可能会允许用户定义的变量名和 Python 的保留字相同 。 参阅笫 2 章荻取关于其他 Python 实现的综述,如 Jython 。

354

I

第 11 章

Python 中的废弃协议 保留宇在 Python 的不断演化中慢慢地祜入这门语言,是一个很有趣的过程。当一个 新功能可能会影响到已有代码的时候, Python 通常会先让其成为可选的,并在之后的

几个版本中开始发布“废弃”

(deprecation) 警告,直到最终正式启用新功能。这种

思路让你有足够的时间注意到警告,并且在迁移到新版本之前更新自己的代码。虽然

这不适用于像 Python 3.0 (它将直接破坏之前代码的运行)这样的主版本发布,但在 大多数情况下是成立的 。

例如,在 Python 2.2 中 yield 是一个可选的扩展,但它在 Python 2.3 中则成为了一个 标准的关键字。 yield 与生成器函数联合使用 。 这是 Python 中少数破坏向后兼容的例

子之一 。 然而 , yield 也是随着时间逐渐变化的 :它 从 2.2 中开始发布废弃警告,直到 2.3 才正式启用新版废除旧版。

类似地、在 Python 2.6 中 with 和 as 成为用于上下文管理器(异常处理的一种新形式) 的新保留宇 。 这两个单词在 Python 2.5 中不是保留字,除非用一条 from

_future_

import 语句(本书稍后介绍)手动打开上下文管理器功能。当你在 Python 2.5 中使用 with 和 as 的时候,会得到即将发生变化的警告 一除非是在 Python 2.5 的 IDLE 版

本中,那里它将默认为你开启这一功能(也就是说、在 Python 2.5 中将 with 和 as 用 作变量名称确实会产生错误 ,但 仅限于其 IDLE GUI 版本中)



命名惯例 除了上述规则外,还有 一 组命名惯例一一它们不是必要的规则,但 一般在实际中都会遵循。

例如,因为前后都带有双下划线的名称(例如,_name—)通常对 Python 解释器有着特殊意义, 所以你应该避免在变扯名中使用这种形式。以下是 Python 遵循的 一 些惯例:



以单一下 划线开头的名称 (_X) 不会被 from module import *语句导入(将在第 23 章说明)。



前后均有双下划线的名称(—x—)是系统定义的名称,对解释器有特殊意义。



以双划线开头,但结尾没有双下划线的名称(__x) 是外围类的本地(又称“重整", mangle) 变 批 (将在第 31 章说明) 。



通过交互式命令行运行时,只有单个下划线的名称(_)会保存最后一 个表达式的结果。

除了这些 Python 解释惯例外,还有其他的 Python 程序员通常会遵循的惯例。例如,本书后 面会看到类名称通常以大写字母开头,而模块名称通常以小写字母开头。此外,名称 self

虽然不是保留字,但在类中 一 般都有着特殊的作用。在第 17 章中,我们会研究另 一 更大种 类的名称,称为内置名称。它们是 Python 预先定义好的名称,但却不是保留字(因此可以

重新赋值: open= 42 行得通,不过你 一 般不会想这么做)。

赋值髦表达式和打印

I

355

名称没有类型,但对象有类型 这一小节主要是对之前所学内容的复习,但记住一定要把 Python 的名称和对象区分开来。

如第 6 章所述,对象有类型(例如整数、列表),并且可能是可变的或不可变的。另一方面, 名称(又称为变址)永远只是对象的引用。名称没有不可变的概念,也没有相关联的类型信息, 除了它们在特定时刻碰巧所引用的对象的类型。 在不同时刻把相同的名称赋值给不同类型的对象,程序允许这样做:

>» X = 0 >» x = "Hello" »> X = [1, 21 3]

#

x bound to an integer object

# Now it's as(Ti11g #

And now it's a list

在后面的例子中,你会看到名称的这种通用化的本质,是 Python 程序设计精心引入的 一种

优势。在第 17 章中你还会学到,名称存在于作用域中,而作用域决定了名称可以在何处使

用 1 一个名称赋值的位置,决定了它在哪里可见注 40 注意:要了解其他的命名建议,试着阅读 Python 的半官方风格指南 PEP 8 中 “Naming conventions" 的讨论。你可以从 http://www.python.org/devlpepslpep-0008 访问该指南,

或者上网搜索 “Python PEP 8" 。从技术上讲,这个文档把 Python 标准库代码的编码标 准形式化了。

尽管很有用,但编码标准通常的警告也适用千此 。 首先, PEP 8 所附带的细节比你在本 书中到目前为止所了解的更详细。同时坦率地说,它变得比需要的更为复杂、严格和主

观一—其中的一些建议并没有完全被 Python 程序员们接受和遵守。此外,目前一些知名 的使用 Python 的公司,也各自制定了不同的编码标准。 不过 PEP 8 确实包含了许多 Python 经验准则 , 对 Python 初学者来说,它是一 篇很好的

学习材料。但是你需要把它的推荐当作指南,而不是奉为真理。

表达式语句 在 Python 中,你也可以把表达式作为语句,也就是让表达式独占 一行。因为这样不会存储 表达式的结果,所以一般只有当表达式存在其他附加的工作效果时才有意义。表达式通常 在以下两种情况中用作语句:

注 4:

如果你用过规则史受限制的语言 , 如 C+ +,你会很惊奇地发现在 Python 中并没有 C++中 const 卢明的概念 ;

虽然 Python 中一些特定的对象是不可变的,但名称总是可以被赋值

新的对象 。 Python 也拥有在类和模块中隐藏名称的方式,但它们和 C++的卢明是不同的(如 果你关注属性隐藏,可以参阅笫 25 章对_X 模块名称的介绍,笫 31 章的_X 类名称的介绍,

以及笫 39 章的 Private 和 Public 类装饰符示例)。

356

I

第 11 章

调用由数和方法 一些函数和方法在进行工作 时而不返回值。这样的函数有时在其他编程语言中被称为

过程 (procedure) 。因为它们不会返回你所需要的值,所以你可以用表达式语句来调 用这些函数。 在交互式命令行下打印值

Python 会在交互式命令行中显示输入表达式的结果。从技术上来讲,这也属千表达式 语句。它们可以看作 print 语句的一种简写形式。 表 ll-4 列出了 Python 中一些常见的表达式语句形式。如果要调用函数或方法,你只需在函 数或方法名后面添上 一 个包含零或多个参数对象(也可以是获得对象的表达式)的圆括号。 表 11-4: 常见的 Python 表达式语句 代码

作用

spam(eggs, ham)

函数调用

spam.ham(eggs)

方法调用

spam

在交互式解释器中打印变量

print(a, b, c, sep=")

在 Python 3.X 中打印

yield x

**

2

yield 表达式语句

表 11-4 中的最后两行是稍微特殊的情况-~正如在本章稍后将看到的,在 Python 3.X 中, 打印是一 个函数调用而且通常独占 一行,而生成器函数(第 20 章介绍)中的 yield 操作也 通常编写为一 条语句。这两者都只是表达式语句的实际例子。

例如,尽管你通常会在单独一行上运行一个 3.X 的 print 调用, print 也会像任何其他函 数调用 一样返回 一 个值(它的返回值是 None, 这是那些不返回任何有意义内容的函数的默

认返回值) :

»> x = print('spam') spam »> print(x) None

# print is a function call expression in 3.X # But it is coded as an expression statement

同时要注意,虽然表达式在 Python 中可作为语句出现,但语句不能用作表达式。一个不是 表达式的语句通常会独占一行,而不会嵌套在更大的语法结构中。例如, Python 不让你把

赋值语句(=)嵌入到其他表达式中,这样做的理由是为了避免常见的编写错误。当用“==" 做相等测试时,不会打成“=”而意外修改变 量的值。第 13 章介绍 Python 的 while 循环时, 你会学习如何在编写代码时应对这种限制。

赋值表达式和打印

1

357

表达式语句和原位置修改 这也引出了 Python l 作中另一个常犯的错误。表达式语句常常用干执行可以在原位置修改 列表的列表方法:

>» L = [ 1, 2] »> L.append(3) >» L

# Appl'nd is cm in-place ch(I11ge

[1, 2, 3] 然而, Python 初学者时常把这种操作写成赋值语句,试图让 L 赋值得到操作后的列表:

»> L = L.append(4) >» print(L)

# 811r append rerums None, not L

# Su we lose our list!

None 然而,这样做是行不通的。对列表调用 append 、 sort 或 reverse 这些原位置修改的运算, 一 定是对列表做原位四的修改,但这些方法在列表修改后井不会返回列表。事实上,它们 返回的是 None 对象。如果你把这类运算的结果赋值回原先的变蜇名,那么只会失去该列表 (而列表很可能在此过程中被垃圾回收)。

这个故事告诉我们,千万别那么做。反之你应当只调用原位置修改操作,而不要赋值调用 的结果。我们会在第 15 章的“常见代码编写陷阱“小节再继续探入讨论,因为这种现象也

会出现在后面儿章要介绍的一些循环语句的文中。

打印操作 在 Python 中, print 语句可以实现打印一它是 一 个程序员友好的标准输出流接口。 从技术上讲,打印将一 个或多个对象转换成相应的文本表示,然后发送给标准输出或其他 类似文件的流。更详细地说,

Python 中的打印操作与文件和流的概念紧密相连:

文件对象方法

在第 9 章中,我们学习了写入文本的文件对象方法(例如, file.write(str)) 。打 印操作是类似的,但更为专注—一文件写入方法是把字符串写入到任意的文件中,而

print 则是默认地把对象 写人 stdout 流,同时加入了 一 些自动的格式化。与写人文件 方法不同,打印操作不需要预先把对象转换为字符串。 标准输出流

标准输出流(也常称为 stdout) 是发送 一 个程序文本输出的默认位置。加上标准输入 流和标准出错流,标准输出流是脚本启动时所创建的 3 种数据连接中的 一 种。标准输 出流通常映射到 Python 程序的启动窗口,除非它已在操作系统的 shell 中被重新定向到 一个文件或管道。

358

1

第 11 章

由千标准输出流在 Python 中可以作为内置的 sys 模块中的 stdout 文件对象来使用(例 如, sys.stdout) ,因此你也可以用文件的写人方法调用来模拟 print 。然而,你也可

以很方便地借助 print, 把文本打印到其他文件或流。 打印是 Python 3.X 和 Python 2 . X 最为明显的差异之 一 。实际上,这种差异通常是绝大多数

Python 2.X 代码不经修改就无法在 Python 3.X 下运行的首要原因。具体来说,你编写打印 操作的方式取决千你使用的 Python 的版本:



在 Python 3.X 中,打印是 一个内置函数,提供关键字参数来支持特殊的使用模式。



在 Python 2.X 中,打印是一 条语句,拥有自己的特定语法。

由于本书既包含 Python 3.X 也包括 Python 2.X, 我们将依次学习这两种打印情况。如果你 有幸只需处理其中 一种版本的 Python 代码,请自由选择你所需阅读的内容。然而,因为你

的需求可能发生变化,所以不妨两种情况都熟悉 一 下。此外,如果你愿意,也可以在最新 的 Python 2.X 发行版中导入和使用 3.X 风格的打印—一这样既可以使用新增的功能,又可

以为之后向 3.X 的迁移铺路。

Python 3.X 的 print 函数 严格地讲,在 Python 3.X 中,打印不是一 条单独的语句形式,它只是上 一 小节所学的表达 式语句的 一 个例子。 print 内置函数的调用通常独占 一 行,但它不会返回任何我们期望的值(从技术上讲,它

返回了 None. 如上节所述)。因为 print 在 3 . X 中是一个常规函数,所以在 3.X 中打印使 用了标准的函数调用语法,而不是 一 种特殊的语句形式。也因为它通过关键字参数提供了

一种特殊的操作模式,所以这种形式更加通用,并且能更好地支持未来的扩展。

相比之下, Python 2.X 的 print 语句有一 些独特的语法,用千支持关闭换行符或是指定目 标文件这样的扩展。此外, Python 2.X 语句完全不支持分隔符的指定;因而相比千 3.X ,在 2.X 中更常见的做法是提前构造好所需的字符串。 Python 3.X 的 print 使用了 一个涵盖一切的统 一 、通用的方式,而不是增加许多特定的语法。

调用形式 从语法上讲,调用 Python 3.X 的 print 泊数有如下的形式 (flush 参数是 Python 3.3 新增的):

print([ object,... ][, sep=''][, end='\n'][, file=sys. stdout][, flush=False]) 在这个正式的表示中,方括号中的各项是可选的,并且可以在 一个给定的调用中省略,而

“=”后面都给出了参数的默认值。通常来说, print 内置函数打印了一个或多个对象的文

本表示,在中间用字符串 sep 来分隔,在结尾加上字符串 end, 通过 file 来指定输出流, 井按照 flush 来决定是否刷新输出缓冲区。

赋值、表达式和打印

I

359

如果你要指定 sep 、 end 、 file 和(在 3 .3 及后续版本中的) flush 的话,就必须使用关键字

参数形式。也就是说,你必须使用 一 种特殊的 "name=value" 语法按照名称而不是位置来 传递参数。第 18 章将深入介绍关键字参数,不过它们却很容易使用。传入该调用的关键字 参数的左右顺序可以随意,但是要保证这些参数放在待打印的对象后面,而它们能协助你 控制 print 操作:



sep 是在每个对象的文本之间插入的一 个字符串,如果没有传入的话,默认是一个单个

的空格,传人一 个空字符串会关闭分隔符。



end 是添加在打印文本末尾的一个字符串,如果没有传人的话,默认是一个\ n 换行符 。 传入 一 个空字符串将避免在打印文本末尾移到下 一输入行,也就是说下一 个 print 将

继续在当前输人行尾部打印。



file 指定了文本将要发送到文件、标准流,或其他类似文件的对象,如果没有传入的话, 默认是 sys.stdout 。你可以传入任何带有一个类似文件的 write(string) 方法的对象, 而真正的文件应该已经为输出打开。



flush 在 Python 3.3 中被引入,其默认值为 False 。它允许 print 强制文本通过输出流 立即刷新给等待中的接收者。通常,打印内容是否被缓冲在内存中是由 file 决定的, 传人 True 值给 flush 会强制刷新输出流。

待打印对象的文本表示,是通过把该对象传入 str 内置函数调用(或 str 在 Python 中的等 价形式)而得到的,如前所述, str 内置函数会为所有的对象返回 一 个“用户友好的“显

示字符串注 5 。在完全没有传人参数的情况下, print 函数会向标准输出流打印一个换行符, 这通常显示为 一个空白行。

Python 3.X 中 print 函数的应用 Python 3.X 中的打印其实没你想象得那么复杂。为了说明这点,让我们运行一 些示例。下 面把多种对象类型打印到默认的标准输出流,其中使用了默认的分隔符和行末格式(这些

默认值也是最常见的情况) :

(:\code> c:\python33\python >» print()

#

Display a blank line

»> x = spam »> y = 99 »> z = ['eggs'] »>

注 5:

从技木上讲,打印在 Python 的内部实现中使用 st工的等价形式,但是效果是相同的 . str 除了扮演字符串转换的角色以外,同时也是字符串的数据类型,并可以通过额外的编码参 数来从原始字节中斛码出 Unicode 字符串,我们将在第 37 章中学习这一知识,占、 ; 后者是

一种高级用法,这里可以放心地忽略 。

360

I

第 11 章

»> print(x, y, z) spam 99 ['eggs']

# Print three objects per defaults

这里不需要像文件写入方法那样,把对象转换为字符串。在默认情况下, print 调用会在 打印的对象之间添加一个空格。要关闭这个空格,你可以给 sep 关键字参数传入一个空字

符串,或者传人 一个你想要的分隔符:

>» print(x, y, z, sep=") spam99['eggs'] >» »> print(x, y, z, sep=',') spam, 99, ['eggs']

#

Suppress separator

#

Custom separator

同样在默认情况下, print 会在输出行尾部添加一个换行符。你可以通过向 end 关键字参 数传入一个空字符串来关闭它并避免换行,或者传人一个含有\ n 符号的自定义行末字符串 来手动换行(下面例子中的第二个,是同一行上用分号隔开的两条语句)

»> print(x, y, z, end='') spam 99 ['eggs']»> »> »> print(x, y, z, end=''); print(x, y, z) spam 99 ['eggs']spam 99 ['eggs'] »> print(x, y, z, end='... \n') spam 99 ['eggs']... >»

#

Suppress line break

#

Two prints, same output line

# Custom Line end

你也可以组合关键字参数来同时指定分隔符和行末字符串一它们的排列顺序是任意的, 但是必须排在所有待打印对象的后面:

>» print(x, y, z, sep='...', end='!\n') spam... 99... ['eggs']! »> print(x, y, z, end='!\n', sep='...') spam... 99... ['eggs'] !

#

Multiple keywords

# Order doesn 't matter

下面展示了如何使用 file 关键字一它对千单次打印,将待打印文本定向到一个输出文件 或者其他的可兼容对象(这其实是流重定向的 一种形式,我们在本小节稍后将回顾这一主 题) :

»> print(x, y, z, sep='...', file=open{'data.txt','w')} »> print(x, y, z) spam 99 ('eggs'] >» print{open('data. txt').read()) spam... 99,.. ['eggs']

# #

Print to a file Back to stdout

#

Display file text

最后,要记住 print 操作提供的分隔符和行末选项只是为了方便起见。如果你需要显示更

具体的格式,就不要以这种方式打印。相反,你需要提前构建一个更复杂的字符串或者在

print 中使用第 7 章介绍过的 字符串工具,井一次性打印该字符串 :

赋值 、 表达式和打印

I

361

»> text ='%s: 为-.4f, %osd'% ('Result', 3,14159, 42) » > print (text) Result: 3.1416, 00042 »> print('%s: %-,4f, %osd'% ('Result', 3,14159, 42)) Result: 3.1416, 00042 下一小节将介绍,我们在 Python 3.X 学习的几乎所有 print 函数的知识点都可以直接用千

Python 2.X 的 print 语句。这合情合理,因为 print 函数的本意就是模拟并改进 Python 2.X 的打印支持。

Python 2.X 的 print 语句 如前所述, Python 2.X 中的打印是 一 条有着特定语法的语句,而非一个内置函数。实际上,

Python 2.X 的打印基本上只是同一主题的一个变体。除了分隔符字符串(在 Python 3.X 中 支持,但 Python 2.X 不支持)和打印缓冲刷新(只在 Python 3.3 中有效),我们在 Python 3.X 的 print 函数中所做的 一切,都可以直接转换到 Python 2.X 的 print 语句中。

语句形式 表 11-5 列出了 Python 2.X 的 print 语句形式,并且给出了它们在 Python 3.X 中的等价形式 以供参考。注意, 2. X 中 print 语句的逗号十分重要,它分隔要打印的对象,而且最终的

逗号关闭了通常添加到打印文本末尾的行末字符(不要和元组语法相混淆)。

">>“语法

通常用千位右移操作,在这里却用来指定一个代替默认 sys.stdout 的目标输出流。 表 11-5:

Python

2.X 的 print 语句形式

Python 2.X 语句

Python

3.X 等价形式

print x, y

print(x, y)

作用

将对象的文本形式打印到 sys.stdout, 在各项之间添加—个空格,并在尾部添加

一个换行符

print x, y,

print(x, y, end=' ')

与上面相同,但是不在文本末尾添加换

行符

print » a file, x, y

print (x, y, file=afile)

将文本传入 afile.write, 而不是

sys.stdout.write

Python 2.X 的 print 语句应用 尽管 Python 2.X 的 print 语句拥有比 Python 3 .X 的函数更加独特的语法,但它同样也易 千

使用。让我们再学习 一些基础示例。默认情况下, Python 2.X 的 print 语句会在逗号分隔 的项之间添加一 个空格,并在当前输出行的末尾添加 一 个换行符:

C:\code> c:\python27\python

362

I

第 11 章

>>> X ='a' »> y ='b' »> print x, y a b 这种格式只是默认的,你可以选择使用或不使用。要省略换行字符(以便能在当前行后面 添加更多的文字),你可以在 print 语句后多加一 个逗号,如表 11-5 第 二行所示(下面又

一 次在同一行内用分号分隔两条语句)

» > print x, y,; print x, y a b a b 同样,为了关闭各项之间的空格,你需要放弃打印这种方式。然而,你可以使用第 7 章介

绍的字符串拼接和格式化工具来构建 一 个输出字符串,并且一 次性打印该字符串:

»> print x + y ab »> print'%s... %s'% (x, y)

a... b 如你所见,除了要使用特殊的语法, Python 2.X 的 print 语句基本上与 Python 3.X 的 print 函数一样易千使用。下一 小节将介绍为 Python 2 . X 的打印指定文件的方式。

打印流重定向 在 Python 3.X 和 Python 2.X 中,打印都默认将文本发送到标准输出流。然而我们也常常把

文本发送到其他地方,例如发送到文本文件中可以把结果存下来,供之后使用和测试。尽 管这样的重定向可以在 Python 自身之外的系统 shell 中实现,但事实上在脚本内重定向 一

个输出流也是很容易做到的。

Python 的 "hello world" 程序 让我们从通常的语言标配

“hello world" 程序开始说起。要在 Python 中打印 “hello

world" 信息,你只需按照 2.X 或 3.X 中的要求来打印这个字符串:

>» print ('hello world') hello world

#

» > print'hello world' hello world

# Print a siring object in 2.X

Print a string object in 3.X

因为表达式结果会在交互式命令行中显示,所以通常连 print 语句也可以省略,只需输入 要打印的表达式即可显示其结果:

» >'hello world' 'hello world'

# Intcractiw e(·hoes

赋值 、 表达式和打印

I

363

这段代码在编程学习中并不重要,但它可以用千说明打印行为。其实, print 操作只是

Python 的 一 个人性化特性,它提供了 sys.stdout 对象的简单接口,再加上一些默认的格 式设置。实际上,如果你想写得更复杂一些,也可以用下面这种方式编写打印操作:

»> import sys »> sys.stdout.write('hello world\n')

#

Printing the hard way

hello world 这段程序显式调用了 sys.stdout 的 write 方法(当 Python 启动并连接输出流的文件对象时, sys.stdout 属性就会事先设置)。 print 操作隐藏了大部分的细节,提供了 一个能够完成

简单打印任务的简单工具。

手动重定向输出流 那么,为什么本书要教你刚才那种复杂的打印方式呢?原因在于,等效的 sys.stdout 打印

方式可以说是 Python 中 一 个常见技术的基础。通常, print 和 sys.stdout 的关系如下所示。 语句:

print(X, Y)

#

Or, in 2.X: print X, Y

等价千:

import sys sys.stdout.write(str(X) + ' ' + str(V) +'\n') 这是通过 str 手动执行两次字符串转换,再通过“+”增加一个分隔符和一个换行,并且调 用输出流的 write 方法。你宁愿编写哪种代码?

(这里主要是想强调 print 的程序员友好

本质.... . .)

显然,较长的手动打印形式单就打印而言并没有什么用处。不过了解这就是 print 语句背 后所做的事是很有益处的,因为你可以把 sys.stdout 重新赋值给标准输出流以外的对象。 换句话说,这提供了 一 种可以让 print 语句将文字发送到其他地方的等效方式。例如:

import sys sys. stdout = open('log. txt','a') ... print(x, y, x)

#

Redirects prints to a file

#

Shows up in log.txt

在这里,我们把 sys.stdout 重设成一 个已打开的名为 log.ext 的文件对象,该文件位于脚本 的工作目录下并以附加模式(也就是把打印文本附加到文件当前的内容后面)打开。重设 之后,程序中任何位置的 print 语句都会将文本写至文件 /og.txt 的末尾,而不是原本的输 出流。 print 语句会很乐意持续地调用 sys.stdout 的 write 方法,而不管 sys.stdout 当 时引用的是什么。因为你的进程中只有 一 个 sys 模块,通过这种方式赋值 sys.stdout 会让 程序中所有的 print 都被重新定向 。

364

I

第 11 章

事实上,正如边栏“请留意 : print 和 stdout" 所述,你甚至可以将 sys.stdout 重设为非文

件的对象,只要该对象满足预期的协议(即拥有 write 方法)。当该对象是一个类时,打 印文本就可以按照你自己编写的 write 方法来定向和处理了。 这种重设输出列表的技巧, 一 般对原本用 print 编写的程序而言更为有用。如果你一 开始 就知道应该输出到文件中,就可以改用文件的 write 方法。不过为了将基千 print 的程序

重定向, sys.stdout 重设为修改每条 print 语句或者使用系统 shell 重定向语法提供一种 便捷的替代方式。

在其他情况下,流可以被重设为各种对象,用千在 GUI 的弹窗中显示文本,或是在类似 IDLE 的 IDE 中填充颜色等。 sys.stdout 重设是一 个通用技巧。

自动流重定向 尽管通过赋值 sys .stdout 的打印文本重定向是一个有用的工具,但是上一节代码中有个潜

在的问题,那就是没有直接的方式可以把输出流的定向切换回来。因为 sys. stdout 只不过

是一个普通的文件对象,所以你总是可以存储它,并在需要时恢复它注 6: C:\code> c:\python33\python »> import sys »>temp= sys.stdout >» sys.stdout = open('log.txt','a') »> print('spam') »> print(1, 2, 3) » > sys. stdout. close() » > sys. stdout = te叩 » > print ('back here') back here >» print(open('log. txt').read()) spam

Save/or restoring later Redirect prints to a file # Prints go to file, not here

# #

# Flush output to disk #

Restore original stream

#

Prints show up here again

#

Result of earlier prints

1 2 3

然而如你所见,像这样的手动保存和恢复原始输出流需要相当多的额外工作。因为这种重 定向输出流的操作出现得非常频繁,所以 Python 引入了 一个特别省事的 print 扩展。 在 Python 3 .X 中, file 关键字允许一 个单次的 print 调用将其文本发送给 一 个文件(或类 文件对象)的 write 方法,而不用费力地重设 sys.stdout 。因为这种重定向是临时的,所

以其他普通的 print 语句还是会继续打印到原本的输出流。在 Python 2. X 中,当 print 语

句以>>开始,后面再跟着输出的文件对象(或其他兼容对象)时,也会有同样的效果。例 如,下面的语句同样把打印的文本发送到 一 个名为 log .txt 的文件:

注 6

在 Python 2.X 和 Python 3.X 中,都可以使用 sys 模块的—stdout—属性,它拥有对 sys. stdout 在程序启动时存储着的原始值的引用 。 不过你还是需要将 sys .stdou t 恢复为 sys . —stdout_来切回原本的轮出流 。 参闵 sys 模块的文档来荻取更多细节 。

赋值、表达式和打印

I

365

log = open ('log. txt','a') print(x, y, z, file=log) print(a, b, c)

# 3.X H Print to a file-like object H Print ro original stdour

log = open('log. txt','a') print » log, x, y, z print a, b, c

# 2.X # Print to a.file-like object # Print to original stdout

如果你需要在同 一 个程序中同时打印到文件以及标准输出流,那么 print 的重定向形式就

很方便。然而、如果你使用这种形式,就要确保提供了 一 个文件对象(或者和文件对象 一

样有 write 方法的对象),而不是 一个文件名称符串。下面是该技术的示例:

(:\code> c:\python33\python >» log = open('log.txt', 飞') >» print(1, 2, 3, file=log) >» print(4, 5, 6, file=log) »> log.close() >» print(7, 8, 9)

# For 2.X: print>> log, 1, 2, 3

#

For 2.X: print 7, 8, 9

7 8 9

»> print(open('log. txt').read(}) 1 2 3

4 5 6

这种扩展的 print 形式也常用千把错误消息打印到标准错误流 sys.stderr 。你既可以使用 标准错误流的 write 方法并手动设置输出格式,也可以使用带重定向语法的打印:

»> import sys »> sys.stderr."'rite(('Bad!'* 8) +'\n') Bad!Bad!Bad!Bad!Bad!Bad!Bad!Bad! »> print('Badl'* 8, file=sys.stderr) Bad!Bad!Bad!Bad!Bad!Bad!Bad!Bad!

# In 2.X: print>> sys.stderr,'Bad!'* 8

既然你已经了解了打印重定向的所有内容,打印和文件 write 方法之间的等价性也就不言 而喻了。下面的交互会话在 Python 3.X 中使用了这两种打印方式,然后把输出重定向到两

个外部文件中,以验证打印了相同的文本:

>» X = 1; Y = 2 »> print(X, Y)

# Print: the easy way

1 2

»> import sys >» sys.stdout.write(str(X) + ' ' + str(Y) +'\n')

#

Print:

#

Redirect text to file

the 加rd

way

1 2 4

>» print (X, Y, file=open ('te叩',飞')) >»

open('temp2' ,飞 ').write(str(X)

+ ' ' + str(Y) +'\n')

# Send to file manually

4

>» b'1 » > b'1

366

I

print(open('templ','rb').read()) 2\r\n ' print (open('temp2','rb'). read()) 2\r\n'

第 11 章

# Binary mode for hytes

如你所见,除非你热爱打字,否则 print 操作通常是显示文本的最佳选择。关于打印和文

件写人等价性的另一 个示例,你可以请参阅第 18 章中 Python 3.X 的 print 函数模拟示例; 它使用这种代码模式,在 Python 2.X 中实现了与 Python 3 . X 的 print 函数等效的函数。

版本中立的打印 最后,如果需要 print 在 2.X 和 3 . X 中都能工作,你有以下一 些方案 。 无论你是在编 写 2.X

代码为 3.X 的兼容性努力奋斗,抑或是编写 3.X 代吗致力千对 2.X 的支持,这都是适用的。

2to3 转换器 其中 一 种方案是,你可以编写 Python 2.X 的 print 语句井使用 Python 3.X 的 2to3 转换脚 本自动将它们转换为 Python 3.X 函数调用。可参阅 Python 3.X 的手册以更详细地了解这一 脚本,它会试图把 Python 2.X 的代码转换成可以在 Python 3.X 下运行。该脚本是一 个很有

用的工具,但如果你只是想让打印操作变得版本中立的话则显得大材小用了。另 一 个相关 的工具叫作社 02, 作用正好相反:将 3.X 的代码转换成可在 2.X 下运行,参阅附录 C 以获

取更多信息。

从_future_导入 此外,你可以通过把下面这条语句编写在脚本顶部或是交互式会话的任何位置,来在

Python 2.X 运行的代码中编写和使用 Python 3.X 的 print 由数调用: from _future_ import print_function 这条语句让 Python 2.X 能够支持 Python 3.X 的 print 函数。通过这种方式,我们可以使用

Python 3.X 的 print 功能,而且如果之后迁移到 Python 3.X 的话也不必修改 print 。这里 给出两条使用注意:



如果这条语句在通过 3.X 运行的代码中出现,则被直接忽略,也就是说为了 2.X 的兼 容性在 3.X 的代码中加入这条语句不会影响在 3.X 中的运行。



这条语句必须出现在所有支持 2.X 打印的源文件的顶部一因为它只为单个文件修改解 析器,所以只导入一 个包含该语句的另 一 个文件是不够的。

用代码消除显示差别 还要记住,简单打印(如表 11-5 第 一 行所示)在 2.X 和 3.X 中都可用,因为所有的表达 式都可以被包在圆括号中,所以在 Python 2.X 中我们总是可以通过添加外围的圆括号,来

伪装成调用一个 Python 3.X 的 print 函数。这么做的主要缺点是,如果有多个或没有待打

印对象的话,它就会从中 产生一个元组,也就是说会打印出额外的外围圆括 号 。例如,在

Python 3.X 中,调用的圆括号中可以列出任意多的对象: 赋值、表达式和打印

I

367

C: \code> c: \python33\python » > print('spam') spam »> print('spam','ham','eggs') spam ham eggs

# 3.X print function call syntax #

These are n1111ip/e argments

上面第一个在 Python 2 . X 中的工作方式一样,但第二个会在输出中产生一 个元组:

C:\code> c:\python27\python »> print('spam') spam »> print('spam','ham','eggs') ('spam','ham','eggs')

# 2.X print statement, enclosing parens

# This is really a tuple object!

在 3.X 中,当没有待打印对象时会强制打印 一个空行,这同样适用千 2.X: 2.X 会显示一个

元组,除非你打印一个空字符串:

c:\code> py -2 » print()

II This is just a line-feed on 3.X

()

»> print('')

#

This is a line-feed in boch 2.X and 3.X

严格地讲,在某些情况下, 2.X 中的输出不仅是多个括号的区别。如果你仔细比对前面的结果, 就会注意到在 2.X 中元组内的字符串也被引号包围。这是因为当 一 个对象嵌套在另 一 个对 象中时,与单独作为顶层对象相比,打印方式可能有所不同。从技术上讲,嵌套效果会使 用 repr 显示,而顶层对象会使用 str 显示,我们在第 5 章中学习过这两种互为替代的显示 方式。 在这里就表现为嵌套在元组中的字符串周围额外的引号,而该元组是在 2. X 中打印多个括 号包围的项而创建的。其他对象类型的嵌套显示可能会有更大的差异,尤其对千那些使用 了运算符重载来定义替代显示的类对象(在第六部分我们将深入介绍运算符重载,并在第

30 章中详细展开)。 为了真正获得可移植性,而不是全范围地开启 3.X 的打印,也为了避免嵌套效果带来的显 示差异,你总是可以将待打印字符串处理成一个单独的对象来实现各个版本之间的统一, 这需要用到第 7 章中学习的字符串格式化表达式、方法调用,或是其他的字符串工具:

>» print('%s %s %s'% ('spam','ham','eggs')) spam ham eggs »> print('{o} {1} {2}'.format('spam','ham','eggs')) spam ham eggs »> print('answer:'+ str(42)) answer: 42 当然,如果你只使用 Python 3.X 的话,完全可以忘记 print 的比对,不过很多程序员虽然 不必编写 2.X, 但却时不时地遇到用 2.X 编写的代码或系统。在本书后面的许多示例中, 我们将使用_future_和版本中立代码这两者来实现 2.X/3.X 的可移植性。

368

I

第 11 章

注意:我在整本书中使用 Python 3.X 的 print 函数调用。之后我会经常使打印版本中立,而当 结果在 2.X 下有不同时,通常会提醒你,但有时候也会不提醒你,因此,请把本注意边

栏当作一个额外的警告一如果你在 Python 2.X 的打印文本中看到额外的圆括号的话, 或者在 print 语句中删除圆括号,从_future—导入 3 . X 打印,使用这里介绍的版本中 立方案重新编写打印代码,或者习惯上这些小小的区别。

请留意: print 和 stdout 理觥 print 语句和 sys.stdout 之间的等价性是相当重要的。这也是为什么我们可以把

sys.stdout 重新赋值给用户自定义(能够提供 write 方法的)的类似文件的对象。因为

print 语句只是把文本传送给 sys.stdout.write 方法,所以你可以通过把 sys.stdout 赋值 给一个对象,来捕荻程序中待打印的文本,并通过该对象的 write 方法来任意地处理 文本 。

例如,你可以把待打印文字发送给 GUI 窗口,或是定义一个带有 write 方法的对象, 让它按照需求把文本发送到多个目的地 。在本 书笫六部分介绍类的时候,你会看到使 用这个技巧的例子,理论上代码如下所示:

class FileFaker: def write(self, string) : # Do something with printed text in string import sys sys. stdout = FileFaker() print(someObjects)

#

Sends to class write method

这能行得通是因为 print 是本书下一部分中将介绍的多态运算: sys.stdout 是什么 不重要,只要它有一个名为 write 的方法(接口)即可 。 在 Python 3.X 中使用 file 关

键字参数,以及在 Python 2.X 中使用 print 扩展形式>>,都会让重定向变得更加简单, 因为我们不再需要显式地重设 sys.stdout 。换句话说,常规的 print 将仍然定向到 stdout 流:

# 3.X: Redirect to object for one print

myobj = FileFaker() print(someObjects, file=myobj)

# Does not reset sys.stdout

myobj = FileFaker() print» myobj, someObjects

# #

2.X: same effect Does 11ot reset sys.stdout

Python 3.X 内置的 input 函数(在 2.X 名为 raw_input) 会从 sys.stdin 文件中读入, 所以你可以用类似方式拦截对读取的请求:使用类来实现类似文件的 read 方法。参 考笫 l0 章中关于 input 和 while 循环的例子来获取关于这个函数的更多背景知识。

赋值表达式和打印

I

369

要注意,因为待打印文字会进入 stdout 流,所以这也是在 Web 上使用 CGI 脚本来 打印 HTML 回复页的方式。同时这也可以让你像往常一样,在操作系统命令行中对 Python 脚本的轮入和输出进行重定向:

python script.py < inputfile > outputfile python script.py I filterProgram Python 的打印操作重定向工具,实质上是 shell 脚本语法在 Python 中的替身 。 参考其 他资源来荻取关于 CGI 脚本和 shell 语法的史多信息 。

本章小结 在本章中,我们开始探入了解赋值语句、表达式以及打印,从而深入研究 Python 语句。这 些都是很容易使用的语法,且都有许多十分便捷的可选替代形式。例如,增量赋值语句以

及 print 语句的重定向形式,能让我们避免一 些手动的代码编写工作。同时,我们也研究 了变量名的语法、流重定向技术,以及各种要避免的常见错误,诸如把 append 方法调用的

结果赋值回变扯等。

下 一章我们将引入 if 语句 (Python 主要的选择工具)的细节,来继续我们的语句之旅。我 们要探入讨论 Python 的语法模型,并学习一下布尔表达式的用法。不过,在继续学习之前,

先用本章结尾的习题来测试一下你在本章所学的知识。

本章习题 1.

举出 三种可以让三 个变批赋值成相同值的方式。

2.

在把 三个变显赋值给可变对象时,为什么你要提高警惕?

3.

L

4.

怎样使用 print 语句来向外部文件发送文本?

=

L. sort() 有什么错误?

习题解答 1.

你可以使用多目标赋值语句 (A= B = C = o) 、序列赋值语句 ( A, B, C = 或者分开三 行上的单独赋值语句 (A=

o,

B=

o,

C = o) 。对 最后一种方式(如第 10

章所述),你也可以把三条单独的语句用分号合并在同一行上 ( A=

2.

如果你用这种方式赋值:

A= B = C = []

370

I

第 11 章

o, o, o)

o; B = o; C = 0) 。

这 三 个名称会引用同 一 个对象,所以对其中 一 个名称进行原位隍修改(例如, A.append(99)) 也会影响其他的名称。只有对列表或字典这类可变对象进行原位笠修 改时,才会如此。对不可变对象而言(例如数字和字符串),则不会受此影响。

3

列表的 sort 方法就像 append 方法,也是对主体列表进行原位置修改:它们都会返回 None, 而不是被修改后的列表。如果把 sort 的返回值赋值给 L, 就会把 L 设为 None,

而不是排序后的列表。正如在本书前面和后面所述(例如第 8 章),新的内置函数 sorted 可以排序任何的序列,并返回具有排序结果的 一个新列表。因为 sorted 井不是

在原位笠修改,所以你可以放心地将其结果赋值给名称。

4.

要让 一 个单次的打印操作打印到 一 个文件,你可以使用 Python 3.X 的 print(X, file=F) 调用形式,或是使用 Python 2.X 的扩展的 print>>

file,

X 语句形式,亦或在

打印前把 sys.stdout 赋值为手动打开的文件并在之后恢复最初的值。你也可以用系统 shell 的特殊语法,把程序所有的打印文字重定向到 一 个文件,但这是 Python 范围以外 的内容了。

赋值、表达式和打印

I

371

第 12 章

if 测试和语法规则

本章介绍 Python 的 if 语句,也就是根据测试结果,从一些备选的操作中做出选择的语句。 因为这是我们第一次深入探索复合语句(内嵌其他语句的语句),在此我们也会比第 10 章

更为详细地讨论 Python 语句语法模型的一般概念。此外因为 if 语句引入了测试的概念, 所以本章也会涉及布尔表达式和 if 三元表达式,并附上一些通用的真值测试的细节。

if 语句 简而言之, Python 的 if 语句是选择操作来执行。 if 及其表达式组成部分是 Python 中主要 的选择工具,处理了 Python 程序中的大多数逻辑。此外,这也是我们讨论的第一种复合语 句。与其他的 Python 复合语句一样, if 语句可以包含其他语句,包括另外的 if 。事实上,

Python 让你在程序中按照顺序组合语句(从而让它们逐一执行),而且可以任意地进行嵌

套(使语句只在诸如选择和循环的特定情况下执行)。

一般形式 Python 的 if 语句属于多数面向过程语言中的典型 if 语句。其形式是 if 测试,后面跟着 一 或多个可选的 elif ("else if" 的简写)测试,以及 一个位千末尾的可选的 else 块。

测试和 else 部分都各对应一段嵌套代码块译注 l ,缩进列在首行下面。当 if 语句执行时, Python 会执行第一个计算结果为真的测试的代码块,或者如果所有测试都为假时,就执行 else 块。 if 语句的一般形式如下:

if test1: 译注 1:

#

if test

如笫 10 章所述,虽然这里使用”代码块"的名字,但是在 Python 中并不存在类似 C 和 Java 的由花括号指明的"块" 。 作为替代, Python 中使用代码的缩进来指明”块“ 。

372

statements1 elif test2: statements2 else: statements3

# Associated block

# Optional e/ijs #

Optional else

基础示例 让我们来看一些实际的 if 语句例子。除了开头的 if 测试及其相对应的代码块外,其余所 有部分都是可选的。因此,在最简单的情况下,其他部分都可省略 :

>» if 1: print ('true') true 要注意,当在这里使用的基本接口中交互地输入时,提示符会在接下去的行中变成“.." (在

IDLE 中,你只会直接向下跳到缩进的行,单击 Backspace 键可以删除自动添加的缩进)。 空白行将终止并执行整个语旬(在命令行中,你可以按两次回车键)。我们之前说过, 1 是

布尔真值(随后我们将看到, True 也有等同的效果),所以这个测试总会成功。要处理假 值结果,可改写成下面带 else 的程序:

»> if not 1: print ('true') ..• else: print ('false') false

多路分支 下面是更为复杂的 if 语旬例子,它添加了所有可选的部分:

>» x ='killer rabbit' » > if x =='roger': print("shave and a haircut") ... elif x =='bugs': print("what's up doc?") ... else: print('Run away I Run away!') Run away! Run away! 这个多行语句从 if 行扩展到 else 下面嵌入的代码块。执行时, Python 会执行第一 次测试 为真的语句下面的嵌套语句,或者当所有测试都为假时执行 else 部分(在这个例子中,就

是这样)。实际上, elif 和 else 部分都可以省略,而且每一 段中可以嵌套 一个以上的语句。 要注意 if 、 elif 以及 else 能组合在 一起的原因在于它们在竖直方向对齐并具有相同的缩进。

if 测试和语法规则

I

373

如果你用过 C 或 Pascal 这类语 言 ,可能会对 Python 中有没有 switch 或 case 语句(根据 变批的值选择执行的动作)的设定感到惊奇。而在 Python 中,多路分支是写成 一 系列的

if/elif 测试(如上例所示),或者偶尔采取索引字典和查询列表的形式。因为字典和列 表可在运行时动态地创建,所以有时会比在脚本中硬编码的 if 逻辑更有灵活性:

»>choice='ham' »> print({'spam': 'ham': , eggs : 'bacon': 1.99

# A dictionary-based'switch' 1.25, # Use has_key or get for default 1. 99, 0.99, 1. 10}(choice])

尽管第一 次接触上面的程序时需要花点时间思考,但这个字典其实可以看作多路分支:根

据键 choice 变量的值进行索引,再分支到 一 组值的其中 一 个,很像 C 语言的 switch 。如 果使用 Python 的 if 语句则稍显冗长,如下所示:

» > if choice =='spam': print(l.25) ..• eli f choice =='ham': print(l.99) ..• elif choice =='eggs': print(0.99) .•• eli f choice =='bacon': print(1.10) ••• else: print('Bad choice')

#

The equivalent if statement

1.99 虽然这么编 写 可能更具有可读性,但像上面的例子那样使用 if 语句有如下的缺点:如果将

它构造成字符串并用上一 章介绍的 eval 和 exec 语句来执行它,你就不能像字典 一样利用 运行时动态创建的优势。在更加动态的编程情景下,数据结构可以提供额外的灵活性。

处理选择语句的默认情况 要注意,当没有匹配的键时,这里 if 的 else 分句就会处理没有匹配的默认情况。正如第 8 章所述,对字典的默认值处理能够通过 in 表达式、 get 函数调用配合下 一 章将要介绍的 带 try 语句的异常捕获来编写。在这里处理基千字典的多路分支语句默认行为时,我们完 全可以使用同样的技巧。作为这 一 用法场景下的又 一 个例子,下面是通过 get 方法处理默 认值的情况:

>»branch= {'spam': 1.25, 'ham': 1.99, 'eggs': 0.99} »> print(branch.get('spam','Bad choice')) 1. 25 >» print(branch.get('bacon','Bad choice')) Bad choice

374

I

第 12 章

位千一条 if 语句中的 in 成员测试也有同样的默认效果:

>» choice "'bacon' >» if choice in branch: print(branch[choice]) ..• else: print('Bad choice') Bad choice 而 try 语句一般通过捕获和处理默认情况所引发的异常,来处理默认情况(更多关千异常 的内容,请参阅第 11 章的概述和本书第七部分的详细介绍)

»> try: print(branch[choice]) ••• except KeyError: print ('Bad choice') Bad choice

处理更大规模的活动 字典适用千将值和键相关联,但如果是那些通过 if 语句编写的更复杂活动呢?在第四部分 你会学到字典也可以包含函数,从而代表更为复杂的分支活动,进而实现一般化的跳转表。 这些函数作为字典的值,通常写成函数名或内联的 lambda, 并通过添加括号调用来触发其

动作。下面是 一个较抽象的例子,但是敬请期待第 19 章中对该部分的介绍,那个时候我们 将已经掌握了更多关于函数的定义:

def function():... def default{):... branch= {'spam': lambda:..., 'ham': function, 'eggs': lambda :... }

# A table of callable function objects

branch. get (choice, default)() 虽然字典式多路分支在处理动态数据的程序中很有用,但多数程序员可能会认为,编写 if

语句是执行多路分支最直接的方式。作为编写代码的 一 个原则:有疑虑的时候,就遵循简 易性原则和可读性原则。这是一种

“Python 式”的习惯。

复习 Python 语法规则 第 LO 章介绍过 Python 的语法模型。现在 ,我们要上升到像 if 这样更大的语句 ,而本节也

是对之前介绍过的语法概念的一次复习和扩展。 一般来说, Python 都有简单、基千语句的 语法。但是,有些特点是我们需要知道的:

if 测试和语法规则

I

37s



语句是逐个运行的,除非你编写了其他内容。 Python 一般都会按照次序从头到尾执行 文件中嵌套块中的语句,但是像 if (还有循环和异常)这种语句会让解释器在程序中

跳跃 。因为 Python 执行程序的途径被称为控制流,所以像 if 这类会对其产生影响的语 句,通常称为控制流语句。



块和语句的边界会自动被解释器识别 。如前所述, Python 的程序块中没有大括 号或 "begin/end" 等分隔字符;反之, Python 使用首行下的语句缩进把嵌套块内的语句组 合起来。同样, Python 语句 一 般是不以分号终止的, 一行的末尾通常就是该行所写语

句的结尾。 一 个特例是,语句可以跨越多行,也可以通过特殊的语法写在同 一 行内。



复合语句=首行+

”:"

+多个缩进语句。 Python 中所有复 合语句(即那些包含嵌套

的语句)都遴循相同格式:首行会以冒号终止,再接 一 个或多个嵌套语句,而且通常

都是在首行下缩进的。缩进语句称为块(有时称为组)。在 if 语句中, elif 和 else 分句是 if 的 一 部分,也是其本身嵌套块的首行。 一 个特例是,如果语句块是简单的非 复合语句.那么它可以与首行放在同一行。



空白行、空格以及注释通常都会被忽略。源文件中 空白行是可选的也是可忽略的(但 在交互式命令行下不会,因为这种情况下它们被用作结束复合语句)。语句和表达式

中的空格几乎都会被忽略(除了在字符串字面址内以及在缩进中时)。注释总会被忽略: 它们以#字符开头(不在字符串字面址内的#),而且延伸至该行的末尾。



文档字符串 (docstring) 会被忽略 , 但会被保存并由工具显示。 Python 支持的另 一种注释, 称为文档字符串(简称 docstring) 。和#注释不同的是,文档字符串会在运行时被保 留下来以便查看。文档字符串出现在程序文件和一些语句顶端的字符串中。 Python 会 忽略它们的内容但在运行时会将它们附加在对象上,从而让它们能被如 PyDoc 的文档 工具显示。文档字符串是 Python 更大型的文档策略的冰山 一 角,本书第 15 章会对它进 行讨论。

图 12-1: 嵌套块代码:一个嵌套块以再往右缩进的语句开始,碰到缩进量较少的语句或文件末 尾肘就结束

376

I

第 12 章

如你所见, Python 没有变量类型声明。单就这 一 点而 言 ,就让你拥有比以前用过的更为简

单的语 言 语法。但是,对千大多数新用户而言,省略许多其他语言用千标识块和语句的大 括号和分号,似乎是 Python 最新颖的语法特点,所以让我们更详细地讨论这方面的意义。

代码块分隔符:缩进规则 如第 l0 章所述, Python 会自动以行缩进检测块的边界,也就是程序代码左侧的空白空间。 向右侧缩进相同距离的所有语句属千同 一 块的代码。换句话说,块内的语句会垂直对齐,

就好像在 一栏 之内。块会在文件末尾或者碰到缩进量较少的行时结束,而更深层的嵌套块

就是比当前块的语句进 一 步向右缩进。有时候,复合语句体可以直接写在首行后面 ( 我们

将在后面讨论这种情况),但大多数情况下它们都以缩进的形式出现在首行的下面 。 例如,图 12-1 示范了下列程序代码的块结构: X =

1

if x: y

= 2

if y: print ('block2') print ('bloc kl') print('blocko') 这段代码包含了 三 个块:第一个(文件顶层代码)完全没有缩进,第 二个(位于外层计语句内) 缩进四格,而第 三 个(位千嵌套 if 下的 print 语句)则缩进八格。 通常来说,顶层(无嵌套)代码必须开始千第 1 列。嵌套块可以从任何列开始。缩进可以 由任意的空格和制表符组成,只要特定的单个块中的所有语句都相同即可。也就是说,

Python 不在乎你怎么缩进代码,只在乎缩进是否一致。不过,每个缩进层级使用 4 个空格 或者一个制表符是通常的惯例,但是 Python 世界中没有绝对的标准。

缩进代码实际应用中是相当自然的事情。例如,如下的代码段(虽然特傻)展示了 Python 代码中通常的缩进错误:

x = 'SPAM' if'rubbery'in'shrubbery': print(x * 8) X +='NI' if x.endswith('NI'): X *= 2 print(x)

#

Error: first line indented

#

Error: unexpected indentation

# Error: inconsistent indentation

这段代码的正确的缩进版本如下所示,即便是这样 一 个刻意而为的示例,正确的缩进也会 使得代码看上去更好:

x = 'SPAM'

if ' rubbery'in'shrubbery': if 测试和语法规则

I

377

print(x * 8) X +='NI' if x.endswith('NI'): X *= 2 print(x)

# Prints 8 "SPAM"

# Pri111s "SPAMNISPAMNI"

在 Python 中,很重要的一点是要明白空格唯一的主要用途就是用在代码左侧作为缩进。在

其他大多数上下文中,空格是可有可无的。尽管这样,缩进确实是 Python 语法中的 一 部分, 而不仅是编程风格:任何特定单 一块中的所有语句都必须缩进到相同的层次,否则 Python

会报告语法错误。这是有意而为之的,因为你不需显式地标识嵌套代码块的开头和结尾, 所以其他语言中符合语法但却异常混乱的代码将在 Python 中不复存在。

如第 10 章所述,把缩进变成语法模型 一 部分,也强化了 一 致性,这是 Python 这种结构化 编程语言中可读性的重要保证。 Python 的语法偶尔也描述成是“所见即所得“一每行程 序代码的缩进毫无歧义地告诉了读者它属千什么地方。这种统一 连贯的代码风格让 Python 程序更易千被维护和重用。

缩进在编程中是再自然不过的事情了,因为它直观地反应了代码的逻辑结构。一致的缩进 代码总是可以满足 Python 的规则。再者,多数文本编辑器(包括 IDLE) 都会在输入时自

动缩进程序代码,这让你能很轻松地遵守 Python 的缩进模型。

避免混合使用制表符和空格: Python 3.X 中新的错误检查 一条首要 的规则是:虽然可以使用空格或制表符来缩进,但在 一段代码块中混合使用这两 者通常不是好主意,因此请只使用其中的一种。从技术上讲,制表符能够保留足够的空间 以便把当前的列数按照最多为 8 的倍数来移动,井且,如果在保持一致性的前提下混用制 表符和空格的话,代码也可以工作。然而,这样的代码可能很难修改。更糟糕的是,撇开 python 语法规则不说,混用制表符和 空格会使得 代码完全难以阅读一一制表符在另 一 个程

序员的编辑器中看上去可能与在你自己的大不相同。 实际上,恰恰由于这些原因, Python 3.X 现在对一段脚本在代码块中混用制表符和空格(混 用导致缩进会依赖制表符与空格的转换)的缩进进行报错。 Python 2 . X 允许这样的脚本运行,

但是,它有一个- t 命令行参数来辅助你检测制表符用法上的不 一 致,还有一个- tt 参数会 对这样的代码进行报错(你可以在一个系统 shell 窗口中使用诸如 python -t main.py 的命

令行来切换)。 Python 3.X 的报错情况等同于 Python 2.X 的- tt 运行标识符。

语句分隔符:行与行间连接符 Python 的语句一般都是在其所在行的末尾结束的。不过,当语句太长、难以单放在一行时, 有些特殊的规则能允许它跨越多行:

378

I

第 12 章



如果使用语法括号对,语句就可横跨数行。如果在封闭的()、{}或[]这类配对括号 中编写代码,那么 Python 就可让你在下一行继续输入语句。例如,括号中的表达式以

及字典和列表字面簸,都可以横跨数行。直到 Python 解释器到达你输人闭合括号)、}或] 所在的行,语句才会结束。语句的续行(第 二 行及第 二 行之后的语句)可以从任何缩 进层次开始,但你应该尽可能让它们垂直对齐以便千阅读。这 一 开放对的规则也适用 千 Python 3.X 和 2.7 中的集合和字典推导。



如果语句以反斜杠结尾,就可横跨数行 。这是一个有点过时的功能,一般并不推荐大 家使用。但是如果语句需要横跨数行,你也可以在前一行的末尾加上反斜杠(未被嵌

入字符串字面扯或注释的\),以表示你要在下一行继续输入。由千可以在较长结构两 侧加上括号以便继续输人的原因,现在反斜杠已经很少使用了。这种方法同时也容易 导致错误:偶尔忘掉一个\通常会产生语法错误,并且可能导致下 一 行默默地被错误地 看作一条新语句,且没有任何提示。这会产生不可预期的结果。



字符串字面量有特殊规则。正如第 7 章所述, 三重引号字符串块可以横跨数行。我们 还在第 7 幸中学到过,相邻的字符串字面批会被隐式地拼接起来,当与前面提到的开 放括号对规则一起使用的时候,你只需把这个结果包含到圆括号中就允许它跨越多行。



其他规则。有关语句分隔字符,这里还要再提几点 。虽然这不常见,但你可以用分号 终止语句:这种惯例有时用千把一个以上的简单(非复合)语句挤进同一行中。此外, 注释和空行也能出现在文件的任意之处。注释(以#字符开头)会在其出现的行的未

尾终止。

一些特殊情况 以下是使用上面提到的开放括号对规则实现行间连续的例子。拥有界限符的结构(例如方

括号中的列表字面量)可以横跨任意多行:

L

l-

-d”y ”," ' , ' GBU ,', od1 oag

,LL # Open pairs may span lines

这同样适用千圆括号(表达式、函数参数、函数头、元组和生成器表达式)和花括号(字

典以及 Python 3.X 和 2.7 中的集合字面址、集合和字典推导)。其中的 一 些是我们将在后 面的各章中要学习的工具,但这条规则自然包括了实际应用中大多数的跨行结构。 如果你喜欢使用反斜杠来继续多行也是可以的,但是这在实际的 Python 代码中并不常见:

if a== band c == d and\ d == e and f == g: print ('olde')

# Backslashes allow cont11111ations…

if 测试和语法规则

I

379

因为任何表达式都可以包含在括号内,所以如果程序代码需要横跨数行,你通常也可以使

用开放括号对技术一一直接把语句的一部分包含在圆括号中:

if (a== band c == d and d == e and e == f): print ('new')

# But parentheses usually do too, and are obvious

实际上,大部分 Python 程序员通常都不喜欢反斜杠,因为它们太容易被忽视井且太容易被

遗漏了。在下面的例子中, x 在有反斜杠时被赋值为 10, 这是本来的意图;如果偶然漏掉

了反斜杠,

x 则被赋值为 6, 并且不会报告错误 (+4 本身是一条有效的表达式语句)。

在带有一个复杂赋值的实际程序中,这可能会引发一个非常令人讨厌的错误注 I: +

X = 1

2

+ 3 \

# Omitting the\ makes this very different!

+4

另一种特殊情况是, Python 允许在相同行上编写一条以上的非复合语句(即语句内未嵌套 其他语句的语句),由分号隔开。有些程序员使用这种形式来节省程序文件的行数,但如 果你让多数代码保持一条语句一行,则会使程序更具可读性:

x = 1; y = 2; print(x)

# More 1han one simple s1a1emen1

正如第 7 章所述,三重引号字符串字面量也能跨多行。此外,如果两个字符串字面蜇彼此

相邻地出现时,那么它们会被拼接起来,这就好像在它们之间放置了一个+ (当和开放括 号对规则一起使用时,包括在圆括号中就允许这种形式横跨多行)。例如,下面的第一个

示例在换行处插人换行字符,并且把 '\naaaa\nbbbb\ncccc' 赋给 S; 而第二个示例隐式地 拼接,并且把 S 赋值为 ' aaaabbbbcccc' 。第二种形式中的 #注释被忽略了(如第 7 章中所述), 但如果注释出现在第一个示例中则会被包含:

·

s = ·'" aaaa bbbb cccc """ S = ('aaaa' 'bbbb' 'cccc')

#

Comments here are ignored

最后, Python 允许你把复合语句的主体上移到头部行,只要该主体只包含简单(非复合)语句。 简单 if 语句及单个测试和动作常常用到这种用法,正如第 10 章中交互式命令行下循环语

句中所做的那样: 注 1

坦率地说, Python 3.0 在做出了如此多重大的改进后,竟然没有废除反斜杠的使用,这点

:

今很多人都感到吃惊!参阅附录 C 中列出的一个 Python 3.0 中移陓功能的表格;有些功能 与反斜杠续行相比危害较轻。再者,由于本书的目的在于教学,而非无端引战,因此我在 这里给出的最好达议就是不要使用反斜杠续行。你通常应当进免在新的 Python 代码中使 用反斜杠续行,即使开发 C 语言的岁月对你而言不可忘却。

380

I

第 12 章

if

1:

print('hello')

# Simple statement 011 header line

你可以组合使用这些特殊情况来编写难读的代码,但本书不建议这么做。经验法则是,试 着让每条语句都在其自身的行上,除了最简单的块外,全都要缩进。这样坚持 6 个月之后,

你就会庆幸自己当初的决定。

真值和布尔测试 比较、相等以及真值的概念已经在第 9 章中介绍过。因为 if 语句是我们第一次见到的实际 中使用测试结果的语句,所以我们要在这里扩展一些概念。尤其是 Python 的布尔运算符和 类 C 语言的布尔运算符有些不同。在 Python 中:



所有对象都有一个固有的布尔真/假值。



任何非零数字或非空对象都为真。



数字零、空对象以及特殊对象 None 都被认作是假。



比较和相等测试会递归地应用到数据结构中。



比较和相等测试会返回 True 或 False (1 和 o 的特殊版本)。



布尔 and 和 or 运算符会返回真或假的操作数对象。



布尔运算符会在结果确定的时候立即停止计算(“短路”)译注 2.

if 语句根据真值来选择执行语句,而布尔运算符用更丰富的方式结合多个测试的结果从而 生成新的真值。更正式地说, Python 中有 三种布尔表达式运算符:

X and Y 如果 x 和 Y 都为真,就是真。

X or Y 如果 x 或 Y 为真,就是真。

not X 如果 X 为假,那就是真(表达式返回 True 或 False) 。 此处, X 和 Y 可以是任何真值或者返回真值的表达式(例如,相等测试、范围比较等)。

布尔运算符在 Python 中是单词(不是 C 语言中的&&、 11 和!)。此外,布尔 and 和 or 运 算符在 Python 中会返回真或假对象,而不是值 True 或 False 。让我们看一些例子,来理解 它的工作原理:

译注 2:

“短路”是一种很常见的布尔表达式运算形式,它意味着当鲜释界计算到笫一个能够决定 整个表达式的值的叶候,会直接返回从而跳过之后的计算 。 例子参阅下面的正文 。

if 测试和语法规则

1 381

»> 2 < 3, 3 < 2

#

Less than: return True or False (I or 0)

(True, False) 上面这样的相对大小比较会返回 True 或 False 作为其真值结果,我们已在第 5 章和第 9 章 学过,但 True 和 False 其实只是整数 1 和 o 的特殊版本(只是打印时不同,其他方面完全

一样)。 另 一 方面, and 和 or 运算符总会返回对象,要么是运算符左侧的对象,要么是右侧的对象。 如果我们在 if 或其他语句中测试其结果,虽然总会得到预期的结果(记住,每个对象本质

上不是真就是假),但不是得到简单的 True 或 False 。 对于 or 测试, Python 会由左至右计算操作对象,然后返回第 一 个为真的操作对象。再者, Python 会在其找到的第一 个真值操作数的地方停止。这通常叫作短路计算,因为 一 旦得出 结果后,就会使表达式其余部分短路(终止) :

» > 2 or 3, 3 or 2

# Return left operand if true

(2, 3)

#

Else, return right operand (true or false)

»> [] or 3 >» [] or {} {} 上 一 个例子的第一 行中, 2 和 3 两个操作数都是真(非零),所以 Python 总是在计算了第

一 个操作数后就停止并返回这个操作数(因为第一 个操作数的真值保证了整个或表达式为 真)。在后面两个测试中,左边的操作数为假(空对象),所以 Python 会继续计算右边的 操作数并将其返回(该测试的结果非真即假)。 一 且知道结果后, Python 的 and 运算就会立刻停止。然而,对 and 而言, Python 由左至右

计算操作数,并且当操作数是 一 个假值对象的时候停止(因为第 一 个计算为假的对象保证 了整个与表达式为假,即假 and 任何值结果都为假) :

» >

and 3, 3 and 2

2

(3, 2)

# Return left operand iffalse #

Else, return right operand (true or false)

>» [) and {} []

»> 3 and [] [] 在这里,第一行的两个操作数都是真,所以 Python 会计箕两侧,并返回右侧的对象。在第 二个测试中,左侧的操作数为假([]),所以 Python 会在该处停止并将其返回作为测试结果。 在最后测试中,左边为真 (3) ,所以 Python 会计算右边的对象并将其返回(碰巧是假的[])。 这些最终的结果都和 C 及其他多数语言相同:如果在 if 或 while 中配合 or 和 and 的定义

进行测试时,你会得到逻辑真或假的值。然而在 Python 中,布尔返回左边或右边的对象, 而不是简单的整数标志位。

382

I

第 12 章

and 和 or 这种行为,乍看似乎很难理解,但是,看一看本章的边栏部分 “请留意:布尔值”

的例子,来了解它是如何帮助 Python 程序员编写代码的。下一小节也介绍了利用这一行为 的一种常用方式,以及在最新的 Python 版本中一种更好记忆的替代方案。

if/else 三元表达式 Python 中布尔运算符的 一种常见作用就是写个表达式 ,像 if 语句那样执行。考虑下列语句, 根据 x 的真值,把 A 设成 Y 或 Z:

if X: A = y

else: A = Z 有时这类语句中涉及的元素相当简单,用四行代码编写似乎太浪费了。在其他情况下,我

们可能想将这种内容嵌套在较大的语句内,而不是将其结果赋值给变晕。因此(坦白地讲, 因为 C 语言有类似工具), Python 2.5 引入了新的表达式形式,让我们可以在一个表达式 中编写出相同的结果:

A= Y if X else Z 这个表达式和前边四行 if 语句的结果相同,但是更容易编写代码。就像这个语句的等效语

句,只有当 X 为真时, Python 才会执行表达式 Y, 而只有当 X 为假时,才会执行表达式 z. 也就是说,这是 一个短路运算,就像上一节介绍的布尔运算符一样,仅仅执行 Y 和 Z 中的 一者。 以下是实际中它的一 些例子:

»> A ='t'if'spam'else'f' >» A

# For strings, nonempty means true

't'

>» A ='t'if''else'f' »> A 'f'

在 Python 2.5 之前(以及 2.5 版以后,如果你坚持的话),相同的效果可以小心地用 and 和 or 运算符的组合来实现。因为它们如同之前讨论过的,不是返回左边的对象就是返回右

边的对象:

A = ((X and Y) or Z) 这样可行,但有个问题:你得假定 Y 是布尔真值。如果是这样,效果就相同: and 先执行, 如果 X 为真,就返回 Y1 如果 X 为假, and 会直接跳过 Y, 从而只返回 Z 。换句话说,我们 得到的是“如果 x 为真则返回 Y, 否则返回 z" 。这等价干三元形式:

A= Y if X else Z

if 测试和语法规则

I

383

当你第一次碰到时,这种 and/or 组合似乎需要花点时间仔细分析后才能理解,但是在

Python 2.5 中已不需要。如果你需要这种结构,就使用更健壮和易千记忆的 if/else 表达式, 或者如果待选项并不简单的话,就使用完整的 if 语句。 此外,在 Python 中使用下列表达式也是类似的,因为 bool 函数会把 x 转换成对应的整数 1 或 o, 然后它们就能作为偏移量从一个列表中挑选出真假值:

A = [Z, Y)[bool(X)] 例如:

» > ['f','t'][ bool (") ] 'f'

»> ['f','t'][bool('spam')] 't I

然而,这并不完全相同,因为 Python 不会做短路运算,无论 X 值是什么,总是会执行 Z 和 Y 。 因为这种复杂性,在 Python 2. 5 以及之后的版本中你最好还是使用更简单、更易懂的 if/

else 表达式。不过,你还是应该少用 ,只有当待选项确实都很简单时才使用。否则最好写 完整的 if 语句,以便千之后的修改。你的同事也会感谢你的所作所为。

然而,你还是会在 Python 2.5 之前版本的代码(以及 一 些还没彻底转变代码习惯的 C 程序

员的代码)中看到 and/or 组合译注 20

请留意:布尔值 Python 的布尔运算符有着一些不寻常的作用,其中一种常见的用法就是通过 or 从一 组对象中做选择。像这样的语句.

X = A or B or C or None 会把 X 设为 A 、 B 、 C 中笫一个非空(为真)的对象,而如果所有对象都为空,就设

为 None 。这样行得通是因为 or 运算符会返回其左右的两个对象之一,这成为 Python 中相当常见的代码揣写技巧:从一个固定大小的集合中选择非空的对象(只要将它 们串在一个 or 表达式中即可) 。 在史简单的形式中,这也通常用来指定一个默认 值。在下面的例子中,如果 A 为真(或非空)的话将 X 设置为 A, 否则,将 X 设置为

default: 译注 2:

事实上, Python 中的 Y

if X else Z 语句和 C 语言中的 X ? Y : Z 语句略有不同 ,它使

代码史具有可读性 。 这种略微不同的顺序,是通过分析大量 Python 代码的使用模式后,

才精心设计成这样的。祁据 Python 的风格,选择这样的顺序也是为了防止前 C 程序员过 度使用它!需要记住的是简单胜于复杂,无论是在 Python 中还是在其他地方都是如此。 如果你确实需要像这样将逻辑打包进表达式中,那么使用语句也许是一种更好的选择。

384

I

第 12 章

X = A or default

理解布尔运算符的短路计算与 if/else 也是非常重要的一点,因为它可以用来进免一 些不必要的代码运行。比如布尔运算符右侧的表达式可能会调用函数来执行某些实质 或重要的工作。因此如果短路规则生效,附加的函数调用就不会发生。

if f1() or f2():... 在这里,如果 f1 返回具值(非空),那么 Python 将永远不会执行 f2 。为了保证两个 函数都会执行,你要在 or 之前调用它们。

tmpl, tmp2

=

fl(), f2()

if tmp1 or tmp2:... 你已经在本章见过该行为的另一种应用了:因为布尔运算的工作方式,表达式 ((A and

B)or C) 也几乎可用来模拟 if 语句(参阅本章对这一形式的详细讨论)。 在前面的章节我们遇到了额外的布尔用法示例。正如笫 9 章所述,因为所有对象本 质都是真或假,所以在 Python 中,直接浏试对象 (if

X:) 比空值比较 (if X

!=

I':) 史为常见和简单。对于一个字符串,这两个测试是等效的。正如笫 5 章所述,

预先设置的布尔值 True 和 False 与整数 1 和 0 是相同的,并且对于初始化变量 (X = False) 、循环测试 (while True: )以及在交互式命令行中显示结果都是很有用的。

还诗参阅本书笫六部分中对于运算符重载的讨论:当我们用类定义新的对象类型的时 候,我们可以用_bool_或_len_方法指定其布尔特性(在 Python 2.7 中_bool_ 叫作

nonzero

)。如果

bool

没有被重载的话,

如果一个对象为空,那么它的_len

就会被调用并返回(此时,

len

会返回 0, 也就是假。这等效于空对象为假)。

最后,作为预习,我们将简单介绍另外两个 Python 中跟 or 链式求值类似的工具。我 们将在之后遇到的 filter 函数调用和列表推导。它们可以被用于求一组要等到运行时 才能确定的候选项的真值(它们会计算所有候选项的真值并返回一切为真的对象),

而 any 和 all 的内置函数可以用来检测是否存在或者所有集合中的元素都为真(尽管 any 和 all 不能用于选择元素)

» > L = [ 1, o, 2, o,'spam','','ham', [ ]] »> list(filter(bool, L)) [1, 2,'spam','ham'] »> [x for x in L i f x] [1, 2,'spam','ham'] >» any(L), all(L) (True, False)

#

Get true values

#

Comprehensions

#

Aggregate truth

正如笫 9 章所述,这里的 bool 函数能直接返回其参数的真假值,其结果与 if 刹试一致。

史多信息参阅笫 14 幸、笫 19 章和笫 20 章。

if 测试和语法规则

I

385

本章小结 本章中,我们研究了 Python 的 if 语句。因为这是第一个复合及逻辑语句,我们也复习了 Python 的 一般语法规则,并比之前更深入地探索了真值和测试运箕。在此过程中,我们也 学习了如何在 Python 中编写多路分支、 Python 2.5 中引进的 if/else 表达式,以及布尔值

在实际代码中的 一些例子。 下一章要展开介绍 while 和 for 循环的内容,并继续探索过程化的语句。我们会学习在 Python 中编写循环的各种方式,其中的 一 些方式胜过其他方式。不过在那之前,先做 一 做

本章习题吧。

本章习题 I.

在 Python 中怎样编写多路分支?

2.

在 Python 中怎样把 if/else 语句写成表达式?

3

怎样让单条语句横跨多行?

4.

True 和 False 这两个单词代表了什么意义?

习题解答 I.

if 语句加多个 elif 分句通常是编写多路分支最直接的方式,但也许并不是最简明或最 灵活的。字典索引运算通常也能实现相同的结果,尤其是字典包含 def 语句或 lambda 表达式所编写的可调用函数。

2.

在 Python 2.5 中,表达式形式 Y

if X else Z 会 在 X 为真时返回 Y, 否则返回 Z 。这相

当千四行 if 语句。 and/or 组合 ((X and Y) or Z) 也以相同方式工作,但更难懂,而 且要求 Y 为真。

3

把语句包裹在括号之中(()、[]、或{})就可以按照需要横跨多行;当 Python 看见闭 合括号时,语句就会结束,同 一 语句的第 2 行开始可以任意地缩进。反斜杠也可以有 同样的作用,但是在 Python 世界里极其不推荐这种做法。

4.

True 和 False 只不过分别是整数 1 和 o 的特殊版本而已。它们代表的就是 Python 中的 布尔真假值。它们可以用来进行真测试、变抵初始化,以及在交互式命令行中打印表 达式结果。在所有的这些情况中,它们充当了 1 和 o 的更可读和易千记忆的替代物。

386

I

第 12 章

第 13 章

while 循环和 for 循环

本章将介绍两种 Python 的主要循环结构(也就是不断重复动作的语句),并以此结束对

Python 中过程化语句的介绍。首先是 while 语句 ,它提供了一种编写通用循环 的方式 1 其 次是 for 语句,它可以遍历序列或其他可迭代对象内的元素,并对每个元素运行一段代码块。

虽然前面已经非正式地提到这两种循环,但在这里,我们还要介绍一 些其他有用的细节。 此外,我们也会在这里研究 一 些在循坏中不太常用的语句(例如 break 和 continue) ,并

且会介绍循环中常用的 一 些内置函数(例如 range 、 zip 和 map) 。 尽管这里介绍的 while 和 for 语句是用来编写重复操作的主要语法,但 Python 中还是有其 他的循环操作和概念。因此,下一章将继续介绍迭代,我们将探索与 Python 迭代协议(被

for 循环所使用的)以及列表推导 (for 循环的近亲)相关的概念。稍后的各 章介绍了更加 特别的迭代工具,如生成器、 filter 和 reduce 。现在,让我们从最基础的内容学起。

while 循环 while 语句是 Python 语言中最通用的迭代结构。简而言之,只要头部行的测试一直计算为 真值,那么它就会重复执行一个语句块(通常有缩进)。它被称为"循坏”是因为控制权 会持续返回到语句 的开头部分,直至 测试为假。当测试变为假时,控制权会传给 while 块 后的语句。最终的效果就是循环主体在顶端测试为真时会重复执行。如果测试结果一开始 就是假,那么绝不执行主体,而且 while 语句也将被跳过。

一般形式 while 语句最完整的形式是:首行以及测试表达式有 一行或多行正常缩进语句构成的主体

387

以及一个可选的 else 部分 (else 部分会在控制权离开循环而又没有碰到 break 语句时执

行)。 Python 会一直计算头部的测试,然后执行循环主体内的语句,直至测试返回假值为止:

while test: statements else: statements

# Loop rest

# Loop body # Optional else #

Run if didn'r exit loop with break

示例 为了演示,我们来看 一些实际中 while 循环的例子。第 一 个例子, while 循环内有一个 print 语句,就是一直打印信息。回想一下, True 只是整数 1 的特殊版本,总是指布尔真值;

因为测试一直为真, Python 会一直执行主体,直至你停止执行为止。这种行为通常称为无 限循环一它不是真的不可结束,但是可能需要你按下 Ctrl+C 组合键来强制结束它:

>» while True: print('Type Ctrl-C to stop me!') 下面的例子会不断切掉字符串的第一个字符,直至字符串为空返回假为止。这样直接测试

对象,而不是使用更冗长的等效写法 (while x

!= ":),可以说是一种很典型的用法。

本章稍后,我们会看见用 for 循环更简单地遍历字符串内元素的其他方式:

, ·spam >» x = ' >» while x: print(x, end='') X = x[1:]

While x is not empty In 2.X use print x, # Strip first character off x

# #

spam pam am m 要注意,这里使用 end='

'关键字参数,来使所有输出都出现在同一行,并在它们之间用

空格隔开;如果你忘了这么做的原理,请查阅第 11 章。这可能会在输出结束的时候使输入

提示符与结果显示在同 一 行上;不过,你可以桉回车键来重置。对于 Python 2.X 的读者: 在 print 语句中,你需要在句尾多加一个逗号来获得相似的效果。

下面的代码会把 a 的值向上累加到 b 的值(但不包含 b) 。稍后,我们也会用 Python 的 for 循环和内置 range 函数来实现一个更简单的版本 :

»> a=O; b=10 >>>日hile

a< b: print(a, end='')

# One way to code counter loops

a+= 1

# Or, a= a+ 1

0 1 2 3 4 5 6 7 8 9

最后,要注意 Python 并没有某些语言中所谓的 “do until" 循环语句。不过我们可以在循环 体底部以一个测试和 break 来模拟类似的功能,这样循环体就能够至少运行一 次:

388

I

第 13 章

while True: ... loop body... if exitTest(): break 为了完全了解这种结构的工作方式,下一节我们将学习 break 语句。

break 、 continue 、 pass 和循环的 else 现在,我们巳看过一些 Python 循环的例子,接下来学习的这两种语句只有嵌套在循环中时

才起作用: break 和 continue 语句。我们也将学习一种行为稍显古怪的循环 else 分句,

而它常常也与 break 语句密切相关。此外,我们还要学习 Python 的空占位语句 pass (它本 身与循环没什么关系,但属于简单的单个单词语句的范畴)。在 Python 中:

break 跳出最近所在的外围循环(跳过整个循环语句)。

continue 跳到最近所在外围循环的头部(来到循环的头部行)。

pass 什么事也不做,只是一条空占位语句。 循坏 else 块

当且仅当循环正常离开时才会执行(也就是没有碰到 break 语句)。

一般循环形式 加入 break 和 continue 语句后, while 循环的一般形式如下所示:

while test: statements if test: break if test: continue else: statements

# #

Exit loop now, skip else if present Go to top of loop now, to test]

#

Run ifwe didn't hit a'break'

break 和 continue 可以出现在 while (或 for) 循环主体的任何位置,但通常会进一步嵌 套在 if 语句中,从而根据某些条件来采取对应的操作。 我们举一 些简单例子来看一看在实际应用中这些语句是如何组合使用的。

pass pass 语句是无运算的占位语句,当语法要求有一条语句却没有任何实际的语句可 写时,就

while 循环和 for 循环

I

389

可以使用它。它通常用千为复合语句编写 一 个空的主体。例如,如果想写个无限循环,每 次迭代时什么也不做,就写个 pass 。

while True: pass

# Type Ctrl-C to stop me!

因为主体只是空语句,所以 Python 会陷入死循环。对千语句而言, pass 几乎就和对象中的 None 一 样,表示什么也没有 。 要注意,

这里的 while 循环主体与头部行位于同 一 行上,并

写 在冒号后面;与 if 语句一样,只有当主体不是复合语句时,才可以这么做 。

本例将永远空转。这可能不是什么有用的 Python 程序(除非你想在寒冷的冬天替你的笔记

本暖暖机)。不过坦率地讲,我暂时也想不到更好的关千 pass 的示例。 以后我们会看到 pass 更有意义的用处,例如忽略 try 语句所捕获的异常,以及定义空的类 对象`用于携带属性并扮演其他编程语言中“结构体”或“记录”的角色。 pass 时常表示“以 后会填上“,也就是暂时填充函数的主体:

def func1(): pass

# Add real code here later

def fune2(): pass 如果函数体为空就会得到语法错误,因此我们可以使用 pass 来替代。

注意:版本差异提示: Python 3.X (而不是 Python 2.X) 允许在可以使用表达式的任何地方使

用.. (即 三 个连续的点号)来省略代码。由千省略号自身什么也不做,这可以当作是 pass 语句的一 种替代方案,尤其是对千随后填充的代码——这是 Python 的 一种 “TBD" (待定内容) :

def funcl(): II Alternative to pass def fune2(): funcl()

# Does nothing

if called

如果不需要具体类型,那么省略号可以和语句头部行出现在同一行,并且可以用来初始 化变批名:

def func1(): def fune2(): »> X = .. . >» X Ellipsis

# Works on same line roo

# Altemative to None

这种表示法是 Python 3.X 中新增的,并且这超越了分片扩展中的" … "的最初意图。因此, 它能否推广开来与 pass 和 None 的这类用法相抗衡,还摇拭目以待。

390

I

第 13 章

continue continue 语句 会立即跳到循环的顶端。此外,它偶尔也帮你避免语句嵌套。下面的例子使 用 continue 跳过奇数。这个程序代码会打印所有小千 10 并大千或等千 0 的偶数。因为 0 是假值,而%是除法余数(模数)运算符,所以这个循环会倒数到 0, 跳过不是 2 的倍数

的数字一它会打印出 8 6 4 2 0: X = 10

while x: X = X-1

!= o: continue print(x, end='')

if

X 为 2

# Or, x -= 1 # Odd? -- skip print

因为 continue 会跳到循环的开头,所以这里你不需要在 if 测试内放置 print 语句。只有

当 continue 不执行时,才会运行到 print 。这听起来有点类似其他语 言中的 “go to" ,的 确如此。 Python 没有 “go to" 语句,但因为 continue 让程序执行时实现跳跃,有关使用 “go to" 所面临的许多关千可读性和可维护性的警告都适用。 continue 应该少用,尤其是刚开 始使用 Python 的时候。例如,如果将 print 放在 if 下面,那么上个例子可能就会更清楚一些: X = 10

while x: X = X-1

if

X 为 2 ==

0:

#

Even? -- print

print(x, end='') 本书后面我们会学习到,引发和捕获异常也能限制化、结构化地模拟 “go to" 语句 1 继续

在第 36 章中学习关千这一技巧的更多信息,那里我们会学习利用异常来结束多重嵌套循环, 这是 一项单独使用下一节的 break 无法完成的功能。

break break 语句会导致立刻从一个循环退出。因为在碰到 break 时,位千 break 后的循环体代

码都不会被执行,所以有时可以引人 break 来避免嵌套化。例如,以下是简单的交互式命 令行下的循环(第 JO 章的一个大型示例的变体),通过 input (在 Python 2.X 中名为 raw_ input) 输入数据,并在用户输入 “stop" 时退出结束:

»> while True: name = input('Enter name:') if name=='stop': break age = input('Enter age:') print('Hello', name,'=>', int(age) Enter Enter Hello Enter Enter

#

**

Use raw_input() in 2.X

2)

name:bob age: 40 bob => 1600 name:sue age: 30 while 循环和 for 循环

I

391

Hello sue=> 900 Enter name:stop 要注意,这个程序在计算平方前,会先把年龄值通过 int 转换成整数。回顾一下,因为用 户输入是一个字符串,所以我们需要做这种转换。在第 36 章中,你会看到 input 也会在用 户键人文件结束符(例如,在 Windows 上按下 Ctrl+Z 组合键或在 UNIX 上按下 Ctrl+D 组

合键)时引发异常;如果你需要考虑文件结束符的情况,则可以用 try 语句把 input 括起来。

循环的 else 当与循环的 else 分句组合使用时 , bre ak 语句通常可以代替其他编程语言中指示循环是 否

正常结束状态的标志位。例如,下面的程序通过搜索是否存在一个大千 l 的因数,来判断 正整数 y 是否为质数:

x = y II 2 while x > 1: if y % X == O: print(y,'has factor', x) break X -=

#

For some y > 1

#

Remainder

#

Skip else

#

Normal exit

1

else: print (y,'is prime')

除了在循环内设置标志位并在循环退出时进行测试外,你也可以像上面那样,在找到因数 的位置使用 break 语句。因此,循环 else 分句可以视为只有在没有找到因数时才会执行。 如果你没碰到 break, 那么该数就是质数。你可以跟踪这段代码来看看它是如何工作的。 如果循环主体从没有执行过,循环 else 分句也会执行,因为此时你也没有在循环体中执行 break 语句。也就是说在 while 循环中,如果首行的测试一开始就是假,那么 else 也会执行 。

因此在上面的例子中,如果 x 一开始就小千或等千 l (例如,如果 y 是 2) ,那么你还是会 得到 “is prime" 的信息。

注意:这个例子能够判断质数,但却不是那么规范。按照严格的数学定义,小千 2 的数字就不 是质数了。确切地说,这个程序碰上负数会失败而对千没有小数部分的浮点数会成功。

还要注意,由千 Python 3.X 的 I 向“真除法”迁移的问题(见第 5 章),因此在 Python 3.X 中必须使用 II 而不是 I (因为我们需要初始的除法来截断余数,而不是保留余数)。 如果你想试验这个代码,一定要看一看第四部分末尾的练习题,那里将该例包在函数中 以便重用。

关千循环 else 分句的更多内容 因为循环 else 分句是 Python 特有的语法,所以一些初学者容易产生困惑( 一 些老手也会 不再使用;我遇见过 一 些甚至从不知道循环里有一 条 else 分句的人)。简而言之,循环

392

I

第 13 章

else 分句就是提供了常见代码编写情形的显式语法:这是让你不必设置和检查标志位,就 能够捕捉循环退出情况的一种编程结构。

例如,假设你要编写 一 个搜索列表值的循环,而且需要知道在离开循环后该值是否已找到,

就可能会采用这种方式(下面代码是故意编写得抽象和不完整的; x 是一个序列,而 match 是一个待编写的测试函数) :

found= False while x and not found: if match(x[o)): print ('Ni') found= True else: X=X[1:] if not found: print('not found')

# Value at front ?

# Slice offfront and repeat

这里,我们通过初始化,设置井随后测试一 个标识符,来判断搜索是否成功。这是 一 段有 效的 Python 程序,也的确可以运行。然而,这一需求正是循环 else 分句所能处理的结构。 下面是 else 的等价版本:

while x: if match(x[o]): print('Ni') break X = x[l:] else: print ('Not found')

# Exit when x empty

# Exit, go around else

#

Only here if exhausted x

这个版本要更简洁 一些。我们不再需要标志位,而是在循环末尾使用 else (和 while 这个 关键字垂直对齐)来取代 if 测试。因为 while 主体内的 break 会离开循环并跳过 else, 所以这是捕捉搜索失败情况的一种更结构化的方式。 有些读者可能注意到,上面例子中的 else 分句等价千在循环后测试 x 是否为空(例如, if

not x: )。虽然这个例子中确实如此,但 else 分句为这种编程模式提供了更显式的语法 (else 分句在这里,明显代表了搜索失败的情况),而且这种显式的为空测试并不适用千一些其

他的情况。当与 for 循环(下一节的主题)组合使用时,循环 else 分句会变得更有用,因 为序列迭代是不受你控制的。

请留意:仿真 C 语言的 while 循环 笫 11 章有关表达式语句的那节指出, Python 不允许语句(例如,赋值)出现在应该 是表达式出现的位置 。 也就是说,每条语句通常情况下必须独占一行,而不能嵌套在

更大型的结构中 。 这意味着下面的常见 C 语言编程模式在 Python 中是行不通的:

while 循环和 for 循环

I

393

while ((x = next()) != NULL) {... process x... } C 语言的赋值运算会返回被赋子的值,但 Python 的赋值语句却只是语句,而不是表达 式。这样就消除了一个众所周知的 C 语言错误:当需要使用“==”时,在 Python 中 是不会被不小心打成“="的 。但 如果你需要类似的行为,至少有三种可以在 Python 的 while 彶环中实现相同效果的方式,而不必在循环浏试中嵌入赋值语句。首先,你 可以配合 break, 把赋值语句移入彶环体中:

while True: x = next(obj) if not x: break ... process x... 其次,你可以配合 if 刹试,把赋值语句移入循环体中:

x = True while x: x = next(obj) if x: ... process x... 最后,你可以把笫一次赋值移到彼环体外.

x = next(obj) while x: ... process x... x = next(obj) 这三种编程模式中,有些人认为笫一种是最不结构化的,但这也似乎是最简单、最常 用的 。简 单的 Python for 循环也可以取代这样的 C 语言循环并且更加 Python 化 ,但 是 C 语言中并没有相对应的工具:

for x in obj:... process x...

for 循环 for 循环在 Python 中是一个通用的序列迭代器:它可以遍历任何有序序列或其他可迭代对 象内的元素。 for 语句可用千字符串、列表、元组或其他内置可迭代对象,以及之后我们

通过类所创建的用户定义的新对象。在第 4 章讨论序列对象类型时我们曾 一 并简要地提过 for, 接下来让我们更正式地讨论其用法。

一般形式 Python for 循环的首行指定了 一个(或一些)赋值目标,以及你想遍历的对象。首行后面 是你想重复的语句块(按照正常缩进规则)

394

I

第 13 章

for target in object: statements else: statements

II Assign object items to larger II Repeated loop body: use targer II Op1io11al else part II If we didn 't hit a'break'

当 Python 运行 for 循环时,会逐个将可迭代对象 object 中的元素赋值给名称 target, 然 后为每个元素执行循环主体。循环主体一 般使用被赋值的目标来引用序列中当前的元素, 就好像它是遍历序列的游标。 for 头部行中用作赋值目标的名称通常是 for 语句所在的作用域中的变址(可能是新的)。 这个名称没什么特别的,甚至可以在循环主体中修改,但当控制权再次回到循环顶端时,

就会自动被设成序列中的下一个元素。循环之后,这个变量 一 般还是引用了最近所用过的 元素,也就是序列中最后的元素,除非通过一个 break 语句退出了循环。 for 语句也支持 一个可选 的 else 块,它的效果与在 while 循环中 一 样:如果循环离开时没

有碰到 break 语句,就会执行(也就是序列所有元素都访问过 f) 。之前介绍过的 break 语句和 continue 语句也可用在 for 循环中,与 while 循环一样。 for 循环的完整形式如下:

for target in object: statements if test: break if test: continue else: statements

#

Assign object items to target

#

Exit loop now. skip else

# Go to top of loop now # If we didn't hit a'break'

示例 我们现在向交互式命令行输入一些 for 循环,来看看它们在实际应用中是如何使用的。

基础应用 如前所述, for 循环可以遍历任何 一 种序列对象。例如,在第 一 个例子中,我们将名称 x

由左至右依次赋给列表中三个元素,而 print 语句会对每个元素都执行一 次。在 print 语 句内(即循环体内),名称 x 引用的是列表中的当前元素:

>>> for x in ["spam", "eggs", "ham"]: print(x, end='') spam eggs ham 下面的两个例子会计算列表中所有元素的和与积。在本章和本书后面,我们会介绍其他 一

些工具,可以自动对列表中的元素应用诸如“+”和“*"的运算,但是它们通常与使用 for 循环一样简单 :

»> sum = o »> for x in [1, 2, 3, 4]: while 循环和 for 循环

I

395

sum= sum+ x

»> sum 10

>» prod = 1 »> for item in [1, 2, 3, 4]: prod*= item

...

>» prod 24

其他数据类型 任何序列都可以用千 for 循环,因为 for 循环是一个泛化工具。例如, for 循环可用于字 符串和元组:

»> S = "lumberjack" >>> T = ("and", "I'm", "okay") »> for x in S: print(x, end='')

#Irerare over a string

... 1 u mb e r j a c k

»> for x in T: print(x, end='')

#

Iterate over a tuple

and I'm okay 实际上,当我们在下 一章学习”可迭代对象”概念时就会了解到, for 循环甚至可以用在

一些根本不是序列 的对象上,如对千文件和字典也有效。

for 循环中的元组赋值 如果你在迭代一个 由元组构成的序列,那么循环目标本身其实可以是 一 组目标。这只是第

11 章元组解包赋值运算的另一个例子而已。记住, for 循环把序列中的对象元素赋值给目 标名称,而赋值运算在任何位置的工作方式都是相同的:

»> T = [(1, 2), (3, 4), (5, 6)] »> for (a, b) in T:

# Tuple assignment at work

print(a, b) 246 135

在这里,第一次经过循环就像是编写 (a,b) = (1,2) ,而第 二次就像是编写 (a,b) =

(3,4),

以此类推。最终效果就是在每次循环中,都会自动解包赋值当前元组。 这种形式通常与在本章后面介绍的 zip 调用 一井使用,从而实现井行遍历。在 Python 中,

它通常还和 SQL 数据库一起使用,其中查询结果表作为像这里的列表 一样序列的序列而返 回一—外层的列表就是数据库表,内嵌的元组就是表中的行,并通过元组赋值提取每 一 列

的信息。

396

I

第 13 章

你也可以很方便地借助 for 循环中的元组解包赋值、配合字典的 items 方法来遍历字典的 键和值,而不必再遍历键并手动索引来获取值:

»> D = {'a': 1,'b': 2,'c': 3} »> for key in D: print(key,'=>', D[key])

a =>

# Use diet keys iterator and index

1

=> 3 b => 2 C

»> list(D.items()) [('a', 1), ('c', 3), ( ' b', 2)] »> for (key, value) in D.items(): print(key,'=>', value)

# Iterate over both keys and values

132 acb >>> ==--

还要特别注意, for 循环中的元组赋值并非一种特殊情况,你可以在单词 for 后面使用所

有符合语法的目标赋值。我们总是可以在 for 循环中手动赋值来解包:

»> T [(1, 2), (3, 4), (5, 6)] >» for both in T: a, b = both print(a, b)

# Manual assignment equivalent #2.X:prints with enclosing tuple "()"

246 135

但是在遍历"序列的序列”时,循环头部行的元组为我们节省了额外的一步。正如第 11 章

所述,在 一 个 for 循环中,即便是嵌套的结构也能够以这种方式自动解包:

>» ((a, b), c) = ((1, 2), 3) »> a, b, c (1, 2, 3)

# Nested sequences work too

»> for ((a, b), c) in (((1, 2), 3), ((4, 5), 6)]: print(a, b, c) 25 36 14

但这也不是特殊情况一—for 循环只是在每次执行循环体之前,运行了我们自己也能运行的 那些赋值而已。任何嵌套的序列结构都能以这种方式解包,也仅仅是因为序列赋值的通用性:

»> for ((a, b), c) in [([1, 2], 3), ['XV', 6]]: print(a, b, c) 1 2 3

while 循环和 for 循环

I

397

X Y 6

for 循环中的 Python 3.X 扩展序列赋值 实际上,由千 for 循环的循环变蜇可以采用任何形式的赋值语句,因此,这里我们也可以

使用 Python 3.X 的扩展序列解包赋值语法,来提取序列中序列的元素或组件。实际上,这 也不是特殊情况,只不过是 Python 3.X 中一种新的赋值形式(正如第 11 章所述) ;因为它 在赋值语句中有效,所以它自动在 for 循环中也有效。 考虑前一小节介绍的元组赋值形式。在每次迭代时, 一 个元组的值披赋给了 一 个元组的名称, 就像是一条简单赋值语句一样:

»> a, b, c = (1, 2, 3) >» a, b, c

#

Tupie assignment

(1, 2, 3)

»> for (a, b, c) in [(1, 2, 3), (4, 5, 6)]:

# Used inforloop

print(a, b, c) 25 36 14

在 Python 3.X 中,由干 一 个序列可以被赋值给一 组更为通用的名称(其中有一个带有足号

的名称可以收集多个元素),因此,我们可以在 for 循环中使用同样的语法来提取嵌套序 列的组件:

»> a, *b, c = (1, 2, 3, 4) »> a, b, c

# Extended seq assignment

(1, (2, 3], 4)

»> for (a, *b, c) in [(1, 2, 3, 4), (5, 6, 7, 8)]: print(a, b, c) 1 [2, 3) 4 5 [6, 7) 8 实际中,这种方式可以从表示为嵌套序列的数据行中 一 次选取多个列。在 Python 2.X 中不 允许使用带星号的名称,不过你可以通过分片实现类似的效果。唯 一 的区别是分片返回 一 个相应数据类型的结果,而星号名称则总会被返回成列表:

»> for all in [(1, 2, 3, 4), (5, 6, 7, 8)]: a, b, c = all[o], all[l:3], all[3] print(a, b, c) 1 (2, 3) 4 5 {6, 7) 8 参阅第 Ll 章了解扩展序列赋值形式的更多内容。

398

I

第 13 章

II Manual slicing in 2.X

嵌套 for 循环 现在,我们来学习更复杂一些的 for 循环。下面的例子示范了同时在 for 中使用循环 else 分句以及语句嵌套。给定对象列表 (items) 以及键列表 (tests) ,这段代码会在对象列 表中搜索每个键,然后报告其搜索结果:

»> items = ["aaa", 111, (4, 5), 2.01] »> tests = [(4, 5), 3.14) >» >» for key in tests: for item in items: if item== key: print(key, "was found") break else: print(key, "not found!")

# A set of objects # Keys to search for

# For all keys # #

For all items Check for match

(4, 5) was found 3.14 not found! 因为内嵌的 if 会在找到相符结果时执行 break, 所以循环 else 分句 一 且被执行到,就会

判定搜索失败。注意这里的嵌套。当这段代码执行时,同时有两个循环在运行:外层循环 会扫描键列表,而内层循环为每个键扫描元素列表。循环 else 分句的嵌套是很关键的:由 干它被缩进至和内层 for 循环位千相同的层次,因此是和内层循环相关联的,而不是 if 或 外层的 for 。

这个例子是教学性的,不过如果我们采用 in 运算符来测试成员关系,那么该例编写起来会 更加简单。因为 in 会隐式地扫描 一个对象来寻找匹配,所以可用千取代内层循环:

» > for key in tests: if key in items: print(key, "was found") else: print(key, "not found!")

#

For all keys

# Let Python check for a match

(4, 5) was found 3,14 not found! 一 般来说,为了获得更简洁的代码和更优异的性能,你应该把尽可能多的工作交给 Python

来完成(如该例所示)。 下 一 个例子是相似的,但不是打印结果,而是创建了 一个列表以便之后使用。它用 for 完

成了一个典型的数据结构任务:收集两个序列(字符串)的共同元素。这可以大致用作求 集合交集的子程序。在循环执行后, res 引用的列表中包含了所有 s eq1 和 seq2 中找到的 共同元素:

»> seql = "spam" >>> seq2 = "scam" while 循环和 for 循环

I

399

>» »> res = [] »> for x in seq1:

# Start empty # Scan first sequence # Common item?

if x in seq2: res.append(x)

#

Add to result end

»> res ['s','a','m'] 不过,这个程序代码只能用在两个特定的变扯上: seq1 和 seq2 。如果该循环可以通用化成 一种能多次使用的工具,则会更好。之后你会学到,这个简单的想法会把我们引向函数, 也就是本书下一部分的主题。 这段代码也展示了经典的列表推导模式(即用迭代和可选的过滤测试,来收集一个结果列

表),井可以被更简洁地编写为:

»> [x for x in seql if x in seq2]

#

Let Python collect results

['s','a','m'] 但你必须继续阅读下一章,才能完整掌握列表推导。

请留意:文件扫描器 一般来说,每当你需要重复一个运算或重复处理某件事的时候,循环就能派上用场. 由于文件包含了许多字符和行,因此,它们也是典型的徙环案例之一。要把文件的内 容一次加载至字符串,你可以调用文件对象的 read 方法:

file= open('test.txt','r') print (file. read())

#

Read contents into a string

但如果要分块加载文件,那么通常要编写一个 while 循环或 for 循环,并在文件结尾 时使用 break 。如果要按字符读取,则下面两种编程方式都能工作

file= open('test.txt') while True: char= file.read(l) if not char: break print(char)

# #

Read by character Empty string means end-of-file

for char in open('test. txt'). read(): print(char) 这里的 for 循环也会处理每个字符,但会一次把文件加载至内存(并假设内存能够装 得下整个文件)。如果要按行或按块读取,你可以使用下面这样的 while 循环编写.

file : open('test. txt') while True: line : file. readline ()

400

I

第 13 章

·

# Read line by line

if not line: break print{line.rstrip{)) file= open('test.txt ' , ' rb') while True: chunk= file.read{10) if not chunk: break print{chunk)

#

Line already has a\n

#

Read byte chunks: up to 10 bytes

你通常会按块读取二进制数据 。 不过,如果要逐行读取文本文件,那么 for 循环是最 易于编写且执行最快的方式:

for line in open('test.txt').readlines(): print(line. rs trip()) for line in open('test.txt'): print(line. rstrip())

#

Use iterators: best for text input

这两个版本在 Python 2.X 和 3 .X 下都可以运行 。 笫一个例子使用了文件 readlines 方 法来一次性把文件载入成每行字符串的列表,最后的例子则借助文件迭代器自动在每 次徙环迭代的时候读入一行 。

最后一个例子也通常是文本文件的最佳选择 : 除了简单,它还适用于任意大小的文件, 因为它不会一次把整个文件都载入到内存中 。 迭代器版本也可能是最快的 , 尽管 I/0

性能也许会随着 Python 2. X 与 3 .X, 以及具体发行版本的不同而有所变化 。 当然,文件 readlines 调用还是很有用的一—例如 ,

当你要按行反转一个文件,并假

设它的大小能够放入内存 。 内置函数 reversed 可以接受一个序列 , 却不能接受一个 生成值的任意可迭代对象 ; 换言之, reversed 可以接受列表,却不可以接受文件对象:

for line in reversed (open ('test. txt'). readlines ()):... 在一些 Python 2 . X 代码中 , 也可以看到要改用 file 来替代 open, 以及使用文件对 象较早的 xreadlines 方法,从而实现与文件的自动行迭代器同样的效果(它就像

是 readlines, 但是不会一次把文件载入到内存中) 。 Python 3.X 中移除了 file 和 xreadlines, 因为它们都是多余的 。 通常,你也应该在新的 2.X 代码中进免使用它们 ,

作为替代 , 在最新的 2.X 发行版本中使用文件迭代器和 open 调用 。

不过,它们可能

出现在更早的代码和资源中 。

参闭库手册以了解更多关于这里用到的调用的内容,同时参阅笫 14 章了解更多关于 文件行迭代的知识 。 也可以查看本章的边栏 “ 请留意 : Shell 命令及其他“;它将这

些相同的文件工具应用于命令行启动器 os.popen 来读取程序轮出。参阅笫 37 章了解 关于读取文件的更多内容;在那里我们将看到 , 文本文件和二进制文件在 Python 3.X 中有着细微的语义差别 。

while 循环和 for 循环

I

401

编写循环的技巧 刚刚学过的 for 循环包含了常见的计数器式循环。由于通常 for 循环比 while 循环更容易写,

也执行得更快,所以 for 循环 一 般是你遍历序列或其他可迭代对象时的首选。事实上,作 为 一 条通用法则,你应该克制在 Python 中使用计数方式的诱惑一Python 提供的迭代工具,

能帮你把像 C 这样低级语言中循环集合体的 工 作自动化。 不过,有些情况下你还是需要以更为特定的方式进行迭代。例如,如果你儒要在列表中每

隔 一 个元素或每隔两个元素进行访问,或是要同时修改列表呢?如果在同 一 个 for 循环内, 并行遍历 一个以上的序列呢?如果你也需要进行索引呢?

你总是可以用 while 循坏和手动索引运算来编写这些独特的循环,但是 python 提供了 一 套 内置函数,可以帮你在 for 循环内定制迭代:



内置由数 range (Python O.X 及之后版本可用)返回 一 系列连续增加的整数,可作为 for 中的索引。



内置函数 zip (Python 2.0 及之后版本可用)返回 一 系列并行元素的元组,可用千在 for 中内遍历多个序列。



内置函数 enumerate (Python 2.3 及之后版本可用)同时生成可迭代对象中元素的值和 索引,因而我们不必再手动计数。



内置函数 map (Python 1.0 及之后版本可用)在 Python 2.X 中与 zip 有着相似的效果, 但是在 3.X 中 map 的这一 角色被移除了。

因为 for 循环可能会比基千 while 的计数器循环运行得更快,所以借助这些工具并尽可能 地使用 for 循环,会让你受益匪浅。让我们在常见的使用场景下,依次看 一 看这些内置函

数吧。我们将会看到,它们的用法在 Python 2.X 和 3.X 中稍有不同,同时它们中的一些要

比其他的更加有效。

计数器循环: range range 作为我们要学习的第 一 个与循环相关的函数,确实有用。在第 4 章我们简单地了解

过它。虽然 range 通常在 for 循环中生成索引,但也可以用在任何需要一系列整数的地方。 在 Python 2.X 中, range 会创建 一 个真实的列表;而在 Python 3.X 中, range 则是一个会

按需产生元素的可迭代对象,因此在 Python 3.X 中我们需要将 range 放在 list 调用中, 才能一次性显示它的所有内容:

>» list(range(S)), list(range(2, s)), list(range(o, 10, 2)) ([o, 1, 2, 3, 4), (2, 3, 4), [o, 2, 4, 6, B])

402

I

第 13 章

当传入 一 个参数时, range 会产生 一 个从零算起的整数列表,但列表中却不包括该参数的值。 当传入两个参数时,第 一 个参数将视为下边界。 range 的第三 个可选参数,则可以提供步

长。当传人第三 个参数时, Python 会对每个连续整数加上步长从而得到结果(步长默认为

+I) 。 range 也可以是非正数或非递增的:

»> list(range(-5, 5)) [-5, -4, -3, -2, -1,

o,

1, 2, 3, 4]

»> list(range(s, -5, -1)) (5, 4, 3, 2, 1, o, -1, -2, -3, -4] 第 14 章将更正式地介绍可迭代对象。那里,我们也会看到 Python 2.X 中有 一 个名为 xrange 的表亲,它和 Python 2.X 中的 range 很像,但不同的是 xrange 并没有一次性在内 存中创建结果列表。这是 一种空间上的优化,在 Python 3.X 中被纳入到新的 range 的生成

器行为中。 虽然 range 结果本身就很有用,但 for 循环才是它们的用武之地。其中之 一是 range 提供

一 种指定具体循坏重复次数的简单方式。例如,要打印 3 行时,可以用 range 生成适当的 整数数字:

»> for i in range(3): print(i,'Pythons')

o Pythons 1 Pythons 2 Pythons 要注意,在 Python 3.X 中 for 循环会强制 range 自动产生结果,因此你不需要在 Python 3 . X 中用 list 来包装(在 Python 2. X 中我们会得到 一 个临时列表,除非用 xrange 来代替 range) 。

序列扫描: while 和 range

vs for

range 调用有时也用来间接地迭代 一 个序列,尽管在这一 角色中它通常不是最佳方案。遍

历序列最简单通常也最快的方式永远是使用简单的 for, 因为 Python 为你处理了大多数细 节:

>» X ='spam' »> for item in X: print(item, end='')

#

Simple iteratwn

s p a m

从内部实现上看, for 循环以这种方式使用时,会自动处理迭代的细节 。 如果你真的想要

显式地掌控索引逻辑,也可以用 while 循环来实现:

while 循环和 for 循环

I

403

»> i = 0 »> while i < len(X):

# while loop iteration

print(X[i], end='') i += 1 s p a m

当然,你也可以使用 for 进行手动索引,也就是用 range 生成供迭代来索引的列表。虽然

这是一个多步骤的过程,但对千生成偏移量(而不是生成位千那些偏移量的元素)来说足 够了:

»> X 'spam'

»> len(X)

# Length of string

4

»> list(range(len(X))) # A/I legal offsets into X [o, 1, 2, 3) »> »> for i in range(len(X}): print(X[i], end='') # Manual rangellen iieration s p a m 要注意,因为在这个例子中我们是用过 一 个偏移量列表来步进 X, 所以为了获得 x 中实际

的元素,我们还需在 X 中进行索引。杀鸡焉用宰牛刀,真的没必要在这个例子中如此费劲。 尽管 range/len 组合也能满足这里的需求,但它也许不是最佳选项。它可能运行得更慢, 井且要付出更多的工作。除非你有特殊的索引需求,否则最好使用 Python 中简单的 for 循

环形式:

»> for item in X: print(item,end='')

#

Use simple iteration ifyou can

作为一个通用准则,尽可能使用 for 而不是 while, 并且不要在 for 循环中使用 range 调用, 除非迫不得已。简单的方案总会更好。不过每一条准则都有它不适用的特例,如下 一节所述。

序列乱序器: range 和 len 尽管对千简单序列扫描并不是那么理想,但上一个例子中所用的编码模式,确实可让我们 在必要时实现更特殊的遍历。例如, 一 些算法能利用序列的重新排序,来完成搜索中的替

代品生成,或是测试不同顺序的影响等。这些情况需要借助偏移量来拆分和拼接序列,就 像下面这样;在第一个例子中 range 的整数提供了重复次数,而在第 二个例子中则提供了 分片位置:

,

»> s ='spam >» for i in range(len(S)): S = S[l:] + S[:1] print(S, end='')

404

I

第 13 章

#

For repeat counts 0.. 3

# Move front item to end

pams amsp mspa spam



s

'spam' »> for i in range(len(S)): X = S[i:] + S[:i] print(X, end='')

# #

For positions 0..3 Rear part + front part

spam pams amsp mspa 如果你感到困惑,可以试着一次只追踪一个迭代。第二个会和第一个产生相同的结果,但 是顺序不同,并且也不会改变原始的变最。因为两者都是使用分片和拼接,所以它们可以 在任意类型的序列上工作,并返回乱序后的相同类型序列一一如果你乱序了一个列表,你 就创建了重新排序后的列表:

»> L = (1, 2, 3) >» for i in range{len{L)): X = L[i:] + L[:i]

# Works on any sequence type

print{X, end='') [1, 2, 3) [2, 3, 1] [3, 1, 2] 我们将在第 18 章用这样的代码产生不同的参数顺序来测试函数,并会将其扩展到函数、生 成器,以及第 20 章中更完整的全排列~是一 个广泛使用的工具。

非穷尽遍历: range vs 分片 上一节的案例都是 range/len 组合的有效应用。我们可以使用这一 技巧来跳过元素,如下 所示:

»> S ='abcdefghijk' »> list(range(o, len(S}, 2))

[o,

2, 4, 6,

s,

10]

»> for i in range(o, len(S), 2): print(S[i], end='') ... a c e g i k 在这里,我们使用 range 产生的列表,每隔 一个访问字符串 s 中的元素。要每隔两个访问元素, 你可以把 range 的第 三 参数改为 3, 以此类推。实际上,你可以使用 range 在保持 for 循 环简洁性的同时,来跳过循环内的元素。

不过在大多数情形下,这可能还算不上今日 Python 的“最佳实践”技巧。如果你真的希望 跳过序列中的元素,第 7 章介绍的分片表达式的扩展 三参数形式,提供了实现相同目标更

简便的方法。例如,要每隔一个地访问 S 中的字符,你可以用步长 2 来分片:

»> S ='abcdefghijk' »> for c in S[::2]: print(c, end='') while 循环和 for 循环

I

40s

a c e g i k 虽然结果是相同的,但对我们来说更容易编写,同时对其他人来说更容易阅读。在这里, 使用 range 的潜在优点的确节省了空间:分片在 Python 2.X 和 Python 3.X 中都会复制字符串,

而 Python 3.X 中的 range 和 Python 2.X 中的 xrange 都不会创建列表;对千较大的字符串 而言,这将节省许多内存。

修改列表: range vs 推导 另 一 个使用 range/len 与 for 组合的常见场景,就是在循环中遍历列表时对其进行修改。

例如,假设你需要为列表中的每个元素都加 1 (也许你正在给 一 个雇员数据库列表中的每 一 位加薪)。你可以通过简单的 for 循坏来实现,但结果可能并不如你所愿:

»> L = [1, 2, 3, 4, 5] >» for x in L: X

+=

1

#

Changes x, not L

>» L [1, 2, 3, 4, 5]

»>)( 6 这并不奏效,因为你修改的是循坏变量 x ,而不是列表 L 。其原因有些微妙。每次经过循环时, x 会引用已从列表中取出来的下一 个整数。例如,第一 轮迭代中, x 是整数 l 。下一 轮迭代中, 循环主体把 x 设为不同对象,也就是整数 2, 却并没有更新 1 所来自的那个列表 I X 和列表 在内存中是两块相互独立的位置。

要真的在我们遍历列表时对其进行修改,我们必须借助索引,在遍历时给每个位置赋 一 个 更新后的值。 range/len 组合可以替我们产生所需要的索引:

»> L = [1, 2, 3, 4, 5) »> for i in range(len(L)): L[i] += 1

# Add one to each item It Or L{i/ = L[i/ + 1

in L

»> L [2, 3, 4, 5, 6] 使用这种方式编写时,随着循环的执行,列表中的内容会相应改变。你却无法简单地使用

for x in L :风格的循环来实现相同的效果,因为这种循环遍历的是实际的元素,而不是 列表的位置。但是,等效的 while 循环又如何呢?这种 while 循环需要额外的工作,并且 取决千你的 Python 版本,有可能运行得更慢(在 Python 2 . 7 和 Python 3.3 中确实比 for 循

环慢,但在 Python 3.3 中却没有慢那么多 ——我们会在第 21 章中学习测试程序耗时的方式):

406

I

第 13 章

»> i = 0 »> while i < len(L): L[i] += 1 i += 1 »> L [3, 4,

s,

6, 7)

然而在这里, range 解决方案也不够理想。一个如下形式的列表推导表达式:

[x+l for x in L) 在今天可能会运行得更快,井实现相同的效果,但没有对原本的列表进行原位置的修改(我

们可以把推导表达式的新列表对象赋值给 L, 但是这样不会更新原本列表的其他任何引用 值)。由千列表推导是循环的一个核心概念,因此下一章将对其进行详细介绍。

并行遍历: zip 和 map 下面介绍的这个技巧将延伸循环的使用范围。如前所述,

内置函数 range 允许我们在 for

循环中以非穷尽的方式遍历序列。以同样的思路,内置的 zip 函数允许我们使用 for 循环 并行访问多个序列一一-在同一个循环内且时间上不重叠。在基础用法中, zip 的输入参数是 一个或多 个序列,而它的返回值是将这些序列井排的元素配对得到元组的列表。例如,假

设我们使用两个列表(或许是按照位置配对的姓名、联系地址的列表)

>» L1 = (1,2,3,4) »> L2 = (5,6, 7,8) 要组合这些列表中的元素,我们可以使用 zip 创建一 个元组对的列表。与 range 一样, zip 在 Python 2.X 中返回一个列表,但在 Python 3.X 中则返回一个可迭代对象,我们必须将其

包含在一个 list 调用中才能一 次性显示所有结果(下一章将详细地介绍可迭代对象)

»> zip(Ll, L2)

»> list(zip(Ll, L2}) ((1, s), (2, 6), (3, 1), (4,

# list() required in 3.X, not 2.x

s)J

这样的结果在其他场景下也适用,不过当与 for 循环配合使用时,它就能支持并行迭代:

»> for (x, y) in zip(Ll, L2): print(x, y,'--', x+y) 1 2 3 4

5 6 7 8

-----

6 8

10 12

在这里,我们遍历了 zip 调用的结果,也就是说 ,从两个列表中提取出来 的元素对。注意,

while 循环和 for 循环

I

407

该 for 循环也使用了我们之前介绍的元组赋值运算,来解包 zip 结果中的每个元组。在第 一次迭代中,就好像我们执行了赋值语句 (x, y) =

(1, 5) 。

最终结果就是在循环中同时扫描了旦和 L2 。我们也可以用 while 循环手动处理索引获得 相同的效果,但这需要更多的输入,而且很可能比 for/zip 运行得要慢。

严格来讲, zip 函数比这个例子所展示的更一 般化。例如, zip 可以接受任何类型的序列(本 质上可以是所有的可迭代对象,包括文件),并且它支持两个以上的参数。对千 3 个参数(如 下所示),它构建了 3 元素元组的 一 个列表 , 其中带有来自每个序列的元素 , 并按照列来 对应(从技术上讲,如果向 zip 输入 N 个参数,我们将得到 N 元素元组的 一 个列表)

>» Tl, TZ, T3 = (1,2,3), (4,5,6), (7,8,9) >» T3 (7, B, 9)

>» list(zip{T1, Tz, H))

# Three tuple f or three arguments

[(1, 4, 7), (2, 5, 8), (3, 6, 9)) 此外,当各个参数长度不等时, zip 会以最短序列的长度为准来截断结果元组。在下面的

例子中,我们把两个字符串 zip 到 一 起以并行地选取字符,但是结果所拥有的元组数目只 会与最短的序列的长度 一 致 :

» > S1 ='abc' »> S2 ='xyz123' »> »> list(zip(S1, S2))

# Truncates at /en(shortest)

[('a','x'), ('b','y'), ('c','z')]

Python 2.X 中 map 的等价形式 只有在 Python 2. X 中,相关的内置函数 map 在传入 None 作为函数参数时,会以类似方式把 序列的元素配对起来。但如果各个参数长度不等,那么 map 会用 None 补齐较短的序列(而 不是按照最短的长度截断)

» > S1 ='abc' , , »> S2 = xyz123

>» map(None, S1, Sz)

# 2.X only.pads to len(longest) [ ('a','x'), ('b','y'), ('c','z'), (None,'1'), (None,'2'), (None,'3')]

这个例子其实利用了内置函数 map 的退化形式,而这在 Python 3 .X 中已经不再支持。 一般

来讲, map 会先输入 一 个函数参数,以及一 个或多个序列参数,然后用从序列中取出的并 行元素来调用函数,最后把结果收集起来并返回。

我们将在第 19 章和第 20 章更详细地学习 map, 但作为一个简单的例子,下面的代码使用

内置函数 ord 来映射字符串中的每个字符,并收集结果(与 zip 一样, map 是 Python 3.X

中的 一个值生成器,因此仅在 3.X 中必须传入 1ist 从而一次性收集井显示其结果)

408

I

第 13 章

»> list(map(ord,'spam')) [115, 112, 97, 109] 这段代码和下面的循环语句效果相同,但 map 通常运行得更快,第 21 章将通过计时来验证

这点:

>» res = [] »> for c in'spam': res.append(ord(c)) »> res [115, 112, 97, 109] 注意:版本差异提示:通过向函数参数传入 None 的 map 函数的退化形式,在 Python 3.X 中已

经不再支持了,因为它和 zip 很大程度上重复了(坦率地说,这与 map 本身的函数应用 目的相去甚远)。在 Python 3 .X 中,要么使用 zip 来最短截断,要么编写循环代码来用

None 补齐。事实上,在学习了更多迭代概念之后,我们将在第 20 章中看到如何做到这点。

使用 zip 构造字典 让我们来学习另 一个 zip 使用案例。第 8 章介绍过,当键和值的集合必须在运行时计算时, 这里的 zip 调用也能很方便地生成字典。现在我们已熟悉了 zip, 让我们更全面地探索它 是如何与字典创建相关联的。如你所学,你可以编写字典字面址或者对字典的键赋值,来 创建字典:

»> 01 = {'spam':1,'eggs':3,'toast':s} >» 01 {'eggs': 3,'toast': 5,'spam': 1} >» » > » > » >

01 = {} 01 ['spam'] = 1 01['eggs'] = 3 01 ['toast'] = 5

不过,如果你的程序是在脚本写好后,在运行时获得字典的键和值列表,那该怎么办呢? 例如,假设你从 一 个用户处收集或从一个文件处推导,抑或从另一个动态源处获取了下列 的键和值列表:

»>keys= ['spam','eggs','toast'] »> vals = [1, 3, 5) 其中 一种将这些列表变成字典的做法,就是将这些列表 zip 起来,并通过 for 循环来并行 地遍历它们:

»> list(zip(keys, vals)) [('spam', 1), ('eggs', 3), ('toast', S)] >» D2 = {} »> for (k, v) in zip(keys, vals): D2[k] = v while 循环和 for 循环

I

409

»>

D2

{'eggs': 3,'toast': s,'spam': 1} 不过,在 Python 2.2 和后续版本中,你可以完全省掉 for 循环,直接把 zip 过的键/值列 表传给内置的 diet 构造函数:

>>> keys = ['spam','eggs','toast']

>» vals = [1, 3, 5] »> D3 = dict(zip(keys, vals)) »> D3 {'eggs': 3,'toast': s,'spam': 1} 内置名称 diet 其实是 Python 中的类型名称(在第 32 章中,你会学到更多有关类型名称的 内容,以及如何通过它们创建子类)。调用 diet 的时候,可以得到类似列表到字典的转换, 但这本质上是一个对象构造的请求。

在下一章中,我们会探讨相关但更丰富的概念,也就是列表推导,它只需单个表达式就 能建立列表;我们还将回顾 Python 3.X 和 2.7 的字典推导,它是代替上面 zip 键/值对加

diet 调用的另一种方案:

»> {k: v for (k, v) in zip{keys, vals)} {'eggs': 3,'toast': 5,'spam': 1}

同时给出偏移量和元素: enumerate 我们最后要介绍的这个循环帮助函数,支持了偏移量和元素的双重使用模式。之前,我们

讨论过通过 range 来生成字符串中元素的偏移量,而不是那些偏移最位置上的元素。不过 在有些程序中,两者都需要:使用元素以及元素的偏移量。传统的做法是,编写 一 个简单 的 for 循环,同时维护 一个当前偏移最 的计数器:

»> s ='spam' » > offset = o »> for item in S: print(item,'appears at offset', offset) offset+= 1 s appears p appears a appears m appears

at at at at

offset o offset 1 offset 2 offset 3

上面的例子可以工作,但在所有最新的 Python 2.X 和 3.X 版本中(从 Python 2.3 开始), 一 个名为 enumerate 的新内置函数已经实现这个功能一其最终效果就是给循环"免费赠

送” 一个计数器,从而保持了自动迭代的简洁性:

410

I

第 13 章

»> s ='spam »> for (offset, item) in enumerate{S): print(item,'appears at offset', offset) s appears p appears a appears m appears

at at at at

offset o offset 1 offset 2 offset 3

enumerate 函数会返 回 一 个生成器对象:这种对象支持迭代协议(下 一章将介绍,井在本 书第四部分中深入讨论)。简而言之,这种对象有一个方法,可以被内置函数 next 调用, 井且在循环的每次迭代返回一个 (index,

value) 元组。 for 循坏会自动遍历这些元组,

井允许我们通过元组赋值将元组内的值解包,这与 zip 十分相似:

>» E = enumerate(S) >>> E

>» next(E) (o,'s') »> next(E) (1,'p')

»> next(E) (2,'a') 我们一 般不会直接看到这一作用机制,因为所有的迭代上下文(包括第 14 章的主题,列表 推导)都会自动执行迭代协议:

»> [c * i for (i, c) in enumerate(S)] ['','p','aa','mmm'] »> for (i, 1) in enumerate(open('test.txt')): print('%s) %s'% (i, l.rstrip())) o) aaaaaa 1) bbbbbb 2) cccccc 要全面理解类似 enumerate 、 zip 和列表推导等迭代概念,我们需要继续学习下一 章,来更 加深入地探究它们。

请留意: shell 命令及其他 之前的 一个 边栏展示了如何将循环应用到文件 。 正如笫 9 章简短提到的, Python 相

关的 os.popen 调用也给出了一个类文件接口,可以读取被触发的 shell 命令的轮出 。 抚然我们已经全面地学习了循环语句.这里展示一个该工具的实际示例 一—要运行一

条 shell 命令并读取其标准轮出文本 , 你需要将该 shell 命令作为一个字符串传入 OS.

while 循环和 for 循环

I

411

open,

然后从它返回的类文件对象中读取文本(如果这在你的计算机上引起了一个

Unicode 镐码问题,笫 25 章关于货币符号的讨论也许会帮到你) :

» > import os

»> F = os.popen('dir') >» F.readline()

#Readlinebyline

'Volume in drive C has no label. \n' >>> F = os. popen ('dir') # Read by sized blocks >» F. read(SO) 'Volume in drive Chas no label.\n Volume Serial Nu'

>>> os. popen ('dir'). readlines () [ O] # Read all lines: index 'Volume in drive C has no label. \n' »> os.popen('dir').read()[ :so] # Read all at once: slice 'Volume in drive Chas no label.\n Volume Serial Nu'

>» for line in os.popen('dir'):

# File line iterator loop

print (line. rstrip()) Volume in drive Chas no label. Volume Serial Number is D093-D1F7 •.. and so on... 上面的代码在 Windows 上运行了一个 dir 目录列举命令,不过任何可通过一条命 令行指令启动的程序 , 都可以用这种方式来开启 。 例如,我们可以利用这种方式来

显示 Windows 的 systeminfo 指今的轮出

cs.system 会直接运行一条 shell 命令,

os.popen 则将 Python 程序连通到这条 shell 命令的轮出流;下面两种方式都能在一个

简单的控制台窗口中显示 shell 命令的轮出 , 但是笫一种方式也许不能在诸如 IDLE 的 GUI 接口中使用.

>» os.system('systeminfo') ... output in console, popup in IDLE... 。

>» for line in os.popen('systeminfo'): print(line.rstrip()) Host Name: MARK-VAIO OS Name: Microsoft Windows 7 Professional OS Version: 6.1.7601 Service Pack 1 Build 7601 ... lots of system information text... 一旦我们得到了 shell 命令的轮出文本形式,就可以使用各种字符串处理工具或技术 来处理它 一— 包括格式化显示和内容转换 :

# Formatted, limited display »> for (i, line) in enumerate(os.popen('systeminfo')): if i == 4: break print('%OSd) %s'% (i, line.rstrip())) 00000) 00001) Host Name:

412

I

第 13 章

MARK-VAIO

00002) OS Name: 00003) OS Version:

Microsoft Windows 7 Professional 6.1.7601 Service Pack 1 Build 7601

# Parse for specific lines, case neutral

»> for line in os.popen('systeminfo'): parts= line.split{':') if parts and parts[o].lower() =='system type': print{parts[l]. strip()) x64-based PC 我们将在笫 21 章中再次看到 os.popen 的使用示例,那里我们将用它读取一个(能够

计时不同代码的)创建命令行的结果;在笫 25 章,它会被用来比较被测试脚本的轮出 。 类似 os.popen 和 as.system (以及这里没有列出的 subpracess 模块 )的工具允许你 使用自己计算机上的每一条命令行程序,但你也可以使用 Python 代码来编写模拟工具。

例如,你可以很容易地在 Python 中模拟 UNIX awk 工具从文本文件中提取列的功能, 使之成为进程中可重用的函数:

# awk emulation: extract column 7from whitespace-delimited file

for val in [line.split() [ 6] for line in open ('input. txt') ] : print(val) Same, but more explicit code that retains result col7 = [] for line in open('input.txt'): cols = line.split() col7.append(cols[6]) for item in col7: print(item)

#

Same, but a reusable function (see next part of book) def awker(file, col): return [line.rstrip().split()[col-1) for line in open(file)]

#

print(awker('input.txt', 7)) print(','.join(awker('input.txt', 7)))

#

List of strings

# Put commas between

Python 自身提供了各种各样数括的类文件接口 一一 包括网站返回的文本和由 URL 标 识的网页 , 不过我们要等到本书笫五部分再介绍包导入,以及如何史一般地荻取我们 这里所使用的工具的方式(例如,下面的例子在 Python 2.X 下可以工作,但你需要使

用 urllib 而不是 urllib.request,

同时 Python 2.X 中的返回值是文本字符串)

»> from urllib.request import urlopen » > for line in urlopen ('http:/ /home. rmi. net/~lutz') : print(line) b'\n' b'\n' b'\n' b"Mark Lutz's Book Support Site\n" ... etc ...

while 循环和 for 循环

I

413

本章小结 在本章中,我们探索了 Python 的循环语旬以及一些和 Python 循环相关的概念。我们深入讨 论了 while 和 for 循环语句,学习了它们的 else 分句。我们也学习了 break 和 continue 语句,

它们只在循环中才有意义,并且介绍了几个在 for 循环中常用的内置工具,包括 range 、 zip 、 map 和 enumerate, 但在 Python 3.X 中刻意缩减了 一部分它们作为可迭代对象的角色。 在下 一 章中,我们将继续讲述迭代的话题,讨论 Python 中的列表推导和迭代协议,这是 和 for 循环密切相关的概念。在那里,我们将揭示这里所学的可迭代工具(例如 range 和

zip) 背后的图景,也将学习它们的一些执行细节。不过和往常 一样,在继续学习之前,先 做 一 做本章的习题。

本章习题 l.

while 和 for 之间主要的功能性区别是什么?

2.

break 和 continue 之间有何区别?

3.

循环的 else 分句在什么情况下会执行?

4.

在 Python 中怎样编写一个基于计数器的循环?

5.

range 在 for 循环中有哪些用法?

习题解答 1.

while 循环是一种通用的循环语句, for 循环被设计用来在一个序列或其他可迭代对象 中遍历各项。尽管 while 可以用计数器来模拟 for 循环,但它需要更多的代码并且运

行起来可能更慢。

2.

break 语旬立即退出一个循环(直接跳到了整个 while 或 for 循环语句的后面), continue 跳回到循环的顶部(跳转到 while 中测试之前的部分,或 for 中的下一次元

素获取)。

3.

while 或 for 循环的 else 分句会在循环离开时执行一次,但前提是循环是正常离开(没

有运行 break 语句)。如果有的话, break 会立刻离开循环,跳过 else 部分。

4.

计数器循环可以用 while 语句编写,并手动记录索引值;或者用 for 循环编写,使用 内置函数 range 来生成连续的整数偏移最。然而如果你只需遍历序列中所有元素,那

么这两种方式都不是 Python 推荐的做法 。 正确的做法是,应当尽可能地使用简单的 for 循环,而避免 range 或计数器 。 因为这样不仅更容易编写,而且通常运行得更快。

414

I

第 13 章

5

内置函数 range 可用在一 个 for 循环中:实现指定次数的重复,按照偏移址而不是偏 移盘处的元素来扫描;在过程中按一 定步长跳过元素,在遍历一 个列表的时候修改它 。

这些摇求井非非用 range 不可,而大多数也都有其他替代方法一一如扫描实际的元素、 三 参数分片,以及列表推导,往往是更好的解决方案(不过那些前 C 程序员有时还是 改不掉数数的习惯!)。

while 循环和 for 循环

I

41 s

第 14 章

迭代和推导

在上一章中,我们学习了 Python 的两种循环语句, while 和 for 。由千它们能够处理程序

所需执行的大多数重复性任务,在序列中迭代的需求是如此常见和广泛,因此 Python 提 供了额外的工具以使其更简单和高效。本章开始介绍这些工具 。 具体而言,本章介绍了

Python 的迭代协议的相关概念一~井且介绍了关干 列表推导的 一 些细节,列表推导是对可迭代对象中的项应用 一 个表达式的 for 循环的 一 种 近似形式。 由千这些工具都和 for 循环及函数相关,我们将在本书中分 三步来介绍它们:



本章在循环工具的背景中介绍它们的基础知识,作为上一 章的某种延续。



第 20 章在基千函数的工具的背景中回顾它们,并将话题扩展到内置和用户定义的生成

器。



第 30 章作为该话题的收尾,将介绍使用类编写的用户定义的可迭代对象。

在本章中,我们还将展示 Python 中其他迭代工具的示例,井接触在 Python 3.X 中可用的新 可迭代对象,在 3.X 中迭代观念的使用将更加枝繁叶茂。

首先注意 一 点:这几章中所介绍的某些概念乍看起来可能有些高级。然而,通过实践,你 将发现这些工具很有用并且很强大。尽管这些工具不是严格必需使用的,但它们已经变成 了 Python 代码中的常用内容,如果你必须阅读他人所编写的程序,那么就应基本理解这些 工具。

416

迭代器:初次探索 在上一节中介绍过, for 循环可以用千 Python 中任何序列类型,包括列表、元组以及字符串, 如下所示:

>»for x in [ 1, 2, 3, 4] : print (x

**

2, end='')

**

3, end='')

#

In 2.X: print x •• 2,

1 4 9 16

>»for x in (1, 2, 3, 4): print(x

...

1 8 27 64

>»for x in'spam': print(x

*

2,

end='')

ss pp aa mm 实际上, for 循环甚至比这更为通用:可用于任何可迭代对象。实际上,对 Python 中所有 能够从左至右扫描对象的迭代工具而言都是如此,这些迭代工具包括了 for 循环、列表推导、 in 成员关系测试,以及内置函数 map 等。 ”可迭代对象”是 Python 语言中一个比较新的概念,但它已在语言设计中普遍存在。本质上,

这就是序列观念的一种通用化:如果对象是实际保存的序列或是可以在迭代工具上下文中 (例如, for 循环)一次产生一个结果的对象,那么就看作是可迭代的。总之,可迭代对 象包括实际序列,以及能按照需求计算的虚拟序列。

注意译注 l :这一主题的术语并没有那么严格。术语“可迭代对象 ”(iterable) 与“迭代器 ”(iterator) 在指代支持迭代的对象的时候,常常是可以互换的。出于明确性,本书更倾向于使 用术语可迭代对象 (iterable) 来指代一个支持 iter 调用的对象,同时使用术语迭代

器 (iterator) 来指代一个 (i ter 调用为传人的可迭代对象返回的)支持 next(I) 调用的对象。 iter 和 next 调用都会在后面进行介绍。 然而,这一约定井不完全通用千整个 Python 世界或是本书,

有时也指代能够迭代的工具。第 20 章采用术语“生成器”

"迭代器”

(iterator)

(generator) 来扩展这

一类别。生成器 指代能自动支持迭代协议的对象,因此生成器本身就是可迭代对象, 不过所有的生成器都能产生结果!

迭代协议:文件迭代器 了解迭代协议最简便的方式之 一 ,就是看一看它是如何与内置类型一起工作的,例 如,文件。 本章将采用如下的输入文件进行演示:

译注 l:

英文原书中的"注意”的内容是关于 iterable 和 iterator 的术语提法分歧 .

迭代和推导

I

417

»> print(open('script2.py').read()) import sys print(sys.path) X = 2

print(x

**

32)

» > open('script2. py'). read() 'import sys\nprint(sys.path)\nx

=

2\nprint(x

**

32)\n'

回顾一 下,在第 9 章中,已打开的文件对象有个名为 readline 的方法,可以 一次从 一个文

件中读取一 行文本。每次调用 readline 方法时,我们就会前进到下一行。当到达文件末尾 时,就会返回空字符串,因此我们可通过检测空字符串来跳出循环:

»> f = open('script2.py') >» f.readline() 'import sys\n' »> f.readline() 'print(sys.path)\n' »> f.readline() 'x = 2\n' »> f.readline() 'print(x ** 32)\n' >» f.readline() ',

# Read a four-line script file in this directory H readline loads one line on each call

#

Last lines may have a\nor not

#

Returns empty string at end-of-file

不过在 3.X 中,文件也有一个名为—next—(在 2.X 中名为 next) 的方法,有着几乎相同 的效果:每次调用时,就会返回文件中的下一 行。唯 一值得注意的区别在千,当到达文件

末尾时,—next_会引发内置的 Stop Iteration 异常,而不是返回空字符串:



»> f = open('script2.py') # _next loads one line on each call too »> f._next_() If But raises an exception at end-of-file 'import sys\n' >» f._next_() # Use f.next() in 2.X, or next(f)in 2.X or 3.X 'print(sys.path)\n' »> f._next_() 'x = 2\n' »> f._next_() 'print(x ** 32)\n' »> f._next_() Traceback (most recent call last): File "", line 1, in Stop Iteration 这个接口基本上就是 Python 中所谓的迭代协议:所有带有—next_方法的对象会前进到下 一 个结果,而当到达 一 系列结果的末尾时,_next_会引发 Stopiteration 异常,这种对

象在 Python 中也被称为迭代器。任何这类对象也能以 for 循环或其他迭代工具遍历,因为 所有迭代工具内部工作起来都是在每次迭代中调用—next_ ,并且通过捕捉 Stop Iteration

异常来确定何时离开。接下来我们将看到,对某些对象完整的迭代协议包括额外的 一 步 iter 调用,但对文件而 言这是不必要的 。

418

I

第 14 章

正如第 9 章和第 13 章所述,这种“魔法”的最终效果就是,逐行读取文本文件的最佳方式 就是根本不要去读取;作为替代,你应该让 for 循环在每轮迭代中自动调用 next, 从而前

进到下一 行。文件对象的迭代器会随着读取的进行,自动载入下 一 行。例如,下面的代码 会逐行读取文件,并打印每行的大写版本,却没有显式地从文件中读取内容:

» > for line in open ('script2. py') : print(line.upper(), end='')

# Use file iterators to read by li11es #

Calls _next~ catches S1oplterario11

IMPORT SYS PRINT(SYS.PATH) X = 2

PRINT(X

**

32)

要注意,这里的 print 使用 end='' 关闭额外添加 一个\ n, 因为行字符串已经自带 一个\ n 换行符(如果没有 end='' ,我们的输出会变成两行隔开;在 2.X 中, 一 个尾部的逗号能起

到与 end 相同的作用)。上例是读取文本文件的最佳方式,原因有 三 点:这是最简单的写法, 运行最快并且从内存的使用情况来说也是最好的。相同效果的原始方式,是以 for 循环调

用文件的 read lines 方法,将文件内容加载到内存,并形成行字符串的列表:

»> for line in open('scriptz.py').readlines(): print(line.upper(), end='') IMPORT SYS PRINT(SYS.PATH) X = 2 PRINT(X ** 32) 这里的 read lines 技术依然能用,但它如今已经不是最佳实践,而且从内存的使用情况来看,

效果很差。实际上,因为这个版本是一 次性把整个文件加载到内存中,所以如果文件太大 以至千计算机内存空间不够用了,甚至不能工作。另 一 方面,由于基千迭代器的版本 一 次 只读 一 行,因此迭代器对这类内存爆炸的问题具有免疫力。基千迭代器的版本还会运行得 更快,不过这取决千 Python 的版本。

如前一章的边栏“请留意:文件扫描器”所述,你也可以使用 while 循环来逐行读取文件:

»> f = open('script2.py') »> while True: line = f.readline() if not line: break print(line.upper(), end='') ... same output... 然而, while 循环会比基千迭代器的 for 循环运行得更慢,因为迭代器在 Python 内部是以 C 语言的速度运行的,而 while 循环版本则是通过 Python 虚拟机运行 Python 字节码的。

任何时候,我们把 Python 代码换成 C 程序代码,速度都会有所提升。不过,也并不绝对,

迭代和推导

I

419

尤其是在 Python 3.X 中。之后的第 21 章会介绍一种计时技术,可以用来衡量像这样的替代

方案的相对速度注 I• 注意:版本差异提示:在 Python 2.X 中,迭代方法名为 X.next( ),并非 X._next_()。出千 可移植性,一个叫 next(X) 的内置函数也同时出现在 Python 3.X 与 2.X (2.6 及以后版

本)中,它用千调用 3.X 中的 X._next_()和 2.X 中的 X.next( )。除了方法名称的不 同,迭代的工作原理在 2.X 与 3.X 中保持一致。在 2.6 和 2.7 中,你只需换用 X.next() 或 next(X) 进行手动迭代即可,在 2.6 之前的版本,需要使用 X.next( ),而不能用 next(X) 。

手动迭代: iter 和 next 为了简化手动迭代代码, Python 3.X 还提供了一个内置函数 next, 它会自动调用一个对象 的_next_方法。如上面的注意边栏所述,它也适用千 Python 2.X 版本 (2.6 及以后版本)。

给定一个迭代器对象 X, 调用 next(X) 等同千 3.X 版本中的 X._next_() (以及 2.X 版本 中的 X.next()) ,但 next(X) 更简单且可移植性更好。例如,对千文件而言,两种形式都 可以使用:

>» f = open('script2.py') »> f._next_()

If

Call iteration method directly

'import sys\n' »> f._next_() 'print(sys.path)\n'

»> f = open('script2.py') »> next(f) 'import sys\n' »> next{f) 'print(sys.path)\n'

# The next(j) built-in calls /_next_J)in 3.X #

next(j) = >[3.X: f._nexr_J)J, [2.X: f.nexr()]

事实上,迭代协议还有一点值得注意。 for 循环在开始时,会首先把可迭代对象传入内置

函数 iter ,并由此拿到一个迭代器,而 iter 调用返回的迭代器对象有着所需的 next 方法。 iter 函数与 next 和_next_很像,在它的内部调用了_iter_方法。

剧透警告:在 2.7 和 3.3 版本中,我用一个 1000 行的文件运行了本章的代码,结果是

注):

文件迭代器仍稍快于 readlines, 而且比 while 徙环快至少 30% (while 徙环在 3.3 中

是 2. 7 中的两倍)。基准浏试的一般性通常受到质抸 一~这里的刹试结果局限于我所使 用的 Python 、计算机和浏试文件。同时 Python 3.X 因为使用了全新的支持与系纨无关的

Unicode 文本 l/0 的库,使这些性能分析变得更加复杂.第 21 章介绍了可以用来给这些 徙环语句计时的工具和技术

420

I

第 14 章

完整的迭代协议 作为一 个更正式的定义,图 14-1 勾勒出了完整的迭代协议的轮廓。这个迭代协议被 Python 中所有的迭代工具使用,并广泛地被许多对象类型所支持。该协议基于分别用在不同的两

个步骤中的两种对象:



可迭代对象:迭代的被调对象,其_iter—方法被 iter 函数所调用。



迭代器对象:可迭代对象的返回结果,在迭代过程中实际提供值的对象。它的

next

方法被 next 运行,并在结束时触发 Stop Iteration 异常。

迭代工具在多数情况下自动执行这些步骤,然而知道这两类对象的角色有助千理解迭代运 行过程。例如,当只需支持单遍扫描的时候,这两种对象是相同的,而迭代器对象通常是 临时的,它们在迭代工具内部被使用。

此外, 一 些对象既是迭代上下文的工具(它们可以主动发起迭代),也是可迭代对象(它 们的返回值可迭代)一一包括第 20 章中的生成器表达式,以及 Python 3.X 中的内置函数

map 和 zip 。 Python 3.X 比之前版本支持了更多的可迭代对象,包括 map 、 zip 、 range 和一 些字典方法,从而避免了将结果列表一 次性全部载入内存。

叩下文 妇嫡

图 14-1 : Python 迭代协议广泛用千 for 销环、推导语法、映射等,得到文件、列表 、 字典和第

20 章中的生成器等的支持。其中一些既是迭代上下文的对象,也是可迭代对象,例如生成器表 达式和 3.X 版本中的一些工具(诸如 map 和 zip) 。另一些对象既是可迭代对象,也是迭代器对象, 它们会在 iter()调用中返回它们自己 在实际代码中,我们可以通过模拟 for 循环内部调用列表的例子,来学习迭代协议中第一步

的具体工作方式:

»> L = [1, 2, 3] >» I = iter(l) »> I._next_()

# Obtain an iterator object # Call next to advance to next item

1

迭代和推导

I

421

>» !._next_()

#

Or use I.next() in 2.X, nex1([) in either line

2

»> !._next_() 3

»> !._next_() ... error text omitted... Stopiteration 最初的一步对于文件来说不是必需的,因为文件对象自身就是迭代器。由千文件只支持 一 次迭代(它们不能通过反向查找来支持多重扫描),文件有自己的

next

方法,因此不

需要像这样返回 一 个不同的对象:

»> f = open('script2. py') >» iter(f) is f True

»> iter(f) is f._iter_() True

»> f._next_() 'import sys\n' 列表以及很多其他的内置对象,由千自身不是迭代器,因此支持多次迭代,例如,在嵌套 的循环中可以存在不同位登的多个迭代。对这样的对象,我们必须调用 iter 来启动迭代:

»> L = [1, 2, 3] »> iter(L) is L False

»> L._next_() AttributeError:'list'object has no attribute'_next_'

»> I = iter(L) »> !._next_() 1

»> next(!)

#



Same as I. next_j)

2

手动迭代 尽管 Python 迭代工具会自动调用这些函数,我们也可以使用它们来手动地应用迭代协议。

如下的交互式命令行内容展示了自动和手动迭代之间的等价性注 2: »> L = (1, 2, 3] >» » > for X in L: print(X

**

2, end='')

# A utomar,c uera//on # Obtains iter, calls _next~ catches exceptions

1 4 9

»> I = iter(L}

# Manual iteration: what for loops usually do

从技木上讲, for 循环内部的调用情况等价于 !._next_ ,而不是这里的 next(I) , 尽管

注 2:

二者非常相似。而在手动迭代的时候,你可以任意选择两者中的一种。

422

I

第 14 章

>» while True: try: # try statement catches exceptions X = next(!) # Or call l._next_in 3.X except Stopiteration: break print(X ** 2, end='') 1 4 9 要理解这段代码,你需要知道, try 语句运行 一个动作并且捕获在运行过程中发生的异常(在 第 11 章中简单提到过异常,之后将在本书第七部分对异常进行深入介绍)。此外你还应该 注意, for 循环和其他的迭代上下文有时候针对用户定义的类做不同的工作,重复地索引 一 个对象而不是运行迭代协议(但会更倾向千迭代协议)。等到我们在第 30 章中学习运算

符重载的时候,会再介绍这 一 内容。

其他内置类型可迭代对象 除了文件以及像列表这样的物理的序列外,其他类型也有其适用的迭代器。例如,遍历字 典键的经典方法是显式地获取它的键列表:

»> D = {'a':1,'b':2,'c':3} >» for key in D.keys(): print (key, D[key]) 123 abc

不过,在最新的 Python 版本中,字典作为一 个可迭代对象自带一个迭代器,在迭代上下文中, 会自动一 次返回 一 个键:

>» I = iter(D) »> next(I) '-. a »> next(!) 'b' »> next(I) 'C, »> next(!) Traceback (most recent call last): File "", line 1, in Stoplteration 最终效果是,我们不再需要调用 keys 方法来遍历字典键——for 循环将使用迭代协议在每 次迭代的时候获取一 个键:

»> for key in D: print (key, D[key])

迭代和推导

I

423

123 abc

虽然我们不能在这里深人细节,但其他的 Python 对象类型也支持迭代协议,因此它们也

可以在 for 循环中使用。例如, shelve (Python 对象的一种按照键访问的文件系统)和 os.popen 的返回结果(上一 章介绍过的用来读取 shell 命令的输出的一个工具)也是可迭 代的:

>» import os

>» P = os.popen('dir') >» P._next_() 'Volume in drive C has no label. \n'

>» P._next_() 'Volume Serial Number is D093-D1F7\n'

»> next(P) TypeError: _wrap_close object is not an iterator 要注意,在 Python 2.X 中, popen 对象本身支持一个 P.next( )方法。在 Python 3.X 中,

它们支持 P._next—()方法,却不支持内置函数 next(P) 。由千 next() 被定义来调用_ next —(),因此这看上去有点不符合通常情况。不过只要我们使用了被 for 循环以及其他 迭代上下文自动采用的完整迭代协议,也就是顶层的 iter 调用,那么 next( )和_next_() 就都能正常工作 (iter 会执行一 些内部必要的步骤,从而使该对象支持 next 调用)

>» P = os.popen('dir') »> I = iter(P} >» next(I} 'Volume in drive C has no label. \n'

>» I._next_() 'Volume Serial Number is D093-D1F7\n' 同时,在操作系统领域, Python 中的标准路径遍历器 os.wa1k 是一个简单的可迭代对象, 但我们也将留到第 20 章中通过例子来学习这个工具的基础—一生成器和 yield 。

迭代协议也是我们必须把某些返回结果包装到一个 list 调用中,才能一 次性看到它们的值 的原因。可迭代对象一 次返回 一个结果,而不是一次性返回 一 个实际的列表:

»> R = range(S) »> R range(o, 5) »> I = iter(R) »> next(I)

#

Ranges are iterables in 3.X

# Use iteration protocol to produce results



»> next(I) 1

>» list(range(S))

424

[o,

1, 2, 3, 4)

I

第 14 章

#

Or use list to collect all results at once

要注意,这里的 list 函数调用在 2.X 中是不必要的(在 2.X 中 range 创建了一个真实的列 表),并且在 3.X 中迭代自动发生的上下文中是可以省略的(例如在 for 循环中)。然而在 3.X

中需要显示值的情况,或是在 2.X 和 3.X 中对桉需生成结果的对象,使用列表行为或者多 重扫描的情况,则需要使用 list 调用(接下来会有更多介绍)。 既然对这一协议已经有了较深入的理解,你应该可以看到它是如何说明上一章所介绍的 enumerate 工具能够以其方式工作的原因:

» > E = enumerate('spam') # enumerate is an i1erable 100 »> E

» > I = iter(E) »> next(I) # Generate results with iteration protocol (0,

o

5 I)

»> next{I) # a r use list 10 force generation to run (1,'p') >» list(enumerate('spam')) [(o,'s'), (1,'p'), (2,'a'), (3,'m')] 我们通常不会看到这种机制,因为 for 循环为我们自动遍历结果。实际上, Python 中可以

从左向右扫描的所有对象都以同样的方式实现了迭代协议,包括下一小节所涉及的主题。

列表推导:初次深入探索 既然已经见过了迭代协议的工作方式,就让我们来看一个非常常用的例子。与 for 循环一

起使用的列表推导,是最主要的迭代协议上下文之一。 在上 一章中,我们学习了在遍历一个列表的时候,如何使用 range 来修改它:

>» L = [1, 2, 3, 4, 5] »> for i in range(len(L)): L[i] += 10 »> L [11, 12, 13, 14, 15] 这可以工作,但如前所述,它可能不是 Python 中优化的“最佳实践"。现在,列表推导表 达式使许多之前的例子都过时了。例如,我们可以用能产生所需结果列表的一个单个表达 式来替代该循环:

» > L = [ X + 10 for X in L1 »> L [21, 22, 23, 24, 25] 最终结果是相似的,但列表推导只需更少的代码,并且运行速度会大大提升。列表推导并 不完全和 for 循环语句版本相同,因为它产生一 个新的列表对象(假设你需考虑一井改变

迭代和推导

I

425

原本列表的多个引用时,那么这将会有所影响),但对千大多数应用程序来说它跟 for 循

环足够接近,并且是一种很普通且方便的方法。所以也值得我们在这里进一 步介绍。

列表推导基础 我们在第 4 章简单介绍过列表推导。从语法上讲,其语法来源干集合理论表示法中的 一 个

结构,该结构对集合中的每个元素应用一个操作,但要使用列表推导井不一定必须知道集 合理论。在 Python 中,大多数人都会发现列表推导看上去就像是一个反着写的 for 循环.

为了从语法上了解它,让我们更详细地剖析上 一节中的例子:

L = [x + 10 for x in L] 列表推导写在一个方括号中,因为它们是最终构建 一 个新的列表的 一 种方式 。 它们以我

们所组合成的一个任意的表达式开始,在这里我们使用 一 个循环变益组合得到表达式 (x+ 10) 。这后边跟着我们现在能看出来的 一 个 for 循环的头部,它指定了循坏变袋,以及 一 个

可迭代对象 (for x in L) 。

要运行该表达式, Python 会在解释器内部执行 一 个遍历 L 的迭代,按照顺序把 x 赋给每个 元素,并且收集对各元素运行左侧表达式的结果。我们得到的结果列表就是列表推导所表 达的内容一-个包含了 x+10 的新列表,针对 L 中的每个 x 。 从技术上讲,列表推导并非真的是必需的,因为我们总是可以用 一个 for 循环手动地构建

一 个表达式结果的列表,该 for 循环像下面这样添加结果:

»> res = [] »> for x in L: res.append(x + 10) »> res [31, 32, 33, 34, 35] 实际上,这和列表推导内部所做的事情是相同的。 然而,列表推导编写起来更加精简,并且由千构建结果列表的这种代码样式在 Python 代码 中十分常见,因此可以将它们用千多种上下文。此外,取决千 Python 版本和代码,列表推

导比手动的 for 循环语句运行得更快(往往速度会快一倍),这是因为它们的迭代在解释 器内部是以 C 语言的速度执行的,而不是以手动 Python 代码执行的。尤其对千较大的数据

集合,使用列表推导能带来极大的性能优势。

426

I

第 14 章

在文件上使用列表推导 让我们来看看列表推导的另一个常见用例,从而更详细地了解它。回顾 一 下,文件对象有 一个 readlines 方法,能够一 次性地把文件载入成 一 个行字符串的列表:

»> f = open('script2.py') » > lines = f. readlines () »> lines ['import sys\n','print(sys.path)\n','x = 2\n','print(x

**

32)\n']

这可以工作,因为结果中的行在末尾都包含了一个换行符(\ n) 。对千多数程序而言,换

行符很麻烦,我们必须小心避免打印的时候留下双倍的空白,等等。如果我们可以一次性 地去除这些换行符,岂不美哉?

每当我们需要在一 个序列中的每项上执行 一 个操作时,就可以考虑使用列表推导。例如,

假设变晕 lines 像前面交互式命令行中一样,如下的代码通过对列表中的每一项运行字符 串 rstrip 方法,来移除右端的空白( 一 个 line[:-1] 分片也能工作,但是,只有当我们能 够确保所有的行都是以\ n 结束的时候才适用。但事实上井非总是这样,尤其对千文件的最 后一行来说) :

»> lines = [line.rstrip() for line in lines] »> lines ['import sys','print(sys.path)','x = 2','print(x

**

32)']

这能如期工作。由千列表推导像 for 循环语句 一样是 一个迭代上下文,因此我们甚至不需 要提前打开文件。如果我们在表达式中打开它,列表推导将自动采用本章前面所述的迭代

协议。也就是说,它会调用文件的 next 方法,每次从文件读取一 行,对它使用 rstrip 表 达式,将它添入结果列表。照旧,我们得到了想要的结果,即对文件中每一 行的 rstrip 结果:

>»lines= [line.rstrip() for line in open('script2.py')] »> lines ['import sys','print(sys.path)','x = 2','print(x ** 32)'] 这个表达式完成了许多隐式的工作,因而我们这里免费获得了这些工作成果——Python 逐

行扫描文件并自动创建了运行结果的 一 个列表。这也是高效编写这类运行的 一 种方式:因 为大部分工作会在 Python 解释器内部完成,所以这可能比等价的 for 循环语句要快,同时 也不会 一 次性把文件全部载入内存中。再次,特别是对千较大的文件 , 列表推导的速度优 势会更加明显。 除了其高效性,列表推导的表现力也很强。在我们的例子中,我们可以 在 迭代时在 一 个文

件的行上运行任何的字符串操作。下面是与我们之前遇到的文件迭代器大 写示例等价的列 表推导,还有几个其他的示例 :

»> [line.upper() for line in open('script2.py')] ('IMPORT SYS\n','PRINT(SYS.PATH)\n','X = 2\n','PRINT(X

**

32)\n'] 迭代和推导

I

427

»>

[line.rst工ip(). upper()

for line in open('script2. py')]

['IMPORT SYS','PRINT(SYS.PATH)','X = 2•,'PRINT(X ** 32)']

» > [line.split() for line in open ('script2. py') ] [['import','sys'], ['print(sys.path)'], ['x','=','2'], ['print(x',

'**' ,

'32)']]

>» [line.replace('','I') for line in open('script2.py')) ['import!sys\n','print(sys.path)\n','x!=!2\n','print(x!* *!32)\n'] >» [('sys'in line, line[:s]) for line in open('script2.py')) [(True,'impor'), (True,'print'), (False,'x = 2'), (False,'print')] 回顾一 下上面第二个例子中的方法,链式调用是有效的,因为字符串方法会返回 一个新的

字符串,所以你可以对该字符串调用其他的字符串方法。最后 一 个例子也展示了我们可以 同时收集多个结果,只要它们被包在一 个元组或列表中。

注意:这里有 一 个小细节:回顾第 9 章中提到的文件对象自身会在垃圾回收的时候自动关闭。

因此,这里的列表推导也会自动地在表达式运行结束后,将它们的临时文件对象关闭。 然而对千 CPython 以外的 Python 版本,你可能需要手动编写代码来关闭循环中的文件

对象,以保证资源能够被立即释放。如果需要复习文件关闭的函数调用,请参阅第 9 章。

扩展的列表推导语法 实际上,列表推导在实际中有更丰富的应用,而且甚至自成 一 套“迷你迭代语言”。让我

们来快速领略一下它们的语法。

筛选分句: if 作为一个特别有用的扩展,推导表达式中嵌套的 for 循环可以有一 个关联的 if 分句,来过 滤掉那些测试不为真的结果项。

例如,假设我们想要重复上一 节的文件扫描示例,但这次我们只需要收集以字母 p 开头的

那些行(也许每一 行的第 一 个字母代表着某种动作代码) 。 我们可以向表达式中添加一 条 if 筛选分句来实现:

»>lines= [line.rstrip() for line in open('script2.py') if line[o] =='p'] »> lines ['print(sys. path)','print(x ** 32)'] 上面的 if 分句会检查文件中每 一行的第 一 个字符是否为 p , 如果不是,就将该行从结果列

表中去除。这是一条相对较大的表达式,但如果我们将它转换为简单的 for 循环语句等价 形式,那么它就很容易理解。通常,我们总是可以通过逐步附加并进一 步缩进每个后续的 部分,来把一 个列表推导转换为 一 条 for 语句:

428

I

第 14 章

»> res = [] >» for line in open('script2.py'}: if line[o] =='p': res.append(line.rstrip())

»> res ['print(sys.path)','print(x

**

32)')

这个 for 语句等价形式也可以工作,但它占了 4 行而不是 1 行,并且运行起来可能要慢很多。 事实上,你可以在必要时将相当一部分代码逻辑浓缩到列表推导中一下面的例子与之前

那个类似,但是通过右边一个更复杂的表达式,筛选出以数字结尾的行(即拥有出现在行 尾换行符之前的数字) :

>» [line.rstrip() for line in open('script2.py') if line.rstrip() [ -1].isdigit()] ['X = 2'] 下面是另一个 if 筛选的例子,其中第一条语句的结果给出了文本文件的总行数,第二条语 旬只用一行就实现了去掉了每一行左右的空格以避免计入空行的功能(这个文件的内容是

审校在原书第一稿中发现的笔误) :

»> fname = r'd:\books\5e\lpse\draft1typos.txt' »> len(open{fname). readlines())

#

All lines

#

Nonblank Lines

263

>» len{[line for line in open{fname) if line.strip() I='']) 185

嵌套循环: for 如果需要的话,列表推导甚至可以变得更复杂-------Wtl 如,我们可以通过编写 一系列 for 分 句,让推导包含嵌套的循环。实际上,它们的完整语法允许任意数目的 for 分句,并且每 个 for 分句都可以带有一个可选的关联的 if 分句。

例如,下面的例子构建了 一 个 x+y 拼接字符串的列表,把 一个字符串 中的每个字符 x 和另 一个字符串中的每个字符 y 拼接起来 。这条代码有效地按顺序收集了两个字符串中字符的

排列:

»> [x + y for x in'abc'for yin'lmn'] ['al','am','an','bl','bm','bn','cl','cm','en'] 依旧,一种理解该表达式的方式是通过缩进其各个部分将它转换为语句的形式。下面是其

等价形式,但可能会更慢一些,这是实现相同效果的一种替代方式:

»> res = [] » > for x in'abc': for y in'lmn': res.append(x + y)

»> res ['al','am','an','bl','bm','bn','cl','cm','en'] 迭代和推导

I

429

如果继续增加复杂程度的话,那么列表推导将变得过干紧致而适得其反。通常,它们用千 简单的迭代;对于更复杂的工作,直接使用 for 语句可能更简单易懂,并且未来也更容易

修改维护。与编程中的通常情况一样,如果某些内容对你来说晦涩难懂,那么它可能不是 一个好方法。 因为列表推导通常需要分为多次来讲解,所以我们这里的讲述需要暂告 一 段落。我们将在 第 20 章以函数式编程工具为线索再次回顾列表推导,同时也会更加形式化地定义它的语法,

辅以更多的例子1 我们会看到,推导语法与函数的关系,就像它与循环语句的关系 一样紧密。

注意:对本书中给出的性能评价的汽信度(包括列表推导和其他)可以理解为代码的相对速度

取决千实际测试的代码和实际使用的 Python 版本,而且结果 会随着不同版本而变 化 。 例如在今天的 CPython 2.7 和 3.3 版本中,列表推导依旧可以比相应的 for 循环在某些 测试用例下快上两倍。但是在其他的用例下可能只快一 点点,甚至在用上 if 筛选分句 后变得比 for 循环慢一 些。 我们将在第 21 章学习如何给代码计时,并将学习如何理解本书用例包中的 listcomp­

speed.txt 文件,该文件对本章中代码进行计时。就目前而言,请记住绝对的性能基准测 试在开源项目中是公认免谈的。

其他迭代上下文 在本书后面,我们将看到用户定义的类也可以实现迭代协议。因此,有时候知道哪些内置 工具 使用了迭代协议是很关键的一一任何利用了迭代协议的 工具,都能在遵循了迭代协议 的任何内置类型或用户定义的类上自动地工作。 到目前为止,我们已经在 for 循环语句的背景下介绍了迭代,原因是本书的这 一 部分内容

主要关注语句。然而别忘了,所有能够在对象中从左到右扫描的内置工具都使用了迭代协议。 这包括我们已经介绍过的 for 循环:

»> for line in open('script2.py'):

# Use file iterators

print(line. upper(), end='') IMPORT SYS PRINT(SYS.PATH) X = 2 PRINT(X ** 32) 除了 for 循环还有很多例子。例如,列表推导和内置函数 map 都与 for 循环很相似。当作

用千 一 个文件时,它们都利用了文件对象的迭代器来逐行扫描,通过_iter 代器并每次调用

430

I

第 14 章

next

方法:

获取 一 个迭

>» uppers = [line.upper() for line in open('script2.py')] »> uppers

**

['IMPORT SYS\n','PRINT{SYS.PATH)\n','X = 2\n','PRINT{X

»> map(str.upper, open('script2.py'))

»> list(map(str.upper, open('script2.py'))) ['IMPORT SYS\n','PRINT{SYS.PATH)\n','X

=

32)\n']

# map is a11 iterable in 3.0

**

2\n','PRINT{X

32)\n']

我们在上一章(以及第 4 章)中介绍过这里用到的 map 调用, map 是一个 内置函数,作用 是把一个函数调用应用千传入的可迭代对象中的每一项。 map 类似千列表推导,但是它更 有局限性,因为它必须传入一个函数而不是一个任意的表达式。在 Python 3.X 中,它本身

还会返回 一 个可迭代对象。因此在 3.X 中,我们必须将它包含到 一 个 list 调用中才能强制

它 一 次性给出所有的值,本章后面会介绍 3.X 中 map 的更多内容。由千 map (像列表推导 一样)与循环和函数都密切相关,因此我们将在第 19 章和第 20 章中再次介绍它们。 Python 还包含了其他许多处理可迭代对象的内置函数。例如, sorted 能够排序可迭代对

象中的各项 I zip 能够组合可迭代对象中的各项; enumerate 能够把可迭代对象中的项与 它们的相对位置进行匹配 1 filter 能够按照 一个函数是否为真来 选择可迭代对象中的项;

reduce 能够针对可迭代对象中的成对的项来运行 一 个由数。它们都接受一 个可迭代对象, 并且在 Python 3.X 中, zip 、 enumerate 和 filter 也会像 map 一样返回一 个可迭代对象。下 面是它们在实际中运行文件的迭代器,并自动逐行读取的代码:

»> sorted(open('script2.py')) ['import sys\n ' , ' print(sys.path)\n','print(x

**

32)\n','x = 2\n')

»> list(zip(open('script2.py'), open('script2.py'))) [('import sys\n','import sys\n'), ('print(sys.path)\n','print(sys.path)\n'), ('x = 2\n','x = 2\n'), ('print(x ** 32)\n','print(x ** 32)\n')] >» list(enumerate(open('script2. py'))) [(o,'import sys\n'), (1,'print(sys.path)\n'), (2,'x (3,'print(x ** 32)\n')]

=

2\n'),

>» list(filter(bool, open('script2.py'))) #nonempty=True ['import sys\n','print(sys.path)\n','x = 2\n','print(x ** 32)\n'] » > import functools, operator >» functools.reduce(operator. add, open('script2.py')) 'import sys\nprint(sys.path)\nx = 2\nprint(x ** 32)\n' 所有这些都是迭代工具,但它们各自有不同的用处。我们在上一章见过 zip 和 enumerate; 在第 19 章讨论函数的时候会介绍 filter 和 reduce, 因此这里暂不详细介绍。这里提到它

们的目的是为了指出它们能通过迭代协议使用文件和其他可迭代对象。 我们在第 4 章中初次见到了在这里所用到的 sorted 函数,同时,我们在第 8 章将其用千字

典。 sorted 是利用了迭代协议的一 个内置函数,它很像列表原有的 sort 方法,但不同的 是 sorted 会返回 一 个新的排序后的列表作为结果并且可以在任何可迭代对象上运行。要注

迭代和推导

I

431

意,与 map 和其他函数不同, sorted 在 Python 3.X 中会返回 一个真正的列表而不是一 个可

迭代对象。 有趣的是,在当今的 Python 中,迭代协议甚至比我们目前已经展示的示例更为普遍一一实 际上 Python 的内置工具集中所有能够从左到右扫描一 个对象的工具,都被定义为在主体对 象上使用了迭代协议。这甚至包括了更高级的工具,例如 list 和 tuple 内置函数(它们从

可迭代对象中构建一个新的对象),以及字符串 join 方法(它将一个子字符串放置到 一个 可迭代对象中包含的字符串之间,来创建 一 个新的字符串)。总之,所有这些都可以在一 个打开的文件上工作并且自动逐行读取:

>» list (open ('script2. py')) ['import sys\n','print(sys.path)\n','x

=

2\n','print(x ** 32)\n']

»> tuple(open('script2.py')) ('import sys\n','print(sys.path)\n','x

=

2\n','print(x ** 32)\n')

»>'&&'.join(open('script2.py')) 'import sys\n&&print(sys.path)\n&&x

=

2\n&&print(x ** 32)\n'

甚至其他一些工具也出人意料地属千这个类别。例如,序列赋值、 in 成员测试、分片赋值 和列表的 extend 方法都利用了迭代协议来扫描,从而自动逐行读取文件:

»> a, b, c, d = open('script2.py') »> a, d ('import sys\n','print(x ** 32)\n')

# Sequence assignment

»> a, *b = open('script2.py') »> a, b ('import sys\n', ['print(sys.path)\n','x

#3.Xextendedform =

2\n','print(x ** 32)\n'])

>»'y = 2\n'in open('script2.py') False »>'x = 2\n'in open('script2.py') True

# Membership test

>» L = [11, 22, 33, 44] »> l[l:3] = open('script2.py') »> L [11,'import sys\n','print(sys.path)\n','x

It Slice assignment

»> L = [11] >» L.extend(open('script2.py')) »> L [11,'import sys\n','print(sys.path)\n','x

=

2\n','print(x ** 32)\n', 44] It lisr.exrend method

=

2\n','print(x ** 32)\n']

由第 8 章可知, extend 可以自动迭代,但 append 却不能一一用 append (或类似的工具) 给 一 个列表添加 一 个可迭代对象并不会对这个对象进行迭代。不过这个对象之后可以从列

表中取出来进行迭代:

»> L = [11]

432

I

第 14 章

# list.append does not iterate »> L.append(open('script2. py')) »> L [11, ] »> list(L[l]) ['import sys\n','print(sys.path)\n','x = 2\n','print(x ** 32)\n']

迭代是一个被广泛支持的强大模型。之前我们也见过内置的 diet 调用,可以接受一个可迭

代的 zip 返回结果(参阅第 8 章和第 13 章)。与 diet 情况相同,让我们来看看 set 调用, 以及 Python 3.X 和 2.7 中新增的集合推导和字典推导表达式。我们曾在第 4 章、第 5 章和 第 8 章见过它们:

» > set(open('script2. py')) {'print(x ** 32)\n','import sys\n','print(sys.path)\n','x = 2\n'} >» {line for line in open('script2.py')} {'print(x ** 32)\n','import sys\n','print(sys.path)\n','x = 2\n'} »> {ix: line for ix, line in enumerate(open('script2.py'))} {o:'import sys\n', 1:'print(sys.path)\n', 2:'x = 2\n', 3:'print(x

**

32)\n'}

实际上,集合推导和字典推导都支持我们在本章前面介绍的列表推导的扩展语法,包括 if 测试:

»> {line for line in open('script2.py') if line[o] =='p'} {'print(x ** 32)\n','print(sys.path)\n'} »> {ix: line for (ix, line) in enumerate(open('script2.py')) if line[o] =='p'} {1:'print(sys.path)\n', 3:'print(x ** 32)\n'} 与列表推导一样,它们都能逐行扫描文件并且挑选以字母 “p" 开始的行。它们最终也如期

构建了集合和字典,但我们已经借助文件迭代和推导语法自动完成了很多工作。在本书的 后面,我们会遇到推导表达式的一个亲戚-生成器表达式。生成器表达式使用同样的语法,

并作用于可迭代对象上,但其本身仍然是可迭代对象:

»> list(line.upper() for line in open('script2. py')) ['IMPORT SYS\n','PRINT(SYS.PATH)\n','X = 2\n','PRINT(X

**

# See Chapter 20 32)\n']

其他的内置函数也支持迭代协议,但坦率地讲,很难用在和文件相关的示例中。例如, sum 调用计算任何可迭代对象中的总和1 如果 一个可迭代对象中任何的或所有的项为真的时候,

any 和 all 内置函数将分别返回 True, max 和 min 分别返回 一 个可迭代对象中最大和最小 的项。与 reduce 一样,如下示例中的所有工具都接受任意的 一 个可迭代对象作为参数,并 利用迭代协议来扫描它,但返回单个的结果:

4,' , ,', ``l

---------((',` 2aa 5, 0 ,' 1 3 ' ,' , saae my1 un1 >1>T>F >5>r>a >>u>1 ,mm ,nn ,ss .1.1 、,'、丿

,' ,' pp

# sum expects numbers only

es ' l

迭代和推导

I

433

»> max([3, 2, 5, 1, 4]) 5

»> min([3, 2, 5, 1, 4]) 1

严格地讲, max 和 min 函数也可应用千文件一它们自动使用迭代协议来扫描文件,并且分

别选择具有最高和最低字符串值的行(然而,我把有效的用例留给你自己来想象)

»> max(open('script2.py'))

# Line with max/min string value

'x = 2\n'

>» min{open('script2.py')) ,加port

sys\n'

还有最后一 个值得介绍的迭代上下文,尽管现在介绍有点超前:在第 18 章中,我们将学习 在函数调用中用到的 一种特殊的* arg 形式,它会把一 个集合的值解包为单个的参数。现在 我们就可以预计,它也会接受任何可迭代对象,包括文件(参阅第 18 章了解该调用语法的 更多细节;第 20 章中将此思想推广到生成器表达式;第 ll 章给出了在 2.X 中使用下列 3.X

的 print 方法的建议) :

»> def f{a, b, c, d): print(a, b, c, d, sep='&') »> f(1, 2, 3, 4) 1&2 &找4

»> f(*[1, 2, 3, 4])

#

Unpacks into arguments

#

Iterates by lines too!

1&2&找4

>>>

»> f(*open('script2.py')) import sys &print(sys.path) &x = 2 &print(x ** 32)

实际上,由千这种调用中的参数解包语法能够接受可迭代对象,你也可以使用 zip 内置函

数来把 zip 过的元组”逆 zip" ,做法是对另一 个 zip 调用,使用之前的或嵌套的 zip 结果 参数(警告:如果你准备在不久的任何时候运行更沉重的机制,那么你可能不应该阅读如

下的示例)译注 2:

»> X = (1, 2) »> V = (3, 4) »> »> list(zip(X, V)) 译注 2:

#

Zip tuples: returns an iterable

这里的飞五 p" 是一个非常有趣的例子 。 读者朋友可以亲自试验一下 , 这里的 zip 中的 可迭代对象的数目和长度都可以是任意的 。 例如 , 可以试试 list(zip(*zip((l,

2, 3, 4),

(5, 6, 7, 8), (9, 10, 11, 12) ))) 。 了解数学中矩阵概念的读者,可以直观地把 zip 想 象成矩阵的转置,每一个可迭代对象如果是矩阵的一行 , 那么运行完 zip 后返回的是这个

矩阵的各列 。 然后 zip(*zip( ))的写法无非只是把矩阵转置了两次而已 , 因此你得到的将 是原来的各个可迭代对象对应的元组 。

434

I

第 14 章

[(1, 3), (2, 4)]

»> »> »> (1, »> (3,

A, B = zip(*zip(X, Y))

# Unzip a zip!

A

2) B 4)

Python 中还有其他工具( 例如内置函数 range 和字典视图对象)会返回可迭代对象,而不 是处理它们。要了解这些工具是如何在 Python 3.X 中被吸收到迭代协议中的,让我们继续 学习下 一 节。

Python 3.X 新增的可迭代对象 Python 3.X 的一个本质改变是它比 Python 2.X 更强调迭代。这一变化连同 Unicode 模型和 强制新式类 一起,是 Python 3.X 版本最彻底的变化。

具体来说,除了与文件和字典这样的内置类型关联的迭代器,字典方法 keys 、 values 和 items 都在 Python 3 .X 中返回可迭代对象,就像内置函数 range 、 map 、 zip 和 filter 所做 的那样。正如上一节 所述,这些由数中的最后 三 个都返回可迭代对象并处理它们。所有这 些工具在 Python 3.X 中都按照请求产生结果,而不是像它们在 Python 2.X 中那样直接创建 结果列表。

对 Python 2.X 版本代码的影响:利与弊 尽管这样会节约内存空间,但它在某种程度上可能会影响到我们的编程风格。到目前为止,

在本书中的各个位置,我们已经把 一 些函数和方法调用结果包在一个 list( ...)调用中, 从而强制它们一 次产生所有的结果:

>» zip('abc','xyz')

#

»> list(zip('abc','xyz')) [('a','x'), ('b','y'), ('c','z')]

It Force

An iterable in Python 3.X (a list in 2.X) list of result~· in 3.X to display

如果我们要将列表或者序列运算应用到大部分按需产生元素的可迭代对象上的话,那么我

们必须使用 一 个类似的变换来索引、分片或者拼接可迭代对象本身。这些工具在 2.X 版本

中返回的结果列表能够直接支持这些运算:

»> Z = zip{{l, 2), (3, 4)) »> Z[o] TypeError:'zip'object is not subscriptable

#

Unlike 2.X lists, cannot index, etc.

正如第 20 章所述,对于新的迭代工具(例如 map 和 zip) ,它们只支持单遍的扫描,如果 要支持多遍扫描,就必须将它们转换成列表一一-与 2 .X 中它们对应的列表形式不同,在 3.X

迭代和推导

I

43s

中的一次遍历会耗尽它们的值:

»> M = map(lambda x: 2 ** x, range(3)) >» for i in M: print(i) 1 2

4 # Unlike 2.X lists, one pass only (zip too)

>» for i in M: print(i) >»

这种转换在 Python 2.X 中不是必需的,因为像 zip 这样的由数会返回结果列表。在 Python 3.X 中,它们返回按需产生结果的可迭代对象。这意味着要在交互式命令行下(并且可能在某

些其他的上下文中)显示结果就需要额外的代码。不过这也对较大的程序很有用,在计算 很大的结果列表的时候,像这样的延迟计算能够节约内存并避免暂停。下面,让我们快速 地刹览 Python 3 . X 中可迭代对象的应用。

range 可迭代对象 在上 一章中,我们学习过内置函数 range 的基本行为。在 Python 3.X 中, range 返回 一 个

可迭代对象来根据请求产生范围中的数字,而不是在内存中构建 一 个结果列表。这取代了 较早的 2.X 中的 xrange (参阅后面的版本差异提示),如果你需要一个实际的范围列表的

话(例如,为了显示结果),就必须使用 list(range( ...))来强制获得它:

C:\code> c:\python33\python »> R = range(10) >» R range(o, 10) »> I = iter(R) »> next{!) 。

#

range returns an iterable, not a list

# Make an iterator from the range iterable # Advance to next result # What happens in for loops, comprehensions,

etc.

»> next{!) 1

>» next{!) 2

»> list(range(10)) [o, 1, 2, 3, 4, s, 6, 7, 8, 9]

# To force a list if required

与 range 调用在 2.X 中返回列表的行为不同,

3 . X 中的 range 对象只支持迭代、索引以及

len 函数。 range 不支持任何其他的序列操作(如果你需要用到更多列表工具的话,请使用

list(...)) : »> len(R) 10 »> R[O]

436

I

第 14 章

#

range also does len and indexing, but no others

O>9>3>4

]( [ e . RnI 1 t >>> >>> -x 、l

Ie 、丿

n t (

# Continue taking from iterator, where

left 。ff

# .next() becomes._next_J), but use new next()

注意:版本 差异提示 :在上 一章中曾经提到, Python 2.X 也有一个名为 xrange 的内置函数, 它很像 range, 但是会根据请求产生元素而不是一次性在内存中创建一个结果列表。由

千这完全是新的基千迭代的 range 在 Python 3.X 中所做的事情,因此 xrange 在 Python 3.X 中不再可用一—它已经被取代了。然而,我们仍会在 2.X 的代码中看到它,尤其是 当 range 因为创建结果列表而影响到内存使用效率的时候。 类似地,由千文件迭代器的出现, Python 2.X 中用来最小化内存使用的 file.

xreadlines() 方法也已经从 Python 3.X 中移除了(正如上一章所述)。

map 、 zip 和 filter 可迭代对象 与 range 类似,内置函数 map 、 zip 和 filter 在 Python 3.X 中也转变成可迭代对象以节约 内存空间,而不再在内存中一次性生成一个结果列表。所有这 3 个函数不仅能像在 Python 2.X 那样处理可迭代对象,而且在 Python 3.X 中会返回可迭代对象作为结果。与 range 不同, 它们本身都是迭代器一在遍历其结果一次之后,它们就用尽了。换句话说,你不能在它 们的结果上拥有多个位千不同位置的迭代器。

在上一章中我们见过一个内置函数 map 的例子。与其他可迭代对象一样,如果你确实需要 一 个列表的话,可以用 list( ...)来强制得到一个列表,但对千较大的结果集来说,默认 的行为可以节省不少内存空间:

>» M = map{abs, (-1, o, 1)) »> M

»> next{M) 1

# map returns an iterable, not a list

# Use iterator manually: exhausts results # These do not support len() or indexing

»> next(M) 。

»> next(M) 1

»> next{M) Stop Iteration » > for x in M: print(x)

# map iterator is now empty: one pass only

>» M = map{abs, (-1, o, 1)) »> for x in M: print(x)

# Make a new iterableliterator to scan again # Iteration contexts auto call next()

迭代和推导

I

437

1 。

1

»> list(map(abs, (-1, o, 1))) [1, o, 1]

#

Can force a real list if needed

上 一 章介绍的 zip 内置函数本身是 一 个迭代上下文,但也以同样的方式返回 一个带有迭代 器的可迭代对象:

»> Z = zip((l, 2, 3), {10, 20, 30)) »> z

#

zip is the same: a one-pass iterator

»> for pair in Z: print(pair)

#

Exhausted after one pass

»> Z = zip{{1, 2, 3), (10, 20, 30}) >» for pair in Z: print{pai.r)

#

Iterator used auromatical/y or manually

#

Manual iteration (iter() nor needed)

»> list(Z) [(1, 10), (2, 20), (3, 30)]

(1, 10) (2, 20) (3, 30) »> »> (1, »> (2,

Z = zip((l, 2, 3), (10, 20, 30)) next(Z) 10) next(Z) 20)

我们在第 12 章中简要讲过的并且将在本书下一 部分中学习的 filter 内置函数,也是类似

的。对千可迭代对象中能够令传入函数返回 True 的各项, filter 会返回它们(如前所述, Python 中 True 包括非空对象,而且 bool 函数调用返回一个对象的真值)

»> filter(bool, ['spam',",'ni'])

»> list(filter(bool, ['spam',",'ni']}) ['spam','ni'] 与本小节讨论过的大多数工具 一样, filter 可以接受 一个可迭代对象进行处理,并返回 一

个可迭代对象在 Python 3.X 中产生结果。同时, filter 也可以被扩展的列表推导语法所计算, 从而自动测试真值:

>» [xforxin ['spam','','ni'] ifbool(x)] ['spam','n i'] >» [xforxin ['spam',",'ni'] ifx] ['spam','ni']

438

I

第 14 章

多遍迭代器 vs 单遍迭代器 对比 range 对象和本小节介绍的内置函数的不同之处十分重要: range 支持 len 和索引, range 不是自己的迭代器(手动迭代时,我们使用 iter 产生一个迭代器),并且 range 支 持在其结果上的多个迭代器,这些迭代器会记住它们各自的位置:

» > R = range(3) >» next(R)

# range allows multiple iterators

TypeError: range object is not an iterator

>» 11 = iter(R) >» next(I1) 。

»> next(I1) 1

»> 12 = iter(R) >» next(I2)

# Two iterators on one range



>» next(I1)

# 11 is at a different spot than 12

2

相反, 3.X 中的 zip 、 map 和 filter 不支持同一结果上的多个活跃迭代器,因此, iter 调用 对遍历这类对象的结果是可选的一一它们的 iter 结果就是它们自身(在 2.X 中这些内置函 数返回支持多遍扫描的列表,因此下面的代码不适用千 2. X)

»> »> >» »>

Z = zip{{1, 2, 3), (10, 11, 12)) 11 = iter(Z) I2 = iter(Z) next{Il) (1, 10) >» next{I1)

# Two iterators on one zip

(2, 11)

>» next{I2)

# (3.X) / 2 is at same spot as 11 !

(3, 12)

>» M = map{abs, (-1, O, 1)) #Dittoformap(andfilter) »> 11 = iter{M); 12 = iter{M) >» print(next(l1), next{Il), next(l1)) 1 0 1

»> next{l2) Stopiteration >» R = range(3) >» 11, 12 = iter(R), iter(R) >» [next(l1), next(ll), next(I1)] [o 1 2] »> next(l2)

# (3.X) Single scan is exhausted! # But range allows many iterators

# Multiple active scans. like 2.X lists



在本书随后(第 30 章)使用类来编写自己的可迭代对象时,我们通常采用针对 iter 调用 返回 一个新的可迭代对象的方式,来支持多个迭代器;单个的迭代器一般意味着一个对象

迭代和推导

I

439

返回其自身。在第 20 章中我们还将看到,生成器函数和表达式的行为就像 map 和 zip 一 样 支持单个的活跃迭代器,而不是像 range 一样。在第 20 章中,我们还将看到一些微妙的例

子:位千循环中的 一 个单遍迭代器试图多次扫描一之前天真地将它们当作列表的代码, 会因为没有手动进行列表转换而失效。

字典视图可迭代对象 最终,

(如我们在第 8 章中简单了解到的)在 Python 3.X 中,字典的 keys 、 values 和

items 方法会返回可迭代的视图对象,它们一 次产生一个结果项,而不是在内存中 一 次产 生全部结果的列表。视图对象在 2.7 版本中也是可用的,但拥有另 一 个特殊的方法名来避

免影响 2.X 中已有的代码。视图项保持和字典中的那些项相同的物理顺序,并且反映对底 层的字典做出的修改。既然你已经对可迭代对象了解甚多,接下来我将在 Python 3.3 的环

境中继续介绍其他内容(你的键顺序可能有所不同)

»> D = dict(a=1, b=Z, c=3) »> D {'a': 1,'b': 2,'c': 3}

»> K = D.keys() >» K

# A view ob,丿ecr in 3.X, nor a Lisi

dict_keys(['a','b','c'])

»> next(K) # Viiews are not iterators themselves TypeError: dict_keys object is not an iterator >» I = iter(K) >» next(I) a »> next(I)

# View iterables have an iterator, # which can be used manually # but does not support /en(), index

'b'

»> fork in D.keys(): print(k, end='')

#AlliIteration contexts use auto

a b c 与所有按需产生值的可迭代对象 一样,我们总可以通过把一 个 Python 3.X 字典视图传入内 置函数 list 中,从而强制构建一个真正的列表。然而,这通常不是必须的,除非要交互式

地显示结果或者使用索引这样的列表操作:

>» K = D.keys() >» list(K)

# Can still force a real list if needed

('a','b','c')

»> V = D.values() »> V dict_values([1, 2, 3]) »> list(V) (1, 2, 3]

440

I

第 14 章

# Ditto for values() and items() views

# Need list() to display or index as list

>» V[o] TypeError:'dict_values'object does not support indexing » > list(V)[ o] 1

>» list(D.items()) [('a', 1), ('b', 2), ('c', 3)]

»> for (k, v) in D.items(): print(k, v, end='') a 1 b 2 c 3 此外, Python 3.X 字典本身仍然是可迭代对象,同时带有一个返回连续的键的迭代器。因此, 你通常不需要在下面的上下文中使用 keys 调用: )>> D {'a': 1,'b': 2,'c': 3} »> I = iter{D} >» next{!}

a

# Dictionaries still produce an iterator # Returns next key 011 each iteration

,

»> next{!} , b'

»> for key in D: print(key, end='')

# Still no need co call keys() ro iterate # But keys is an iterator in 3.0 too!

a b c 最后,再次提醒,由千 keys 不再返回一个列表,因此按照排序的键来扫描一个字典的传统 编码模式在 Python 3.X 中不再有效。相反,你必须像下面这样,要么首先用一个 list 调用 来转换 keys 视图,要么在一个键视图或字典自身上使用 sorted 调用。我们在第 8 章中见 过这种用法,但是对 2.X 版本的程序员来说仍然有必要再次强调:

»> D {'a': 1,'b': 2,'c': 3} »> fork in sorted(D.keys())): print(k, D[k), end='') a 1 b 2 c 3

>» fork in sorted(D): print(k, D[k], end='')

...

# "Best practice" key sorting

a 1 b 2 c 3

其他迭代话题 正如在本章介绍中所提到的,我们还将在第 20 章中结合函数,来学习列表推导和可迭代对 象的更多内容,在第 30 章结合类来学习它们。在后面,我们将看到:



通过使用 yield 语旬,用户定义的函数可以被转换为可迭代的生成器函数。



当编写在圆括号中时,列表推导会转变为可迭代的生成器表达式。



用户定义的类通过_iter_ 或 _getitem—运算符重载变得可迭代。

迭代和推导

I

441

特别地,通过使用类定义的用户定义可迭代对象,我们可以在本章遇到的所有迭代上下文中,

使用任意的对象和操作。通过仅仅支持迭代这 一 种操作,对象就能用在更广阔、更多样的 上下文和工具中。

本章小结 本章中,我们介绍了 Python 中循环相关的概念,对 Python 的迭代协议进行了第一 次实质

性的讨论:这是非序列对象参与迭代循环以及列表推导的方式。如前所述,列表推导类似 千 for 循环,会将表达式施加到任何可迭代对象中的所有元素。此外,我们还看到了其他 内置迭代工具的使用,并且学习了 Python 3.X 中关千迭代的最新变化。

这就是我们对每条过程化语句的学习。下一章要讨论 Python 代码中的文档选项,来结束本 书这一部分。尽管没有直接涉及编码的具体细节,但是文档也是通用语法模型的 一 部分, 而且是良好程序的必要组成部分。下一章中,我们也会做一下本书这 一 部分的练习题,然 后把注意力转向例如函数这样的较大结构。不过与之前一样,继续学习之前,先做 一 下这 里的习题。

本章习题 1.

for 循环与可迭代对象之间有什么关系?

2.

for 循环与列表推导之间有什么关系?

3.

举出 Python 中的 4 种迭代上下文。

4.

目前逐行读取 一 个文本文件的最佳方式是什么?

5

你会在中世纪西班牙宗教法庭上看到哪些被使用的武器?

习题解答 l.

for 循环会使用迭代协议来遍历可迭代对象中的每一个项。它首先通过把可迭代对象 传入 iter 函数从可迭代对象拿到 一个迭代器,然后在每次迭代中调用该迭代器对象的

—next —方法 (3.X 中),并捕捉 Stop Iteration 异常,从而决定何时停止循环。_ next —函数在 2.X 中被称为 next, 并在 3.X 和 2.X 被内置函数 next 调用。支持迭代 协议模型的任何对象,都可以用千 for 循环以及其他迭代上下文中。然而对千一些自 身就是迭代器的对象,用于初始化的 iter 调用是可有可无的。

2.

两者都是迭代工具和上下文。列表推导是执行常见 for 循环任务的简洁且高效的方法:

对可迭代对象内所有的元素应用一个表达式,并收集其结果。你可以把列表推导转换 成 for 循环,而列表推导表达式的一 部分的语法看起来就像是 for 循环的首行。

442

I

第 14 章

3.

Python 中的迭代上下文包括 for 循环、列表推导、内置函数 map 、 in 成员关系测试表达式,

以及内置函数 sorted 、 sum 、 any 和 all 。迭代上下文还包括内置函数 list 和 tuple 、 字符串 join 方法以及序列赋值运算。所有这些都使用了迭代协议(参阅上面习题 l 的

答案)来一次一 个元素地逐个遍历可迭代对象。

4

如今从文本文件中读取文本行的最佳方式就是不要显式地读取:更好的替代方案是, 在迭代上下文中打开文件(例如 for 循坏或列表推导),然后让迭代工具在每次迭代

中执行该文件的 next 方法,自动一 次扫描一行。从代码编写的简洁性、执行速度以及 内存空间使用等方面来看,这种做法通常都是最佳的。

5

我会说下面的所有回答都是正确的:恐惧、惘吓、漂亮的红色制服、舒服的椅子以及

柔软的枕头译注 30

译注 3:

原书作者在此引用了 The

Spanish Inquisition. 迭代和推导

I

443

第 15 章

文档

本书的这一部分主要讲解了用千编写 Python 代码的文档的技术和工具。尽管 Python 代码 本身可读性就很强,但在适当的位置放上一些“人们可读"的注释,也能很大程度上帮助

其他人了解你的程序所做的工作。我们将看到, Python 包含了可以让你更轻松、更简便地 编写文档的语法和工具。具体来说,本章将介绍的 PyDoc 系统可以把模块内的文档,渲染

成 shell 中的普通文本或是浏览器中的 HTML 页面。 虽然文档是一个与工具相关的概念,但在这里介绍该主题的原因有二:一,文档涉及了

Python 的语法模型 1 二,文档是那些努力想了解 Python 工具集的读者的学习资源。就后面 这个原因而言,本章也将展开介绍第 4 章中首次给出的文档资源指南。与往常 一 样,因为

本章结束了这一部分,除了章节习题外,结尾还包括一些常见陷阱的提醒以及本部分的练 习题。

Python 文档资源 本书已经介绍过, Python 预置的功能数益惊人:内置函数和异常、预定义的对象属性和方法、 标准库模块等。而且,我们确实只触及了上面这些主题的冰山一角。

通常困扰初学者的前几个问题之一,是怎么找到有关这些内置工具的资料。本节提供了 一些 Pyth on 可用的文档资源。此外,还会介绍文档字符串 (docstring) 以及使用它们的 PyDoc 系统。这些话题相对核心语言本身算是外围的话题。但是, 一 且你编写代码的水平 达到编写本书这一部分的例子和练习题时,这就成为十分重要的知识了。 如表 15-1 所示,你可以从很多地方查找 Python 的资料,而且一般表中各项的理解难度从

上到下递减。由于文档是实际编程中相当重要的工具,因此我们会在接下来的几节中讲解 表 15-1 中的各项内容。

444

表 15-1 : Python 文档资琼

形式

作用

#注释

源文件文档

dir 函数

以列表显示对象中可用的属性

文档字符串:—doc

附加在对象上的源文件文档

PyDoc:

help 函数

交互式命令行中的对象帮助

PyDoc: HTML 报告

浏览器中的模块文档

Sphinx 第三方工具

为大型项目提供更丰富的文档

标准手册集

官方的语言和库描述

网络资源

在线教程、示例等

已出版的书籍

商业化加工后的参考文本

#注释 如前所述,井号注释是我们为代码编写文档最基本的方式。因为 Python 会忽略#之后的所 有文字(只要#不位千字符串字面址内),所以你可以在“#”后面插人任何对程序员有意 义的文字和说明。不过,这种注释只能从源代码文件中看到。要编写用途更广泛的注释,

你需要使用文档字符串。 实际上,目前的最佳实践都表明,文档字符串是较大型功能性的文档(例如, 实现这些功能”)的最优选择,#注释则更适用千较小代码的文档(例如,

“该文件能 “这个神奇的

表达式可以达到这个目的"),并最好被限制在脚本或函数 一 条或 一 小组语句的范围内。 我们马上会介绍文档字符串 1 不过,首先来看看如何查看这些对象。

dir 函数 如前所述,内置的 dir 函数是抓取对象内所有可用属性列表的 一 种简单方式(例如,对象 的方法以及较简单的数据项)。如果不向 dir 内传入参数,则可以列出调用者作用域内的 变址。更有用的是,它也能够传人任何有属性的对象,包括被导入的模块、内置类型和数

据类型的名字。例如,要找出如标准库中 sys 模块内什么是可用的,你就可以导入 sys' 并将它传入 dir:

>» import sys >» dir(sys)

, winver ' _displayhook_',... more names omitted.. . ,'winver'] ['_displayhook_',...

文档

I

445

上面的结果来自 Python 3.3, 我省略了大部分被返回的名称,因为它们会在其他版本中稍有

变化,你最好在自己的 Python 上运行看一看。实际上, sys 目前有 78 种属性译注 1, 但我 们通常只关心名称不以双下划线开头(双下划线开头通常意味着与解释器相关)的 69 种属 性或是名称不以任何下划线开头(单下划线开头通常意味着非正式的私有属性实现)的 62

种属性。这是上一章中列表推导极佳的使用案例:

»> len(dir(sys))

# Number names in sys

78

»> len([x for x in dir(sys) if not x.startswith('_')]}

# Non _X numes only

69

»> len([x for x in dir(sys) if not x[o] =='_'])

#

Non underscore names

62

要找出内置类型的对象提供了哪些属性,你可以运行 dir 并传人所需类型的字面量或已存 在的实例。例如,要查看列表和字符串的属性,你可以传入空列表和空字符串:

»> dir([]) ['_add_','_class_','_contains_' _',... more...,'append','clear','copy', , , 'count','extend','index','insert','pop','remove','reverse','sort']

»> dir('') ['_add_','_class_','_contains_',... ,... more...,'split','splitlines', more...,'split','splitlines' , , 'startswith','strip','swapcase','title','translate','upper','zfill'] dir 在所有内置类型上的运行结果都包含了 一组属性 ,这组属性与该类型的实现相关(从技 术上讲,它们是运算符重载方法) ;与模块中的一样,它们的开头和结尾都是双下划线,

从而保证了其独特性。在本书目前,你可以安全地忽视它们(它们用千面向对象编程)。 例如,列表有 45 个属性,但其中只有 11 个是正常命名的方法(即不以双下划线开头)

>» len{dir([])), len{[x for x in dir([]) i f not x.startswith{'_')]) (45, 11)

»> len(dir(")), len([x for x in dir(") if not x.startswith{' —')]) (76, 44) 实际上,为了过滤掉一般程序不感兴趣的双下划线项,你同样可以运行列表推导,只不过

需要将结果列表打印出来。例如,下面是 Python 3.3 中列表和字典中正常命名的属性:

»> [a for a in dir{list) if not a.startswith{'_')] ['append','clear','copy','count','extend','index','insert','pop', 'remove','reverse','sort']

>» [a for a in dir(dict) if not a.startswith('_')] ['clear','copy','fromkeys','get','items','keys','pop','popitem', ' setdefault','update','values']

译注]

446

在较新的 Python 3.5.3 版本中, sys 有 81 种属性 。

I

第 15 章

这看似要输入很多内容来得到一个属性列表,但下一章中我们将学习如何把这段代码包装

进一 个可导入、可重用的函数中,这样我们就不必反复输入它:

>» def dir1(x): return [a for a in dir(x) if not a.startswith('_')]

# See Part TV

»> dir1(tuple) ['count','index'] 你也可以传入一 个类型名称而不是字面值,来列出内置类型的属性 :

>» dir(str) == dir('')

#

Same result.type name or literal

True

»> dir(list) == dir([]) True 这样做行得通是因为像 str 和 list 这些名称之前曾是类型转换器,而如今实际上已是

Python 中的类型名称。调用这些名称会启用对应的构造函数,从而生成该类型的实例。在 第六部分讨论类时,我们会介绍构造函数和运算符重载方法的更多知识。 dir 函数常常用千提醒:它提供属性名称的列表,但并没有告诉你那些名称的意义。关干 这些额外的信息,我们要继续学习下一种文档资源。

注意:

Python 开发中的 一些 IDE (包括 IDLE) ,拥有在图形界面上自动列出对象中属性的功

能,从而可以看作是 dir 的替代。例如,当在一 个对象名后面输入 一 个点 号, 暂停或按 下 Tab 键时, IDLE 会在 一 个弹出选择窗口中自动列出这个对象的属性。这可以看作是 一种自动补全功能,尽管并不是 一 种信息源。第 3 章介绍了更多关千 IDLE 的内容。

文档字符串:

_ doc_

除了#注释外, Python 还支持可自动附加在对象上的文档,从而可以在运行时查看。从语 法上讲,这种注释被 写 成字符串,放在模块文件、函数以及类语句的顶部,位千任何可执

行代码之前(不过,#注释以及 UNIX 风格的护可以放在它们前面)。这些字符串被正式 地称为文档 字符串。 Python 会自动装载文档字 符串的文本,使其成为相应 对象的

doc

属性。

用户定义的文档字符串 例如,考虑下面的文件 docstrings.py 。 其文档字符串出现在文件顶端,以及函数和类的开头。

在这里,虽然我使用的是 三 重引号的多行块字 符串 , 但任意种类的字符串都能使用;就像 下面在类中的单引号或双引号包围的单行字符串是可以的,只不过这样就无法采用多行文

本了。我们目前尚未学习 def 或 class 语句,所以除了它们顶端的 字 符串外,这里其他关 千 它们的内容都可以忽略:

文档

I

447

Module documentation Words Go Here spam= 40 def square(x): function documentation can we have your liver then? return x

**

2

# square

class Employee: "class documentation" pass print(square(4)) print (square. _doc_) 该文档协议的意义在千,你可以在文件被导入后,继续让注释保存在_doc_属性中以供查

看。因此,要显示该模块及其对象关联的文档字符串,我们只需要导入该文件,直接打印 其_doc—属性(即 Python 储存文档字符串中文本的地方)即可:

»> import docstrings 16

function documentation can we have your liver then?

»> print(docstrings._doc_) Module documentation Words Go Here

» > print (docstrings. square._doc_) function documentation can we have your liver then?

>>> print (docstrings. Employee._doc_J class documentation 要注意,一般你都需要使用 print 来打印文档字符串;否则你会得到一个嵌有换行符\ n 的 字符串。 你也可以把文档字符串附加到类的方法中(将在第六部分中介绍),但因为方法只是嵌套

在类中的 def 语句,所以这也不算特例。要取出模块中类的方法的文档字符串,你可以通 过点号路径来访问: module.class.method._doc_ (参考第 29 章中方法的文档字符串的

例子)。

448

I

第 15 章

文档字符串标准和重要性 如前所述,今天的通用实践推荐只在有关表达式、语句或一小组语句的小型文档中使用井

号注释。文档字符串更适用千文件、函数或类中的更高级、更广阔的功能型文档,并且已 经成为 Python 软件预期的 一部分。除了这些守则,你还需要自行决定文档的内容。

尽管一些公司有内部标准,但关千文档字符串的文本应该包含些什么,其实并没有普适的 标准。目前已经有许多标记语言和模板协议(例如, HTML 或 XML) ,但它们似乎都没有 在 Python 世界中流行起来。坦率地讲,要让程序员手动编写 HTML 来为代码添加文档,基 本上是天方夜谭。要求编写 HTML 格式的注释不仅看上去是无理取闹,而且通常也不符合

文档编写的惯例。 一 些程序员并没有给予文档应有的重视。很多情况下,如果你阅读的源代码有一丝注释,

那都算非常走运了(且不说那些注释是否有被及时更新)。不过,本书强烈建议你详细地 为代码编写文档,因为这是写好代码很重要的步骤。不过,目前确实没有关于文档字符串

结构的标准;如果你想要编写文档,那么在今天一切内容都是可接受的。就像编写代码本 身 一 样,你可以自行决定是否创建文档内容并及时更新,当然你最好使用常识来加以判断。

内置文档字符串 Python 中的内置模块和对象也使用了相似的技术,从而添加了超越 dir 调用所返回的信息。

例如,要查看一个内置模块更具可读性的说明时,你可以导人它,并打印其_doc

字符串:

>» import sys >» print(sys._doc_) This module provides access to some objects used or maintained by the interpreter and to functions that interact strongly with the interpreter. Dynamic objects: argv -path -modules ... more

command line arguments; argv[O] is the script pathname if known module search path; path[o] is the script directory, else'' - - dictionary of loaded modules text omitted...

内置模块中的函数、类以及方法在其—doc

属性内也有附加的说明信息:

» > print (sys. get ref count._doc_) getrefcount(object) -> integer Return the reference count of object. The count returned is generally one higher than you might expect, because it includes the (temporary) reference as an argument to getrefcount(). 你也可以通过文档字符串来阅读内置函数的说明:

>» print (int. _doc_) int(x(, base]) -> integer 文档

I

449

Convert a string or number to an integer, if possible. A floating point argument will be truncated towards zero (this does not include a . . . more text omitted. ..

»> print(map._doc_) map(func, *iterables) --> map object Make an iterator that computes the function using arguments from each of the iterables. Stops when the shortest iterable is exhausted. 虽然你可以通过查看文档字符串来获取内置工具的大最信息,但其实你不必这样做:下 一 节的主题 help 函数会为你自动完成这件事。

PyDoc: help 函数 文档字符串技术如此有用,以至千 Python 最终专门添加了 一 个工具,使文档字符串更易干 显示。标准的 PyDoc 工具是一段 Python 程序,用于提取文档字符串及相关的结构化信息,

并将它们排版成外观精美的多种报告。开源领域还有很多其他的工具可以用来提取和排版 文档字符串(包括支持结构化文本的工具,你可以上网搜索一探究竟),但 Python 在其标 准库中附带了 PyDoc 。 你有很多种方式可以启动 Py Doc, 包括命令行脚本选项,这样可以保存结果文档以供之后 查看(我们在前面和 Python 库手册中都有讲解)。也许两种最主要的 PyDoc 接口是内置的 help 函数同 PyDoc 基千 G 口和基千 Web 的 HTML 报告接口。在第 4 章我们简短地介绍了 help 函数;它会启用 PyDoc 从而为任何 Python 对象生成简单的文字报告。在这种模式下 , 帮助文本看上去就像类 UNIX 系统上的 "manpage" ,实际上在多页文本的情况下,使用与

UNIX 的 "more" 在无图形界面下相同的分页方式。你需要按下空格键移动到下一页, 按 下回车键移动到下 一行 ,以及按下 Q 键退出: >>>加port sys »> help(sys.getrefcount) Help on built-in function getrefcount in module sys:

getrefcount(...) getrefcount(object) -> integer Return the reference count of object. The count returned is generally one higher than you might expect, because it includes the (temporary) reference as an argument to getrefcount(). 要注意,当你调用 help 时,并不一 定要导入 sys, 但如果是想用 help 来获取 sys 的文档 信息时,就需要导人 sys; help 期待你传入 一 个对象的引用值。在 Python 3.3 和 2.7 中,你

可以传人模块名称的字符串,来获取尚未被导入的模块的文档信息(例如, help('re' )、 help('email.message')) ,但是对千这种模块名称字符串的支持,可能会随 Python 版本 的不同而变化。

4so

I

第 15 章

对诸如模块和类的大型对象而 言 , help 显示的内容会分解成多个部分,其中的导言部分如 下所示。你可以自己试着在交互式命令行下运行,来查看完整的报告(下面的示例是在 3.3

上运行的) :

»> help(sys) Help on built-in module sys: NAME

sys MODULE REFERENCE

http://docs.python.org/3.3/library/sys ... more omitted... DESCRIPTION

This module provides access to some objects used or maintained by the interpreter and to functions that interact strongly with the interpreter. .•. more omitted... FUNCTIONS

— displayhook —=displayhook(...) displayhook(object) -> None ... more omitted...

DATA

_stderr _ = help(dict) Help on class diet in module builtins: class dict(object) I diet() -> new empty dictionary. I dict(mapping) -> new dictionary initialized from a mapping object's ... more omitted...

>» help(str.replace) Help on method_descriptor: replace(...) S.replace (old, new[, count]) -> str Return a copy of S with all occurrences of substring ... more omitted...

»> help(''.replace) ... similar to prior result...

»> help(ord) Help on built-in function ord in module builtins: ord(...) ord(c) -> integer Return the integer ordinal of a one-character string. 最后, help 函数也能像对内置模块那样,用在你自己的模块上。下面是为之前编写的

docstrings.py 文件生成报告。同理,其中一部分是文档字符串,而另一部分是查看对象的 结构而自动提取的信息: >>>加port

docstrings

»> help{docstrings.square) Help on function square in module docstrings: squa-re(x) function documentation can we have your liver then?

»> help{docstrings.Employee) Help on class Employee in module docstrings: class Employee(builtins.object) I class documentation

I

¥

... more omitted...

»> help(docstrings) Help on module docstrings: NAME doc strings DESCRIPTION Module documentation Words Go Here

4s2

I

第 15 章

CLASSES

builtins.object Employee class Employee(builtins.object) I class documentation |

... more omitted... FUNCTIONS

square(x) function documentation can we have your liver then? DATA

spam= 40 FILE

c:\code\docstrings.py

PyDoc: HTML 报告 help 函数显示的文本能够满足大多数场景的要求,尤其是在交互式命令行模式下。对于 那些习惯了丰富表现媒体的读者,这可能看上去略显单薄。本节将展示基千 HTML 风格 的 Py Doc, 它将在 Web 浏览器中更图形化地渲染模块文档,甚至可以自动为你打开页面。

Python 3.3 中的运行方法发生了改变: •

3.3 版本之前, Python 配有一个用千提交搜索请求的简易 GUI 桌面客户端。这个客户

端将启动一个 Web 浏览器窗口,来查看通过自动运行的本地服务器生成的文档。



从 3.3 版本起,之前的 GUI 客户端被一个全浏览器接口方案所取代,该方案在一个网 页中融合了搜索和显示,并且可以与 一 个自动运行的本地服务器进行通信。



Python 3.2 承前启后,既支持原本的 GUI 客户端方案,也支持 3.3 版本中强制的新式全 浏览器模式。

因为本书的读者既面向最新、最广泛的用户,也而向使用旧的、被实践检验过的 Python 的 大众,所以我们将在这里同时探索这两种方案。我们要记住,这些方案的不同只与顶层的

用户界面有关,文档显示的内容几乎是相同的。在其中任何一种方案下, PyDoc 既可以在 一 个控制台中生成文本,也可以生成 HTML 文件,之后用任何你喜欢的方式来查看。

Python 3.2 及后续: PyDoc 的全浏览器模式 从 Python 3.3 开始,在 Python 2.X 中和早期 3.X 发行版本中出现的 PyDoc 原本的 GUI 客户 端模式已经不再可用。该模式在 Python 3 . 2 中出现,既可通过 Windows 7 或之前版本中的

一 个“模块文档“开始桉钮项启动,也可通过命令行 pydoc -g 启动。这 一 GU1 模式在 3.2

文档

I

453

中是被弃用的,不过你需要仔细观察才能发现-—-在我机器上的 Python 3.2 版本中, GUI

模式可以正常工作,井且没有任何警告。 然而在 Python 3.3 中不再有这一 换式,它被命令行 pydoc -b 所替代,这既会启动 一 个本地 运行的文档服务器,也会打开一 个可作为搜索引绞客户端和进行页面显示的 Web 浏览器。

浏览器会打开在 一 个有着增强功能的模块索引页面。你还有其他使用 PyDoc 的方式(例如, 将 HTML 页面保存到 一 个文件以便后续查看),所以从操作上讲只是 一 个相对较小的变化。 要在 Python 3.2 及后续中启动 PyDoc 较新的浏览器唯 一 模式,你可以任意选择下面 三种命 令:它们都使用- m 这 一 Python 命令行参数在模块导入搜索路径中定位 PyDoc 的模块文件。

第 一 条假设 Python 位千你的系统路钤上;第 二条使用了 Python 3.3 中最新的 Windows 启动 器;如果前两种方案不行,那么第 三 条提供了 Python 的完整路径。参阅附录 A 了解更多关

干- m 的知识,参阅附录 B 获取关于 Windows 启动器的简介 。

c:\code> python -m pydoc -b Server ready at http://localhost:62135/ Server commands: [ b] rows er, [ q] uit server> q Server stopped c:\code> py -3 -m pydoc -b Server ready at http://localhost:62144/ Server commands: [b]rowser, [q]uit server> q Server stopped c:\code> C:\python33\python -m pydoc -b Server ready at http://localhost:62153/ Server commands: [ b] rows er, [ q] uit server> q Server stopped 无论你怎么运行这个命令行,其效果都是启动 PyDoc 并使其作为在一个专有(默认情况下 可以是任何没有被使用的)端口上的 Web 服务器,弹出 一 个 Web 浏览器窗口作为客户端.

显示一 个页面,其中包含了模块搜索路径(包括 Py Doc 被启动的目录)上所有可导入换块 的文档的链接。 PyDoc 的顶层网页界面截图如图 15-1 所示。

除橾块索引以外, PyDoc 的网页也包括顶端的输入框,可允许你请求一个特定模块的文档 页面 (Get) 并搜索相关的项 (Search) ,这继承自 GUI 客户端中的输人框。你也可以单击

页面上方的链接转到起始页 (Module Index) 、常见 Python 主题 (Topics) 、语句和一 些 表达式的概览 (Keywords) 。

注意图 15-1 中的索引页列出了栈块和当前目录(本书为 C:\code, PyDoc 通过之前的命令 行在这里启动)下的顶层脚本。 PyDoc 大多时候是用来为可导入模块生成文档,但有时也 可用千为脚本显示文档。 一 个被选择的文件必须导入从而渲染它的文档,同样如前所述, 导入将运行一 个文件的代码。由下模块在运行时通常只是定义工具,因此这 一 般不会有影响。

454

I

第 15 章

凸 Pydoc Index of

Mod

x

卢·

- -

--

-勹-

➔ C 心 localhost:62196 心M毗 l血sBoo.~ G HomeM砒L.. ,, PythonProgram.". 。如llyMe 亟·~··臼70qf叩立I” * 仁



~酝酗芯R

-

`罩雪

1 ,尸一

~

,

千.

-

贼峭-





斤壬

旷·

-

~

-

占片一

为如 33.0[v釭上出征”.MSC"蠡1600“ 比“心)l

W吵如

-

-

--

--

昌盲俨干;

-

一二~切上之汤~.



--·``凛



-

J

.下---__-.一

-

--

-

爆贮心曰已嗽.一幽一- -

--

M 伽五 : &皿

I E@ l“叩

I



f-

中仁召口

仁”习油 m妇加2O22

ill m

凶 h c咋c5

_.

吱c妇

谝刊”“

funrtmh ti,,-

」-





妇巧

叩 12

~

」-

书l



im

吾三



叩“~”

三」百 i选l

“如

上 竺 」 」 』必

亏三百一」三 -



r^

勹l 玉玉 I

|index ofModu1es

-mr

仑' 三

00归沁加砒s

IP

中习

巾介?? 加 -q#B 信叩3

m干叩m? 伽平四百3



图 15-1 : Python 3.2 及后续版本中全浏览器 PyDoc HTML 接口的顶层索引起始页,从 3.3 版

本开始它替代了早期 Python 中的 GUI 客户端 不过,如果你请求了 一 个顶层脚本文件的文档,那么启动 PyDoc 的 shell 窗口都会为任何用 户的交互提供脚本的标准输入与输出服务。最终效果就是 一 个脚本的文档页会在该脚本运

行并打印完它的输出之后才显示。虽然, 一 些脚本会比另 一 些在这点上工作得更好 1 比如, 交互式输入与 PyDoc 本身的服务器指令,有时会有些怪异地相互交错。 一 且你得到图 15-1 的起始页,指定模块的文档页对千新的全浏览器模式和早期的 GUI 客户

端方案这二者本质上是相同的,除了浏览器模式的页面顶部多了 一 个额外的输入框。例如,

图 15-2 展示了新的文档显示页面——针对两个用户定义的模块(我们将在本书下 一部分中 编写这两个模块,作为第 21 章中的基准测试案例学习)来打开。无论是哪种方案,文档页

都包含自动创建的超链接,从而允许你在应用程序相关组件的文档间随意点击切换。例如, 你也会发现可以打开被导入模块页面的链接。 因为它们所显示页面的相似性,下 一 节关 千 3.2 之前版本的 PyDoc 和其截屏的知识总体上

也适用千 3.2 之后的版本。所以即使你在使用最新的 Python, 也要确保继续阅读以获取更

文档

455

多的提示。从效果上看, 3.3 的 PyDoc 直接砍掉了之前 3.2 版本 GUI 客户端扮演的"中间人”, 但保留了它的浏览器和服务器。

Python 3.3 中的 PyDoc 仍旧支持其他原本的使用模式。例如, pydoc - p port 可用来设置 PyDoc 服务器的端口, pydoc

-w module 仍旧将模块的 HTML 文档 写入一个名为 module.

html 的文件,以便之后查看。只有 pydoc -g GUI 客户端模式被移除并被 pydoc -b 所替代 。 你也可以运行 PyDoc 来生成纯文本格式的文档(本章之前介绍过这种 UNIX "manpage" 风 格的页面)一下面的命令行等效千 Python 交互式命令行模式下的 help 调用:

c: \code> py -3 -m pydoc timeit

# Command-line rexr help

c:\code> PY -3 »> help("timeit")

# brreracrive prompr rexr help

作为一个交互式系统,最好的做法是把 PyDoc 基千 Web 的界面用千测试驱动,所以我们在

这里将省略其使用细节,你可以查阅 Python 官方手册来获取更多细节和命令行选项。也要 注意, PyDoc 的服务器和浏览器功能大部分“免费地”源自千 Python 标准库可移植模块中

将这些功能自动化了的工具(例如 web browser 、 http.server) 。你可以参考标准库文件 pydoc.py 中 PyDoc 的 Python 代码来获取额外的细节和启发。

改变 PyDoc 的颜色 在本书纸质版中你可能看不出来,但如果你有本书电子版或是自行启动了 Py Doc, 就 会注意到 PyDoc 选用的颜色并不一定符合你自己的审美。不幸的是,目前还没有简单 的定制 PyDoc 颜色的方式。这些颜色是在源代码中被硬编码的,既不能作为函数或命 令行的参数被传入 , 也不能在 PyDoc 自身的配置文件或全局变量中被修改。

除此以外,在一个开源的系统中,你总是可以修改代码——PyDoc 位于 Python 标 准库的 pydoc.py 文件中,在 Windows 平台上 Python 3.3 版本中这个目录就是 C:\ Python33\Lib 。它的颜色是被硬编码的 RGB 值,即代码中内嵌的十六进制字符串。例

如,字符串 '#eeaa77' 指定一个 2 字节 (16 位)的值,来表示红绿篮的程度(十进

制下是 238 、 170 和 119) ,这产生了一种有着少许橙色的函数横条。同理,字符串 '#ee77aa' 渲染了一种暗粉色的颜色,被用在包括类和索引页横条等的 9 个地方。

想要调整,你可以搜索这些颜色值字符串并用你自己喜欢的颜色替换它们 。 在 IDLE 中,一次对正则表达式”#\ w{6}" 的"编辑/查找”就能定位到这些颜色字符串(按 照 Python re 模块模式的语法,这将匹配到#后面带 6 个字母数字字符的字符串;更

多细节请参阅库手册)。 为了挑选颜色,在大部分有颜色选择对话框的程序中,你都可以在颜色与 RGB 值进

456

I

第 15 章

飞 y 、 Pydoc:module 沁 C

D. localh竺切1~帜be吽hlml

售 M毗血's 加o矗.衢 Home- Marte L. _

`

"

-

- PylOOII Pl'ogd8690d,l2. :MSCv. 1600" 泣 (AM的)] `石山扑s-7 凸 叩oc: module nmer 其

I 旷;:r-

+-

I .一no.py:“"....“ °2

•=• ""

口鲁e耋亡土酌., 仁仁0 匕"C ·,C归r

“ l l · , or. "江 OE P吓片“' ,...,..,.,., "'仁.,江·••'• -

Rep山C”“”“”“让.

芦圭莘. ,

11

c·吐·c.: cr`···nd一”...。“' 中C晕耋,

中工,C`2

I ~勹喇mnn-liae

1~心

m=

·cr丘g

琶冥C.e心 •

合 三

C I I) localhos饬4?10八ime『ht叩

-----一--·

,崝心加gram... • O'Reilly 如io ,.. ~、

•。叩"心叩心

J

印 3 3:0 [vll 0过沁. MSCv 160064 比 (AMD64)]

cod••n芷”“'b.ncm”工...人

n3i, ay."... ";...,

~

誓咖哑s8oo."髻 11ome· M虹 L..

`匕_—___-—-—-

如恤归妇玉心主竺呼

^

二三一=/ -一______—— j

!,

I!

·Il\即

l

0工也已”0也 t口红I

妇stolto..k哼刃 . ,叩立, 血. "p.p...匕叮墨)

···c or to,.l., (已,仁 ot rq“1 ms ot It oc.l o2

m干“'mm9 0t'叩C,

l

ual eia, •丘ce 心..“'c ot 中· 9”~·· 0r.丘~ C l co eloc· 11 · Tb1. ... •• ..心 pr.C上..... •• cb. .ym:一

··C釭9 迦 C”“黜 o, 仁归 2”c.仁 工矗eor心.

t口3CII 工中星亡山恤... 丁ot.1 C,... co m .etumO Itot.l t”正, 1··C 工··吐C)

图 15-2: Python 3.2 及之后版本中 PyDoc 模块的显示页面,在顶部带有输入框。页面显示了 将在本书下一部分(第 21 章)编写的两个模块

行来回转换;本书的示例中有一个能实现相同功能的 GUI 脚本 setcolor.py 。 在我的 PyDoc 版本中,我采用#008080 (青绿色)替换掉了所有的# ee77aa 消除暗粉色。用 #cococo (灰色)替换#ffc8d8 同样地消除类的文档字符串的浅粉色背景。 这种“外科手术”不适合胆小鬼 (Py Doc 的文件目前有 2600 行代码),但确实是一 个不错的代码维护训练 。

当替换诸如# ffffff 和#000000 (白和黑)的颜色时,要格

外小心 。 同时,要确保修改前首先对 pydoc.py 文件进行各份,从而给自己留下退路 。 pydoc.py 文件使用了我们还没有遇到过的工具,但当你进行这些战术上的改变时,却 可以安全地忽略其他代码。 确保跟进 PyDoc 在配置方面的变化;这似乎是未来主要能有的改进 。 实际上,已有

一些工作正在进行中: Python 开发者清单的议题 10716 便是旨在通过让 PyDoc 支持

CSS 样式表,来实现更加灵活的用户定制化界面。如果它获得成功,那么用户就可以 在外部的 CSS 文件而不是 PyDoc 的源代码中选择颜色和其他显示选项 。

文档

I

457

另一方面,现阶段在 3.4 版本之前 , 该议题都不会计划实现,这也要求 PyDoc 的用

户精通 CSS 代码一一不幸的是, CSS 本身的结构并不简单 , 因此许多 Python 用户对 它不够熟悉导致无法改变 。 例如 ,

当我写下这些的时候,提案的 PyDoc CSS 文件

已经有 234 行代码了,对那些不是很熟悉 Web 开发的人而言,意义不大(为了定制 PyDoc, 就让人们去学习 Web 开发似乎并不明智!) 。

现在 3.3 版本中的 PyDoc 已经支持了一个提供一些定制选项的 CSS 样式表,但似乎并 不全心全意,并且只配有一个空的样式表 。 在这个问题充分讨论并觥决之前,修改代 码似乎是最好的选择 。 无论如何, CSS 样式表都远远超出这本讲 Python 书的讨论范 围 一—你可以上网搜索史多细节,也可查看未来 Python 发行版本对 PyDoc 开发的提示 。

Python 3.2 及更早: GUI 客户端 本节将介绍 PyDoc 原本的 GUI 客户端模式,面向使用 3.2 及更早版本的读者,同时也给出

更多的 PyDoc 常见使用场 景 。本节承接前 一节的基础内容,并且不再赘述相互 重 复 的内容, 因此如果你使用老版本的 Python, 要确保浏览过一 遍上一 小节 。 如前所述,在 Python 3.2 中 PyDoc 提供了 一 个顶层 GUI 接口( 一 个简单却是可移植的用来

提交请求的 Python/tkinter 脚本),以及一 个文档服务器 。 客户端的请求被路由到服务器, 服务器会生成报告并弹出浏览器窗口进行显示。除了需要你提交搜索请求外,该过程儿乎 是全自动的。

http://localho丈7464/

IH

open browser Search for[glob

quit serving 式op

est.tesl_glob -(no description) est.test_globat - Verify that warnings are is

go to selected

hide results

圈 15-3 :在 3.2 及更早版本中的 PyDoc 顶层搜索引擎 GUI 客户端:输入你要查询文档的模块 的名称,按下回车键选择该模块,然后按下 “go to selected" browser" 来查看所有可用的模块)

458

1

第 15 章

(或省略模块名称并按下 ”open

要在该模式下启动 Py Doc, 你一般要先启动图 15-3 中截图的搜索引效 GUI 。这既可以通过 选择 Windows 7 及之前版本中 Python 的开始菜单栏内的 Module Docs 项来启动,也可通

过运行 Python 标准库目录下的 pydoc .py 脚本,井加 上 - g 命令行参数米 启动:该脚本位于 Windows 的 Llb 目录下,不过你可以使用 Python 的- m 标签来避免输入脚 本路径:

c:\code> c:\python32\python -m pydoc -g c:\code> PY -3.2 -m pydoc -g

# #

Explicit Python path Wi11dows 3.3+ launcher version

输入你感兴趣的模块名称,再按下回车键, PyDoc 会探入到模块导入搜索路径 (sys.

path) ,查询被请求的模块和它的文档。 当你找到想要的项目后,选中它并点击 “go

to selected" 。 PyDoc 会打开 一 个网页浏览器,

来显示以 HTML 格式渲染的报告。图 15-4 展示了 PyDoc 为内 置的 gl ob 模块所显示的 页 面。注意此页 Modules 部分下的超链接-—升尔可以点击它们跳转到相关(已导人)模块的

PyDoc 页面。对于更大型的网页, PyDoc 也会生成指向页面不同部分的超链接。 与 he lp 函数接口 一样, GUI 界面既能用在内骰模块上,也能用在用户定义的模块上。图 l5-5 显示的是针对我们之前编 写的 docstrings .py 模块文件所产生的页面。 你需要确保模块的外层目录位千模块导入搜索路径中---如前所述, PyDoc 必须先导入 一· 个文件才可以渲染它的文档。这也必须包括当前的工作目录——因为 PyDoc 可能不会检查

它被启动的那个目录(不论如何,当从 Windows 的开始菜单启动时,当前工作目录很可能 亳无意义),所以你可能帣要扩展 PYTHON PATH 设 笠来使其工作。在 Python 3.2 和 2.7 版本中,

我需要给 PYTHONPATH 加上一个““使 PyDoc 的 GUI 客户端模式能查询它通过命令行被启 动的目录:

c:\code> set PYTHONPATH=.;%PYTYONPATH% c:\code> PY -3 . 2 -m pydoc -g 如果要查看 3.2 版本中新的全浏览器 pydoc

-b 模式下的当前目录,这项设置也是必须的。

然而, Python 3.3 在它的索引列表中自动包含了".",因此无需路径设置就可以阅览 PyDoc 启动目录中的文件一-寸吝是一 个微小的却值得注意的改进。

PyDoc 还能以许多这里没有提及的方式定制和启动,你可以参考 Python 标准库手册中的内 容来了解更多的细节。学完本节后你需要记住的最重要的 一 点就是, PyDoc 基本上“免费”

帮你实现了文档报告的功能。如果你善于在文件中使用文档字符串,那么 PyDoc 会帮你收 集并排版它们以便显示。 PyDoc 只能帮助类似函数和模块的对象,但它提供了 一 种简便的

方式来读取这些工具的中级文档。 PyDoc 的报告比单纯罗列属性更有用.却比不上标准手 册那样详尽完整 。

文档

I

459

D Python: module gtot. x

C' -[j localhost74印/glob.html L= -

----

l- -

沁己 Other. boo伽arks

二飞

~Ma六 lutz's Boo.. CJ Ho叩 -Ma戊 L.,令 Python Program... 愚 O'Reilly Media · -· --

1

卜hdh

|glob ril 已口正 9lobbu,g uti_卫ty.

R红己·

U3c

n,e 卢立釭"1

R.cum m

ot”廿.....crmn0 ·卢rh叩亡严 C它 em.

-y

C心亡”n

su,ple

1”“cor 口辽吐 y1e出•

Th• pae» import builtins »> dir(builtins) ['ArithmeticError','AssertionError','AttributeError','BaseException', 'BlockingIOError','BrokenPipeError','BufferError','BytesWarning', ... many more names omitted... 'ord','pow','print','property','quit','range','repr','reversed', 'round','set','setattr','slice','sorted','staticmethod','str','sum', 'super','tuple','type','vars','zip'] 这个列表中的变批名组成了 Python 中的内置作用域。概括地讲,前一半是内置的异常`而

后一 半是内置函数。特殊名称 None 、 True 和 False 也在这个列表当中,尽管在 3.X 版本中 它们被当作保留字。因为 Python 会在 LEGB 查找中的最后自动查找这个模块,你将“免费” 得到这个列表中的所有变量名。也就是说,你能够使用这些变量名而不需要导入任何模块。

因此,有两种方式引用一个内置函数:利用 LEBG 法则,或者手动导入 builtins 模块:

>» zip

# The normal way

»> import builtins >» builtins.zip

II The hard way.for customizations

»> zip is builtins.zip True

# Same object, different lookups

其中第 二 种实现方式有时在更高级的用法中是很有用的,这些用法我们会在下 一 章的边栏 里遇到。

重定义内置名称:有好有坏 细心的读者也许注意到了由千 LEGB 查找的流程.会使它在第一处找到变氮名的地方生效。 也就是说,在局部作用域中的变最名可能会覆盖在全局作用域和内置作用域中有着相同变

量名的变量,而全局变扯名有可能覆盖内置的变量名。例如, 一 个函数创建了 一 个名为 open 的局部变晕井将其进行了赋值:

def hider(): open='spam' open('data.txt')

#

Local variable, hides built-in here

#

Error: rhis no longer opens a file in rhis scope!

无论如何,这会将存储于内置(外部)作用域中名为 open 的内置函数隐藏起来。这样的话,

名称 open 在函数内不能再用来打开文件一一它现在是一个字符串,而不是开启工具函数。 如果你不需要在这个函数中打开文件的话,这不是问题,但如果你计划通过这 一 名称打开

文件就会触发 一 个错误。

这在交互式命令行下甚至会更容易出现,它作为 一 个全局的模块作用域来工作:

»> open = 99

492

I

第 17 章

II Assign in global scope, hides built-in here too

现在,为你的变址使用 一个内置的名称不会有任何本质上的错误,只要你不再使用原始的 内置版本。毕竟,如果这些真的被禁止,我们岂不是要记住完整的内置名称渚单并保留其

中所有的名称。在 3.3 版本中这 一 模块中拥有超过 140 个名称,这显然是 一 项严格限制且 令人胆怯的工作:

»> len(dir(builtins)), len([x for x in dir(builtins) if not x.startswith('_')]) (148, 142) 事实上,有时在高级编程中你可能真的想要在自己的代码中重定义内置名称来替换原有的 名称~定义 一 个定制的可以验证读取企图的 open (参阅本章边栏“打破 Python 2.X 的小宇宙”获取关千这 一 方面的更多内容)。

然而,重定义一 个内置名称往往是个错误,并且让人头疼的是, Python 对千这个问题并不 会处理为警告消息。类似 PyChecker (见网络)这种工具可以警告这样的错误,但此时知识 才是你最好的防范措施:不要重定义 一 个你需要使用的内置名称。如果你偶然在交互式命 令行下用这种方式重新赋值了 一 个内置名称,你要么重启会话,要么执行一 条 del name 语 旬来移除你作用域中的重定义,从而恢复内置作用域中的原始名称。 注意,卤数也能简单地使用局部变址名隐藏同名的全局变盐,但是这有着更广泛的用途, 并且这也是局部作用域存在的意义

因为它们使潜在的名称冲突最小化,函数是自包含

的命名空间作用域:

X

=

88

#

Global X

def func(): # Local X : hides global,but we want this here

X = 99

func() print(X)

#

Prints 88: unchanged

这里,函数内部的赋值语旬创建了 一 个局部变批 X, 它与函数外部模块文件的全局变址 X 是完全不同的变量。其结果是,如果在 def 内不添加 global (或 nonlocal) 声明的话,就 没有办法在函数内改变函数外部的变扯,正如下一小节所介绍的。

注意:版本差异介绍:实际上,那段像绕口令的描述会更绕 一 些。这里所使用的 Python 3.X

builtins 模块,在 Python 2.X 中叫作_ built in_。另外,在大多数全局作用域, 包括交 互 式会话中,都预先设置了名称_ builtins_ (带有 s) ,来引用 3. X 中名为 builtins 的模块和 2.X 中名为_builtin—的模块 。 所以你总是可以在不导入的情况下 使用

builtins

,但是不可以导入那个名称本身一一它本身是 一个预设变量,而不是

一个模块的名称。

也就是说,在 Python 3 . X 中, buil tins

is _buil tins—的运行结果在导入 builtins

后是 True; 而在 Python 2.X 中_builtin_ is _builtins_的运行 结 果在导入

buil tin

后是 True 。 这里的要点是,我们通常可以直接运行 dir(_builtins

作用域

)来查

I

493

石内隍作用域,而不必在 Python 3.X 和 Python 2.X 中导入.但是我们披建议在 3 . X 的实 际工作和用户化中使丿Tl

built ins.

在 2. X 中相应地使用_built in_。谁说讲清楚这些

内容很容易呢?

打破 Python 2.X 的小宇宙 还有一件在 Python 中可以做但不应该去做的事 -

由于名称 True 和 False 在 Python

2.X 中是内五作用域中的变量而不是保留宇,用诸如 True

=

False 的一条语句来重

新为它们赋值就成为可能 。 不要担心.实际上这么做不会破坏通用的逻辑一致性!这

条语句只是在那个作用域中重新定义了单词 True.

让 True 看上去返回了 False 。 所

有其他的作用域仍然在内五作用域中查找 True 和 False 最初的定义 。 史为有趣的是,在 Python 2.X 中,可以使用—built in_. True

=

False, 在生个

Python 进程中把 True 重置为 False 。 这样做行得通是因为一个程序中只有一个内五

作用域模块,由所有它的客户端共享 。 唉,不过这种类型的赋值在 Python 3.X 中已 经取消了,因为 True 和 False 都被看作是具正的保留宇,就像 None 一样 。 然而,在

Python 2.X 中,它把 IDLE 置于一种特殊的恐慌状态,它会重新设置用户代码进程(换 言之、孩子们,别在家里尝试了) 。

然而,这种技术可能有用 , 可以用来说明底层的命名空间模型、对于必须把 open 这 样的内置函数修改为定制函数的工具编写者来说也会有用 。 通过在内置作用域中重新 赋值一个函数的名称,你为进程中的每一个模块都进行了定制化的重置 。 如果是这样, 你很可能需要记住原始版本,以便从定制化设置中调用 一— 事实上,我们将在后面的

边栏 “ 请留意.定制 open n 中看到实现这样一个定制 open 的方法`在我们探索过嵌 套作用域闭包和状态保留选项之后将会学习它 。

此外,再次注意, PyChecker 同其他(如 PyLint) 的笫三方工具一样会警告常见的编 程错误,包括对内置名称的偶然性赋值(在这些工具中,这通常叫作“阴影化”一个

内置名称) 。 通过这样的工具来检查你最初编写的一些 Python 程序来看看它们指出了 什么是个不错的主意 。

global 语句 global 语句和它的 3 .X 近亲 nonlocal 语句是 Python 中唯一看起来有些像声明语句的语句。

但是,它们并不是类型或大小的 声 明,而是命名空间的 声 明 。 global 语句告诉 Python 函数 计划生成一 个或多个全局变扯名 -一- 也就是说,存在干整个模块内部作用域(命名 空 间) 的变最名。

494

I

第 17 章

我们已经在前边讲过了 global 。这里只是做一个总结:



全局变掀是在外层校块文件的顶 层 被赋值的变址名。



全局变乱如果是在函数内被赋值的话,必须经过声明 。



全局变屈名在函数的内部不经过声明也 II[以被引用。

换句话说, global 允许我们修改 一 个 def 外的模块文件顶层的名称。正如我们将在随后看 到的, nonlocal 语句几乎是相同的,但它适用于外层 def 的局部作用域内的名称,而不是 外围模块中的名称。

global 语句包含了关键字 global, 其后跟着 一个或多个由逗号分开的变趾名。当在由数主 体被赋值或引用时,所有列出米的变撬名将披映射到整个模块的作用域内 。 例如: X = 88

II

Global X

def func(): global X X = 99

#

Global X: outside def

func() print(X)

II

Prinrs 99

这个例子中我们增加了 一 个 global 声明,以便在 def 之内的 X 能够引用在 def 之外的 x.

因为这里它们是相同的变摸,所以改变函数内部的 X 也会改变函数外部的 X 。下面有 一 个 global 使用的例子:

y, z = 1, 2 def all_global{): global x X =

y +

Z

#

Global variables in module

# Declare globals assigned II No

need to declare y, z: LEGB rule

这里, X 、 y 和 z 都是 all_global 函数内的全局变盟。 y 和 z 是全局变批,因为它们不是在 函数内赋值的; x 是全局变扯,因为它通过 global 语句使自己明确地映射到了模块的作用域。

如果不使用 global 语句的话, x 将会由千赋值而被认为是局部变址。 注意, y 和 z 并没有进行 global 声明。 Python 的 LEGB 查找规则会自动从楼块中找到它们。 此外,注意 x 在函数运行前的外围模块中甚至并不存在。如果这样的话,函数内的第 一 条 赋值语句将自动在模块中创建 x 这个变匮。

程序设计:最少化全局变量 普通的函数和特别的全局变盘会引发 一 些更大的设计问题。我们的函数之间如何通信?尽

管在开始编 写 自己的较大型函数时,这其中的 一 些问题会变得更加明显, 一 些预先的指导

作用域 I

49s

之后也许会帮助你避免这些问题。一般而言,函数应该依赖形参与返回值而不是全局变朵,

但是我得解释一下为什么。 在默认情况下,函数内部赋予的变批名是局部变最,所以如果希望在函数外部对变址进行 改变,必须编写额外的代码 (global 语句)。这是有意而为的。这似乎已成为 Python 中的

一种惯例,如果想做些“错误” 的事情,就得多编写代码。尽管有些时候 global 语句很有 用,然而在 def 内部赋值的变量名默认为局部变量,通常这是最好的约定。将其改为全局

变址会引发一些软件工程问题:由于变量的值取决于函数调用的顺序,而函数自身是任意

顺序进行排列的,导致程序调试或理解起来变得很困难。 例如,思考一下这个模块文件,可认为它已导入并在其他地方使用: X

=

99

def funcl():

global X X = 88

def fune2():

global X X = 77

现在,

假设你的任务就是修改或重用这段代码。这里 X 的值会是什么?确切地说,如果不

确定引用的时间,这个问题就是亳无意义的。 X 的值与时间相关联,因为它的值取决千哪 个函数是最后一次被调用的(有时我们无法单从这个文件说明白)。 最终结果就是,为了理解这段代码,你必须去跟踪整个程序的控制流程。此外,如果重用 或修改了代码,你必须随时记住整个程序。在这种情况下,如果使用这两个函数中的 一 个

的话,就必须要确保没有使用另一个函数。它们依赖千(也就是说,耦合千)全局变批。 这就是使用全局变最带来的问题:不像那些依赖于局部变量的由自包含函数构成的代码, 全局变最使得程序更难理解和重用。 另 一 方面,除去使用类似嵌套作用域闭包的工具或运用类的面向对象的编程方式,全局变

批也许就是 Python 中最直接保持共享状态信息的方式一函数为了其下次调用需要记住的 信息。局部变量在函数返回时将消失,而全局变量不会。我们会看到,其他技巧也可以实 现这样的目标,并且允许保留多份不同的信息,但对千简单的使用场景而言,这些做法通 常比直接将值移到全局作用域更复杂。 此外, 一些程序委托一个单一的模块文件来收栠所有的全局变批;如果这是你所期望的效果, 那么就没有不妥。在 Python 中使用多线程进行并行计算的程序实际上也是要依靠全局变量

496

I

第 17 章

的。因为全局变撒在并行线程中在不同的函数之间成为共享内存,所以扮演了通信工具的

角色注 20 就目前而言,在不熟悉编程的情况下,最好尽可能地避免使用全局变量一—它们容易使程 序变得难以理解和重用,而且会在所存储的数据不止一份的情形下失效。试试通过传人参 数然后返回值替代一下。六个月以后,你和你的同事都会感谢你没有使用那么多全局变最。

程序设计:最小化跨文件的修改 这是另一个和作用域相关的设计问题:尽管能直接修改另一个文件中的变量,但是往往我 们都不这样做。本书下 一 部分将更深入地讨论第 3 章中介绍的模块文件。为了说明它们与

作用域之间的关系,考虑下而这两个模块文件: # first.py X = 99

Hsecond.py import first print(first.X) first.X = 88

It This code doesn't know about second.py

references a name in another file refi But changing it can be too subtle and implicit

# OK: #

第一个模块文件定义了变最 X, 这个变址在第二个文件中袚打印并通过赋值修改了。注意,

我们必须在第 二 个文件中导入第一个模块才能得到它的值一一就像我们学到的那样,每个

模块都是自包含的命名空间(变量名的包),而且我们必须导入一 个模块才能从另 一 个模 块中看到它内部的变批。这是关千模块的一个要点:通过在文件的层次上隔离变量,它们 避免了跨文件的名称冲突,这与局部变批避免跨函数的名称冲突的方式很像。 事实上,不管怎样,按照本章的主题来讲,一个模块文件的全局作用域 一 且被导人就成了 这个模块对象的一个属性命名空间:导入者自动得到这个被导入的模块文件的所有全局变

量 的访问权,因为在一个文件被导 入后,它的全局作用域就转变为 一个对象的属性命名空间。 在导入第一个模块文件后,第 二 个模块打印它的变撮,然后将其变最赋了一个新值。引用 这个模块的变批来打印它是没问题的一—这就是通常模块之间连接起来组成一 个更大型系 统的方式。然而赋值语句 first.X 的问题在于,这种做法过千含糊了:无论是谁来负责维护

注 2

多线程与程序的其余部分并行地执行函数调用,由 Python 的标准库模块_ thread 、 threading 和 queue (在 Python 2.X 中是 thread 、 threading 和 Queue) 提供支持。因为 所有的线程函数在同一进程中运行,全局作用域在它们之间经常作为一种共享内存的形式 来使用(线程之间既可以在全局作用域中共享名称,也可以在进程的内存空间中共享对象). 线程普逼会被用来在 GUI 中做一些长期运行的任务,一般可以实现非阻塞操作并会最大

化 CPU 容量。这已经超出了本书的范围;参考 Python 库手册和前言中所罗列的进阶教材 (如 O'Rei lly 的《 Python 编程》 )来获取史多细节。

作用域 I

497

或重川第 一 个模块,都不 一 定知道导入链上有某个任意遥远的模块可以在他运行的时候修

改 X 。实际上,第 二 个模块可能处千 一 个完全不同的目录下,而且很难找到 。

尽管这样的跨文件变篮在 Python 中总是能被修改,但它们通常比我们想要的更微妙。再者, 这会让两个文件产生过强的耦合:因为它们都与变矗 x 的值相关,如果没有其中 一 个文件

的话很难理解或重用另 一 个文件。这种隐含的跨文件依赖性,在最好的情况下会导致代码 不火活,在最坏的情况下会引发错误。 这里再说 一 次最好的解决办法就是别这样做:在文件间进行通信最好的办法就是通过调 用函数,传递参数,然后得到其返回值 。 在上面的例子中.我们最好编写 一 个访问函数去

管理这种变化:

II first.py X = 99 def setX(new): global X X = new

It Accessor make external changes explir # And can manage access in a single place

# sernnd.py

import first first.setX(88)

If Call the function instead of changing directly

虽然这摇要更多的代码而且看上去只是 一 个简单的改变,但是在可读性和可维护性上有着

天壤之别:当别人仅阅读第一个模块文件时看到这个函数,会知道这是一 个接口,并且知 道这仵改变变址凡换句话说,它去除了令人惊讶的因素(这种因素在软件项目中并非好事)。 虽然我们无法避免修改跨文件的变益,但是通常的做法是最小化文件间变最的修改,除非 在整个桯序中被广泛地接受。

注意:在第六部分遇到类时,我们将看到用干编写屈性访问器的类似的技巧。不同千校块,类

也可以通过运算符重载自动拦截属性的获取,甚至当访问器没有袚用户使用时也可以。

其他访问全局变量的方式 有意思的是,由下全局变批构成了 一 个被导入的对象的屈性,我们能通过使用导入外层模

块并对其属性进行赋值来模拟 global 语句,就像下边这个模块文件的例子一 样。这个文件 中的代码先通过变篮名然后通过索引 sys.modules 导入外层模块,其中包含已载入的模块 表(关于这个表的更多内容将在第 22 章和第 25 章介绍) # lhismod.py

var= 99 def local():

498

I

第 17 章

# Global variable

== module attribute

var= o def glob1(): global var var+= 1 def glob2{}: var = o import thismod thismod.var += 1 def glob3(): var = o import sys glob= sys.modules['thismod'] glob.var+= 1

# Change local l'r.r

# Declare global (r;orma/) # Change global var

Change local var lmporr my.I.elf # CI} { IIIge globul vur

# #

II Change local ,•ar II Import sysrem table # Get 111od11/e obje门 (or use -11a111e_) # Chunge global var

def test(): print(var) local(); globl (); glob2 (); glob3 () print(var) 在运行时,这会给全局变阰加 3 (只有第 一 个函数不会影响全局变显)

>» import thismod >» thismod.test() 99

102

» > thismod. var 102 这很有效,并且表明全局变址与模块的属性是等效的,但是为了清晰地表达你的想法,这

种方式比直接使用 global 语句需要做更多的工作 。 正如我们已经看到的, global 允许我们修改 一个函数之外的模块中的名称。它还有 一 个叫

作 n onlocal 的近 亲,也可以用来修改外层函数中的名称---{!!是,要理解它的用处,我们 希要首先概括地介绍外层闭数 。

作用域和嵌套函数 到现在为止,我有意地忽略了 Python 的作用域规则中的一部分,因为它在实际悄拱中很少 见到 。 但是,现在到了探入学习 一 下 LEGB 查找规则中 E 这个 字 母的时候了 。 E 这 一层 是 在 Pyt hon 2.2 增加的;它包括了忏意外层函数局部作用域的形式。嵌套作用域有时也叫作

静态嵌套作用域。实际上,嵌套是 一 种代码 写 法上的表述 : 嵌套的作用域对应于程序呃代 码文本中物理上和句法上的嵌在代码结构 。

作用域

I

499

嵌套作用域的细节 在增加了嵌套的函数作用域后,变址的查找规则变得稍微复杂了一些。在一个函数内:



一个引用 (x) 首先在局部(函数内)作用域查找变址名 X; 之后会在代码句法上的外 层函数中的局部作用域,从内到外查找;之后查找当前的全局作用域(模块文件) ;

最后查找内置作用域(模块 builtins) 。然而, global 声明会直接从全局(模块文件) 作用域进行查找。



在默认情况下, 一个赋值 (X = value) 会创建或改变当前作用域中的变 批名 X 。如果 x 在由数内部声明为全局变量,它会创建或改变整个模块的作用域中变量名 X 。另 一方面, 如果 x (仅)在 3.X 版本中的函数内声明为非局部变址,赋值会修改最近的嵌套函数的 局部作用域中的名称 X 。

注意, global 声明会将变量映射至外层模块。当嵌套函数存在时,嵌套函数中的变匮也许 会被引用,但它们需要 3.X 的 nonlocal 声明才能修改。

嵌套作用域举例 为了阐明上 一 小节的要点,让我们用 一 些真正的代码来说明。下面是 一 个外层函数作用域

的例子(将这些代码输入到一个脚本文件或交互式命令行中来实时运行它) #

X = 99

Global scope name: not used

def fl(): X = 88

# Enclosing def local

def f2(): print(X)

#

Reference made in nested def

f2 () fl()

# Prints 88: enclosing def local

首先,这是 一 段合法的 Python 代码。 def 是一 个简单的可执行语句,可以出现在任意其他

语句能够出现的地方,包括嵌套在另一个 def 之中。这里,嵌套的 def 在函数伈调用时运 行,这个 def 将生成一个闭数,并将其赋值给变显名 f2, 位是 fl 的局部作用域内的一个

局部变量。可以说, f2 是 一 个临时函数,仅在 fl 内部执行的过程中存在(并且只对 fl 中 的代码可见)。

但是,注意伈内部发生了什么。当打印变量 x 时, X 引用了存在千外层函数 fl 的局部作 用域内的变最 x 的值。因为函数能访问在物理上处千外层的 def 声明内的变噩名,通过 LEGB 查找规则, f2 内的 X 自动映射为 fl 中的 X 。 这种嵌套作用域查找即便在外层的函数已经返回后也是有效的。例如,下面的代码定义了 一个函数,该创建并返回另一个函数,这也代表了一种更为普遍的使用模式:

soo

I

第 17 章

def fl(): X = 88 def f2(): print(X) return f2

# Remembers X in enclosing def scope # Return/2 加t do11't call it

action = fl() action()

# Call it now: prints 88

# Make, return function

在这段代码中,对 action 名称的调用 本质上运行了 f1 运行时我们命名为 f2 的函数。这能

行得通是因为 Python 中的函数与其他 一 切 一 样是对象,因此可以作为其他函数的返回值传 递回来。更为重要的是, f2 记住了 f1 的嵌套作用域中的 X ,尽管 f1 已经不处千激活状态-__

这将引领我们进入下一主题。

工厂函数:闭包 这种代码行为的称呼因人而异,既可以叫作闭包 (c losure) 也可以叫作 工厂函数 (factory

function) -前者把它描述成一 种函数式编程技巧,而后者将它认为是 一 种设计模式。不 论叫法如何,这里讨论的函数对象能够记忆外层作用域里的值,不管那些嵌套作用域是否

还在内存中存在。从结果上看,它们附加了内存包(又称为状态记忆),它对千每个被创 建的嵌套函数副本而 言都是局部的,从这 一作用来看,它们经常提供了一种类的简单替代 方法。

一个简单的函数工厂 工厂函数(又名闭 包)有时用千事件处理器程序,这些程序需要对运行时的情况做出即时

响应。例如,想象 一 个 GUI, 当它必须为用户输入定义动作,而用户输入往往在编写时是 不可预测的。在这样的情况下,我们需要一个函数,用来创建并返回另一个函数,而被创 建的函数彼此之间可能带有不同的信息。

为了言简意陔地说明这点,考虑下面的函数,它是在交互式命令行下被输入的(就像之前

说过的,这里省略了行接续提示符..)

»> def maker(N): def action(X): return X ** N . return action

# #

Make and return action ac1ion retains N from enclosing scope

这定义了一个外层函数,用来简单地生成井返回一个嵌套的函数,却并不调用这个内嵌的 函数: maker 创造出 action, 但只是简单地返回 action 而不执行它。如果调用外部的函数:

»> f = maker(2) # Pass 2 to argument N »> f

作用域

I

so1

我们得到的是生成的内嵌函数的 一 个引用。这个函数是在内嵌的 def 运行时创建的。如果 现在调用这个从外部函数得到的返回值: `,'、,'

f f ( ( 34 >> >9>1 >>

# Pass 3 to X, N remembers 2: 3 #4

6

** 2

** 2

我们调用了内嵌的函数一一maker 内部的名为 action 的函数。换言之,我们正在调用

maker 函数创建井传回的一个内嵌函数。 不过也许这里最不平常的一点是,虽然在调用 action 时 maker 已经返回了值并退出,但是 内嵌的函数记住 T 整数 2 ,即 maker 内部的变杂 N 的值。实际上,在外层嵌套局部作用域

内的 N 被作为执行的状态信息保留了下来,井附加到生成的 action 泊数上,这也是当它 稍后被调用时我们返回其参数平方的原因。 同样重要的是,如果再调用外层的函数,可以得到一个新的有不同状态信息的嵌套函数。 也就是说,当调用新函数时我们得到了参数的 三次方而不是平方,但是最初的函数仍像往

常一样是平方:

»> g = maker(3) »> g(4)

g remembers 3, J remembers 2 H 4** 3

#

64

»> f(4)

#

4** 2

16 这能够奏效,因为每次这样对一个工厂函数的调用,都将得到属于调用自己的状态信息的

集合。在我们的例子中,我们赋给名称 g 的函数记住了 3, 而赋给名称 f 的函数记住了 2, 因为每个函数都有自己的状态信息,这些状态信息由 maker 中的变昼 N 保持。 这是一 种相对高级的技术,除了在那些拥有齿数式编程语言背般的程序员间大为流行外,

你在大多数代码中并不经常见到。另 一 方面,嵌套作用域常常被 lambda 函数创建表达式利 用(本章稍后将展开介绍它)一因为它们是表达式,几乎总是嵌套在一个 def 中。例如, 在之前的示例中 一 个 lambda 表达式可以替代 def:

»> def maker(N): return lambda X: X

>» h = maker(3) >» h(4)

** N

# lambda junctions retain srate too

#

4 ** 3 again

64

想了解更多实际工作中的闭包例子,可参考本章后面的边栏“请留意:定制 open" 。那里 使用了类似的技巧,在嵌套作用域中存储信息以便稍后使用。

so2

I

第 17 章

注意:排版提示:在本在中,我已经开始在列举交 互式示例时省略 ”... "接续行提示符,它可 能在你的界面中出现也可能不在你的界面中出现(它们在 shell 中出现,而在 IDLE 中不

出现)

。从现在起将遵从这 一惯例,方便你从本书电子版或其他地方剪切和粘贴大型代

码示例。我假设现在你已了解缩进规则,并已输人大杂的 Python 代码,而下面的一 些卤 数和类过于大型,逐字输入起来有些困难。

我会单独或在文件中列举越来越多的代码,井在文件和交互式输入之间任意切换;当你 看到“>>>”提示符时,表明代码是交互式输入的,当省略”>>>”时, 一 般可以剪切

和粘贴到你的 Python shell 中。如果这样做失败了,你仍可以通过逐行粘贴或在文件中 编辑来运行它。

闭包 vs 类:回合 1 对一 些人来说,类(在本书的第六部分会完全讲述)是一个更好的实现这种状态记忆的选择,

因为它们用属性赋值来更加显式地创建它们的内存。类也直接支持闭包函数所不支持的其 他工具,比如通过继承的定制化和运算符重载。类通过类方法这种形式更自然地实现了多 样的行为。因为存在着这样的不同,类在实现功能更完整的对象上往往表现得更好。 但是,当记忆状态是唯 一 的目标时,闭包函数经常提供一 个轻批级的可行的替代方案。它

们为每 一 次调用提供局部化存储空间,来存储一个单独的内嵌函数所需的数据。当我们增 加前面所讲的 3.X nonlocal 语句来允许嵌套作用域状态改变时,这点尤其正确(在 2.X 版

本中,嵌套作用域是只读的,因此其使用受到更多限制)。 从更广阔的视角来看, Python 拍数在调用之间记忆状态有多种方式。尽管一 般的局部变袋

的值在函数返回后就消失了,但你也可以通过全局变最、类实例中的属性、外层作用域中 的引用、参数默认值和函数属性这五种方式在不同的函数调用之间持有值。有些人会把可 变参数的默认值也算在内(然而其他人也许并不认同这么做)。 下面,我们将预习基于类的替代方案,并在本章稍后讲述泊数属性,然后在第 18 章了解参 数和默认值参数的全部故事。为了帮助我们评判默认值参数在状态记忆上的优劣,下 一 小

节会给出翔实的介绍让我们启程。

注意:

当 一 个 class 嵌套在一 个 def 中时,闭包也可以披创建:外层由数的局部名称的值会被 类中的引用,或类中的 一 个方法函数所保存。参阅第 29 章获得关千嵌套类的更多信息。 我们在稍后的例子(比如,第 39 章的装饰器)里将看到,这样的代码中的外层 def 扮演 了 一 个类似的角色:它成了 一 个类工 广 ,并为嵌套类提供状态记忆 。

使用默认值参数来保存外层作用域的状态 在较早版本的 Python (2.2 版本之前)中,上 一节中的代码执行会失败,因为嵌套的 def 与

作用域

I

so3

作用域没有 一 点关系:下面的代码 f2 中 一 个变最的引用只会在 f2 的局部作用域、全局作

用域 (f1 函数以外)以及内置作用域中查找。因为它将跳过外层函数的作用域,从而引发

错误。为了解决这 一 问题,程序员 一 般都会将默认参数的值传递给(记住) 一 个外层作用 域内的对象:

def fl(): X = 88 def f2(x=x): print(x) f2()

# Remember enclosing scope X with defaults

fl()

# Prints 88

这种代码风格能在所有的 Python 版本中工作,而且你也仍会在 一 些现存的 Python 代码中 看到这样的例子。实际上,对千循环变噩这仍是必需的,我们马上就会明白,这就是今天

它值得我们学习的原因。简而言之,出现在 def 头部的 arg=val 语句表示参数 arg 在调用 时没有值传入进来的时候,默认会使用值 val 。这种语法用来显式地赋值外层作用域并使

其保存。 特别地,在这里的 f2 改进版中 X=X 意味着参数 x 会默认使用外层作用域中 x 的值,这是由 千第 二个 x 在 Python 进入内嵌的 def 之前就已经完成其求值,所以它仍将引用 f1 中的 x 。 实际上,默认值参数会记住 f1 中 x 的值:对象 88 。 上面这些都相当复杂,而且它完全取决千默认值进行求值的时刻。实际上,嵌套作用域查 找规则之所以加入到 Python 中就是为了让默认值参数不再扮演这种角色。如今, Python 会 自动记住所需要的外层作用域的任意值,从而能在内嵌的 def 中使用。

当然,对大部分代码而言,最好的处理方式就是直接避免在 def 中嵌套 def, 这会使程序 更加简单

在 Pythonic 的世界观里,扁平通常胜千嵌套。下面的代码就是前边例子的等

价形式,这段代码避免了嵌套在一起。注意,就像这个例子一样,在某一个函数内部调用 一个之后才定义的函数是可行的,只要第二个函数定义的运行是在第一 个函数调用前就行,

在 def 内部的代码直到这个函数实际调用时才会被求值。

» > def ft(): X = 88 f2(x)

# Pass x along instead of nesting

# Forward reference OK

>» def f2(x): print(x)

#

Flat is still often better than nested!

>» fl() 88 如果你采用这样的方法来避免嵌套,你几乎可以忘掉 Python 中嵌套的作用域概念。另一方面,

闭包(工厂)函数中内嵌的函数在现代 Python 代码中是相当普遍的,比如 lambda 函数一 它在 def 中几乎自然地嵌套地出现,且常常依赖千嵌套作用域层,正像下一节要解释的那样。

so4

I

第 17 章

嵌套作用域,默认值参数和 lambda 尽管我们今天可以看到 lambda 表达式在 def 中的使用越来越多,但是当你开始编码或阅读 它们时,你可能更需要关注嵌套函数作用域。我们之前简要介绍过 lambda, 但直到第 19

章才会深入学习它,简单地说,它就是一个表达式,生成之后会调用的一个新函数,与 def 语句很相似。不过由千它是一 个表达式,因此能用在 def 中不能使用的地方,例如,在列 表或是字典字面最之中。

像 def 一 样, lambda 表达式也为其所创建的函数引入新的局部作用域。多亏了嵌套作用域 查找层, lambda 能够看到所有它们所在函数中可用的变量。因此,以下的代码(我们之前 见过的工厂函数的 一个变种)能够运行,但仅仅是因为如今能够应用嵌套作用域规则了:

def func(): X =

4

action= (lambda n: x return action

**

# x remembered from enclosing def

n)

x = func () print(x(2))

# Prints 16, 4

在还没引入嵌套函数作用域的岁月里,

** 2

程序员需要借助默认值参数从上层作用域传递值给

lambda, 就像为 def 所做的那样 。例如 ,下面的代码对千所有版本的 Python 都可以工作:

def func(): X =

4

action= (lambda n, x=x: x return action

**

n)

#Passxinmanually

由千 lambda 是表达式,所以它们自然而然地(或者十分正常地)嵌套在 def 中。因此,它

们某些意义上成了后来在查找规则中新增外层函数作用域的最大初始受益者。在大多数情 况下,我们不再需要通过默认值参数给 lambda 传递值了。

循环变量可能需要默认值参数,而不是作用域 在已给出的规则中有一个值得注意的特例(这也是我介绍上面这种也许过时的默认值参数 技巧的原因) :如果在函数中定义的 lambda 或者 def 嵌套在一个循环之中,而这个内嵌函

数又引用了一个外层作用域的变批,该变量被循环所改变,那么所有在这个循环中产生的 函数会有相同的值一一也就是在最后一次循环中完成时被引用变量的值。在这样的情形中, 你必须仍然使用默认值参数来保存变量的当前值。 这可能是相当探奥的 一 种情况,但在实践中它比你想象中的更经常出现,尤其是在为一个

GUI 中大量的组件生成回调处理器函数的代码中 -一书iJ 如,在同 一 行内负责所有按钮的按 钮点击事件的处理函数。如果这些处理函数是在循环中创建的,你就需要十分小心地用默

认值参数来保存状态,否则所有按钮的回调最后可能会做相同的事情。

作用域

I

sos

这里举一个将这一现象浓缩到简单代码中的例子:下面的程序试图创建一个函数的列表, 其中每个函数都记住外层作用域中当前变显 1 的值:

>» def makeActions (): acts = [] for i in range(s): acts.append(lambda x: i ** x) return acts

# Tries ra remember each i #

But all remember same last i.l

>» acts = makeActions () >» acts[o]

不过,这并不能达成目标:因为外层作用域中的变蜇在嵌套的函数被调用时才进行查找,

所以它们实际上记住的是同样的值,也就是在最后一次循环迭代中循环变蜇的值。换句话说, 当在下面的所有调用中传人底数 2 时,我们从列表中的每个函数得到的结果都是 2 的 4 次方,

因为列表中所有这些函数的 1 值都是 4:

# All are 4 ** 2,

»> acts[o](2)

4=value 叶 last

i

16

»> acts[1](2)

#

This should be 1 ** 2 (1)

16

>» acts[2](2)

# This should be 2

** 2 (4)

16

»> acts[4](2)

# Only rhis should be 4

** 2 (16)

16 这是唯一 一 种还需要我们动用默认值参数来显式保持外层作用域中值的情况,而不是简单

地使用外层作用域的引用。也就是说,为了让这类代码能够工作,必须使用默认值参数来 传人当前外层作用域中的值。因为默认值参数的求值是在嵌套函数创建时就发生的(而不 是该函数之后被调用时),所以每一个函数记住了属于自己的变址 1 的值:

>» def makeActions (): acts= [] for i in range(s): acts.append{lambda x, return acts »> acts = makeActions() >» acts[o](2)

i式: i

** x)

# Use defaults instead # Remember current i

#0

** 2

# 1

** 2



»> acts[1](2) 1

»> acts[2](2)

#

2 ** 2

4

>» acts[4](2)

#4

**

2

16

这君上去是一种刻意而为且容易改变的编码实现,不过当你开始编写更大型程序时这也许

so6

I

第 17 章

就需要着重考虑了。我们会在第 l8 章中对默认值参数和第 l9 章中对 lambda 做更详细的介

绍,所以你之后也可能希望回过来重新复习这一小节的内容注 3. 任意的作用域嵌套 在结束这个话题之前,我们要注意作用域可以做任意的嵌套,但是当名称被引用时只会查

找外层函数 def 语句(而不是类,将在第六部分介绍)

>» def ft{): X

= 99

def f2{): def f3(): print(x)

# Found in JI's local scope!

f3{)

f2() >>> f1( )

99

Python 在完成当前函数局部作用域的查找后 ,会从内向外查找所有外层 def 的局部作用域, 再之后查找模块的全局作用域或内置模块。然而,这类代码在实际中却极少出现。再次提醒, 在 Python 中,我们说过“扁平胜千嵌套",这对千嵌套作用域闭包而言也是适用的。除了 在极少数的情境下,尽可能地少定义嵌套函数会让你和同事的生活变得更美好。

Python 3.X 中的 nonlocal 语句 上 一 小节中,我们介绍了即使外层函数已经返回,内嵌函数仍可以引用其外层函数作用域

中的变量的方式。事实上,在 Python 3.X (而不是 2.X) 中,我们还可以修改这样的外层作 用域变批,只需在一条 nonlocal 语句中声明它们。有了这条语句,内嵌的 def 就可以对外

层函数中的名称进行读取和写入访问。 nonlocal 语句通过提供可改写的状态信息,让嵌套 作用域闭包变得更加有用。

nonlocal 语句在形式和作用上都和前面介绍过的 global 语句很像。 nonlocal 和 global

一样,声明将要在外层作用域中修改的名称。和 global 的不同之处在千, nonlocal 作用 干外层函数的作用域中的一个名称,而不是所有 def 之外的全局模块作用域;而且在声明 nonlocal 名称的时候,它必须已经存在千该外层函数的作用域中,也就是说,它们只能现 成存在千外层函数中,而不能由内嵌 def 中的第 一 次赋值来创建。

注 3:

在“函数陷阱"一节中.我们也将看到对默认参数使用像列表和宇典这样的可变对象(例

如 def f(a= []))有一个类似的问题一—因为默认参数的实现附加于函数上的单独的对象, 可变的默认参数在调用之间记忆状态,而不是每一次调用都重新初始化 。 因人而异,这眈 可看作是另一种状态记忆的实现.也可以认为是 Python 语言的奇怪一隅,史多相关信息 请参阅笫 21 幸末尾 .

作用域

1

507

换句话说, nonlocal 既允许对外层函数的作用域中名称的赋值,又限制该名称的作用域查

找只能在外层 def 中进行。最终效果是为那些不想要或不需要带有属性、继承和多态行为

的类的使用场景,提供了一种更加可靠和直接的可更改状态信息的实现。

nonlocal 基础 Python 3.X 引入了一条新的 nonlocal 语句,它只在函数内部才有意义: def func(): nonlocal namel, name2,...

#

OK here

>» nonlocal X SyntaxError: nonlocal declaration not allowed at module level nonlocal 语句允许内嵌函数修改定义在语法上位千外层的函数的作用域中的 一个或多个

名称。在 Python 2.X 中,当一个团数 def 嵌套在另一个函数中,内嵌的函数可以引用外层 def 作用域中通过赋值定义的任何名称,但却不能修改它们。而在 Python 3.X 中,在 一 条 nonlocal 语句中声明嵌套的作用域,就使得内嵌的函数能对这些名称赋值,从而修改这些 变最值。 这使外层函数能够提供可写的状态信息,以便在随后调用嵌套函数时能记住这些信息。可 以修改状态对嵌套函数而言更加有用(例如,外层作用域中的一个计数器)。在 Python 2.X

中,程序员通常使用类或其他方案来实现类似的目标。由千内嵌函数已经成为实现状态记 忆的一种较为常见的编码模式,而 nonlocal 则使它能更广泛地应用。 nonlocal 语句除了允许修改外层 def 中的名称外,还会强制引用的发起,这点很像 global 语句, nonlocal 使得对该语句中列出的名称的查找从外层的 def 的作用域中开始,而不是

从该函数的局部作用域开始。也就是说, nonlocal 也意味着“完全略过我的局部作用域”。

实际上,当执行到 nonlocal 语句的时候, nonlocal 中列出的名称必须在一个外层的 def 中被提前定义过,否则将引发一个错误。最终效果和 global 很相似: global 意味着名称

位千外层的模块中, nonlocal 意味着名称位于外层的 def 中。 nonlocal 甚至更严格:作用 域查找被限定只在外层的 def 中。也就是说, nonlocal 名称只能出现在外层的 def 中,而 不能在模块的全局作用域中或 def 之外的内置作用域中。 nonlocal 的引入并没有改变通用的名称引用作用域规则;它们仍然像以前 一样工作,即遵

从前面所描述的 “LEGB" 规则。 nonlocal 语句的主要作用是能让外层作用域中的名称被 修改,而不仅仅是被引用。然而,在函数中使用 global 和 nonlocal 语句都在某种程度上

收紧甚至限制了查找规则:



global 使得作用域查找从外围模块的作用域开始,并且允许对那里的名称赋值。如果 名称不存在千该模块中,作用域查找将继续进入到内置作用域,但是,对全局名称的 赋值总是在模块的作用域中创建或修改它们。

508

1

第 17 章



nonlocal 将作用域查找限制为只在外层的 def 中,同时要求名称已经存在那里,井允 许对它们赋值。作用域查找不会继续进入到全局或内置作用域。

在 Python 2 . X 中,对外层 def 作用域名称的引用是允许的,但对其赋值却是禁止的。然而,

我们总是可以使用带有显式属性的类来实现与 nonlocal 相同且可更改的状态信息的效果 (并且在某些情境下,这么做可能会更好);全局变批和函数属性有时候也能实现类似的目的。 稍后我会更详细地介绍这一点,现在,让我们通过一些实际的代码来更具体地了解它们。

nonlocal 应用 下面这些例子都在 Python 3.X 中运行过。 Python 3.X 中对外层 def 作用域的引用与 Python

2.X 中的结果一样:在下面的代码中, tester 创建并返回函数 nested 以便随后调用,而 nested 中的 state 引用遵从常规的作用域查找规则来映射 tester 的局部作用域:

C:\code> c:\python33\python »> def tester(start): state= start def nested(label): print(label, state) return nested

#

Referencing nonlocals works normally

# Remembers state in en.closing scope

>» F = tester(o) >» F('spam') spam o »> F('ham') ham O 然而在默认情况下,不允许修改外层 def 作用域中的名称;这也是 Python 2.X 的 一 般情况:

>» def tester(start): state= start def nested(label): print(label, state) state+= 1 return nested

#

Cannot change by default (never in 2.X)

»> F = tester(o) » > F('spam') UnboundLocalError: local variable'state'referenced before assignment

使用 nonlocal 进行修改 现在,在 Python 3.X 下,如果在 nested 中把 tester 作用域中的 state 声明为 一 个 nonlocal, 我们就可以在 nested 函数中修改它 。即使通过名称 F 调用返回的 nested 函数时 , tester 已 经返回并退出了,这 也是有效的:

»> def tester(start): state= start

#

Each call gets its own state 作用域

1

509

def nested(label): nonlocal state print(label, state) state+= 1 return nested

>» F = tester(o) »> F('spam')

#

Remembers state in enclosing scope

# Allowed to change it if nonlocal

#

Increments state on each call

spam o »> F('ham') ham 1 »> F('eggs') eggs 2 通常使用外层作用域引用时,我们可以多次调用 tester 工厂(闭包)函数,以便在内存中

获得其状态的多个副本。外层作用域中的 state 对象本质上被附加到了返回的 nested 函数 对象中,每次调用都产生一个新的、单独的 state 对象,以至千更新一个函数的 state 不

会影响到其他的。如下代码继续前面的交互式命令行程序:

>» G = tester(42) »> G('spam')

# Make a new tester that starts at 42

spam 42

»> G('eggs') eggs 43

# My state i11formation updated to 43

>» F('bacon') bacon 3

# But F's is where it left off: at 3 # Each call has different state information

从这层意义上讲, Python 的 nonlocal 变量比某些其他语言中典型的函数局部变量更加函数 式:在闭包函数中, nonlocal 是基千调用的、多副本的数据。

边界情况 尽管 nonlocal 变量很有用,也需要注意它们的一些微妙之处。首先,和 global 语句不同, 当执行一条 nonlocal 语句时, nonlocal 名称必须已经在一个外层 def 作用域中被赋值过, 否则会得到一个错误,也就是说,你不能通过赋值动态地在外层作用域中创建一个新的

nonlocal 名称。 事实上, nonlocal 名称在外层或内嵌函数被调用之前就已经在函数定义的 时候被检查了:

»> def tester(start): def nested(label): nonlocal state state= o print(label, state) return nested

#

Nonloca/s must already exist in enclosing def!

SyntaxError: no binding for nonlocal'state'found

»> def tester(start): def nested(label): global state s10

I

第 17 章

# Clo加ls

don't have to exist yet when declared

state= o print(label, state) return nested

# This creates the name in the module now

» > F = tester(o) »> F{'abc') abc o >>> state 。

其次, nonlocal 将作用域查找限制为只对外层的 def1 nonlocal 名称不会在外围模块的全 局作用域或所有 def 之外的内置作用域中查找,即便这些作用域中存在相同的名称:

>» spam = 99 >» def tester(): def nested(): nonlocal spam print('Current=', spam) spam+= 1 return nested Syntax 丘 ror:

# Must be in a def not the module!

no binding for nonlocal'spam'found

一且你意识到 Python 通常不能确定在哪个外层作用域中创建一个全新的名称,这些限制就

有意义了。在前面的程序中, spam 应该在 tester 中赋值,还是在外围的模块中赋值?由 f 这是有二 义性的, Python 必须在函数创建的时候就解析 nonlocal, 而不是在函数调用的 时候。

为什么选 nonloca巨状态保持备选项 考虑到嵌套函数引入的额外复杂性,你可能会为此感到烦恼。尽管很难在我们的小示例中 看到这点,但在很多程序中,状态信息是非常重要的。虽然函数可以返回结果,但它们的

局部变橇 一 般不会保留,而我们有时需要让这些值在调用之间被保持。此外桉照不同的使 用场景,其他 一 些应用程序又要求这些值不同。 如前所述, Python 中有各种不同的方式来”记住"跨函数和方法调用的信息。尽管各有利弊,

nonlocal 确实增强了对外 层作用域的引用 : nonlocal 语句允许在内存中保持可更改状态的

多个副本。它解决了那些类和全局变批不需要或适用的简单状态记忆蒂求,不过函数属性 经常能够可移植性更强地扮演相似的角色。让我们回顾这些状态记忆备选项,来看看它们

是如何一步步发展起来的。

nonlocal 变量的状态:仅适用千 Python

3.X

正如我们在前面小节所看到的,如下的代码允许在外层作用域中保持和修改状态。对

作用域

I

s 11

tester 的每次调用都会创建一个自包含的可变信息包,这些可变信息的名称不会与程序的

任何其他部分产生冲突:

»> def tester(start): state= start def nested(label}: nonlocal state print(label, state) state+= 1 return nested

#

Each call gets its own state

#

Remembers stale in enclosing scope

#

Allowed to change it if nonlocal

»> F = tester(o) »> F('spam')

# State visible within closure only spam o »> F.state AttributeError:'function'object has no attribute'state'

只有当变量必须改变时我们才需要将其声明为 nonlocal (其他外层作用域名称的引用照例 被自动保留),而且 nonlocal 名称在外层函数外面是不可见的。 遗憾的是,这段代码只能在 Python 3.X 中工作。如果你在使用 Python 2.X, 可以根据不同

的目标选择其他方案。下面 三 个小节会介绍 一 些替代方案。这些小节中的 一 些代码使用了 我们目前还没介绍过的工具,你也可以把这看作是一种预习。不过这里我们会让例子保持

简单,这样你可以在学习过程中进行比较和对照。

全局变量的状态:只有一份副本 在 Python 2.X 和较早版本中实现 nonlocal 效果的一种常见方式,就是直接把状态移到全

局作用域外(外围的模块)

»> def tester(start): global state state= start def nested(label): global state print(label, state) state+= 1 return nested

»> F = tester(o) »> F{'spam')

# Move it out to the module to change it # global allows changes in module scope

# Each call increments shared global state

spam o »> F('eggs') eggs 1 在这个例子中,这是有效的,但它需要在两个函数中都使用 global 声明,并且有可能引起 全局作用域中的名称冲突(万一 ”状态”已经被使用了呢?)。更糟糕但更为微妙的问题是, 它只允许模块作用域中保存状态信息的单个共享副本一如果再次调用 tester, 将会重置 模块的 state 变量,以至于先前的调用将会看到自己的 state 被覆 盖 :

s12

I

第 17 章

>» G = tester(42) >» G('toast')

# Resets state's single copy in global scope

toast 42

»> G('bacon') bacon 43 » > F('ham') ham 44

# But my counter has been overwrillen!

如前所述,当你使用 nonlocal 和嵌套函数闭包而不是 global 的时候,对 tester 的每次调 用都会拥有属千自己的 state 对象的单独副本。

类的状态:显式属性(预习) 在 Python 2.X 和较早版本中针对可变状态信息的另 一 种方式是使用带有属性的类,从而让 状态信息的访问比隐式的作用域查找规则更明确。作为 一 个附加的优势,类的每个实例都

会得到状态信息的 一 个新副本,这是 Python 的对象模型的 一 个天然的副产品。类同时也支 持继承、多态行为和其他工具。 我们还没有详细介绍过类,作为一个以对比为目标的简要的预习,下面的代码采用类来重

新实现之前的 tester/nested 函数,它们被创建时在对象中显式地记录状态。为了理解这

段代码的意义,你需要知道像这样的 一 个 class 中的 def 与一个普通的 def 在功能上完全 一样,除了函数的 self 参数会自动接收隐式的调用主体之外(通过调用类自身创建的一 个

实例对象)。当类被调用时,名为_init_的函数将自动运行:

>» class tester: def _init_(self, start): self.state= start def nested(self, label): print(label, self.state) self. state += 1

>» F = tester(O) >» F. nested('spam')

# Class-based alternative (see Part VI) # O n object construction, # save state explicitly in new object

# Reference slate explicitly # Changes are always allowed # Create instance, invoke

— inil_

# Fis passed 10 self

spam O

»> F.nested('ham') ham 1 在类中,无论属性需要改变还是仅仅被引用,我们都会显式地保存每个属性,而且它们在 类外部也是可访问的。同嵌套函数和 nonlocal 一样,类支持所保存的数据存在多个副本:

>» G = tester(42) >» G.nested('toast')

# Each instance gets new copy of state # Changing one does not impact others

toast 42 >» G. nested('bacon') bacon 43

作用域 I

s13

>» F.nested('eggs') eggs 2 >»F.state 3

II F's state is where ir left off II State may be accessed outside class

在本书后面将更深入地介绍,这里我们只需略施小计,就可以使用运算符重载把类对象用作 可调用函数。_call_拦截了一个实例上的直接调用,因此我们不衙要调用一个命名的方法:

>» class tester: def _init_(self, start): self.state = start def _call_(self, label}: print(label, self.state) self.state+= 1 »> H = tester(99) >>> H('juice') juice 99 » > H('pancakes') pancakes 100

# lnrercept direct i11s1ance ca/ls #So.nested() 1101 required

II /11vokes _call_

目前,你不必急于理解这段代码中的细节;它大体上只是 一 次预习,用千和闭包进行 一 般 性比较。我们将在第六部分探入探讨类,井在第 30 章看到_call_这样的特定运贷符重载 工具。这里要提醒的关键点是类可以让状态信息更明显,通过利用显式属性赋值而不是隐 式作用域查找。此外,类属性总是可更改的,并且不要求使用 nonlocal 语句。类具有的可

扩展性能实现带有更多属性和行为的更为丰富的对象。 尽管使用类存储状态信息通常是可以遵从的经验规则,在这种状态只是一 个计数器的情况

下,它们也可能有些大材小用了。这样的简单状态情况比你想象得要更常见,在这种情境下, 嵌套的 def 有时候比编写类要更轻量级,尤其是如果你还不熟悉 OOP 的话。此外,还有一

些场景下,嵌套的 def 可能实际上比类表现得更好

继续阅读第 39 章中对方法装饰器的

介绍,那里有一个示例,它大大超出了本章学习的范围!

函数属性的状态: Python 3.X 和 Python 2.X 的异同 作为 一 种可移植且常常更简单的状态记忆备选项,我们有时可以使用函数属性(用户定义

的直接附加给函数的名称)来达到与 nonlocal 相同的效果 。当你给外层工厂函数生成的

内嵌函数附加用户定义的属性时,它们也可以作为基千调用、多副本和可写的状态,就同 nonlocal 作用域闭包和类属性一样。这样的用户定义的属性名称不会与 Python 的内 置名称 冲突,而且与 nonlocal 一 样,只对千必须改变的状态变扯时才有必要使用;其他的作用域 引用会正常地保留和工作。 关键是,函数属性与类 一样是版本间可移植的,而不同千 nonlocal, 函数属性在 Python 2.X 和 Python 3.X 中都能使用。事实上,它们自从 2.1 版本后就可用了,远远早千 3 . X 版本的 nonlocal 。因为不管怎么样,工厂函数在每 一 次调用时都会创建 一 个新的函数,这并不要

s14

I

第 17 章

求额外的对象:新的函数属性成为调用的状态,这同 nonlocal 变量的工作方式非常相似, 同样和内存中生成的函数有关联。 此外,函数屈性允许状态变显从内嵌函数的外部被访问,就像类属性那样;而如果使用 nonlocal 、状态变址只能在内嵌的 def 内部被直接看到。如果你需要在外部访问一个调用

计数器,在这 一 栈型下就只是 一 个简单的函数属性获取。 这里给出了最后这种基千该技术的示例一它通过附加给内嵌函数的属性替代了 nonlocal 。 这种方式在有些人第一 眼看来可能不那么符合直觉;通过函数名而不是简单的变釐来访问 状态,然后必须在内嵌的 def 之后初始化。但是,它的可移植性会好很多,允许从内嵌的 函数之外访问状态变批,并且无须再多写 一 行 nonlocal 声明:

»> def tester(start): def nested(label): print(label, nested.state) nested.state+= 1 nested.state= start return nested

nested is in e11closi11g scope II Change artr, 1101 nested itself # Initial state after June defined If

>» F = tester(o) »> F('spam')

#

spam o »> F('ham') ham 1 »> F.state

# Can access slate out泣 idefunctions too

Fis a'nested' with state al/ached

2

因为对外层函数的每一 次调用都产生一 个新的内嵌函数对象,这 一 方案与 nonlocal 闭包和

类 一样,能支持基千调用的多副本可更改数据,这也是全局变批所不能提供的使用模式:

»> G = tester(42) » > G('eggs')

# G

has own state, doesn't overwrite F's

eggs 42 » > F('ham') ham 2

»> F.state

# State is accessible und per-call

3

»> G.state 43



F is G

#

Different ftmction objectl

False 这段代码依赖千一 个事实:函数名 nested 是包围 nested 的 tester 作用域中的一个局部变 最`因此,它可以在 nested 内自由地引用。这段代码还依赖千这样 一 个事实:在原位置修

改一 个对象井不是给 一 个名称赋值;当它自增 nested.state 时,它是在修改对象 nested 引用的 一部分,而不是 nested 名称本身。由于我们不是要真的在外层作用域内给 一 个名称 赋值,因此不衙要 nonlocal 声明。

作用域

I

s, s

Python 3.X 和 2.X 都支持函数属性;我们将在第 19 章进 一 步探索它们。重要的是,我们将 看到 Python 在 2.X 和 3 . X 中使用了同 一 命名惯例,从而保证作为函数属性所赋值的任意名

称不会同内部实现相关的名称冲突,这使得命名空间等价于 一 个作用域。先抛开主观因素, 函数属性的作用确实同 3.X 中较新的 nonlocal 相重叠,这使得后者从技术上来讲是多余的, 可移植性也更差。

可变对象的状态:来自 Python 历史中的隐蔽幽灵? 作为 一 个相关的提示,在 2.X 和 3.X 版本中不声明其名称 nonlocal 而改变外层作用域中的 一 个可变对象也是可能的。例如,下面的代码能和上一个版本一 样工作,具有相同的可移 植性并提供基千调用的可更改状态:

def tester(start): def nested(label): print(label, state[o]) state[o] += 1 state= [start] return nested

# #

Leverage in-place mutable change Extra syntax, deep magic?

这里利用了列表的可变性,而且与函数属性一 样依赖于原位置对象修改不会将一 个名称归

类为局部这一 事实。不过,这也许比函数属性或是 3.X 的 nonlocal 更加隐晦,而且这也是 一 种比函数属性还要早的技术,今天似乎处千从聪明的骇客到黑魔法的连续谱中的某个地 方!你应当尽量使用带名字的函数属性而不是列表和数值偏移盐,尽管你可能会在要用到 的代码中见到它。 总结一下:全局变量、非局部变扯、类和函数属性都提供了可更改的状态记忆的备选项。

全局变最只支持单副本的共享数据,非局部变量只能在 3.X 中被改变;类需要你掌握 OOP 的基本知识,类和函数属性都提供版本间可移植的解决方案,同时它们也允许从持有状态

的可调用的对象自身的外部直接访问状态。同理,对你的程序来说,最好的工具取决千你 的程序的目的。

我们将在第 39 章 一个更为实际的上下文中回顾这里介绍的所有状态备选项,并介绍装饰器 ( 一 个天生就与多级状态记忆相关的工具)。在选择状态备选项时需要考虑一 些额外因素(例 如,性能),这里受篇幅限制将不再探索它们(我们将在第 21 章学习如何测试代码运行速 度)。目前,是时候继续前进探索参数传递模式了。

请留意:定制 open 作为另一个实际中的闭包实例,考虑将内置的 open 调用改为一个定制的版本,如同 本章开头的边栏“打破 Python 2.X 的小宇宙”里建议的那样 。 如果定制版本需要调用 原始版本,必须在改变之前保存它以便稍后恢复,这是一个经典的状态记忆情境 。 此外,

s16

I

第 17 章

如果我们想让同一个函数支持多个用户化定制,全局变量是行不通的,因为我们需要 基于客户的状态。

下面的代码(位于文件 makeopen.py 中,针对 Python 3.X 编写)是实现这个目标的一 种方式(在 2.X 中,改变内置作用域名称并打印)。它使用了嵌套作用域闭包来记住 一个稍后将使用的值,而没有使用类,因为类可能需要比这里更多的代码·

import buil tins def makeopen(id): original= builtins.open def custom(*kargs, **pargs): print('Custom open call %r:'% id, kargs, pargs) return original(*kargs, **pargs) builtins.open = custom 为了对进程中的每一个模块改变 open, 这段代码在外层作用域内保存了原始版本(以 便后面可以调用它)之后,在内置作用域中将其重新赋值为一个使用嵌套 def 编写的 定制版本 。 这段代码也可以作为预习,因为它带依赖星号的参数形式,用于收集和稍

后肝包为 open 设计的任意位置的关键词参数(这是下一章会出现的主题)。不过, 这里的主要魔法是嵌套作用域闭包:旧作用域查找规则找到的定制化 open 保留了原 始版本以供接下来使用:

>>> F = open('script2. py') # Call built-in open in builrins »> F.read() 'import sys\nprint(sys.path)\nx = 2\nprint(x ** 32)\n'

» > from makeopen import makeopen » > makeopen('spam')

# Import open resetter function # Custom open calls built-in open

»> F = open('script2.py') # Call custom open in builtins Custom open call'spam': ('script2.py',) {} »> F.read() 'import sys\nprint(sys.path)\nx = 2\nprint(x ** 32)\n' 因为每一次定制过程都在自己的外层作用域中记住了上一个内置作用域版本`它们甚 至能以全局变量不支持的方式自然地被嵌套~对 makeopen 闭包函数的每一次调用都 会记住它自己版本的 id 和 original, 所以多重定制化过程也可能运行:

» > makeopen('eggs') >» F = open('script2.py')

# Nested customizers work roof #Becauseeachretainsownstate

Custom open call'eggs': ('script2.py',) {} Custom open call'spam': ('script2.py',) {} »> F.read() 'import sys\nprint(sys.path)\nx = 2\nprint(x ** 32)\n' 同样,我们的函数简单地增加了追踪内置函数的可能的嵌套调用,但是常规的技巧也 会有其他方面的应用 。一投基于类的等价代码可能需要更多的代码量,因为它需要在

作用域

I

s17

对象属性中显式地保存 id 和 original 的值 。 不过这也需要比我们目前所掌握的史多 的背景知识,所以就把下面的代码当作是笫六部分的预习吧·

import builtins class makeopen: If See Parr VI: call catches self() def _init_(self, id): self.id= id self.original= builtins.open builtins.open = self def _call_(self, *kargs, **pargs): print('Custom open call %r:'% self.id, kargs, pargs) return self.original(*kargs, **pargs) 这里需要提醒的一点是,类会史加明确,但在状态记忆是唯一目标时也会需要额外的

代码 。 我们之后会见到史多的闭包使用案例,尤其是在笫 39 章探索装饰器时,那里 我们会发现在扮演某些特定角色时,闭包实际上比类更受欢迎 。

本章小结 这一 章,我们学习了与函数相关的两个关键概念中的第 一 个:作用域,它决定了变量在使 用时是如何被查找的。正如我们所学的那样,变量成为其赋值所在的泊数定义的局部变最, 除非它们被特定地声明为全局变量或非局部变批。我们也探索了 一些更高级的作用域概念,

包括嵌套函数作用域和函数属性。最后,我们学习了一些通用的设计理念,例如,避免使 用全局变匮和进行跨文件间的修改。

在下 一章中,我们将继续介绍函数,讲解与函数相关的第 二 个重要概念,即参数传递。你 将发现,参数通过赋值传递给一 个函数,但 Python 也提供了工具允许函数灵活地确定如何 传递其各项的值。在继续学习之前,让我们来做做本章的习题,回顾在这里已经介绍过的 作用域概念 。

本章习题 l.

下面的代码会输出什么?为什么?

»> X ='Spam' »> def func(): print(X)

»> func() 2

下面的代码会输出什么?为什么?

» > X ='Spam'

»> def func():

518I

第 17 章

X ='NII'

>» func() »> print(X) 3.

下面的代码会打印什么内容?为什么?

» > X ='Spam' >» def func(): X ='NI'

print(X)

»> func() » > print(X) 4

下面的代码会输出什么?为什么?

>» X ='Spam' »> def func(): global X X ='NI'

>» func() »> print(X) 5

下面的代码会输出什么?为什么?



X ='Spam'

>» def func(): X ='NI'

def nested(): print(X) nested()

>» func() »> X 6.

这段代码在 Python 3.X 下会输出什么?为什么?

»> def func(): X ='NI'

def nested(): nonlocal X X ='Spam'

nested() print(X)

»> func() 7.

举出 三种或更多在 Python 函数中保持状态信息的方式。

作用域

I

s19

习题解答 ).

这里的输出是 'Spam' ,因为函数引用的是外围模块中的全局变址(因为不是在由数中 赋值的,所以被当作全局变量)。

2

这里的输出也是 'Spam' ,因为在函数中赋值变最会将其变成局部变量,从而隐藏了同

名的全局变扯。 print 语句会找到没有发生改变的全局(模块)作用域中的变批。

3.

这会在一行上打印 'NI' ,在另一行打印 'Spam' ,因为函数中引用的变量会找到其局部 变批,而 print 中引用的变量会找到其全局变量。

4

这次只打印了 'NI' ,因为全局声明会强制函数中赋值的变量引用其外围的全局作用域

中的变量。

5.

这个例子的输出还是 'NI' 一行,' Spam' 在另一行,因为内嵌函数中的 print 语句会在 外层函数局部作用域中发现变批名,而末尾的 print 会在全局作用域中发现这个变量。

6

这个示例打印出 'Spam ' ,因为 n onlocal 语句 (Python 3.X 中可用,但 Python 2.X 中 不可用)意味着在内嵌函数中对 X 赋值,将修改外层函数局部作用域中的 X 。没有这条

语句,这个赋值会把 X 当作是内嵌函数的局部变批,使它成为一个不同的变最,那么 这段代码会打印出 'NI' 。

7

尽管函数返回的时候局部变最的值已经不在了,我们可以使用共享的全局变最、内嵌 函数中对外层函数作用域的引用,或者使用默认值参数来让一个 Python 函数保持状态 信息。函数属性有时候允许把状态附加到函数自身,而不是在作用域中查找。另一种

替代方式是使用类和 OOP ,有时候比其他任何基千作用域的技术更好地支持状态记忆, 因为它通过属性赋值而变得显式,我们将在第六部分介绍这一备选项。

s20

I

第 17 章

第 18 章

参数

第 17 章介绍了 Python 的作用域背后的细节一即代码中定义和查找变量的位置。正如我

们所学过的,在代码中一个变批名定义的位置很大程度上决定了它的含义。本章通过学习 Python 中的参数传递的概念继续介绍函数一也就是对象作为输入传递给函数的方式。正

如我们将看到的,参数 (argument, 也称为 parameter) 被赋值给一个函数中的名称,但是, 它们更多地与对象引用相关,而不是与变量作用域相关。我们还将介绍 Python 所提供的额

外工具,如关键字参数 、 默认值参数和任意参数收集器和提取器,它们赋予了参数传递给 函数的方式以更广泛的灵活性,我们在本章的讲述中将举例说明。

参数传递基础 本书在前面曾提过参数是通过赋值来传递的。对千初学者来说,这点稍有些混乱而不够清晰, 因而我会在这一 节中详细阐述。下面是给函数传递参数时的一些简要的关键点:



参数的传递是通过自动将对象赋值给局部变量名来实现的。函数参数(调用者传递的(可 能的)共享对象的引用)在实际中只不过是 Python 赋值的又 一例子而已。因为引用是 以指针的形式实现的,所有的参数实际上都是通过指针传入的。作为参数被传递的对 象从来不会自动复制。



在函数内部赋值参数名不会影响调用者。在函数运行时,函数头部的参数名是一个新的、 局部的变量名,这个变量名是在函数的局部作用域内的。函数参数名和调用者作用域 中的变批名是没有关联的。



改变函数的可变对象参数的值也许会对调用者有影响。换句话说,因为是直接把传入 的对象赋值给参数,函数能够原位置改变传入的可变对象,因此结果可能会影响调用者。 可变参数可以作为函数的输入和输出。

521

更多关千引用的细节请参看第 6 章。我们在第 6 章中所学的关千引用的知识也适用千函数 参数,虽然对参数名的赋值是自动并且隐式的。

Python 的通过赋值传入的机制与 C++的引用参数作法井不完全相同,但是在实际中,它与 C 和其他语言的参数传递模式相当相似。



不可变参数本质上传入了“值"。像整数和字符串这样的对象是通过对象引用而不是 复制传入的,但是因为你无论如何都不可能在原位置修改不可变对象,最终的效果就 像创建了 一份 副本。



可变对象本质上传入了”指针"。类似千列表和字典这样的对象也是通过对象引用传

人的,这一点与 C 语言使用指针传递数组很相似:可变对象能够在函数内部做原位置 的改变,这与 C 的数组很像。

当然,如果你从来没有使用过 C 也能很容易地理解 Python 的参数传递模式:它只是把对象 赋值给变量名,井且无论可变对象或不可变对象都是如此。

参数和共享引用 为了说明参数传递在实际应用中的特性,考虑如下的代码:

»> def f(a): a= 99

# a is assigned to (references) the passed object # Changes local variable a only

»> b = 88 >» f(b) >» print(b)

# a and b borh reference same 88 initially # b is not changed

88 该例中,在使用 f(b) 调用函数的时候,变量 a 被赋值了对象 88, 但是, a 只存在干被调用 的函数之中。在被调函数中修改 a 对千主动调用由数的地方没有任何影响;它直接把局部 变量 a 重置为一个完全不同的对象。

这就是变量名没有别名关联的含义,也就是说对函数内的 一 个参数名的赋值(例如, a=99) 不会神奇地影响到主调函数作用域中 b 这样的 一个变量。参数名称初始可能共享传 递的对象(它们本质上是指向这些对象的指针),但只是临时的,也就是在由数刚开始被 调用的时候。只要对参数名重新赋值,这种共享关系就结束了。

至少,这是对参数名称自身赋值的情况而言的。当参数袚传人了像列表和字典这样的可变 对象时,我们还需注意到对这一类对象的原位置修改可能在函数退出后依然有效,并由此

影响到调用者。下面是一个展示上述行为的例子:

s22

»> def changer(a, b):

# Arguments assigned references ro

a = 2

# Changes local name's value only

I

第 18 章

ob_」ecr:,

b[o] ='spam'

# Changes shared object in place

»> X = 1 »> L = [1, 2] »> changer(X, L) »> X, L

# Caller: # Pass immutable and mutable objects # X is unchanged, Lis different!

(1, ['spam', 2]) 在这段代码中, changer 函数给参数 a 赋值,并给参数 b 所引用对象的 一 部分赋值。这两 个函数内的赋值从语法上讲相差不大,但是从结果上看却大相径庭:



因为 a 是在函数作用域内的局部变量,第 一 个赋值对函数调用者没有影响,它仅把局 部变量 a 修改为引用 一 个完全不同的对象,而并没有改变调用者作用域中的名称 X 的

绑定。这和之前的例子是相同的。



参数 b 也是一 个局部变摄名,但是它被传人了 一个可变对象(在调用者作用域中名为 L 的列表)。因为第二 个赋值是 一 个原位置发生的对象改变,对函数中 b[O] 赋值的结果

会在函数返回后影响 L 的值。

实际上, changer 中的第 二条赋值语句没有修改 b, 因为我们修改的是 b 当前所引用的对象 的 一 部分。这种原位置修改,只有在被修改对象的生命周期比函数调用更长的时候,才会 影响到调用者。名称 L 也没有改变,也就是说它仍然引用着同 一 个修改后的对象 。 但是, L 在调用后看上去有所变化,因为它引用的值已经在函数中被修改过了。从函数调用的效

果上来看,列表名 L 同时充当了函数的输入和输出 。 图 18-1 展示了在函数被调用后和函数代码运行前的这个瞬间,变量名/对象之间的绑定关系。

变量名

对象

调用考

函数

图 18-1: 引用:参数。因为参数是通过赋值传递的,函数中的参数名可以在调用肘与调用者作 用域中的变量实现共享对象。因此,在囡数中原位詈修改可变对象参数能够影晌调用者。这里,

因数中的 a 和 b 在囡数一开始调用肘与变量 X 和 L 引用了相同的对象。通过变量 b 对列表的修 改,在滔数调用返回后使 L 发生了改变

参数

I

523

如果这个例子还是令人困惑的话,你也可以这样理解,对传入参数的自动赋值的效果等价

于运行一系列简单赋值语句。对千第一个参数,参数赋值对千调用者来说没有什么影响:

»> X = 1 »> a = X »> a = 2 >» print{X)

# They share the same object

# Resets'a'only,'X'is still 1

1

但是对第 二 个参数的赋值就会影响参与调用的变量,因为它是原位置修改:

>» »> »> »>

L = [1, 2]

b = L b[o] ='spam' print{L} ['spam', 2]

# They share The same object #

In-place change:'L'sees the change too

在第 6 章和第 9 章中对于共享可变对象的讨论可以说明这种现象:对可变对象的原位置修 改会影响该对象的其他引用。这里,实际效果表现为一个参数能同时作为函数的 一个输入 和输出。

避免修改可变参数 对可变参数的原位置修改的行为不是 一个错误,因为它只是参数传递在 Python 中工作的方 式,并且在实际中应用广泛。在 Python 中,默认通过引用传入函数的参数,因为这通常也 是我们所希望的。这意味着不需要创建多个副本就可以在我们的程序中传递较大的对象,

并且能够按照需求很方便地更新这些对象。实际上,正如我们将在本书第六部分看到的, Python 的类模型依赖千原位置修改一个传入的 "self" 参数,从而更新对象状态。

然而,如果我们不希望函数内部的原位置修改影响到传入的对象,也可以像第 6 章学过的 那样直接创建 一个显式的可变对象的副本。对千函数参数,我们总能通过 Python 3 . 3 中的

list 、 list.copy 或者无参数切片,在调用时复制列 表 :

L = [1, 2] changer(X, L[:])

# Pass a copy, so our'L'does not change

如果不想在函数运行的过程中改变传入对象,我们同样可以在函数内部进行复制:

def changer(a, b): b = b[:]

a

#

Copy input list so we don't impact caller

= 2

b[o] ='spam'

# Changes our list copy only

这两种复制机制中并不阻止函数修改对象,但它们都避免让这些修改影响到调用者。为了 真正防止改变,我们总能把可变对象转换为不可变对象来杜绝这种问题。例如,元组在试 图改变时会抛出 一 个异常:

s24

I

第 18 章

L = [1, _2] changer(X, tuple(L))

·

#

Pass a IUple, so changes are errors

这种方案使用了内置的 tuple 函数,可以用 一 个序列(实际上是任意的可迭代对象)中的 所用元素创建一个新的元组。这种方法从某种意义上说有些极端:因为它强制函数披编写

为绝不改变传人的参数,从而给函数加上了比原本更多的限制,所以通常应该避免这么做(或 许将来你会发现对千一 些调用来说改变参数是很有用的 一 件事)。使用这种技术还会让函 数丧失对参数使用列表方法的能力,包括那些并不会在原位置修改对象的方法。 这里最需要记住的就是,函数能够改变传入的可变对象(例如,列表和字典)。这不会是 一 个麻烦,并且有时候这对千某些用途很有用处。此外,原位置修改传入的可变对象的由数, 可能是有意而为的,例如,修改可能是 一 个定义良好的 API 的 一 部分,而我们不应该通过

创建复制来违反该 API 。 然而,你也必须意识到这个特性:如果在你没有预期的情况下外部对象发生了改变,检查 一 下是不是由某个被调用的函数引起的,并且有必要的话在传入对象时创建复制。

模拟输出参数和多重结果 我们已经讨论了 return 语句并在多个例子中使用了它。这里有另 一种使用该语句的方式:

因为 return 能返回任意种类的对象,所以它也能返回多个值,做法是将这些值封装进一个 元组或其他的集合类型中。实际上,尽管 Python 不支持 一 些其他语言所谓的”按引用调用“

的参数传递,我们 一 般能通过返回元组并将结果赋值给调用者中原有的参数变显名来模拟 这种行为:

»> def multiple(x, y): X

= 2

y = [3, 4) return x, y

»> X = 1 »> L = (1, 2] >» X, L = multiple(X, L) >» X, L

# Changes local names only # Return multiple new values in a tuple

# Assign results to caller's names

(2, (3, 4]) 看起来这段代码好像返回了两个值,但是实际上只有一个 : 一 个包含有 2 个元素的元组,

这里省略了它可选的圆括号。在调用返回后,我们能使用元组赋值解包被返回元组的各部分。 (如果你忘记了为什么可以这么做,回顾一下第 4 章、第 9 章的“元组”以及第 11 章的"赋

值语句“。)这种代码模式的最终效果就是通过显式赋值既传回了多个结果,又模拟了其 他语言中的输出参数。 X 和 L 在调用后发生了改变,但这也是符合代码预期的。

参数

I

525

注意:在 Python 2.X 中解包参数:前面的例子用元组赋值解包了由数返回的元组。在 Python 2.X 中,也可以在传递给函数的参数中自动解包元组。在 Python 2.X (仅限千)中,通过如

下头部定义的一个函数:

def f((a, (b, c))): 可以用与期望的结构相匹配的元组来调用: f((1, (2, 3) ))分别将 a 、 b 和 c 赋值为 1 、 2 和 3 。当然,传入的元组也可以是 (f(T) ) 调用前就已经创建好的一个对象。这种 def 语法在 Python 3.X 中已经不再支持。作为替代,你可以像下面这样编写该函数:

def f(T): (a, (b, c))

=

T

以便在一条显式赋值语句中完成解包。这种显式形式在 Python 3.X 和 Python 2.X 中都有

效。参数解包在 Python 2.X 中据说是一个含 糊并且很少用到的功能(除了那些用到的代 码)。此外, Python 2.X 中的函数头部只支持元组形式的序列赋值;更 一般 的序列赋值 (例如, def f((a,

[b, c] )):)在 Python 2.X 中也会因语法错误而无效,而在 Python 3.X

强制为必须用显式赋值形式。相反,在调用中的任意序列都能成功地与函数头部的元组

匹配(比如, f((1, [2, 3] ))和 f((1, "ab"))) 。 元组解包参数语法在 Python 3.X 的 lambda 泊数参数列表中也是不允许的:参阅本书第

20 章中边栏“请留意:列表推导和 map” 给出的一个 lambda 解包的例子。有些不对称的是, 元组解包赋值在 Python 3.X 的 for 循环对象中仍然是自动化的,参阅第 13 章的例子。

特殊的参数匹配模式 如前所述,参数在 Python 中总是通过赋值传入的。传入的对象袚赋值给了在 def 头部的变 扯名。而在这一模型的上层, Python 提供了额外的工具,来改变调用中参数对象和头部的 参数名的配对关系。这些工具都是可选的,但是它们让我们可以编写支持更灵活的调用模 式的函数,同时你可能会遇到一些用到这些工具的库。 在默认情况下,参数按照从左到右的位置进行匹配,而且你必须精确地传入和函数头部参

数名一样多的参数。然而,你也能通过形式参数名、提供默认的参数值以及对额外参数使 用容器三种方法来指定匹配。

参数匹配基础 在学习语法细节之前,我需要强调一下,这些特殊模式是可选的,并且只能解决变品名与 对象的匹配问题;匹配完成后在传递机制的底层依然是赋值。事实上,这些工具对千编写

代码库的人来说,要比对应用程序开发者更有用。尽管你自己不会用到这些模式,但是你 也有可能碰上它们,因此这里列出 一个关千匹配模式 的大纲。

s26

I

第 18 章

位置次序:从左至右进行匹配

这是一般情况,也是我们目前为止最常用的方法,即通过位置进行匹配把参数值传递 给函数头部的参数名称,匹配顺序为从左到右。 关键字参数:通过参数名进行匹配

此外,调用者可以通过在调用时使用参数的变量名,即使用 name=value 这种语法,来 指定函数中的哪个参数接受某个值。 默认值参数 : 为没有传人值的可选参数指定参数值 如果调用时的传入值过少的话,函数自身能够为参数指定接受的默认值,这将同样用

到语法 name=value. 可变长参数 (Varargs) 收集:收集任意多的基千位置或关键字的参数

泊数能够用 一 个星号“*”或两个星号“**“开头的特殊参数,来收集任意多的额外参数。 这个特性常常叫作可变长参数,借鉴自 C 语言中的可变长度参数列表工具;在 Python 中,

参数被收集到一个普通的对象中。 可变长参数解包:传人任意多的基千位置或关键字的参数 调用者也能使用*语法去将参数集合解包成单个的参数。这个“*”与在由数头部的“*”

恰恰相反:在函数头部的“*”意味着收集任意多的参数,而在调用者中意味着解包任 意多的参数并将它们作为离散的值单独传入。 keyword-only 参数:必须按照名称传递的参数

在 Python 3 . X 中(不包括 Python 2.X) ,函数也能用关键字参数来指定必须通过名称(而 不是位置)来传递的参数。这样的参数通常用来定义实际使用的参数以外的配置选项。

参数匹配语法 表 18-1 总结了与特殊参数匹配模式相关的语法。 表 18-1: 困数参数匹配表 位置

解释

func(value)

调用者

常规参数:通过位置匹配

func(name=value)

调用者

关键字参数:通过名称匹配

func(*iterable)

调用者

将 iterable 中所有的对象作为独立的基于位置的参数传入

func(**dict)

调用者

将 diet 中所有的键/值对作为独立的关键字参数传入

def func(name)

函数

常规参数:通过位置或名称匹配

def func(name=value)

函数

默认值参数值,如果没有在调用时传入的话

def func(*name)

函数

将剩下的基于位置的参数匹配并收集到—个元组中

def func(**name)

函数

将剩下的关键字参数匹配并收集到 —个字典中

def func(*other, name)

函数

在调用中必须通过关键字传入的参数(仅限 3.X)

def func(*, name=value)

函数

在调用中必须通过关键字传入的参数(仅限 3.X)

语法 ·—-- -—--———

参数

I

s27

这些特殊的匹配模式可以分为函数调用和函数定义两个阶段来理解:



在函数调用中(表中的前 4 行),简单值按照位置匹配。如果使用 name=value 的形式, 则会告诉 Python 按照参数名匹配,这称为关键字参数。在调用中使用* iterable 或者 **diet 允许我们在一个序列(以及其他迭代器)或字典中对应地封装任意数擞的基干

位置或者关键字的对象,并且在把它们传递给函数的时候,将它们解包为分开的、独 立的参数。



在函数头部中(表中的剩余行),一个简单的 name 是按照位置或变量名匹配的(取决 千调用者传人参数的方式),但是 name=value 形式指定了一个默认值。* name 形式把 多出来不能匹配的基千位置的参数收集到一个元组中,而** name 形式则把多出来不能

匹配的关键字参数收集到 一 个字典之中。在 Python 3.X 中,所有在* name 或一 个单独 的*之后的那些普通或带默认值的参数名称,都是 keyword-only 参数,并且必须在调

用时通过关键字传入。 在这些语法中,关键字参数和默认值参数也许是 Python 代码中最常用的。在本书前面,我

们已经使用过这两种形式:



我们已经使用关键字参数来指定 Python 3.X 的 print 函数的选项,但是,关键字参数 其实更加通用-~这让调用过程能携带更

多的信息。



我们之前也见过默认值参数,将它们用作一种从外层函数作用域向内传入值的方式, 但是它们其实比这更通用:它们允许我们将任意参数变成可选的,并在函数定义中提 供了参数的默认值。

正如我们将看到,函数头部的默认值参数和函数调用中的关键字的这些组合,允许我们进

一步选择需要覆盖的参数默认值。 简而言之,特殊参数匹配模式让你可以更自由地选择必须传入函数的参数个数。如果 一 个 函数指定了参数的默认值,这些值就可以在传入值过少的时候被使用。如果一个函数使用

了带“*”可变长参数列表的形式,你就能够传入任意多的参数,而带“*“变址名会将多 余的参数收集到数据结构(元组或字典)中进而在函数中使用。

更深入的细节 如果你选择使用并组合特殊参数匹配模式, Python 将要求你所编写的模式可选部分遵循下

面这些顺序规则 :



在闭数调用时,参数必须按此顺序出现:所有基千位贾的参数 (value) ,之后是所有 关键字参数 (name=value) 和* iterable 形式的组合,之后是** diet 形式。

528

I

第 18 章



在泊数头部,参数必须按此顺序出现:所有一般参数 (name) ,之后是所有默认值参 数 (name=value) ,之后是* name (或是 Python 3.X 中的*)形式,之后是所有 name 或 name=value 的 keyword-only 参数(在 Python 3.X 中),之后是** name 形式。

在函数调用和函数头部中,如果出现** arg 形式的话,必须写在最后。如果你使用任何其

他的顺序来混合这些参数,你就会得到一个语法错误,因为其他顺序的混合会有二义性。 Python 内部大致是使用以下的步骤来在赋值前匹配参数的:

I

通过位置分配无关键字参数。

2.

通过匹配名称分配关键字参数。

3.

将剩下的非关键字参数分配到* name 元组中。

4

将剩下的关键字参数分配到** name 字典中。

5

把默认值分配给在头部未得到匹配的参数。

在此之后, Python 会检查以确保每个参数只被传入了 一 个值。如果不是这样的话,将引发 一个错误。当所有的匹配都完成了, Python 再把传入的对象赋值给参数名称。 Python 中实际使用的匹配算法会更复杂一些(例如,它必须考虑 Python 3.X 中的 keyword­ only 参数),因此,要了解更为详细的介绍,请参考 Python 的标准语言手册。这不是必须

阅读的材料,但是深人 Python 的匹配算法能够帮助你理解一些令人费解的情况,特别是当 各种模式被混合使用的时候。

注意:仅在 Python 3.X 中,函数头部中的参数名称还可以有 一 个注解值,其指定形式为

name:value (或在要给出默认值时,为 name:value=default 形式)。这只是参数的一 个额外语法,并不会增加或修改这里所介绍的参数顺序规则。函数自身也可以拥有一个

注解值,以 def f()->value 的形式给出。 Python 将注解值附加到函数对象上。更多细 节请参阅第 19 章对函数注解的介绍。

关键字参数和默认值参数的示例 所有这些用代码来解释要比前文的描述更简单直接。如果你没使用任何特殊匹配语法, Python 默认会按照位置从左至右匹配变量名(这点和大多数其他语言类似)。例如,如果

你定义了 一 个需要 三个参数的函数,就必须使用 三个参数来调用它:

»> def f(a, b, c): print(a, b, c) >» f(l, 2, 3) 1 2 3

这里,我们基千位置传入值: a 匹配到 I, b 匹配到 2, 依此类推(这在 Python 3.X 和 Python 2.X

参数

I

s29

中都同样有效,但是,在 Python 2.X 中会显示额外的元组圆括号,因为我们这里使用了

Python 3 .X 的 print 调用)。

关键字参数 在 Python 中调用函数的时候,你可以更具体地指定传递内容与传递名称的关系。关键字参 数允许我们通过名称匹配,而不是基千位置。还是同一个函数:

>» f{c=3, b=Z, a=l) 1 2 3

例如,这个调用中的 C=3 意味着将 3 传递给名为 c 的参数。更准确地讲, Python 将调用中

的名称 c 匹配到在函数定义头部的名为 c 的参数,然后将值 3 传递给了那个参数。最终的 效果就是这次调用与上一次调用结果一样,但是要注意在使用了关键字后参数从左至右的

关系就不再重要了,因为现在参数是通过名称匹配,而不是基千位置匹配。我们甚至可以 在一个调用中混合使用基千位置的参数和关键字参数。在这种情况下,所有基千位置的参

数首先按照从左至右的顺序匹配头部的参数,再进行通过名称的关键字匹配。

»> f(1, C=3, h=2)

#

a gets 1 by position, b and c passed by name

1 2 3

大多数人第一次见到这种形式的时候,都想知道为什么非得使用这样的工具。关键字在

Python 中通常扮演着两个角色。首先,它们让你的调用变得更自文档化一些(假设你使用 了比 a 、 b 和 c 更好的参数名)。比如,下面就是 一 个很好的例子:

func(name='Bob', age=40, job='dev') 因为这样的调用要比直接由逗号分隔的 三 个裸露的值的调用包含更多含义(尤其在大型程 序中) :关键字参数在调用中起到了数据标签的作用。第二个主要角色与默认值参数有关,

我们接下来马上介绍。

默认值参数 我们在上一 章讨论嵌套函数作用域时,涉及了一些默认值参数的内容。简而言之,默认值 参数允许我们让特定的参数变为可选的。如果没有传入值的话,在函数运行前参数就会被 赋予默认值。例如,下面是这个函数需要一个参数,另外两个参数默认:

»> def f(a, b=2, c=3): print(a, b, c)

# a required, b and c optional

当调用这个函数时,我们必须为 a 提供值,无论是用基千位置的参数还是关键字参数实现。

然而,为 b 和 c 提供值是可选的。如果我们不给 b 和 c 传递值,它们会默认地分别赋值为 2 和 3:

s30

I

第 18 章

»> f(1)

# Use defaults

1 2 3

»> f(a=l) 1 2 3

当给泊数传递两个值的时候,只有 c 得到默认值,并且当有三个值传递时,不会使用默认值:

»> f(1, 4)

# Override defaults

1 4 3

»> f(1, 4, 5) 1 4 5 最后,这是关键字参数和默认值参数一起使用后的情况。因为它们都破坏了通常的从左至 右的基千位置映射,关键字参数从本质上允许我们跳过带默认值的参数:

»> f(l, C=6)

If Choose defaults

1 2 6

这里, a 通过位置拿到了 1, C 通过关键字拿到了 6, 而 b, 在两者之间,通过默认值拿到了 2 。

请小心别把在函数头部与函数调用时的特殊的 name=value 语法混为一谈。在团数调用时, 这意味着通过名称匹配的关键字参数,而在函数头部,它为一个可选的参数指定了默认值。 无论是哪种情况,这都不是一条赋值语旬(尽管跟赋值语句长得一样)。它是在这两种上 下文中特殊的语法,改变了默认的参数匹配机制。

混合使用关键字参数和默认值参数 下面是介绍关键字参数和默认值参数在实际应用中的一个稍微大一些的例子。在该例中, 调用者必须总是传人至少两个参数(匹配 spam 和 eggs) ,不过另外两个是可选的。如果

省略它们, Python 则会把头部定义的默认值给 toast 和 ham :

def func(spam, eggs, toast=O, ham=O): print((spam, eggs, toast, ham)) func(l, 2) func(1, ham=l, eggs=O) func(spam=l, eggs=O) func(toast=l, eggs=2, spam=3) func(l, 2, 3, 4)

#

First 2 required

II Output:(). 2, 0, OJ # Output: (I, 0, 0, I)

II Outpllt: (). 0, 0, OJ Output: (3. 2, I , 0) II Output: (I, 2, 3, 4)

#

再次强调,如果在调用时使用了关键字参数,那么关键字参数的顺序将不再影响其匹配。 Python 通过名称匹配,而不再是位置。调用者必须提供 spam 和 eggs 的值,不过它们既可 以基千位置也可以使用关键字变蜇。再说一遍,请记住 name=value 形式在调用时和 def 中 有两种不同的含义:在调用时代表关键字参数,而在函数头部代表默认值参数。

参数

I

s31

注意:注意可变默认值参数:在上一 章的脚注中提到过,如果你将一 个默认值参数编写为 一 个

可变对象(例如, def f(a=[])) ,同 一 个可变对象会在函数的所有调用中被反复地使 用——即使它在函数中被原位置修改了。最终的效果是这个参数会默认从上一 次调用中 继续持有它的值,而不是重置为在 def 头部定义的原始值。为了每次都重置一个新的值, 你需要把赋值语句移到函数的函数体中 。 可变默认值参数允许状态记忆,但这通常违背 直觉。因为这是一个相当常见的陷阱,我们将在这 一 部分第 2l 章中列出的"陷阱”表 中进 一步探索。

可变长参数的实例 “*”和“**”作为最后两种匹配的扩展,旨在让函数支持接受任意多的参数。它们都可以

出现在由数定义或是函数调用中,并且它们在两种位置下有着相关的目的。

函数定义中:收集参数 其中第一种用法,是在函数定义中把不能匹配的基干位置的参数收集到 一 个元组中:

>» def f(*args): print(args) 当这个函数袚调用时, Python 将所有基千位置的参数收集到一个新的元组中,并将这个元 组赋值给变量 args 。因为它是一个普通的元组对象,所以能被索引或在一 个 for 循环中遍 历等:

»> f() ()

»> f(l) (1,)

»> f(l, 2, 3, 4) (1, 2, 3, 4) “**"的特性与之类似,但是它只对关键字参数有效。它将这些关键字参数收集到 一 个新

的字典中,这个字典之后将能够用 一 般的字典工具处理。也就是说,

“**”形式允许你将

关键字参数转换成字典,从而让你能使用诸如 keys 调用、字典迭代器遍历等( 当 传人关键 字参数时,这基本上与 diet 调用类似,但区别在千``**”会返回 一 个新的字典)

»> def f(**args): print(args) »> f() {}

>» f(a=1, b=2) {'a': 1,'b': 2} 最后,函数头部能够混合使用 一般参数,

“*“以及“**”来实现极其灵活的调用签名。例

如,在下面的代码中, 1 按照位置传递给 a, 2 和 3 被收集到 pargs 位置元组中, x 和 y 被

放入 kargs 关键字词典中:

s32

I

第 18 章

>» def f(a, *pargs, **kargs): print(a, pargs, kargs) »> f(t, 2, 3, X=1, y=2) 1 (2, 3) {'y': 2,'x': 1} 这样的代码比较少见,但是会出现在需要支持多重调用模式的函数中(例如,要实现向前 兼容性的函数)。实际上,这种特性能够混合成更复杂的形式,以至千第一眼看上去有些

语义模糊,我们会在本章稍后回顾这个概念。首先,让我们看一下在函数调用时而不是定 义时使用“*”和“**”都发生了什么吧。

函数调用中:解包参数 在所有最新的 Python 版本中,我们也能在调用函数时使用“*“语法。在这种上下文中, 它与函数定义中的意思相反。也就是说,它会解包参数的集合,而不是创建参数的集合。 例如,我们能给一个由数传递含有四个参数的元组,并且让 Python 自动将这个元组解包成 相应的参数。

»> def func(a, b, c, d): print(a, b, c, d) »> args = (1, 2) »> args += (3, 4) »> func(*args)

# Same asjunc(J, 2, 3, 4)

1 2 3 4

类似地,在函数调用时,

“**”会以键/值对的形式把一个字典解包为独立的关键字参数:

>>> args ={'a': 1,'b': 2,'c': 3} »> args['d'] = 4 »> func(**args)

# Same asfunc(a=l, b=2, c=3, d=4)

1 2 3 4 同样,我们在调用时能够以非常灵活的方式混用一般参数、基千位置的参数以及关键字参数

»> func{*{1, 2), **{'d': 4,'c': 3}) 1 2 3 4 >» func(1, *{2, 3), **{'d': 4}) 1 2 3 4 »> func(1, C=3, *(2,), **{'d': 4}) 1 2 3 4 >» func(1, *(2, 3), d=4) 1 2 3 4 »> func(1, *(2,}, c=3, **{'d':4}) 1 2 3 4

# Same asjunc(l, 2, d=4, c=3) # Same asjunc(l, 2, 3, d=4) # Same asjunc(l, 2, c=3, d=4) # Same asjunc(l, 2, 3, d=4) #Sameasjunc(l,2, c=3,d=4)

在你编写脚本时,如果遇到不能预计将要传入函数的参数数盐的情况,使用这种代码是很 方便的;你可以在运行时创建一个参数的集合,并可以通过这种方式泛化地调用函数。同样,

别混淆函数头部和函数调用时“*“广**"的语法:在函数头部,它意味着收集任意多的参数, 而在函数调用时,它能解包任意多的参数。在两种情况下,一个星号代表基于位置的参数, 两个星号代表关键字参数。

参数

I

533

注意:正如 第 14 章所学的,在调用时的 * pargs 形式是一个迭代卜一下文,因此从技术上讲 它接受所有的可迭代对象,而不仅是这里的例子所示的元组或其他序列。例如, 一

个文件对象可以跟在“*”后而工作,并将它的所有行解包为单个的参数(例如, func(*open('fname' )) 。等我们学完生成器后.就可以在第 20 立中 学习这 种传参方式 的应用。

Python 3.X 和 Python 2.X 都支持这种通用性,但是,它只对调用成立一调用中的 *pargs 允许任何可迭代对象,但是在 def 头部中的相同格式却总是把额外的参数绑定到 一 个元组。这个头部行为在思想上和语法上都与我们在第 11 章所介绍的 Python 3.X "*"

扩展序列解包赋值形式类似(例如 x,*y = z) ,尽管那种星号 的使用方法总是创建列表 而非元组 。

泛化地使用函数 前面小节的示例可能有些学术化(也可以说十分探奥),但它们比我们想象得更为常用。 很多程序需要以一种泛化的形式来调用任意的函数,即在运行前井不知道拯l 数的名称和参 数。实际上,特殊的”可变长参数”调用的真正强大之处在 千 ,在编写 一 段脚本之前不需

要知道 一 个函数调用需要多少参数。例如,你可以使用订逻辑来从 一 系列函数和参数列表 中选择,并且泛化地调用其中的任何 一 个(下面例子中的 一 些函数是假想的) :

if sometest: action, args = funcl, (1,) else: action, args = func2, (1, 2, 3) ... etc... action(*args)

# Call fu11c I with 011e arg in rhis case

# Call J11nc2 with three args here #

Dispalch generically

这样就能同时利用“*”的形式以及函数本身是对象的事实(也就是说,函数可以用任意的 变址名来引用和调用)。更 一 般地说,每 当你无法 预计参数列表时,这种可变长参数调用 语法都是很有用的。例如,如果你的用户通过一个用户界面选择了一个任意的函数,你可 能在编写自己的脚本的时候无法直接硬编码一 个函数调用。要解决这个问题,你可以直接

用序列操作创建 一 个参数列表,并且用带星号的语法来解包参数以调用它:

» >... define or import func3... »> args = (2,3) »> args += (4,) »> args (2, 3, 4)

»> func(*args) 由千这里的参数列表是作为元组传入的,程序可以在运行时创建它。这种技术对 千那些测

试和计时其他函数的函数来说也很方便 。 例如,在下面的代码中,我们通过传递任何发送 进来的参数来支持具有任意参数的任意函数(这是本书例程包中的 traceO.py 文件)

def tracer(func, *pargs, **kargs): 534

I

第 18 章

fl Accept arbitrary arguments



print('calling:', func. name_) return func(*pargs, **kargs)

#

Pass along arbitrary arguments

def func(a, b, c, d): return a+ b + c + d print(tracer(func, 1, 2, c=3, d=4)) 这段代码使用了内置的绑定到所有函数的_name _属性(正如你所预期的,它就是函数的

名称符串),井使用星号来收集并解包要被追踪的函数。换句话说,当这段代码运行的时候, 参数首先被 tracer 植入,然后被可变长参数调用语法传递:

calling: func 10

关千该技术的另一个例子,参考前一章末尾的预习,在那里它被用千重置内置的 open 函数。

我们将在本书后面编写更多这 一 功能的其他例子,尤其请参阅第 21 章的序列计时示例,以 及我们将在第 39 章中编写的装饰器工具。这是在许多工具中都存在的一种常见的技术。

过时的 apply 内置函数 (Python

2.X)

在 Python 3.X 之前,*args 和** args 可变长参数调用语法的效果可以通过一个名为 apply 的内置函数来实现。这一最初的技术在 Python 3.X 中已经废除了,因为它现在是多余的了

(Python 3.X 清除了众多的这类含糊不清但已经存在多年的工具)。不过它在所有 Python 2.X 的发行版中仍然可用,井且你可能会在旧的 Python 2.X 代码中遇到它。 简而言之,下面是前面的 Python 3.X 例子等价的代码:

func(*pargs, **kargs) apply(func, pargs, kargs)

# #

Newer call syntax: junc(*sequence, **diet) Defunct built-in: apply(junc. sequence, diet)

例如,考虑如下的函数,它接受任意多的基千位置的参数和关键字参数:

»> def echo(*args, **kwargs): print(args, kwargs) »> echo(l, 2, a=3, b=4) (1, 2) {'a': 3,'b': 4} 在 Python 2.X 中,我们可以用 apply 泛化地调用它,或使用现在 Python 3.X 中要求的调用 语法:

»> pargs = (1, 2) » > kargs = {'a': 3,'b': 4} »> apply(echo, pargs, kargs) (1, 2) {'a': 3,'b': 4} »> echo(*pargs, **kargs) (1, 2) {'a': 3,'b': 4}

参数

I

535

这两种模式在 2.X 中也都对内置函数有效(注意 2.X 中加在长整型后面的字母 “L")

>» apply(pow, (2, 100)) 1267650600228229401496703205376L »> pow(*(2, 100)) 12676S0600228229401496703205376L 解包调用语法形式比 apply 函数新,通常也推荐使用它,并且是 Python 3.X 中必需的。

(从

技术上讲, apply 语法在 2.0 版本中被引人,在 2.3 版本的文档中被标为弃用,在 2 . 7 版本

中仍然可以无警告地使用,最终在 3.0 及之后的版本中被彻底废除)新的调用语法除了与 def 头部的*收集器具备形式上的对称性、实际上总体上需要更少的键盘录人之外,还允

许我们传递额外的参数而不必手动扩展参数序列或字典:

» > echo (o, c= S, *pargs, **kargs)

#

Normal, keyword, *sequence, **dictionary

(0, 1, 2) {'a': 3,'c': S,'b': 4} 也就是说,这种调用语法形式更为通用。既然它在 Python 3.X 中是必需的,你现在应该忘 掉所有关千 apply 的知识(除非,它出现在你必须使用或维护的 Python 2.X 代码中)。

Python 3.X 的 keyword-only 参数 Python 3.X 将函数头部的顺序规则一 般化,以允许我们指定 keyword-only 参数一一即必须 只桉照关键字传人并且永远不会被基千位置参数来填充的参数。如果想要一个函数既处理 任意多个参数,又接受可选的配置项的话,这种技术就能派上用场。 从语法上讲, keyword-only 参数编写为出现在参数列表中* args 之后的有名参数。所有这

些参数都必须在调用中使用关键字语法来传递。例如,在如下的代码中, a 可以按照名称或 位置传入, b 收集所有其他的基千位置参数,而 c 必须只按照关键字传递。在 3.X 版本中:

>» def kwonly(a, *b, c): print(a, b, c)

»> k树only(1, 2, c=3) 1 (2,) 3 »> kwonly(a=l, c=3) 1 {) 3 »> kwonly(1, 2, 3) TypeError: kwonly() missing 1 required keyword-only argument:'c' 我们也可以在参数列表中使用一个“*”字符,来表示一个函数不会接受一个可变长度的参

数列表,但是仍然期待跟在“*”后面的所有参数都作为关键字参数传入。在下面这个函数 中, a 同样可以按照位置或名称传入,但 b 和 c 必须按照关键字传入,而且不允许其他额 外的基千位置传入:

>» def kwonly(a, *, b, c):

536

I

第 18 章

print(a, b, c)

>» kwonly(1, c=3, b=2) 1 2 3

>» kwonly(c=3, b=2, a=1) 1 2 3

>» kwonly(1, 2, 3) TypeError: kwonly() takes 1 positional argument but 3 were given »> kwonly(1) TypeError: kwonly() missing 2 required keyword-only arguments:'b'and'c' 你仍然可以对 keyword-only 参数使用默认值,即使它们出现在函数头部中的“*”的后面。 在下面的代码中, a 可以按照名称或位置传人,而 b 和 e 是可选的,如果要使用的话必须

按照关键字传入:

*, b='spam', c='ham'): print(a, b, c)

>» def kwonly(a, »> kwonly(1)

1 spam ham »> kwonly(1, c=3) 1 spam 3 >» kwonly(a=l) 1 spam ham »> k何only(c=3, b=2, a=1) 1 2 3

>» kwonly(1, 2) TypeError: kwonly() takes exactly 1 positional argument but 2 were given 实际上,带默认值的 keyword-only 参数都是可选的,但是,那些没有默认值的 keyword­ only 参数真正地变成了函数必需的 keyword-only 参数:

*, b, c='spam'): print(a, b, c)

>» def kwonly{a,

»> kwonly(1, b='eggs') 1 eggs spam >» kwonly(l, c='eggs') TypeError: kwonly() missing 1 required keyword-only argument:'b' >» kwonly{1, 2) TypeError: kwonly(} takes 1 positional argument but 2 were given

*, b=l, c, d=2): print(a, b, c, d)

>» def kwonly{a,

>» k忖only(3, c=4) 3 1 4 2 >» kwonly{3, c=4, b=S) 3 5 4 2 >»

k树only{3)

Type Error: kwonly(} missing 1 required keyword-only argument:'c' »> kwonly(1, 2, 3) TypeError: kwonly(} takes 1 positional argument but 3 were given

参数

I

537

顺序规则 最后,注意 keyword-only 参数必须指定在一个单个星号后面,而不是两个星号一有名参 数不能出现在** args 任意关键字形式的后面,并且一个**不能独自出现在参数列表中。 这两种做法都将产生语法错误:

»> def kwonly(a, **pargs, b, c): SyntaxError: invalid syntax »> def 虹only(a, **, b, c): SyntaxError: invalid syntax 这意味着在一个函数头部, keyword-only 参数必须编写在** args 任意关键字形式之前,且 当二者都有的时候,必须编写在* args 任意位置形式之后。每当一个参数名出现在* args

之前,它可能是基千位置的默认值参数,而不是 keyword-only 参数:

»> def f(a, *b, **d, c=6): print(a, b, c, d)

# Keyword-only before**!

SyntaxError: invalid syntax

>» def f(a, *b, c=6, **d): print(a, b, c, d)

# Collect args in header

>>> f(1, 2, 3, x=4, y=5) 1 (2, 3) 6 {'y': s,'x': 4}

#

»> f(1, 2, 3, x=4, y=S, c=7) 1 (2, 3) 7 {'y': s,'x': 4}

# Override default

»> f(l, 2, 3, C=7, X=4, y=5) 1 (2, 3) 7 {'y': 5,'x': 4}

# Anywhere in keywords

>» def f(a, c=6, *b, **d): print(a, b, c, d)

# c is not keyword-only here!

Default used

>» f(l, 2, 3, X=4) 1 (3,) 2 {'x': 4} 实际上,在函数调用中类似的顺序规则也是成立的:当传入 keyword-only 参数时,它们必 须出现在一个**args 形式之前。 keyword-only 参数可以编写在*args 之前或者之后,也可 以包含在**args 之内:

538

»> def f(a, *b, c=6, **d): print(a, b, c, d)

# KW-only between * and **

»> f(l, *(2, 3), **dict(x=4, y=S)) 1 (2, 3) 6 {'y': 5,'x': 4}

# Unpack args at call

»> f(l, *(2, 3), **dict(x=4, y=S), C=7) SyntaxError: invalid syntax

#

>» f{1, *(2, 3), c=7, **dict(x=4, y=S)) 1 (2, 3) 7 {'y': 5,'x': 4}

# Override default

»> f{1, c=7, *{2, 3), **dict(x=4, y=S)) 1 (2, 3) 7 {'y': s,'x': 4}

#

>» f(l, *(2, 3), **dict(x=4, y=5, C=7)) 1 (2, 3) 7 {'y' : s,'x': 4}

# Keyword-only in **

1

第 18 章

Keywords before **args'

After or before *

你可以结合前面正式介绍的通用参数顺序规则,来自己跟踪和分析这些情况。在这些刻意

编写的例子中,它们可能是最困难的情况,但是,在实际中有可能会遇到它们,特别是那 些编写库和工具供其他 Python 程序员使用的人。

为什么要使用 keyword-only 参数? 那么,为什么要关心 keyword-only 参数呢?简而言之,它们让一个函数既接受任意多个要

处理的基千位置参数又接受作为关键字传入的配置项这件事变得很容易。尽管它们的使用 是可选的,如果没有 keyword-only 参数的话,就要完成额外的工作来为这些选项提供默认 值并校验没有传入多余的关键字。

假设 一 个函数处理一 组传入的对象,并且允许传人一个跟踪标志:

process(X, Y, Z) process(X, Y, notify=True)

II Use flag's default II Override flag default

没有 keyword-only 参数的话,我们必须使用* args 和**args, 并且手动地检查关键字,但

是,有了 keyword-only 参数就可以节省许多代码。下面的语句保证了不会有基千位置参数 与 notify 参数发生错误匹配,井且要求它如果传入则作为一个关键字传入:

def process(*args, notify=False):... 在本章后面“模拟 Python 3.X print 函数” 一节中 ,我们将继续看到 一 个更实际的例子,

因此我将在那里介绍余下的内容。参阅本书第 2l 章的迭代选项计时案例,那里给出了 keyword-only 参数语法的另 一个 例子。关千 Python 3.X 中的其他函数定义扩展,请继续阅

读第 l9 章中对函数注解语法的讨论。

min 提神小例 让我们来介绍一些更为实际的内容吧。为了帮助你巩固这 一 部分的内容,让我们通过 一 个 练习来说明参数匹配工具在实际中的应用。

假设你想编写 一 个函数,这个由数能够计算任意参数集合和任意对象数据类型集合中的最 小值。也就是说,这个函数应该接受零个或多个参数:希望传递多少就可以传递多少。此外, 这个函数应该能够使用 Python 中所有的对象类型:数字、字符串、列表、字典的列表、文件,

甚至 None. 第 一 个要求提供了 一 个能够充分发掘'`*”的优势的天然示例:我们能把参数收集到 一 个元 组中,并且可以通过简单的 for 循环依次遍历处理每一个参数。问题的第 二部分定义很简单: 因为每个对象类型都支持比较,所以我们没有必要为每种类型都创建 一 个由数(这是多态

概念的 一 种应用)。我们能够不考虑其类型直接比较,而让 Python 根据不同的对象决定井 执行正确的比较。

参数

I

s39

满分 下面文件介绍了编写这个操作的 三种方案,其中之 一 是 一 位我课上的学生提出来的(这个 例子在培训中通常作为一个小组练习,用来打消午饭后的食困) :



第 一 个函数取回了第一个参数 (args 是一个元组),并且使用分片去掉第 一 个元素从

而遍历剩余的参数(把一个对象和自己比较是没有意义的,尤其当这个对象是一 个较 大的结构时)。



第二个版本让 Python 自动获取第 一 个参数以及其余的参数,因此避免了 一 次索引和

分片。



第三个版本通过对内置函数 list 的调用将元组转换为 一个列表,之后调用 list 内置的 sort 方法来实现比较 。

Python 中的 sort 方法是用 C 语言编写的,所以有时它要比其他方案运行得快,而前两种

方案的线性搜索会让它们在绝大多数情况下都要更快注 j 。文件 mins.py 包含了所有三种解 决方案的代码:

def minl(*args): res= args[o] for arg in args[l:]: if arg < res: res= arg return res def min2(first, *rest): for arg in rest: if arg < first : first= arg return first def min3(*args): tmp = list(args) tmp.sort() return tmp[o] 注 l:

#

Or, in Python 2.4+: return sorted(args)[Oj

事实上,这是相当复杂的 。 Python 的 sort 子程序使用 C 语言偏写 , 并使用了一个高度 优化的算法,该算法将利用序列中元素部分有序的性质来进行排序 。 这一算法被称为

"t imsort" ,来源于它的创始人 Tim Peters , 同时在该算法的文档中声称它有着“超自然 的性能 ” (对于排序而言 , 已经相当好了!) 。 然而,排序本质是一个 O(n log(n))的操作(如 果采用分治法 , 必须将序列先切分再拼接回去),而前面两种方法采用一次线性的从左到

右的扫描 。 最 终 的效果是 , 如果参数序列是部分有序的 , 排序方法 会 史快,但反之则史慢(这

点在 Python3 . 3 版本下的浏试中依然成立) 。 尽 管 如此 , Python 的性能会随着时间发生改变 , 而且排序是用 C 语言进行实现的这点能够极大地推动性能的优化 ;

关于史加精确的分析,

你应当用 time 或 timeit 模块对这几种方案分别计时-我们将在笫 21 章 中学习这方面的

技术 。

s40

I

第 18 章

print(min1(3,4,1,2)) print(min2("bb", "aa")) print(min3([2,2), [1,1), [3,3))) 所有的这 三 种方案在文件运行时都产生了相同的结果 。 你自己可以试着在交互式命令行下

输入一 些调用来测试这些方法。 % python mins.py

aa (1, 1] 注意上边这 三种方法都没有测试无参数传入的情况。它们可以做这样的测试,但是在这里做 是没有意义的一对千所有的这三种解决方案,如果没有参数传入的话, Python 都会自动地 引发一个异常。第一种方案会在尝试获取 0 号元素时引发异常;第 二种方案会在 Python 检 测到参数列表不匹配时发生异常 1 第三种方案会在尝试最后返回元素 0 时发生异常。 这就是我们希望得到的结果一一因为函数支持所有数据类型,其中没有有效的信号值让我

们能传回标记一个错误,所以我们需要依赖异常的引发。当然这也不是绝对的(例如,有 时如果你希望在运行到会自动触发错误的代码前避免某个行为的发生,你就需要自己测试

错误),但通常最好假设参数在函数代码中有效,并且当它们不是这样时让 Python 来帮你 引发 一 个错误。

附加分 如果你能够让这些函数来计算最大值而非最小值,那么你就能在这里得到附加分。这点比 较容易:头两个泊数只需要将小千号改为大千号 , 而第 三 个只需要返回 tmp[-1] 而不是 tmp[O] 。如果你想拿满分,请确保函数名也修改成了 max (尽管这从严格意义上讲是可选的)。 你也可以让一 个函数可以同时计算最小值或最大值,通过使用内置的 eval 函数(参考库手

册,以及本书中的内容,尤其是第 10 章)来求值比较表达式字符串,或是通过传入 一 个任 意的比较函数 。 文件 minmax.py 展示了如何实现后一种方案:

def minmax(test, *args): res= args[o] for arg in args[l:]: if test(arg, res): res= arg return res def lessthan(x, y): return x < y def grtrthan(x, y): return x > y

If See also:

print(minmax(lessthan, 4, 2, 1, s, 6, 3)) print(minmax(grtrthan, 4, 2, 1, 5, 6, 3))

#

lambda, eval

Self-test code

% python minmax.py

参数

I

s41

16

与此相同,泊数作为另一种参数对象也可以传入一个函数。例如,为了编写 max (或者其他)

函数,我们可以直接传人相应种类的 test 函数。这看起来像是额外的工作,但是这种将函 数泛化(而不是剪切和粘贴来改变函数名中的 一 个字符)的核心思想意味着在未来我们只 需要修改一个版本就可以了,而不是两个。

结论 当然,所有这些不过是一 次编码的练习而已。我们没有必要编写 min 或 m ax ,

因为这两个

函数都是 Python 内置好的!我们在第 5 章介绍数值工具的时候简略地介绍过它们,并且在 第 14 章介绍迭代上下文的时候再次遇到过它们。内置版本的函数工作起来基本上与我们自

己编写的函数一模一样,不过它们出干优化运行速度的目的采用 C 语言编写,并能接受一 个单个的可迭代对象或多个参数。然而,尽管它在这一上下文中是多余的,但我们在这里 使用的通用编码模式很可能在其他情况下有用。

通用 set 函数 现在,让我们看一个实际中更常用的使用特殊参数匹配模式的例子吧。在第 16 章的末尾,

我们编写了一个返回两个序列公共部分的函数(它将挑选出在两个序列中都出现的元素)。 这里是一个能对任意数量的序列(一个或多个)取交集的函数,它使用可变长参数的匹配 形式* args 来收集传入的参数。因为参数是作为一个元组传入的,我们可以在一个简单的 for 循环中处理它们。只是出千好玩的目的,我们将编 写一个 union 函数,来从任意 多的 参数中收集所有在任意操作对象中出现过的元素:

def intersect(*args): res=[] for x in args[o]: if x in res: continue for other in args[1:): if x not in other: break else: res.append(x) return res def union(*args): res=[] for seq in args: for x in seq : 订 not x in res: res.append(x) return res

Scan first sequence Skip duplicates # For al/ other args # ltem i11 each 011e? # No: break out of loop # Yes: add items to end

#

#

# For all args # For all nodes # Add new items to result

因为这些工具可能具有重用的价值(并且在交互式命令行下它们有些太大了以至千很难重 s42

I

第 18 章

新输入),我们会把这些函数保存为 一 个名为 inrer2.py 的模块文件(如果你已经忘记了模

块和导入是如何工作的,请参阅第 3 章的介绍或者参阅第五部分更多关千模块的内容)。

在这两个泊数中,参数在调用时都是作为元组 args 传入的。与最开始的 intersect 函数一

样,这些函数也都对任意类型的序列有效。在这里,它们处理了字符串、混合类型以及两 个以上的序列:

% python

»> from inter2 import intersect, union >» sl, sz, s3 = "SPAM", "SC小 ”,“SL矶" »> intersect(s1, s2), union(s1, s2)

#

Two operands

#

Mixed types

(['S', ' A','M'], ['S','P','A','M','C'])

>» intersect([1,2,3), (1,4)) [1]

»> intersect(s1, s2, s3)

# Three operands

['S'''A'''M'l

»> union(s1, s2, s3) ['S','P','A','M','C','L'] 为了更彻底地测试,下面的代码编写了 一 个函数,把这两种工具按照不同的顺序应用到参 数上,这里使用了我们在第 13 章中见过的 一 个简单的打乱顺序的技术。这个函数使用了该 技术,通过分片在每次循环中将第 一 个元素移到末尾,通过使用“*”来解包参数,通过排

序使结果变得可以比较:

»> def tester(func, items, trace=True): for i in range(len(items)): items= items[1:] + items[:1] if trace: print(items) print(sorted(func(*items)))

»> tester(intersect, ('a','abcdefg','abdst','albmcnd')) ('abcdefg','abdst','albmcnd','a') ['a'l ('abdst','albmcnd','a','abcdefg') ['a'] ('albmcnd','a','abcdefg','abdst') ['a'l ('a','abcdefg','abdst','albmcnd') ['a'l

>» tester(union, ('a','abcdefg','abdst','albmcnd'), False) ('a','b','c','d','e','f','g','l','m', ' n','s','t'] ['a','b','c','d','e','f','g','l','m','n','s','t '] ['a','b','c','d','e','f','g','l ' ,'m','n','s','t'] ['a','b','c','d','e','f','g','l','m','n','s','t ']

» > tester (intersect, ('ba','abcdefg','abdst','albmcnd'), False) ['a','b'] ['a','b'] ['a','b']

参数

I

543

['a','b'] 这里的参数顺序打乱没有产生所有可能的参数顺序(如果要得到所有的顺序,就需要用到 全排列,对千 4 个参数的情况而言有 24 种不同的顺序),但是却足以检查参数顺序是否会 影响运行结果。如果你进一步测试,会发现交集和并集中都不会出现重复对象,这一 点保 证了它们与数学上集合操作的统一性:

»> intersect([l, 2, 1, 31, (1, 1, 4)) [1]

»> union([l, 2, 1, 3], (1, 1, 4)) [1, 2, 3, 4]

»> tester(intersect, ('ababa','abcdefga','aaaab'), False) ['a','b'] ['a','b'] ['a', ' b'] 然而,从算法角度还可以做更多的优化,但是由千下面的注意部分所提到的原因,我们把 对这段代码的进一步优化留作推荐习题。同时注意到在我们的测试函数中对参数顺序的打

乱可以作为一种通用的工具,测试器会因为我们把这一 功能委托给另 一 个函数而变得简单, 让那个函数自由地创建或生成参数组合,只要这些参数组合能够完成匹配:

»> def tester(func, items, trace=True): for args in scramble(items): ... use args... 实际上,我们将在第 20 章中看到这个例子被进 一 步修改,直到那时我们学完如何编写用户 定义的生成器之后,再回来处理这里剩下的最后 一 点。我们也将在第 32 章中最后一次重新

编写集合操作,并在本书第六部分的习题中用类和方法来扩展列表对象。

注意:因为 Python 现在有一 个新的 set 对象类型(在第 5 章介绍过) , 本书中所有关千集合处 理的例子严格意义上都没有存在的必要。之所以介绍它们,不过是用来说明编写函数的 技术,只具有教学意义。由于 Python 的不断地改进,本书中的例子有时也会显得过时 。

模拟 Python 3.X print 函数 为了圆满结束本章,让我们来看参数匹配的最后一个例子。这里看到的代码针对 Python 2.X

或更早的版本(它在 Python 3.X 下也能工作,但是没有什么实际价值) :它使用* args 任 意基于位置参数元组以及** args 任意关键字参数字典来模拟 Python 3.X print 函数所做的 大多数工作。 Python 本来可以将这段代码作为 3.X 中的 一 个选项而不是完全移除 2.X 的

print 语句,但是 3 .X 采取的做法是与 2.X 的过往划清界限。

正如我们在第 11 章所学的,这实际上不是必蒂的,因为 Python 2.X 程序员总是可以通过如 下形式的 一 个导入(可用千 Python 2.6 和 2.7) 来开启 Python 3 .X 的 print 函数:

544

I

第 18 章

from _future_ import print_function 不过为了说明 一般性的参数匹配,如下的文件 print3.py 用少晕可重用的代码完成了同样的

工作,通过构建待打印的字符串,并将其按照配置参数来定向:

#!python """ Emulate most of the 3,X print function for use in 2.X (and 3.X}. Call signature: print3(*args, sep='', end='\n', file=sys.stdout) """

import sys def print3(*args, **kargs): sep = kargs. get('sep','') # Keyword arg defaults end = kargs. get('end','\n') file= kargs.get('file', sys.stdout) output ='' first= True for arg in args: output+=(''if first else sep) + str(arg) first= False file.write(output + end) 为了测试它,将其导人到另一个文件或交互式命令行中,并像 Python 3 . Xprint 函数那样使 用它。这里是 一 个测试脚本 testprint3.py (注意,该函数必须叫作 “print3", 因为 “print"

在 Python 2.X 中是保留字) :

from print3 import print3 print3(1, 2, 3) print3(1, 2, 3, sep='') print3(1, 2, 3, sep='...') print3(1, [2], (3,), sep='...') print3(4, 5, 6, sep=", end=") print3(7, 8, 9) print3()

# Suppress separator # Various object types # Suppress newline # Add newline (or blank line)

import sys print3(1, 2, 3, sep='??', end=',\n', file=sys.stderr)

#Redirecttofile

在 Python 2.X 下运行的时候,我们得到了与 Python 3.X 的 print 函数相同的结果:

C:\code> c:\python27\python testprint3.py 1 2 3

123 1... 2... 3 1... [2)... (3,)

4567 8 9 1??2??3. 尽管在 Python 3.X 中没有意义,但运行后的结果是相同的。照例, Python 语 言 的通用性 设计允许我们在 Python 语言自身中原型化或开发概念。在这个例子中,参数匹配工具在

参数

I

545

Python 代码中与在 Python 的内部实现中 一样的灵活。

使用 keyword-only 参数 有趣的是这个例子可以使用本章前面介绍的 Python 3.X keyword-only 参数来编写,从而自 动验证配置参数。下面的这段代码变体(在 prin t3 _alt l.py 文件中)说明了这 一 点:

#!python3 "Use 3.X only keyword-only args" import sys def print3(*args, sep='', end='\n', file=sys.stdout): output ='' first= True for arg in args: output+=(''if first else sep) + str(arg) first= False file.write(output + end) 这个版本与最初的版本一样有效,并且它是说明 keyword-only 参数如何方便好用的典型例 子。最初的版本假设所有的基于位置的参数都要打印,并且所有的关键字参数都只用千配

置选项。大多数情况下这样就够了,但是,其他额外的关键字参数都被默默地忽略掉了。 例如,如下的 一个调用正好对 keyword-only 参数形式产生一个异常:

»> print3(99, name='bob') TypeError: print3() got an unexpected keyword argument'name' 但是,在最初的版本中会默默地忽略掉 name 参数。为了手动检测多余的关键字,我们可以 使用 diet.pop( )删除取回的传入项,并检查字典是否为空。下面的代码(在 print2_alt2. PY 文件中)是 keyword-only 参数版本的一个等价版本。它会用 raise 语句触发一个内置的

异常,几乎与 Python 本身的行为 一样(我们将在第七部分中更加深人地学习这一 内容)

#!python "Use 2.X/3.X keyword args deletion with defaults" import sys def print30(*args, **kargs): sep = kargs. pop ('sep','') end = kargs. pop('end','\n') file = kargs. pop('file', sys. stdout) if kargs: raise TypeError('extra keywords: %s'% kargs) output='' first= True for arg in args: output+=(''if first else sep) + str(arg) first= False file.write(output + end) 它和之前 一样有效,但是它现在也会捕获外部的关键字参数:

546

I

第 18 章

»> print3(99, name='bob') TypeError: extra keywords: {'name':'bob'} 这个版本的函数可以在 Python 2.X 下运行,但是,它和 keyword-only 参数版本相比需要额 外加 4 行代码。遗憾的是,这个例子中的额外代码是不可避免的~eyword-only 参数版

本只能在 Python 3.X 下工作,这否定了我编写这个例子的首要目的: 一个对 Python 3.X 的 模拟程序,只能在 Python 3.X 下工作,这简直亳无意义!在只需要运行千 Python 3.X 的程 序中, keyword -only 参数可以简化既接受参数又接 受选项的这类函数。在第 21 章的迭代计 时案例学习中,有 Python 3.X 的 keyword-only 参数的另 一 个示例。

请留意:关键字参数 正如你看到的,高级参数匹配模式可能更复杂。它们在你的代码中也基本上是可选的、 你可以选择只用简单的位置匹配,并且当你刚开始编程的时候,这可能是一个不错的

主意。然而,由于一些 Python 工具使用了它们,了解一些关于这些模式的常识是很 重要的 .

例如,关键宇参数在 tkinter 中扮演着很重要的角色。 tkinter 实际上是 Python 中的 标准 GUI API (该模块在 Python 2.X 中的名称是 Tkinter) 。我们在本书的多个地方 简单地介绍过 tkinter.

但只是介绍了当创建 GUI 组件的时候的调用模式、关键字

参数设置配置项 。 例如 , 这种调用形式:

from tkinter import* widget= Button(text="Press me", command=someFunction) 使用 text 和 command 关键字参数创建了一个新的按钮,并定义了它的文字以及回调 函数 。 因为一个部件设置选项的数目可能很多,关键宇参数能让你从中选择。如果不 使用它们的话,.你要么必须祁据位置列举出所有可能的选项,要么期待一个明智的基 于位置的参数的状认协议来处理每一个可能选项的排列。

Python 中的许多内置函数都期待我们通过关键字参数来指定使用模式选项,这些选项 不一定有默认值。例如,我们在笫 8 章学习过的 sorted 内置函数:

sorted(iterable, key=None, reverse=False) 期持我们传入一个待排序的可迭代对象,但是,也允许我们传递可选的关键宇参数来

指定一个字典排序键和一个反向排序标志,其欢认值分别为 None 和 False 。由于我们 通常不会使用这些选项,因而可以忽略它们而使用戏认值。

如前所述, diet 、 str.format 和 3.X 中 print 调用也可以接受关键字-~尽管这些用

法依赖本章介绍的参数传递模式,我们却不得不在史早的章节中就介绍他们。(只可惜, 那些改变 Python 语言的人早就熟知 Python ! )

参数

I

547

本章小结 在本章中,我们学习了与函数相关的两个关键概念中的第 二 个:参数,即对象是如何传递 给函数的。正如我们所学到的,参数通过赋值传入函数中,也就是通过对象引用(其实就

是指针)。我们还学习了 一 些更高级的扩展,包括默认值参数和关键字参数,使用任意多 个参数的工具,以及 Python 3 . X 中的 keyword-only 参数。最后,我们还学习了可变参数如

何表现出与其他的对象共享引用 一样的行为。也就是说,除非对象在传入时被显式地复制, 否则在函数中修改一 个传入的可变对象会影响到调用者。

下 一 章我们继续介绍函数,讨论 一 些与函数相关的更高级的概念:函数注解、递归、 lambda 以及 map 和 filter 这样的函数工具。这些概念很多都源自千这样的一个事实:泊数

是 Python 中的普通对象,并且支持 一些高级的和相当灵活的处理方式。在学习这些主题之前, 让我们先通过本章的习题来复习学过的参数概念。

本章习题 本章习题中的大多数问题,会在 2. X 版本中得到略微不同的结果,例如当打印多个值的 时候会带有圆括号和逗号。为了在 2.X 中得到与 3.X 完全相同的答案,你可以在运行前从

_future_库中导入 print_function 。 ].

如下代码的输出是什么?为什么?

»> def func(a, b=4, c=S): print(a, b, c)

>» func(l, 2) 2.

如下代码的输出是什么?为什么?

>» def func(a, b, c=S): print(a, b, c)

>» func(l, C=3, b=2) 3.

如下代码的输出是什么?为什么?

>» def func(a, *pargs): print(a, pargs)

»> func(1, 2, 3) 4.

如下代码将打印出什么?为什么?

»> def func(a, **kargs): print(a, kargs)

»> func(a=l, c=3, b=2) 5. 548

如下代码将打印出什 么 ?为什么?

I

第 18 章

»> def func(a, b, c=3, d=4): print(a, b, c, d) »> func(1, *(5,6)) 6.

如下代码的输出是什么?为什么?

>» def func(a, b, c): a= 2; b[o] ='x'; c['a'] ='y' »> 1=1; m=[t]; n={'a':o} >» func{l, m, n) >» 1, m, n

习题解答 1.

这里的输出是 “1 2 5" ,因为 1 和 2 按照位置传递给了 a 和 b, 井且 c 在调用中袚忽略 了从而得到默认值 5 。

2.

这次的输出是 “1 2 3" : 1 按照位置传递给 a, 2 和 3 按照名称传递给 b 和 c (当像这 样使用关键字参数的时候,从左到右的顺序无关紧要)。

3

这段代码打印出 “1

(2, 3)", 因为 1 传递给 a, *pargs 把其他的基千位置参数收集

到一个新的元组对象中。我们可以用任何迭代工具来遍历这个收集了剩余基千位置参 数的元组(例如, for arg in pargs:...) 。

4

这次,代码打印出 “1,

{'b': 2

c': 3}

,因为 1 按照名称传递给 a, **kargs 把

其他关键字参数收集到 一 个字典中。我们可以用任何迭代工具来遍历这个收集了剩余 关键字参数的字典(例如, for key in kargs:...) 。注意 , 字典的键顺序可能随着 Python 版本和其他因素而有所不同。

5.

这里的输出是 “1 5 6 4" : 1 按照位置匹配 a, 5 和 6 按照从* name 中解包出的位置匹 配给 b 和 C (6 覆盖了 c 的默认值),而 d 则获得默认值 4, 因为它没有被传入一个值。

6.

这里输出的是 "(1,

['x'],

{'a':'y'})" -函数中的第一处赋值不会影响调用者,

但接下来的两个确实会有所影响,因为它们原位置改变了传入的可变对象。

参数

I

549

第 19 章

函数的高级话题

这一章将介绍一系列更高级的与函数相关的话题:递归函数、函数属性和注解、 lambda 表 达式,函数式编程工具例如 map 和 filter 。这些都是相对高级的工具,根据你的工作内容, 可能在日常的工作中不会碰到它们。由千它们在某些领域中有用,因此你有必要对它们有 个基本的理解:例如, lambda 在 GU] 中很常用,同时函数式编程技巧在 Python 代码中也

越来越常见。 之所以使用函数,部分是由千函数的接口,所以我们在这里也探索一 些通用的函数设计原则。 下 一章会继续这 一 高级话题,在这里已经学习的函数工具的基础上,进一步介绍生成器函

数和表达式,并再次回顾列表推导。

函数设计概念 我们现在已经学习了 Python 中函数的基本知识,因此用几句介绍背景的话来开始本章 。当 你开始使用函数时,就要面对如何将组件聚合在 一 起的选择了。例如,如何将任务分解成

为更有针对性的函数(内聚性)、函数将如何通信(耦合性)等。你需要深人考虑函数的 大小等概念,因为它们直接影响到代码的可用性。这里的 一 些内容属于结构分析和设计的

范畴,但是,它们和其他概念一样也适用千 Python 代码。 我们在第 17 章研究作用域的时候介绍过关于函数和模块耦合的观念,这里是给对函数设计 原则较为陌生的读者的一些通用的指导方针的复习。



耦合性:在输入时使用参数,输出时使用 return 语句。 一般来讲,你要力求让函数独 立千它外部的东西。参数和 return 语句通常就是隔离对代码中少数醒目位置的外部依

赖关系的最好办法。

550



耦合性:只在真正必要的情况下使用全局变量。全局变量(即外层模块中的变量名) 通常是 一 种鳖脚的函数间进行通信的方法。它们引发了依赖关系和性能损耗的问题,

会让程序难以调试、修改和重用。



耦合性:不要改变可变类型的参数,除非调用者希望这样做。函数可以改变传入的可 变类型对象,但是就像全局变氢一样,这会导致调用者和被调用者之间的强耦合性,

这种耦合性会导致函数过千局限和不友好。



内聚性:每一个函数都应该有一个单一的、统一的目标。在袚精心设计后,每一个函 数中都应该做一件事:这件事可以用 一 个简单的说明句来总结。如果这个句子很宽泛(例 如,

“这个泊数实现了整个程序”),或者包含了很多的连接(例如,

“这个函数让

员工产生并提交了 一 个比萨订单”),你也许就应该想想是不是要将它分解成多个更

简单的函数。否则,你将无法重用在 一 个函数中把所有步骤都混合在一起的代码。



大小:每一个函数应该相对较小。从前面的目标延伸过来就比较自然,但是如果函数 在显示器上需要翻几页才能看完,也许就到了应该把它分开的时候。特别是对于以简

单明了而著称的 Python 代码而言,一个过长或者有着深层嵌套的函数往往就是设计缺 陷的征兆。因此你需要保持简单,保持简短。



耦合性:避免直接改变其他模块文件中的变量。我们在第 17 章中介绍过这个概念,接 下来我们将在本书下一部分学习模块时再回顾它。作为参考,记住在文件间改变变昼

会导致模块文件间的耦合性,就像全局变蜇产生函数间的耦合 一 样:这会让模块难以 理解和重用。你应该尽可能通过函数来访问,而不是直接使用赋值语句。 图 19-1 总结了函数与外部世界通信的方法。输人可能来自左侧的组件,而结果能以右侧的 任意 一 种形式输出。优秀的函数设计者倾向千只使用参数作为输人,以及只使用 return 语 句作为输出。 当然,前面的设计法则有很多特例,其中一些与 Python 对 OOP 的支持相关。我们将在第

六部分看到, Python 的类依赖千修改传入的可变对象:类的函数通过修改自动传人的 self 参数的屈性,来修改每个对象的状态信息(例如, self.name='bob') 。另外,如果没有使 用类,全局变朵通常是模块中函数保持调用中单副本状态的最佳方式。只有意料之外的副 作用才是危险的。

不过通常来讲,我们应尽可能地把函数和其他编程组件对外部的依赖性最小化。函数的自

包含性越好,它就越容易被理解、复用和修改。

函数的高级话题

I

551

,

输入

输出

.

I I I 全局变量 』 寸一?仁 文件/流 I 皿

修1

返回语句

可变参数

文件/流 』

; __ _, - —- -_ ,, ',、 、 -

T

-

-

- -

|'

,、

i



其他函数

图 19-1: 涵数的执行环境。尽管滔数可以通过多种方法获得输入产生输出,但是使用参数作

为输入, return 语句并配合可变参数的改变作为输出肘,函数往往更容易理解和维护。同肘在 Python 3.X 中(不适用千 2.X) ,输出也可以是外层滔数作用域中被特殊声明过的非局部变量

递归函数 我们在第 9 章中介绍核心类型的比较时提到过递归。在第 17 章开始部分讨论作用域规则的 时候,我们也简单说过 Python 支持递归函数,即直接或间接地调用自身以进行循环的由数。 在本小节中,我们将探索它在函数代码中的样子。 递归是略显高级的话题,并且它在 Python 中相对罕见,一部分是因为 Python 的过程式语

句包括较为简单的循环结构。然而,它仍是一项应该了解的实用技术,因为它允许程序遍 历拥有任意的、不可预知构型和深度的结构(例如,计划旅行路线、分析语言、使用爬虫 在网页上爬链接)。递归甚至是简单循环和迭代的替代品,尽管它不 一 定是最简单或最高 效的一种。

用递归求和 让我们来看一些例子。要对一个数字列表(或者其他序列)求和,我们可以使用内置的 sum

函数,或者自己编写一个更加定制化的版本。下面是用递归编写的一个定制求和函数的示例:

>» def mysum(L): if not L: return o else: return L[O) + mysum(L[1:])

>» mysum([1, 2, 3, 4, s]) 15

552

I

第 19 章

# Call myse[Jrecursively

在每 一 层,这个函数都递归地调用自己来计算列表剩余值的和,这个和随后加到前面的一 项中。当列表变为空的时候,递归链路结束并返回 0 。当以这种方式使用递归的时候,对千

函数调用的每 一 个打开的层级来说,在运行时调用栈上都有自己的一个函数局部作用域的 副本,也就是说,这意味着 L 在每个层级都是不同的。

如果这很难理解(对于新程序员来说,常常难以理解),尝试给函数添加 一 个 L 的打印并 再次运行它,从而在每个调用层级记录下当前的列表: >、,[[[

(( ))LuU, f s dm2345 L L :... mypie sis[45 mtor unne e y , 't2l > > ' , ', > > rf1( tee 0L nn3 r r .. +

1 ·^-、丿

mysum( L

一『

[[rL1

l

-L

, 4, s-、丿

1,l u345 m,'l , 5 12345]5

--



r t

# Trace recursive levels # L shorter at each level

,l

正如你所看到的,在每个递归层级上,要加和的列表变得越来越小,直到它变为空邑~

归循环结束。最终的和随着递归调用在返回结果上的展开而计算出来。

编码替代方案 有趣的是,我们可以使用 Python 的三元 if/else 表达式(在第 12 章介绍过)在这里保存

某些代码资产。我们也可以针对任何可加和的类型一般化(就像第 18 章最小值的示例中那 样,我们这里也假设输入不为空来简化问题),也就是通过使用 Python 3 . X 的扩展序列赋

值(见第 11 章),使分离第一 项和序列剩余项变得更简单:

def mysum(L): return o if not L else L[O] + mysum(L[1:])

# Use ternary expression

def mysum(L): return L[O) if len(l) == 1 else L[O) + mysum(l[1:]) #Anytype,assumeone def mysum(L): first, *rest = L return first i f not rest else first + mysum(rest)

# Use 3.X ext seq assign

其中后两个函数会由千传入空的列表而失败,不过好处是它们能作用千支持+操作的任何 对象类型,而不只是数字 :

»> mysum([1])

# mysum({/) fails in last 2

1

函数的高级话题

I

553

»> u,ysum([1, 2, 3, 4, s]) 15

>» mys um(('s','p','a','m'))

' spam , »> mysum(['spam','ham','eggs'])

# But various types now work

'spamhameggs' 你可以亲自运行这些函数来加深理解。如果研究这三个变体,你将会发现:



后两者在输入单个字符串作为参数时也有效(例如 mysum

('spam')) ,因为 字 符串是

单字符字符串的序列。



第三种变体在任意可迭代对象上都有效,包括打开的输入文件 (mysum(open(name))), 但前两种则不行,因为它们使用了索引(第 14 章介绍了文件对象上的扩展序列赋值语

句)。



如果把函数头部改为 def mysum(first, * rest) ,尽管看上去类似千第 三 种变体,但 根本没法工作,因为它期待多个单独的参数,而不是单个可迭代对象。

别忘了,递归可以是直接的,就像目前为止给出的例子 一 样,也可以是间接的,就像下面

的例子一 样( 一 个函数调用另 一 个函数,后者反过来调用其调用者)。最终的效果是相同的, 尽管这里的每个层级都有两个函数调用:

»> def mysum(L): if not L: return o return nonempty(L)

#Calla function that calls me

»> def nonempty(L): return l[ o] + mys um(l[ 1:])



mysum([l 心 2.2,

# Indirectly recursive

3.3, 4.4])

11.0

循环语句 vs 递归 尽管递归对上一 小节的求和的例子有效,但在这一场景中,它可能过于追求技巧了 。 实际上, 递归在 Python 中并不像在 Prolog 或 Lisp 这种更加深奥的语 言 中那样常用,因为 Python 强

调像循环这样的简单的过程式语句,循环语旬通常更为自然。例如, while 常常使得事情 更为具体 一 些,并且它不需要定义支持递归调用的函数:

>» L = [1, 2, 3, 4, 5] »> sum = o »> while L: sum += L[o] L = L[1:]

»> sum 15

554

I

第 19 章

更好的是, for 循坏会自动为我们进行迭代,让我们在很多情况下不必使用递归(而且在

几乎所有情况下,递归在内存空间和执行时间方面都效率低下)

>» L = (1, 2, 3, 4, 5] >» sum = o »> for x in L: sum += x >» sum 15 有了循环语句,我们不需要在调用栈上为每次迭代都保留一个局部作用域的副本,并避免

一般的函数调用相关的开销(在第 2 1 章的时候,我们将通过 一个计时器案例来学习比较这 样的替代方案的执行时间)。

处理任意结构 另 一 方面,递归(以及我们马上会看到的等价的显式基千栈的算法)能够遍历任意形状的 结构。作为递归在这种场景中应用的一个简单例子,考虑像下面这样的 一 个任务:计算 一

个嵌套的子列表结构中所有数字的总和:

[1, [2, [3, 4], sl, 6, [7, Bl]

#

Arbitrarily nested sublists

简单的循环语句在这里不起作用,因为这不是一个线性迭代。嵌套的循环语句也不够用, 因为子列表可能嵌套到任意的深度并且以任意的构型嵌套一没有什么方法可以知道要编 写多少嵌套循环来处理所有的情况。相反,下面的代码使用递归来对应这种一般性的嵌套, 以便顺序访问子列表: # file sumtree.py

def sumtree(L): tot= o for x in L: if not isinstance(x, list): tot+= X else: tot+= sumtree(x) return tot

L = [1, [2, [3, 41, print(sumtree(L))

SL

6, [7, 8]]

If

For each item ar this level

#

Add numbers directly

#

Recur for sublists

#

Arbitrary nesting

# Prints 36

# Pathological cases

print(sumtree([1, [2, [3, [4, [S]]]]])) print(sumtree([[[[[1], 2], 3), 41, s]))

If If

Prims 15 (righ动eavy) Prints /5 (left-hem'y)

留意这段脚本末尾的测试案例,看看递归是如何遍历其嵌套的列表的。

函数的高级话题

I

555

喔严

递归 vs 队列和栈 有时理解这一点是很有帮助的:在内部, Python 通过在每一次递归调用时把信息压人调用 栈顶来实现递归,因此它记住了在什么地方返回以及在什么地方稍后继续。事实上,不使 用递归调用而实现递归风格的过程式编程一般来讲也是可能的,你可使用自己的显式的栈 或队列追踪剩余步骤。 例如,下面的代码与上一个例子一样来计算和,但是当它要访问调用主体中的项时,使用 了一个显式列表来安排进度;列表中最前面的一项总是接下来被运算与求和:

def sumtree(L): tot= o items= list(L) while items: front= items.pop(o) if not isinstance(front, list): tot+= front else: items.extend(front) return tot

# Breadth-first, explicit queue # Start with copy of top level # Fetch/delete front item # Add numbers directly # sys.getrecursionlimit() 1000 »> sys.setrecursionlimit(10000) »> help(sys.setrecursionlimit)

#

!000 calls deep default

# Allow deeper nesting # Read more about it

这里允许设置的最大值在各个平台上有所不同。不过,对千那些使用栈或队列的程序就无 需考虑这一点,同时这类程序也能更好地控制遍历过程。

更多递归示例 尽管本小节的这个例子是刻意编写的,但它是 一 类更大的程序的代表;例如,继承树和模 块导入链拥有类似的通用结构,同时计算全排列这样的任务也需要用到任意多层嵌套循环

的结构。实际上,我们在本书后面更为实用的示例中将再次使用递归:



在第 20 章的 permute.py 中,打乱任意序列。



在第 25 章的 reloadall.py 中,遍历导入链。



在第 29 章的 classtree .py 中,遍历类继承树。



在第 3l 章的 lister.py 中,同样遍历类继承树。



在附录 D 的本书该部分的两个练习解答中:倒计时和阶乘。

这些用法中的第二 个和第 三 个也会检查已访问的状态来避免循环和重复。尽管出千简单性 和高效率的考虑,我们在处理线性迭代任务时,通常更推荐使用简单循环语句而不是递归, 但我们还是会发现递归有着像后面的示例一样的不可替代的应用场景。 此外,有时候需要意识到程序中无意隐含的递归的潜在性。正如你将在本书后面看到的,

类中的一些运算符重载方法,例如—setattr—、—getattribute—甚至是—repr—,在 使用不正确的情况下,都有潜在的可能会造成循环递归。递归是 一种强大的工具,但只有 在你理解并能掌控它的时候才能发挥出巨大的威力!

函数对象:属性和注解 Python 函数比我们想象得更为灵活。正如我们在本书的这一部分所看到的, Python 中的闭 数不仅是一个编译器的代码生成规范: Python 中的函数是不折不扣的对象,其本身全部存

储在内存块中。同样,它们可以在程序中自由地传递以及间接调用。它们也支持与调用几

乎无关的操作,例如属性存储和注解。

558

I

第 19 章

间接函数调用:

“一等“对象

由于 Python 由数是对象,我们可以编写通用的程序来处理它们。泊数对象可以赋值给其他 的名称、传递给其他函数、嵌入到数据结构中、从 一 个函数返回给另 一 个由数,等等,就

好像它们是简单的数字或字符串。函数对象同时能支持 一个特殊操作:它们可以由 一 个函 数表达式后面的括号中的列表参数调用。然而,函数和其他对象 一 样,属千同 一 个通用的

类别。 这通常被称为一 等对象模型;它在 Python 中是普遍存在的,也是函数式编程的 一个必要部分。 我们将在本章和下 一章更加完整地探索这一编程模式;因为它的主旨是建立在函数调用概 念的基础之上,所以函数必须被当作数据进行对待 。 我们在前面的例子中已经学习了 一 些函数的常规使用,但这里通过一个快速回顾来帮助强

调对象模型。例如, def 语句中的名称并没有什么特殊含义:它只是当前作用域中的 一个 变最赋值,就好像它出现在=符号的左边一 样。在 def 运行之后,函数名仅仅是 一 个对象 的引用-我们可以自由地把这个对象赋给其他的名称并且通过任何引用来调用它:

»> def echo(message):

#

Name echo assigned to ftmction object

print(message)

» > echo('Direct call') Direct call

>» x = echo >» x ('Indirect call!')

# Call object through original name

II Now x references the Junction too # Call object through name by adding ()

Indirect call! 由千参数通过赋值对象来传递,因此你可以很容易地把泊数作为参数传入其他函数中。在 下面的例子中、被调函数只需把参数添加到括号中来调用传人的函数:

»> def indirect(func, arg): func(arg)

»> indirect(echo,'Argument call!') Argument call!

# Call the passed-in object by adding() #

Pass the f1111ctio11 to another function

我们甚至可以把函数对象存入数据结构中,就好像它们是整数或字符串 一 样。例如,下面 的程序把函数两次嵌套到 一 个元组列表中,作为 一 种动作表。 Python 复合类型可以包含任 意类型的对象,这里并不是一种特例:

»> schedule = [ (echo,'Spam!'), (echo,'Ham!') ] >» for (func, arg) in schedule: func(arg)

# Ca/lfi'unc1ions embedded in containers

Spam! Ham!

函数的高级话题

I

559

这段代码只是遍历 schedule 列表,每次遍历的时候使用一个参数来调用 echo 函数(注意 for 循环头部的元组解包赋值,我们在第 13 章中介绍过)。正如第 17 章的示例所示,函 数也可以被创建并返回,以便在其他地方使用,而在这一模式下创建的闭包也能保留外层 作用域中的状态:

»> def make(label): # Make a function but don't call it def echo(message): print(label +':'+ message) return echo »> F = make('Spam') >>> F('Ham!') Spam:Ham! »> F('Eggs I') Spam:Eggs!

#

#

Label in enclosing scope is retained Ca// the junction that make returned

Python 通用的一等对象模型和无须类型声明使该编程语言有了很强的灵活性。

函数自省 由千函数是对象,我们可以用常规的对象工具来处理函数。实际上,函数比我们所预期的

更灵活。例如, 一且 创建一个函数,就可以像往常一样调用它:

»> def func(a): b ='spam' return b * a »> func(8) 'spamspamspamspamspamspamspamspam' 但是,调用表达式只是定义在函数对象上工作的一个操作。我们也可以通用地检查它们的 属性(如下代码在 Python 3 . 3 坏境下运行,但 Python 2.X 中的结果与之类似)

» > func. name 'func' >» dir(func) ['_annotations_','_call_','_class_','_closure_','_code_', ... more omitted: 34 total... , , repr ','_setattr_','_sizeof_','_str_','_subclasshook_' —一——一—, _ _,_ _ ] 自省工具也允许我们探索实现细节。例如,函数已经附加了代码对象,代码对象提供了诸 如函数局部变址和参数等方面的细节:

»> func. code

»> dir{func._code_J ['_class_','_delattr_',' _ dir_',' _ doc_ ' , ' _eq_, ——— 一一一一— ... more omitted: 3 7 total...

s6o

I

第 19 章

- format — ,

__ge_ '

,

, , , _ ,'co_cellvars','co_code','co_consts','co_filename co_argcount _ ' `_' '_ , . _ , ', , ' lnotab ,'co_ ·freevars' ,'co_kwonlyargcount flal!s'.'co _Jreevars 'co firstlineno'.'co ineno','co_flags','co , ' , ','co_stacksize' ' , ' , ` ames'] nlocals names','co name','co co_name','co_names','co_nlocals','co_stacksize','co_varn

»> func. code.co varnames ('a','b') >» func. _code_. co_argcount 1

工具编写者可以利用这些信息来管理函数(实际上,我们还将在第 39 章的装饰器中实现对

函数参数的验证)。

函数属性 函数对象不仅限千前面小节中列出的系统定义的属性。正如我们在第 17 章中所学习到的,

Python 2.1 之后也可能向函数附加任意的用户定义的属性: »> func

>» func. count = o >» func. count += 1 >» func. count 1

»> func.handles ='Button-Press' » > func. handles 'Button-Press' »> dir(func) ['_annotations _','_call_','_class_','_closure_','_code_' , ' , ... and more: in 3.X all others have double underscores so your names won't clash... str _','. subclasshook' ,'count','handles'] 如果你去阅读 Python 自身标准库的代码实现,会发现存储在函数中的数据都遵循了一定的 命名规范,这些规范能保证函数内的数据不会与你可能用到的数据名称相冲突。在 3.X 版

本中,所有函数的内部名称都有前置和末尾的双下划线(“—X_") ;虽然 2.X 版本遵循 同样的方案,但是也会赋值一些以 “func_X" 开始的名称:

c:\code> PY -3 »> def f(): pass »> dir(f) ... run on your own to see... »> len(dir(f)) 34

>» [x for x in dir(f) if not x.startswith('_')] []

c: \code> PY -2 »> def f(): pass >» dir(f) .. . run on your own to see...

函数的高级话题

I

s61

>» len(dir(f)) 31

»> [x for x in dir(f) if not x.startswith('_')] ['func_closure','func_code','func_defaults','func_dict','func_doc', 'func_globals','func_name') 如果你很小心,不以这种方式来命名属性,那么你就可以安全地使用函数的名称空间,就 好像它是你自己的名称空间或作用域一样。 正如我们在第 17 章中看到的,这样的属性可以用来直接把状态信息附加到函数对象,而不

必使用全局、非局部和类等其他技术。和非局部不同,这样的属性可以从函数自身所在的 任何地方被访问,甚至可以从其代码外部访问。

从某种意义上讲,这也是模拟其他语言中的“静态局部变址”的一种方式一一这种变址的 名称对千一个函数来说是局部的,但是,其值在函数退出后仍然保留。属性与对象相关而

不是与作用域相关(并且必须通过其代码内部的函数名来引用),但总的效果是类似的。 此外,正如我们在第 17 章所学. 当属性附加千通过其 他工厂函数生成的函数之上时,它们 也支持多个副本、基千调用以及可 写 的状态记忆,非常像非局部闭包和类实例属性 。

Python 3.X 中的函数注解 在 Python 3.X 中(但不包括 Python 2.X) ,也可以给函数对象附加注解信息,即与函数的

参数和结果相关的任意用户定义的数据。 Python 为声明注解提供了特殊的语法,但是, 它自身不做任何事情 1 注解完全是可选的,并且,出现的时候只是直接附加到函数对象的

_annotations

属性以供其他工具使用。例如,这些工具也许会在错误检测的场景中使用

注解。 我们在上一章中介绍了 Python 3.X 的 keyword-only 参数;注解则进一步使函数头部的语法

通用化。考虑如下的不带注解的函数,它编写为带有 3 个参数并且返回一个结果:

>» def func(a, b, c): return a+ b + c

»> func(1, 2, 3) 6

从语法上讲,由数注解编 写在 def 头部行,作为与参数和返回值相关的任意表达式 。对千

参数,它们出现在紧随参数名之后的冒号之后;对千返回值,它们编写千紧跟在参数列表 之后的一个->之后。例如,以下这段代码注解了前面函数的 3 个参数及其返回值:

»> def func(a:'spam', b: (1, 10), c: float) -> int: return a+ b + c

>» func(1, 2, 3) 6

s62

I

第 19 章

调用一个注解过的函数,像以前一样,不过,当注解出现的时候, Python 将它们收集到字 典中并将它们附加给函数对象本身。参数名变成键,如果编写了返回值注解的话,它存储 在键 '`return” 下(这已足够使用,因为这 一 保留字不能用作参数名称),而注解键的值赋 给了注解表达式的结果:

>» func. annotations {'c': ,'b': (1, 10),'a':'spam','return': } 由于注解只是附加到一个 Python 对象的 Python 对象,因而可以袚直接处理。下面的例子 只是注解了 3 个参数中的两个,并且能通用地遍历附加的注解:

»> def func(a:'spam', b, c: 99): return a+ b + c »> func(1, 2, 3) 6

» > func. annotations {'c': 99,'a':'spam'} »> for arg in func._annotations_: print(arg,'=>', func._annotations_[arg]) C => 99 a=> spam

这里有两点值得注意。首先,如果编写了注解的话,仍然可以对参数使用默认值一—注解(以 及:字符)出现在默认值(以及=字符)之前。例如,下面的 a:'spam'= 4 意味着参数 a

的默认值是 4, 并且用字符串 'spam' 注解它:

>» def func(a:'spam'= 4, b: (1, 10) = 5, c: float = 6) -> int: return a+ b + c >» func(1, 2, 3) 6 »> func() # 4 + 5 + 6 (all defaulrs) 15 >» func(1, c=lO) # 1 + 5 + 10 (keywords work normally) 16 >» func. annotations {'c': ,'b': (1, 10),'a':'spam','return': } 其次,还要注意前面例子中的空格都是可选的 一~尔可以在函数头部的各部分之间使用空 格,也可以不用,但省略它们对某些读者来说可能会降低代码的可读性(对于另 一 些人来

说可能会提高!) :

>» def func(a:'spam'=4, b:(1,10)=5, c:float=6)->int: return a+ b + c >» func(1, 2) #1+2+6 9 >» func. annotations {'c': ,'b': (1, 10),'a':'spam','return': } 函数的高级话题

I

563

注解是 Python 3.X 中的新功能,它的一些潜在的用途还有待开发。很容易想象,注解可以

用作参数类型或值的特定限制,并且较大的 API 可能使用这一功能作为注册函数接口信息 的方式。 实际上,我们将在第 39 章中看到 一 个潜在的应用,那里,我们将看到注解作为函数装饰器

参数的一种替代方法。函数装饰器是一个更为通用的概念,其中,信息编写千函数头部之外, 因此不仅限于 一 种用途。和 Python 自身 一样 ,注解是一 种需要你发挥想象力来加以利用的 工具。

最后注意,注解只在 def 语句中有效,在 lambda 表达式中无效,因为 lambda 的语法已经 限制了它所定义的函数工具的用法。下面我们就来介绍 lambda 语法。

匿名函数: lambda 除了 def 语句之外, Python 还提供了一种生成函数对象的表达式形式。由千它与 Lisp 语言

中的一个工具很相似,所以称为 lambda 注 1 。与 def 一样,这个表达式创建了 一个之后能 够调用的函数,但是它返回该函数本身而不是将其赋值给一个变量名。这也就是 lambda 有

时称为匿名(也就是没有函数名)函数的原因。实践中,它们常常以内联函数定义的形式 出现,或者用作推迟一 些代码的执行。

lambda 表达式基础 lambda 的一般形式是关键字 lambda 后面跟上一 个或多个参数(与一个 def 头部内用括号 括起来的参数列表极其相似),之后是一个冒号,再之后是一个表达式:

lambda argument1, argument2,... argumentN :expression using arguments 由 lambda 表达式所返回的函数对象与由 def 创建井赋值后的函数对象工作起来是完全一 样 的,但是 lambda 有一些不同之处让其在扮演特定的角色时相当有用。 lambda 是一个表达式,而不是语句。因为这一点, lambda 能够出现在 Python 语法不允



许 def 出现的地方。例如在一个列表字面量中或者函数调用的参数中。而使用 def 语句

虽然函数能通过名称引用,但是必须在其他地方创建。作为一个表达式, lambda 返回 一 个值(一个新的函数),可以选择性地被赋值给一个变量名。相反, def 语句总是需要 在头部将一个新的函数赋值给一 个名称,而不是将这个函数作为结果返回。

注 I:

lambda 容易让人们感到胆怯 。 这样的反应似乎来源于名称 “lambda" 本身 一 一个来自

Lisp 语言的名称,得名自 lambda 演算,而 lambda 演算是一种符号化逻辑 。但是在 Python 中 , 它只是语法上引入这类表达式的一个关键宇。抛开艰深的数学遗留问题, lambda 使用起

来比你想象得更问单。

s64

I

第 19 章



lambda 的主体是一个单独的表达式,-而不是一个代码块。 lambda 的主体简单得就好像 放在 def 主体的 return 语句中的代码一样。你可以直接将结果写成 一 个裸露的表达式,

而不是显式地返回。由于它局限于 一 个表达式,因此 lambda 通常要比 def 功能要小: 你只能在 lambda 主体中封装有限的逻辑,连 if 这样的语句都不能使用。这是有意设 计的,其目的是限制程序的嵌套: lambda 是一个为编写简单的函数而设计的,而 def 用来处理更大的任务。

除了这些差别, def 和 lambda 都能完成同样种类的工作。例如,我们见过如何使用 def 语 句创建函数:

»> def func(x, y, z): return x + y + z »> func(2, 3, 4) 9

不过,你可以使用 lambda 表达式达到相同的效果,通过显式地将结果赋值给一个变量名, 之后就能通过这个变量名调用这个函数:

» > f = lambda x, y, z : x + y + z »> f{2, 3, 4) 9 这里的 f 被赋值给 lambda 表达式创建的函数对象。这也是 def 的工作方式 , 只不过 def 的 赋值是自动的。 默认参数也能够在 lambda 参数中使用,就像在 def 中使用 一 样:

>>> x = (lambda a="fee", b="fie", c="foe": a+ b + c) >» x("wee") 'weefiefoe' 在 lambda 主体中的代码与 def 内的代码一样都遵循相同的作用域查找规则。 lambda 表达

式引入的 一 个局部作用域更像 一 个嵌套的 def 语句,会自动从外层函数、模块以及内置作

用域中(通过 LEGB 规则,并根据第 17 章 )查找变量名:

» > def knights(): title='Sir' action = (lambda x: title +''+ x) return action »> act = knights() »> msg = act('robin') >» msg 'Sir robin'

# Title in enclosing def scope # Return a function object # 'robin'passed to x

# act: a function, not its result » > act

函数的高级话题

I

565

在这个例子中,对千 Python 2.2 之前的发行版来说,变批名 title 的值通常会以默认参数

值的形式传人。如果忘记其中的原因,你可以复习第 17 章中与作用域相关的内容。

为什么使用 lambda 通常来说, lambda 起到了一 种函数速写的作用,让你可以在使用它的代码中内嵌 一 个函数 的定义。它们完全是可选的,你总能使用 def 来替代它们,而且如果你的函数需要 lambda

表达式不能轻易提供的完全语句的力最的话,你就应该使用 def 语句。但是当你只需要在 使用代码的地方内联地嵌入 一小段可执行代码时,它们会带来更简洁的代码结构。 例如 , 我们在稍后会看到的回调处理器,通常被编写成一 个嵌入在注册调用的参数列表中

的单行 lambda 表达式,而不是使用在文件其他地方的 一个 def 来定义,之后引用那个变批

名(作为示例,参看边栏“请留意: lambda 回调”)。 lambda 也常用来编写跳转表,也就是动作的列表或字典,能够按需执行相应的动作。如下 段代码所示:

L = [lambda x: x lambda x: x lambda x: x

** ** **

# lnline junccion definition

2, 3, 4]

If A list of three callable Junctions

for f in L: print(f(2))

#

Prints 4, 8, 16

print(L[0](3))

#

Prims 9

当需要把小段的可执行代码编写进 def 语句从语法上不能编写进的地方时, lambda 表达式

作为 def 的 一 种速写来说是最为有用的。例如,前面的代码片段,可以通过在列表字面扯 中嵌人 lambda 表达式来创建一个含有 三个函数的列表。 def 是不能在列表字面最中工作的,

因为它是一 个语句,而不是表达式。对等的 def 代码需要占用临时性函数名称(可能同其 他名称相冲突),而函数定义也会位千想要使用的上下文外(可能在百余行代码以外的地方)。

def fl(x): return x def f2(x): return x def f3(x): return x

566

** ** **

2 3

#

Define named functions

4

L = [fl, f2, f3]

II Reference by name

for f in L: print(f(2))

# Prints 4, 8, 16

print(L[o](3))

#

I

第 19 章

Prints 9

多分支 switch 语句:尾声 实际上,我们可以用 Python 中的字典或者其他的数据结构构建更多通用的动作表,来实现

想要的效果。下面是以交互式命令行给出的另 一 个例子:

»> key ='got' »> {'already': (lambda: 2 + 2), 'got': 'one':

* 4), ** 6)}[key]()

(lambda: 2 (lambda: 2

8

这里,当 Python 创建这个临时字典的时候,每个嵌套的 lambda 都生成井留下一个在之后 能够调用的函数。通过键索引来取回其中一个函数,而括号使取出的函数被调用。与在第

12 章中向你完全展示的 if 语句的扩展用法相比,这样编写代码可以使字典成为更加通用 的多路分支工具。

如果不采用 lambda 实现这种工作,你需要使用三个出现在文件中其他地方的 def 语句来替 代,也就是在函数被使用的字典之外 , 并通过名称来引用函数:

>» def fl(): return 2 + 2

» > def f2(): return 2 * 4 >» def f3(): return 2

**

6

»> key ='one' »> {'already': fl,'got': f2,'one': f3}[key]() 64

这也能实现相同的功能,但是你的 def 也许会出现在文件中的任意位置,即使它们只有很 少的代码。 lambda 表达式所提供的代码相邻性能够很好地处理那些只用在单一上下文中的

函数:如果这里的三个函数不会在其他的地方使用到,那么将它们的定义作为 lambda 嵌入 在字典中就是很合理的了。不仅如此, def 格式要求为这些小函数创建变量名,这些变扯

名也许会与这个文件中的其他变量名发生冲突(也可能不会,但总是有可能)注 20 lambda 也可以在函数调用参数列表里作为内联临时函数的定义,并且该函数在程序中不在

其他地方使用时也是很方便的。在本章稍后学习 map 时,我们会再介绍一 些例子。

注 2:

有一次 , 我的一个学生提出可以在这样的代码中跳过分发表字典 。

因为如果函数名称同

其字符串查询键相同的话,可以运行 eval (funcname) ()来触发调用。尽管这种方式在这 个例子中是对的而且有时也是有用的 , 但是和我们之前见到的一样(例扣,笫 10 章) , eval 相对慢一些(它必须编译和运行代码),也更不安全(你必须信任字符串来源)



更为基础地,在 Python 中跳跃表一般被归为多态方法分发.调用方法会基于对象的类型 做“正确的事情 ”。 想了斛为什么,继续阅读第六部分 。

函数的高级话题

I

567

如何(不)让 Python 代码变得晦涩难懂 由于 lambda 的主体必须是单个表达式(而不是一些语句),因此你只能将有限的逻辑封装 到一个 lambda 中。当然,如果你清楚自己在做什么,那么你就能把 Python 中大部分的语 句编写成基千表达式的等价代码。

例如,如果你希望在 lambda 函数体中进行 print,

可以在 Python 3.X 中直接编写

print(X) ,因为 3 .X 中的 print 是 一个调用表达式而不是语句。你也可以在 Python 2.X 或

3.X 中编写 sys.stdout.write(str(x)+'\n') 来确保它是一 个可移植的表达式(回忆第 11 章,其实这就是 print 实际上所做的)。类似地,要在一个 lambda 中嵌套选择逻辑,可以 使用第 12 章曾经介绍过的 if/else 三元表达式,或者对等的但需要些技巧的 and/or 组合。 正如之前学习过的,如下语句:

f1 .1e ae :b:c s

能够用以下两种大致等效的表达式来模拟:

b if a else c ((a and b) or c) 因为这些类似的表达式能够放在 lambda 中,所以它们能在 lambda 函数中实现选择逻辑:

»> lower = (lambda x, y: x if x < y else y) »> lower('bb','aa') aa »> lower('aa','bb') aa 此外,如果需要在 lamdba 函数中执行循环,你可以嵌入 map 调用或列表推导表达式(它们

是我们在上一章中见过的工具,将在本章及下一章进行复习)这样的工具来实现:

» > import sys »> showall = lambda x: list(map(sys.stdout.write, x)) »> t = showall(['spam\n','toast\n','eggs\n']) spam toast eggs »> showall = lambda x: [sys.stdout.write(line) for line in x] »> t = showall(('bright\n','side\n','of\n','life\n')) bright side of life »> showall = lambda x: [print(line, end='') for line in x] »> showall = lambda x: print(*x, sep='', end=·'')

568

1

第 19 章

# 3.X: must use list # 3.X: can use print

II Same: 3.X only # Same: 3.X only

使用表达式来模拟语旬有一个限制:例如,你不能直接达到 一 个赋值语句的效果,不过诸 如内置的 setattr 、命名空间中的_diet—和能在原位置修改的可变对象的方法等这类工 具有时可以替代语句,而且函数式编程技巧会带你深入到充满着复杂表达式的黑暗王国。 尽管我现在告诉了你这些技巧,但务必在万不得已的情况下才使用。不然 一不小心,它们 就会导致不可读(也称为晦涩难懂)的 Python 代码。 一般来说,简单胜千复杂,明确胜于 晦涩,而且 一个完整的语句要比神秘的表达式好。这就是为什么 lambda 仅限千表达式。如 果你有更复杂的代码要编写,可使用 def, 而 lambda 则针对较小的 一 段内联代码。从另 一 个方面来说,你会发现适度地使用这些技术也是很有用处的。

作用域: lambda 也能嵌套 lambda 主要受益于嵌套函数作用域查找(我们在第 17 章见到的 LEGB 规则中的 E) 。作

为 一 个复习,在下面的例子中, lambda 出现在 def 中(这是很典型的情况)。并且在外层 函数被调用的时候,嵌套在内的 lambda 能够获取到在外层函数作用域中变量名 x 的值:

>» def action(x): return (lambda y: x + y)

#

Make and return function, remember x

>» act = action(99) >» act

»> act(2) # Call what action returned 101

在上一章中关千嵌套函数作用域的讨论没有说明的一种情况,就是 lambda 也能够获取任意 外层 lambda 中的变量名。这种情况有些隐晦,但是想象一下,如果我们把上一个例子中的 def 换成 lambda:

»> action = (lambda x: (lambda y: x + y)) >» act = action(99) »> act(3) 102

>» {(lambda x: {lambda y: x + y))(99))(4) 103

这里嵌套的 lambda 结构让函数在被调用时创建一个函数。在以上两种情况中,嵌套的 lambda 代码都能够获取外层 lambda 函数中的变量 x 。这可以 工 作,但是这种代码相当令人

费解。出千对可读性的考虑, 一 般最好避免使用嵌套的 lambda 。

函数的高级话题

I

569

请留意: lambda 回调 lambda 的另一个常见应用就是为 Python 的 tkinter GUI API (这个模块在 Python 2.X 中叫作 Tkinter) 定义内联的回调函数。例如,如下的代码创达了一个按钮,这个 按钮在按下的时候会打印一行消息,假设 tkinter 在你的计算机上可用的话(它在

Windows 、 Mac 、 Linux 和其他操作系统上是默认可用的) :

import sys # Tkinter in 2.X from tkinter import Button, mainloop x = Button( text ='Press me', command=(lambda:sys.stdout.write('Spam\n'))) x.pack(} mainloop() #This may be optional in console mode

# 3.X: print()

这里,回调处理器是通过传递一个用 lambda 所生产的函数作为 command 的关键宇参 数来注册的。与 def 相比, lambda 的优势就是处理桉钮动作的代码都应被安排在这里, 并嵌入到按钮创建的调用中 。

实际上, lambda 直到事件发生时才会执行处理器。在按钮按下时, write 调用才会发 生,而不是在按钮创建时就发生,并当事件发生时有效地“知道”它应当写的宇符串 。 从 Python 2.2 开始,因为嵌套的函数作用域规则也迨用于 lambda, 所以 lambda 也可 以很容易地作为回调处理器来使用。 lambda 能自动查找编写时所在的函数中的变量名,

并且在绝大多数情况下,都不再需要传入默认参数。这使得我们能很容易地访问特殊 的 self 实例参数,因为 self 参数是外层的类方法函数中的局部变量(我们会在笫六 部分介绍更多关于类的内容) 。

class MyGui: def makewidgets(self): Button(command=(lambda: self.onPress("spam"))) def onPress(self, message): ... use message... 在 Python 的早期版本中,甚至连 self 都必须要作为戏认参数传入 lambda 中 。 我们 将在后面看到,那些带有_call_和绑定方法的类对象也经常扮演回调处理的角色,

你可以查看笫 30 章和笫 31 章中对这些内容的介绍 。

函数式编程工具 按照大多数的定义,今天的 Python 混合支持多种编程范式:过程式(使用基础的语句), 面向对象式(使用类)和函数式。对千函数式编程, Python 提供了一整套进行函数式编程

的内置工具一它们把函数作用千序列和其他可迭代对象。这套工具包括在一 个可迭代对

570

1

第 19 章

象的各项上调用函数的工具 (map) ,使用一个测试函数来过滤项的工具 (filter) ,还有

把函数作用在成对的项上来运行结果的工具 (reduce) 。 尽管有时边界有些模糊,根据绝大多数定义, Python 的函数式编程的兵器库里也包括一等 对象模型(前面介绍过)、嵌套作用域闭包、匿名函数 lambda 、生成器和推导语法(下一章)、

品数装饰器和类装饰器(本书最后的高级主题部分)。就目前而言,让我们以介绍自动应 用其他丞数到可迭代对象的内置函数来圆满结束本章吧。

在可迭代对象上映射函数: map 程序对列表和其他序列常常要做的 一 件事,就是对每一个元素都进行一个操作并把其结果

收集起来:例如在数据库的表中选择列,公司雇员的支付增额字段,解析邮件附件等。 Python 中有丰富的工具能够简化这些集合性操作的代码编写。例如,在下面的例子中,可

以简单地使用 for 循环来更新一个列表中所有的数字:

>» counters = [1, 2, 3, 4] >>>

» > updated = [] >» for x in counters : updated.append(x + 10)

#

Add JO 10 each item

»> updated [11, 12, 13, 14) 因为这样的操作十分常见, Python 也提供了内置工具来帮你完成大部分的工作。 map 函数

将被传入的函数作用到 一 个可迭代对象的每一个元素上,并且返回 包含了所有这些函数调 用结果的 一个列表。如下所示:

»> def inc(x): return x +

10

»> list(map(inc, counters))

# Function to be run

# Collect results

(11, 12, 13, 14) 我们在第 13 章和第 14 章中曾简短地介绍过 map, 在那里我们用它来实现对一个可迭代对 象中的每个元素都应用 一 个内置函数。这里,我们会传人 一 个自定义的函数来更加一般化 地使用它,也就是对列表中的每一个元素都应用这个函数: map 对列表中的每个元素都调

用了 inc 函数,并将所有的返回值收集到 一个新的列表中。别忘了, map 在 Python 3.X 中 是一个可迭代对象,所以在这里我们要使用一个 list 调用来强制它产生所有的结果以显示;

而这在 Python 2.X 中不是必需的(如果你忘了这一 点,请参阅第 14 章)。 由千 map 期待传入 一个函数并会应用这个函数,它也恰好是 lambda 常常出现的地方之一 :

>» list(map((lambda x: x + 3), counters)) [4,

s,

#

Function expression

6, 7)

函数的高级话题

1

571

在上面的例子中,函数会为 counters 列表中的每一个元素加 3 。因为这个函数不会在其他 的地方用到,所以将它写成了一行的 lambda 。因为这样使用 map 和 for 循环是等效的,稍 微多加一些代码,你就能编写一个自己的一般性映射工具了:

»> def mymap(func, seq): res= [] for x in seq: res.append(func(x)) return res 假设函数 inc 仍然像前面出现时那样,我们可以用内置函数或自己编写的等价形式将其映 射为一个序列(或其他可迭代对象)

»> list(map(inc, (1, 2, 3)))

# Built-in is an iterator

[11, 12, 13]

»> mymap(inc, [1, 2, 3))

# Ours builds a list (see generators)

[11, 12, 13] 然而,

map 是内置函数,它总是可用的,也总是以同样的方式工作,同时还有 一 些性能方

面的优势(在一些使用模式下它比自己手工编写的 for 循环更快,我们将在第 21 章证明这 一 点)。此外, map 还有比这里介绍的更高级的使用方法。例如,给定多个序列参数, map 会按照顺序,并行地从各个序列中逐项取出 一组又 一 组参数,然后传入函数中:

»> pow(3, 4)

# 3**4

81

»> list(map(pow, [1, 2, 3], [2, 3, 4]))

# 1**2, 2**3. 3**4

[1, 8, 81] 对千多个序列, map 期待将一个 N 参数的函数用千 N 序列。这里, pow 函数在每次调用中 都用到两个参数:传入 map 的每个序列中各取一个。代码中模拟这个多序列的普遍性也不 需要太多额外的工作。不过我们会把相关内容推迟到下 一章中,也就是当我们见过 一 些额 外的迭代工具后再做这件事。 map 调用同我们在第 14 章学习过的列表推导表达式较为相似,下 一 章将从函数式编程的视

角再次学习它;

»> list(map(inc, (1, 2, 3, 41)) [11, 12, 13, 14]

»> [inc(x) for x in (1, 2, 3, 4]]

# Use () parens ro generate items instead

[11, 12, 13, 14)

在某些情况下,目前 map 比列表推导运行起来更快(例如,当映射 一 个内置泊数时),而 且它所编写的代码也较少。另 一 方面, map 对每一 个元素都应用了函数调用而不是任意的 表达式,它是 一种不那么常规的工具,而且经常需要额外的帮助函数或 lambda 表达式。此 外,用圆括号而不是方括号来包围 一 个推导,能创建 一 个按需产生值的对象,这样既节省

了内存又减少了程序的禾响应时间,这 一 点与 3 . X 版本中的 map 十分相似。我们将在下一 章使用圆括号包围椎导表达式的方法。

572

1

第 19 章

选择可迭代对象中的元素: filter map 函数是 Python 函数式编程工具集中一个主要也相对明确的代表。它的近亲: filter 和 reduce 分别实现了基于一个测试函数选择可迭代对象的元素,以及向 “元素对“应用函数 的功能。

由千它也返回一个可迭代对象,在 3.X 版本中 filter (和 range 一样)需要一个 list 调用 来显示它的所有结果。例如,下面的缸ter 调用能挑出一个序列中大千零的元素:

»> list(range(-5, s))

#

An iterable in 3.X

#

An iterable in 3.X

[-5, -4, -3, -2, -1, o, 1, 2, 3, 4)

»> list(filter((lambda x: x > o), range(-5, s))) [1, 2, 3, 4)

之前在第 12 章的边栏我们简要提过 filter. 在第 14 章探索 3.X 可迭代对象时也遇见过它。

对千序列或可迭代对象中的元素,如果函数对该元素返回了 True 值,这个元素就会被加入 结果列表中。与 map 一样,这个函数也能粗略地用 一 个 for 循环来等效,但它是内置的、

简明的,通常也运行得更快:

>» res = [] »> for x in range(-5, 5): if X > O: res.append(x)

#

The statemem eq11ivale11t

»> res [1, 2, 3, 4] 还有一 点和 map 很像, filter 可以用列表推导语法来模拟,后者经常有更简单的结果(尤 其当它能避免创建一个新函数时),当我们希望延迟结果的产生时,也可以使用相似的生 成器表达式。不过我们将把这个故事剩下的部分留到下一章中讲述:

>» [x for x in range(-5, 5) if x > o] [1, 2, 3, 4]

#

Use () to generate items

合并可迭代对象中的元素: reduce 函数式 reduce 调用在 Python 2.X 中只是一个简单的内置函数,但是在 Python 3 . X 中则位 千 functools 模块中,而且变得更复杂。它接受井处理一个迭代器,但是 ,它自身不是一 个可迭代对象,它会返回 一 个单独的结果。下面是两个 reduce 调用,用千计算 一个列表中 所有元素加起来的和以及乘起来的乘积:

>>> from functools import reduce »> reduce((lambda x, y: x + y), [1, 2, 3, 4])

# Import in 3.X, not in 2.X

10

»> reduce((lambda x, y: x * y), [1, 2, 3, 4]) 函数的高级话题

I

s73

24 每 一 步, reduce 将当前的和或乘积以及列表中的下一个元素传给列出的 lambda 函数。在 默认条件下,序列中的第一个元素初始化了起始值。这里等效千对第一个调用的 for 循环, 在循环中使用了额外的代码:

>» L = [1,2,3,4) »> res = l[o] »> for x in l[1:]: res= res+ x »> res 10

不过编写自己的 reduce 版本其实相当直接。如下的函数不仅模拟了内置 reduce 函数的大 多数行为,而且可以帮助说明其一般性的操作:

»> def myreduce(function, sequence): tally= sequence[o] for next in sequence[1:]: tally= function(tally, next) return tally »> myreduce{{lambda x, y: x + y), [1, 15

»> myreduce{{lambda x, y: x

*

y), [1,

2,

3, 4, s])

2,

3, 4, s])

120

内置的 reduce 还允许将一 个可选的第三个参数放置千序列的各项之前,进而当序列为空时 充当一个默认的结果,但是,我们把这一扩展留作一个建议的练习。

如果这引发了你的兴趣,你或许也会对内置的 operator 模块感兴趣,其中提供了内置表达 式对应的函数,并且对千函数式工具来说,这个模块使用起来很方便(要了解关千这一模 块的更多内容,请参阅 Python 的库手册)。

»> import operator, functools »> functools.reduce(operator.add, [2, 4, 6])

# Function-based +

12

>» functools.reduce((lambda x, y: x + y), [2, 4, 6]) 12

总之, map 、 filter 和 reduce 支持强大的函数式编程技术。正如前面提到的,很多人也倾 向千把如下的工具归人 Python 的函数式编程工具集中,包括嵌套函数作用域闭包(也称为 函数工厂)和匿名函数 lambda (这两者在前面都讨论过),还包括生成器和推导语法,我 们在下一章会讨论这些主题。

574

l

第 19 章

本章小结 本章介绍了和函数相关的高级概念:递归函数,函数注解, lambda 表达式函数,常用函数 工具如 map 、 filter 、 reduce, 以及通用的函数设计思想。下一章将继续高级话题,介绍生 成器,并再次回顾可迭代对象及列表推导~些工具既与函数式编程相关又与循环语句 相关。在继续学习之前,请看本章的练习,以确保已经掌握了这里所介绍的概念。

本章习题 1.

lambda 表达式和 def 语句有什么关系?

2.

为什么要使用 lambda?

3.

请比较和区分 map 、 filter 和 reduce 。

4.

什么是函数注解,如何使用它们?

5.

什么是递归函数,如何使用它们?

6.

编写函数的通用设计规则是什么?

7.

说出三种或三种以上函数同调用者通信结果的方式。

习题解答 1.

lambda 和 def 都会创建函数对象,以便之后调用。不过,因为 lambda 是表达式,它会 返回 一个函数对象,而不是将这个对象赋值给一 个名称,同时它可以嵌入到由数定义 中那些 def 语法上无法出现的地方。 lambda 只允许单个隐式的返回值表达式。因为它 不支持语句代码块,因此,不适用千较大的函数。

2.

lambda 允许我们“内联“小单元的可执行代码,推迟其执行,并且以默认参数和外 层 作用域变量的形式为其提供状态。 lambda 的使用不是必需的,我们总是可以编写一条 def 来替代它,并且用名称来引用该函数。 lambda 很方便,以嵌套小段的推迟的代码,

这些代码将不会在程序的其他地方用到。它们通常出现在 GUI 这样基千回调的程序中, 井且与 map 和 filter 这些期待一个处理函数的函数式工具密切相关。

3.

这 3 个内置函数都对 一 个序列(或其他可迭代)对象应用另 一 个函数,并收集结果。 map 把每 一 项传递给函数并收集结果, filter 收集那些被函数返回 True 值的项,

reduce 通过对一 个累加器和后续项应用函数来计算一 个单独的值。和其他两个函数不 同, reduce 在 Python 3.X 的 functools 模块中可用,而不是在内置作用域中可用; reduce 在 2.X 中则是一 个内置函数。

函数的高级话题

1

575

4.

~数注解在 Python

3.X (3.0 及其以后的版本)中可用,并且是对函数参数及结果的语

法上的修饰,它会被收集到一个字典中并赋值给函数的_annotations_属性。 Python 在这些注解上没有放置语义含义,而是直接将其包装,以供其他工具潜在地使用。

5.

递归函数调用本身可以直接地或间接地进行,从而实现循环。它们不仅可以用来遍历

任意形状的结构,也可以用来进行一般性迭代(尽管后一种角色用循环语句来编写往 往更加简单高效)。递归经常被使用显式栈或队列的代码来模拟和替代,后者能获得

对遍历过程的更多控制。

6.

函数通常应该较小,尽可能自包含,拥有单一的、统一的用途,并且通过输人参数和 返回值与其他部分通信。如果你希望修改的话,它们可以使用可变的参数来与结果通信, 而一些类型的程序也隐含了其他通信机制。

7

函数可以通过 return 语句、改变传入的可变参数以及设置全局变最来传回结果。我们 通常不提倡使用全局变量(除非在一些很特殊的情况下,如多线程程序),因为它们 会让代码更加难以理解和使用。通常 return 语句是最佳的,但是如果你希望的话,改 变可变的对象也是不错的(甚至是有用的)。函数也能同诸如文件和套接字这样的系

统设备结果通信,不过这些内容超出了这里所讨论的范围。

576

1

第 19 章

第 20 章

推导和生成

本章将继续高级函数主题,并再次回顾第 4 章、第 14 章所介绍的推导和迭代概念。由于推

导既与上一章的函数工具(例如 map 和 filter) 相关,又与 for 循环相关,我们将在这里 再次复习它。我们还将回顾可迭代对象,以便学习生成器函数及其相关的生成器表达式:

这是 一种用户定义的按需产生结果的方式。 Python 中的迭代也适用于用户定义的类,但是,我们将推迟到第六部分介绍这一点,也就 是在学习运算符重载的时候再介绍。因为这是最后一次介绍内置的迭代工具,我们将概括 目前为止所遇到的各种工具。接下去的第 21 章将比较它们之间的一些相对性能,以作为一

个规模更大的案例分析的学习。在此之前,让我们先继续讲述推导和迭代的内容,并扩展 到值生成器的话题。

列表推导与函数式编程工具 正如本书开头所言, Python 支持面向过程、面向对象和函数式的编程范式。事实上, Python 拥有一 系列具有函数式本质的工具,就像前一章所提到的闭包、生成器、 lambda 表

达式、推导、映射、装饰器、函数对象以及更多。这些工具允许我们以更加强大的方式应 用并组合函数,并通常作为类和 OOP 的替代品,提供状态保持和代码编写解决方案。 例如,在上一章中我们学习了 map 和 filter 这样的函数式编程工具。作为 Python 早期的函 数式编程工具,它们借鉴自 Lisp 语言,作用是将操作映射到可迭代对象并收集最终的结果。

这是 Python 编程中 一种相当常见的任务, Python 最终产生了 一种新的表达式~lj 表推导, 它甚至比我们前面学习的工具更灵活。

在 Python 的演化史中,列表推导最初受到了函数式编程语言 Haskell 的启发,发端千

Python 2.0 版本。简而言之,列表推导把任意一个表达式而不是一个函数应用千 一 个迭代对

577

象中的元素。同样,它可以作为更加通用的工具。在之后的版本中,推导的潜力得到了更 大的挖掘~字典甚至我们本章将探索的值生成表达式。推导语法不再仅仅局 限于列表。 我们曾在第 4 章的预习中第一次遇到列表推导,并在第 14 章学习循环语句的时候对其进行

了进一步介绍。但是因为它们与 map 和 filter 这样的函数式编程工具相关,所以我们将在 本章回顾这一话题的内容。事实上,这个特性并没有与函数绑定在一起。正如我们所见到的, 列表推导可以成为一 个比 map 和 filter 更通用的工具,只不过有时候我们可以类比基千函 数的替代实现来更深入地理解它。

列表推导 vs

map

让我们举一个例子来说明基础知识吧。正如我们在第 7 章见到过的, Python 的内置 ord 函 数会返回一个单个字符的整数编码 (ch r 内置函数是它的逆过程,它将 一 个整数编码转换

为字符)如果你的字符恰好属千 ASCII 字符集的 7 位编码表示范围内的话,这一函数的返 回值恰好为该字符的 ASCII 编码:

>» ord ('s') 115 现在,假设我们希望收集整个字符串中的所有字符的 ASCII 编码。也许最直接的方法就是 使用一个简单的 for 循环,并将结果添加到列表中:

»> res = [] »> for x in'spam': res.append(ord(x))

# Manual results collection

»> res [115, 112, 97, 109] 既然我们现在知道了 map, 我们就能使用 一 个单个的函数调用达到相似的结果,而不必逐

项添加列表,从而实现起来更简单:

»> res = list(map(ord,'spam')) »> res [115, 112, 97, 109]

#

Apply junction to sequence (or other)

然而,我们还能使用列表推导表达式得到相同的结果—一相较千 map 把一 个函数映射遍 一

个可迭代对象,列表推导把一 个表达式映射遍一 个序列或其他可迭代对象:

>»res= [ord(x) for x in'spam'] »> res [115, 112, 97, 109]

# Apply expression to sequence (or other)

列表推导对一个序列中的值应用 一个任意表达式,将其结果收集到 一 个新的列表中并返回。

从语法上说,列表推导包括在 一 对方括号中,这是为了提醒你它们构造了一个列表。它们 578

1

第 20 章

的简单形式是在方括号中编写一个表达式,在后边跟随着的看起来就像一个 for 循环的头 部一样的语句,这个表达式和 for 循环头部中有着相同的变量名的变灶。 Python 之后将收

集这个隐含循环中每次迭代产生的结果。 上一个例子的效果与手动进行 for 循环和 map 调用相似。然而,当我们希望对一个可迭代 对象应用一个任意表达式(而非函数)的时候,列表推导可以变得更方便。

>» [x •• 2 for x in range(10)] [o, 1, 4, 9, 16, 2s, 36, 49, 64, 81] 这里,我们收集了 0~9 数字的平方(我们只是在交互式命令行下将它打印出来,如果你需

要保留它的话,可以把它赋值给 一 个变扯)。如果要用 map 调用实现相似的效果,我们可

能带要编写 一个小函数来实现平方操作。因为在其他的地方不需要这个函数,通常(但不 是必须)我们会通过使用 lambda 将其写成内联的形式,而不是另外编写 一 条 def 语句:

** 2), range(10))) [o, 1, 4, 9, 16, 2s, 36, 49, 64, 81]

»> list(map((lambda x: x

这能达到相同的效果,并且只比等效的列表推导编写稍多一点点的代码。不过它只是稍有

一 点复杂(当然一且在你理解了 lambda 后,就不那么复杂了)。然而对于更高级的表达式, 通常列表推导会使用更少的代码。下一小节将告诉你为什么会这样。

使用 filter 增加测试和循环嵌套 列表推导甚至要比现在所介绍的更通用。例如,我们在第 14 章中介绍过,可以在 for 之后 编写 一 个 if 分句,用来增加选择逻辑。使用了 if 分句的列表推导可以当成 一 种与上一章

讨论过的内置的 filter 类似的工具,它们会在 if 分句不是真的情况下跳过一些可迭代对象 的元素。

这里举一 个从 0~4 中选择出的偶数的例子。正如上一小节中的 map 可以替代列表推导,这 里的 filter 版本必须编写 一个小的 lambda 函数作为测试表达式。为了对比,这里也给出了

等效的 for 循环:

>» [x for x in range(5) if x % 2 [o, 2, 4] >» list(filter((lambda x: x % 2 [o, 2, 4]

== o]

== o), range(s)))

>» res = [] »> for x in range(5): if

X 为 2

== O:

res.append(x)

»> res [o, 2, 4] 推导和生成

1

579

所有的这些都是用了求余(求除法的余数)运算符%来检测该数是否是偶数。如果一个数 字除以 2 以后没有余数,它就一定是偶数。同样,这里的 filter 调用代码没有比列表推导

长很多。然而,我们可以在列表推导中添加一个 if 分句或者任意的表达式,从而使它只通 过单条表达式就能达到 filter 和 map 一样的效果:

>» [x ** 2 for x in range(lO) if x % 2 == o] [o, 4, 16, 36, 64] 这里,我们收集了 0~9 中偶数的平方。右边的 if 分句让 for 循环跳过值为假的数字,而左 边的表达式能够计算平方。与之等效的 map 调用将要做更多的工作:我们需要在 map 迭代

中组合 filter 选择过程,这使表达式明显变得更加复杂:

»> list(map((lambda x: x**2), filter((lambda x: x % 2 == o), range(10)))) [o, 4, 16, 36, 64]

标准推导语法 事实上,列表推导还能更加通用。在最简单的形式下,你必须 一 直编写一个增最表达式和 一个单独的 for 分句:

[ expression for target in iterable ] 尽管所有其他子句部分是可选的,它们的出现能让推导表达更加丰富的迭代过程一一你可 以在一个列表推导中编写任意数量的嵌套的 for 循环,并且每一 个都能有可选的关联的 if 测试(这些 if 测试的作用效果和 一个 filter 类似)。通用的列表推导的结构如下所示。

[ expression for target1 in iterable1 if condition1 for target2 in iterable2 if condition2... for targetN in iterableN if conditionN ] 集合与字典的推导以及马上要介绍的生成器表达式,使用了一样的语法,尽管它们使用的 是不同的括号字符(大括号或者可省略的小括号),而且字典推导以两个用冒号隔开的表

达式开始(分别代表键和值)。

我们在上一节中讨论了 if 筛选分句。当 for 分句嵌套在一 条列表推导中时,它们工作起来 就像等效的嵌套的 for 循环语句。例如:

»> res »> res

= [x + y for x in [o, 1, 2] for y in [100, 200, 300]]

[100, 200, 300, 101, 201, 301, 102, 202, 302] 这和下面更为冗长的代码有相同的效果:

>» res = [] »> for x in [o, 1, 2]: for yin [100, 200, 300]: res.append(x + y) 58O

I

第 20 章

>» res [100, 200, 300, 101, 201, 301, 102, ·202, 302) 尽管列表推导创建了结果列表,请记住它们能像任意的序列和其他可迭代类型一样进行迭 代。这里有一 小段类似的代码,能够遍历字符串而非数字列表,并收集它们拼接后的结果:

» > [ x + y for x in'spam'for y in'SPAM'] ('s5','sP','sA','sM','pS','pP','pA','pM', 'as'''aP'''aA'''aM'''ms' ' . mP'''mA','mM'l 每一个 for 分句都能带有 一个 if 筛选器,不论这个循环嵌套得有多深。不过下面代码的应 用场景在这个程度上,巳经开始变得越来越难以想象(跟多维数组如出一辙) :

>» [x + y for x in'spam'if x in'sm'for yin'SPAM'if yin ('P','A')) ['sP','sA','mP','mA'] >» [x + y + z for x in'spam'if x in'sm' for y in'SPAM'if y in ('P','A') for z in'123'if z >'1'] ['sP2','sP3','sA2','sA3','mP2','mP3','mA2','mA3'] 最后,下面是 一 个类似的列表推导,它说明了在嵌套的 for 分句中添加针对数值对象(而

非字符串) if 选择的效果:

»> [(x, y) for x in range(S) if x % 2 == o for y in range(S) if y % 2 == 1] [(o, 1), (o, 3), (2, 1), (2, 3), (4, 1), (4, 3)] 这个表达式组合了 0~4 中的偶数和 0~4 中的奇数。其中 if 分句在每次迭代中筛选出了所需 的元素。下面是等效的基千语句的代码:

>» res = [] >» for x in range(s): if X % 2 == O: for yin range(s): if y % 2 == 1: res.append((x, y)) »> res [(o, 1), (o, 3), (2, 1), (2, 3), (4, 1), (4, 3)] 回顾 一 下,如果你对一 个复杂的列表推导的作用感到困惑的话,你总能将列表推导的 for

和 if 分句进行相互嵌套(连续将后面的分句缩进到右边),从而得到等效的语句。虽然你 得到的结果要长得多,但是却更加清晰可读,尤其对那些与基本语旬类似的情形。 而与最后 一 个例子等效的 map 和 filter 的形式,往往会更复杂并带有深层的嵌套,所 以我 在这里不进行展示。这部分代码就留给大师、前 LISP 程序员或是代码狂魔们作为练习吧!

推导和生成

1

581

示例:列表推导与矩阵 当然,并非所有的列表推导都是刻意而为的。让我们看一个更高级的列表推导应用,来加 深理解。正如第 4 章、第 8 章中所展示的,使用 Python 编写矩阵(也称为多维数组)的一 个基本的方法就是使用嵌套的列表结构。例如,下面代码使用列表的列表定义了两个 3x3 的矩阵。

»> M = [[1, 2, 3), [4, 5, 6), [7, 8, 9))

»> N = [[2, 2, 2), [3, 3, 3), [4, 4, 4)1 对干这样的结构,我们总是能够使用正常的索引操作来获取一行,以及行中的 一 列: >,、,

]6] --,-M”5H 1 1 >4> >(>6

#

Row 2

l l -

2

』__

-

# Row 2, item 3

不过,列表推导也是处理这样结构的强大的工具,因为它会自动为我们扫描行和列。例如, 尽管这种结构按行存储了矩阵,为了选出第二列,我们能简单地进行按行迭代,之后提取 出所需要列中的元素,或者就像下面一样通过在行内的位置进行迭代:

»> [row[1] for

ro树 in

M]

#

Column 2

#

Using offsets

(2, 5, 8]

>» [l'l[row][1] for row in (o, 1, 2)] (2, 5, 8]

如果给定了位置,我们也能简单地执行像提取出对角线位置的元素这样的任务。下面的第 一个表达式使用 range 来生成列表的偏移拭,并且在之后使用相同 的偏移 摄来索引行和列 :

首先取出 M[o][o] ,之后是 M [1] [1] ,等等。第 二个表达式通过调整列坐标,取出了矩阵 的副对角线(我们假设矩阵有相同数目的行和列)

»> [1, »> [3,

[M[i][i] for i in range{len(M))] 5, 9] [M[i][len(M)-1-i] for i in range(len(H)}] 5, 7]

#

Diagonals

原位置修改这一矩阵需要对偏移量进行赋值(如果不是方矩阵,则需要使用两次 range)

»> L = ((1, 2, 3], (4, 5, 6]] »> for i in range(len(L)): for j in range(len(L[i])): L(i](j] += 10

582

I

第 20 章

# Update in place

»>

L

[[11, 12, 13], [14, 15, 16]] 我们不能指望用列表推导达到 一 模 一 样的效果,因为它们会生成新的列表。不过我们总可

以通过将结果赋值回原先的变盐名,以达到类似的效果。例如,我们可以对矩阵中的每个 元素都应用 一 个操作,井将结果整理成一个单行向扯或者 一个相同形状的矩阵:

» > [ col + 10 for row in M for col in row] 1 斗 14,

[11, 12,

# Assign to M to retain new value

15, 16, 17, 18, 19]

+ 10 for col in row] for row in 川 [[11, 12, 13], [14, 15, 16], [17, 18, 19]]

» > [[ col

为了理解这些列表推导语句,我们可以将它们翻译回等效的简单语句形式,让右侧的部分 拥有更多的缩进(作为下面例子中的第一 个循环),井且当推导袚嵌套在左侧时,创建 一

个新的列表(就像下面的第 二个循环)。正如其等效的简单语句所示,上面例子中的第 二 条语句之所以能够正常运行,原因在千对行的循环是一 个外部的循环:对千每 一行,它将

运行嵌套的列循环来创建结果矩阵中的 一 行:

»> res = [] »> for row in M:

#

for col in r叩: res.append(col + 10)

Statement equivalents

It Indent parts further right

»> res [11, 12, 13, 14, 15, 16, 17, 18, 19)

>» res >» for

= [] r叩 in M: tmp = [] for col in r01111: tmp.append(col + 10) res.append(tmp)

# Left-nesting starts new list

»> res [[11, 12, 13], [14, 15, 16], [17, 18, 19]] 最后,稍加 一 点创意,我们也可以用列表推导来合并多个矩阵中的值。下面的首行代码创

建了 一 个单层的列表,其中包含了矩阵相应位置元素的乘积,然后通过嵌套的列表推导来

构建具有相同值的 一 个嵌套列表结构:

»> M [[1, 2, 3), [4,

»>

s,

6), [7, 8, 9))

N

[[2, 2, 2), [3, 3, 3), [4, 4, 4))

»>

[M[ro树][ col] * N(ro忖][ col] for [2, 4, 6, 12, 15, 18, 28, 32, 36]

ro何 in

range(3) for col in range(3)]

»> [[M[row][col] * N[row][col] for col in range(3)] for

ro碱 in

range(3)]

[[2, 4, 6), [12, 15, 18), [28, 32, 36))

推导和生成

I

583

最后一个表达式是有效的,因为对行的循环又一次是外层的循环,它等效千如下基千语句 的代码:

res = [] for row in range(3): tmp = [] for col in range(3): tmp.append(M[row][col] * N[row][col]) res.append(tmp) 更有趣的是,我们可以用 zip 配对两个矩阵中要相乘的对应元素——下面的列表推导与循

环语句形式都产生了和上面例子中一样的嵌套列表逐对相乘的结果(而且因为 zip 在 3.X 中是一个值生成器,这些语句要比看上去的更加高效) :

[[coll* col2 for (coll, col2) in zip(rowl, row2)] for (rowl, row2) in zip(M, N)] res= [] for (rowl, row2) in zip(M, N): tmp = [] for (coll, col2) in zip(rowl, row2): tmp.append(coll * col2) res .append(tmp) 列表推导版本与其等效的基于语句代码相比,只需要一行代码,并且可能对大型矩阵来说,

运行得更快,而且形式上也越来越难以理解。这就引出了下一小节的内容。

不要滥用列表推导:简单胜千复杂 (KISS) 译注 l 拥有了这样的通用性,列表推导能很容易变得难以理解,特别是在嵌套的情况下。一些编

程任务本身就很复杂,而我们却无法进一 步简化它们(参阅接下来的全排列例子)。类似 列表推导这样的工具在使用得当时功能强大,而你在脚本中使用它们也是无可厚非的。 但与此同时,上一节中的代码让嵌套的复杂性更上一层楼,而且实话实说,这种炫技般的 复杂性有时制造了一种代码越是晦涩程序员的水平越显得高深的假象。由千这类工具倾向

千吸引一部分人,因此我必须在此澄清它们的适用范围。 本书中出千教学目的展示了高级列表推导例子,但在实际应用中,在不必要的情况下使用

复杂和有技巧性的代码不但在工程意义上糟糕的,而且违背软件公共利益。重申第一章的 一句话:编程不是为了 炫耀聪明和晦涩,而是为了让你的代码 清楚地表达它们的目的。 或者,援引 Python 的 import this 中的座右铭:

简单胜于复杂。

译注 1: KISS (Keep It Simple and Stupid) ,中文为简单原则 。

584

1

第 20 章

编写复杂的列表推导代码也许是一种学术式的乐趣,但在那些终有一天要被他人阅读的代 码中亳无意义。

因此,如果你刚开始编写 Python, 我建议使用简单的 for 循环,并在特定合适的情况下使

用推导或者 map 。

“保持简洁“法则就在这里生效了,像往常一样:代码的可读性比它的

精简性更重要。如果你需要将代码转换成语句来理解它们,那么你一开始就应当使用语句。 换句话说,古老的 KISS 原则依旧适用~ "愚蠢"。

另一方面:性能、简洁性、表现力 尽管如此,在这种情况下,目前额外的复杂度能带来可观的性能优势:基千对运行在目前 Python 下的测试, map 调用比等效的 for 循环要快两倍,而列表推导往往比 map 调用还要 稍快 一 些。这种速度上的差距通常随着实际代码和 Python 版本而不同,不过一般是由千 map 和列表推导在解释器中以 C 语言的速度来运行的,因此比在 Python 虚拟机 (PYM) 中 以步进运行的 for 循环代码要快得多。 另一方面,列表推导赋予了代码迷人甚至是必要的简洁性,使代码在行数上打折,但其意 思却百分百地保留了下来。此外,许多人也认为代码的表现力与可理解性也得到了提升。

因为 map 和列表推导都是表达式,从语法上来说,它们能在 for 循环语句不够出现的地方 使用。例如,在一个 lambda l'E! 数的主体中或者是在一个列表或字典字面批中。 因此, map 和列表推导值得你去学习并适用于简单的迭代,尤其是你的程序对运行速度要

求较高的话。然而,由于 for 循环让逻辑变得更清晰,我们通常因为其简单而推荐使用, 它们常用来编写更直接的代码。在使用时,你应该尽量让 map 调用和列表推导保持简单。 对千更复杂的任务,你应当换用完整的语句。

注意:正如之前所说,这里提到的性能结论的 一般化取决于调用模式以及 Python 自身的优化和 改变。例如,最近的 Python 版本加快了简单 for 循环语句的速度。不过在某些代码中, 列表推导仍然很显著地快千 for 循环甚至快千 map, 而 map 会在必须使用函数调用或内

置函数的情况下胜出。而上面这种速度比较的结果不是随意作出的——你可以使用标准 库中 的 time 模块中的工具对这些 代码进行计时,包括在 2.4 版本中新增的 time it 模块, 或者请继续阅读下一 章,在那里我们会证实上面那段话中的观点。

请留意:列表推导和 map 这里介绍一些实际应用中史现实的列表推导和 map 的例子。我们在笫 14 章中用列表

推导轩决过笫一个问题,在这里将复习它并增加等价的基于 map 的替代方案。回顾文

件的 read lines 方法将返回以换行符\ n 结束的行(下面的代码假设当前目录中存在 一个三行的文本的文件)

:

推导和生成

I

585

>» open('myfile'). readlines () ['aaa\n','bbb\n ' ,'ccc\n'] 如果你不想要每行行末的换行符,可以使用列表推导或 map 调用在一个步骤之内从所

有的行中将它们都去掉 (map 的返回结果在 Python 3.X 中是可迭代对象,因此,我们 必须在 list 调用中运行它们以一次性看到其所有结果) :

»> [line.rstrip() for line in open('myfile').readlines()] ['aaa','bbb','ccc']

»> [line.rstrip() for line in open('myfile ' )] ['aaa','bbb','ccc']

>» list(map((lambda line: line.rstrip()), open('myfile'))) ['aaa','bbb','ccc'] 这里最后两个使用了文件迭代器;正如笫 14 章中介绍的 , 这意味着你在这种迭代上 下文中不需要额外的方法调用就能读取文件的每一行 。 map 调用的代码要比列表推导

稍长一些,但是无论哪种方法都不需要显式地进行结果列表的创建 。 列表推导还能作为一种列投影操作来使用 。 Python 的标准 SQL 数据库 APl 将查询结 果按照如下格式返回:数据库中的一个表是一个列表,数据库中的一个行就是一个元 组 , 而数据库中每列的值就是元组中的元素:

>» listoftuple" [('bob', 35,'mgr'), ('sue', 40,'dev')] 一个 for 徙环能够手动从选定的列中提取出所有的值 , 但是 map 和列表推导只需一步

就能做到这一点,并且速度更快:

»> [age for (name, age, job} in listoftuple] (35, 40)

»> list(map((lambda row: row[1]), listoftuple)) (35, 40) 笫一种方法利用元组赋值来斛包列表中的行元组,笫二种方法使用索引 。 在 Python 2.X (但不包括 Python 3 . X, 参阅笫 18 章关于 Python 2. X 参 数觥包的说明)中 , map 也 可以对其参数使用元组斛包:

# 2.X only

»> list(map((lambda (name, age, job}: age), listoftuple}} [35, 40] 更多关于 Python 的数据库 API 讨 参 考其他的书箱和 资源。 除了运行 函 数和表达式之间的 区 别 、 Python 3 . X 中的 map 和列表推导的最大区别是: map 是一个可迭代对象 , 按需产生结果 ;

为了同样达到内存节雀与运 行 叶间加快 , 列

表推导必须 编写 为生成器 表 达式 , 后者也是 本 章 的 核 心主题 之一 。

586

1

第 20 章

生成器函数与表达式 如今 Python 对延迟提供了更多的支持:它提供了在需要的时候才产生结果的工具,而不是 立即产生结果。我们已经在内置工具看到了这种设计趋势:文件对象按需逐行读取,而 3.X 中类似干 map 和 zip 的函数则按需产生元素。然而这种惰性求值并不局限千 Python 自身的 内置工具。具体来说,下面介绍的两种语言特性也能让用户定义的操作推迟结果的计算:



生成器函数( 在 2 .3 版本及之后可以使用) :使用常规的 def 语句进行编 写,但是使用

yield 语句 一 次返回 一个结果,在每次结果产生之间挂起和恢复它们的状态。



生成器表达式(在 2.4 版本及以后可以使用) :类似于上一小节的列表推导,但是 ,它 们返回按需产生结果的 一 个对象,而不是创建一个结果列表。

由千 二 者都不会 一 次性地创建 一 个列表,它们节省了内存空间,井且允许计算时间分摊到 各次结果请求上。我们将看到,这 二者最终都通过实现我们在第 14 章所介绍的迭代协议来 施展它们延迟结果的魔法。

这些功能井不是全新的(生成器表达式早在 Python 2.2 中就可以使用),而且在今天的 Python 代码中相当常见 。 Python 的生成器概念大量借鉴了其他的编程语言,尤其是 Icon 。

如果你只熟悉简单的编程模型的话,那么它们第一眼看上去确实不符合常规。尽管如此, 你会发现生成器在适当情况下是一 种很强大的工具。此外,由千它们是泊数、列表推导和

迭代思想的一种自然的扩展,因此你实际上已经掌握了 一 些编写生成器的知识。

生成器函数: yield vs return 在本书的这一部分中,我们已经学习了编写接收输入参数并立即传回单个结果的常规函数。 然而,我们也能编写 一 种可以传回 一 个值并随后从其挂起的地方继续的函数。这样的函数

叫作生成器函数(在 Python 2.X 和 3.X 均可使用),因为它们随着时间产生 一 系列的值。

一 般来说,生成器函数和常规函数 一 样,事实上也是用常规的 def 语句编写的。然而,当 创建时,它们被特殊地编译成 一 个支持迭代协议的对象。并且在调用的时候它们不会返回

一 个结果:它们返回 一 个可以出现在任何迭代上下文中的结果生成器。我们在第 14 章学习 了可迭代对象,而图 14-l 也总结了它们的操作 。这里,我们将再次回顾它们,看看它们是

如何与生成器相关的。

状态挂起 和返回 一 个值并退出的常规函数不同,生成器函数能够自动挂起并在生成值的时刻恢复之

前的状态并继续函数的执行。因而,它们能很方便地替代提前计算整个一 系列值或是在类 中手动保存和在类中恢复状态的方式。由千生成器函数在挂起时保存的状态包含它们的代 码位置和整个局部作用域,因此当面数恢复时,它们的局部变盐保持了信息井且使其可用。

推导和生成

1

587

生成器函数和常规由数之间主要的代码不同之处在千,生成器函数产生 (yield) 一 个值,

而不是返回 (return) 一个值。 yield 语句会挂起该函数并向调用者传回 一 个值,但同时也 保留了足够的状态使函数能从它离开的地方继续。当继续时 , 函数在上一个 yield 传回后 立即继续执行 。 从函数的角度来看,这允许其代码随着时间产生 一 系列的值,而不是 一 次 性全部计算出它们并在诸如列表的内容中传回它们。

与迭代协议集成 要真正地理解生成器函数,你需要知道它们与 Python 中的迭代协议的概念密切相关 。 正如 我们已经看到的,迭代器对象定义了 一个_next_方法(在 2.X 中为 next) ,它要么返回

迭代中的下 一 项,要么引发 一个特殊的 Stopiteration 异常来终止迭代 。一个可迭代对象 的迭代器用 iter 内置函数来接收值,而对于自身就是迭代器的可迭代对象而言这一步骤是

空操作。 如果支持该协议的话, Python 的 for 循环以及其他的迭代上下文,会使用这种迭代协议来 遍历 一 个序列或值生成器(如果不支持,迭代则返回重复索引的序列)。任何支持这 一 接 口的对象都能在所有的迭代工具中工作。 为了支持这一协议,函数必须包含 一 条 yield 语句,该函数将袚特别编译为 生成器:它们

不再是普通的函数,而是作为通过特定的迭代协议方法来返回对象的函数。当调用时,它

们返回 一 个生成器对象,该对象支持用 一 个自动创建的名为—next —的方法接口,来开始 或恢复执行。 生成器函数也可以有一 条 return 语句,不过总是出现在 def 语句块的末尾,用千终止值的

生成:从技术上讲,可以在任何常规函数退出动作之后,引发一个 Stopiteration 异常来 终止值的生成。从调用者的角度来看,生成器的_next_方法将恢复函数执行并且运行到

下一个 yield 结果的传回或引发一 个 Stop Iteration 异常 。 最终效果就是生成器函数,被编写为包含 yield 语句的 def 语句,能自动地支持迭代协议,

并且可以用在任何迭代上下文中以随着时间根据需要产生结果。

注意:正如第 14 章所提到的, 在 Python 2.X 和更早的版本中,迭代器对象定义了 一 个名为 next 的方法而不是_next —方法。这包括了我们在这里使用的生成器对象。在 Python

3 .X 中,这个方法被重新命名为_next_。 next 内置函数作为 一个方便的、可移植的工 具,在用法上: next(!) 等 同千 Python 3.X 中的 !. _next_()和 Python 2 . 6 与 2.7 中的 I.next( )。在 Python 2.6 之前,程序直接调用 I.next() 而不是手动地迭代。

生成器函数的应用 为了讲清楚基础知识,请看如下代码,它定义了 一 个 生 成器函数,这个函数随 着 时间会不 断地生成一 系列的数字的平方:

588

I

第 20 章

»> def gensquares(N): for i in range(N): yield i ** 2

#

Resume here later

这个函数在每次循环时都会 yield 一 个值,之后将其返还给它的调用者。当它被暂停后, 它的上一个状态保存了下来,包括变量 i 和 N, 井且在 yield 语句之后被马上收走控制权。 例如,当用在一个 for 循环中时,第一 次迭代开始函数的运行并得到它的第一 个结果,在 循环中每一 次执行函数的 yield 语句时,控制权都会再返还给函数:

»> for i in gensquares(S): print(i, end=':')

# Resume the function #

Print last yielded value

0 : 1 : 4 : 9 : 16 : >> >

为了终止值的生成,函数可以使用 一 个无值的返回语句,或者在函数主体最后简单地让控 制权脱离。 对大多数人来说,这一过程第一 眼看来似乎有些隐晦(要不就是特别神奇)。它确实有点绕。

如果你想看看在 for 里面发生了什么,可以直接调用生成器函数:

>» x = gensquares(4) >» X

你得到的是一 个生成器对象,它支持第 14 章中介绍的迭代器协议:生成器由数自动被编译

为生成器并被返回。也就是说,被返回的生成器对象有 一 个—next —方法,该方法可以开 始这个函数,或者从它上次 yield 值后的地方恢复,并且在得到 一 系列值的最后一个时,

引发 Stoplteration 异常。为了方便起见,在 Python 3.X 中 next(X) 内置函数(在 2.X 版 本中为 X.next( ))为我们调用了对象 x 的 x._next—()方法:

»> next(x)

# Same as x._next_J) in 3.X



>» next(x)

# Use x.next() or next() in 2.X

1

»> next(x) 4

>» next(x) 9

»> next(x) Traceback (most recent call last): File "", line 1, in Stoplteration

推导和生成

1

589

正如我们在第 14 章学习过的 for 循环(以及其他的迭代上下文),以同样的方式与生成器 一 起工作:通过重复调用_next _方法,直到捕获一个异常。对千 一 个生成器,其结果就

是随着时间产生值。如果被迭代的对象不支持迭代协议,那么 for 循坏将使用索引协议作 为替代。

注意,顶层的属千迭代协议的 iter 调用在这里不衙要写出,因为可迭代对象就是它们自身

的迭代器,且只支持单次的主动扫描。换句话说,生成器将它们自己返回给 iter 方法,因 为它们直接支持 next 方法。这在本章后面我们将遇到的生成器表达式中也是成立的(之后 将详细讨论) :

»> y = gensquares(S) >» iter(y) is y

# Returns a generator which is its own iterator # iter() is not required: a no-op here

True

»> next(y)

# Can run next()immediately



为什么要使用生成器函数? 在给出了生成器基础代码后,你可能会好奇为什么你之前从未想过要编写 一 个生成器。在 这节的例子中,我们也可以创建一 个一 次就产生所有值的列表:

»> def buildsquares(n): res=[] for i in range(n): res.append(i return res

**

2)

»> for x in buildsquares(s): print(x, end=':') 0 : 1 : 4 : 9 : 16 :

对千这样的例子,我们还可以使用 for 循环、 map 或者列表推导的技术来实现:

** 2 for n in range(S)]: print(x, end=':')

»> for x in [n

0 : 1 : 4 : 9 : 16 :

>» for x in map((lambda n: n

**

2), range(s)):

print(x, end=':') 0 : 1 : 4 : 9 : 16 : 然而,生成器对千大型程序而言,在内存使用和性能方面都更好。它们允许函数避免预先 做好所有的工作,在结果的列表很大或者在处理每一 个结果都需要很多时间时,这 一 点尤

其有用。生成器将产生一 系列值的时间分散到每一次的循环迭代中 去。 尽管如此,对于更多高级的应用,它们能为在每次迭代间手动存储对象状态的需求提供了

s9o

I

第 20 章

一 种更加简洁的替代方案。有了生成器,函数作用域中的变址就能自动地保存和恢复注 lo 我们将在本书的第六部分讨论基于类实现的可迭代对象。 生成器函数的应用场景比我们目前为止所介绍的更加广泛。它们可以处理或是返回任何类

型的对象,而且还能像可迭代对象那样出现在第 14 章的迭代上下文中,这些上下文包括 tuple 调用、枚举和字典推导:

»> def ups(line): for sub in line.split(','): yield sub. upper()

#

>>>让 ple(ups('aaa, bbb, ccc'))

# All

Substring generator iterario11 conrerts

('AAA','BBB','CCC')

»> {i: s for (i, s) in enumerate(ups('aaa,bbb,ccc'))} {o:'AAA', 1:'BBB', 2:'CCC'} 马上我们会看到具有同样价值的生成器表达式:这是一个能够将函数的灵活性与推导语法 的简洁性结合到 一 起的工具。在本章后面我们也将看到生成器有时能让不可能成为可能, 通过把不能 一 次性产生的结果集合分次产生出来。不过,先让我们来探索 一 些生成器函数 的高级特性吧。

扩展生成器函数协议: send

vs next

在 Python 2.5 中,生成器函数协议中增加了 一 个新的 send 方法。 send 方法生成一系列结

果的下 一 个元素,这一 点就像—next —方法一 样,但是它也提供了一种调用者与生成器之 间进行通信的方式,从而能够影响生成器的操作。 从技术上来讲, yield 现在是一个表达式的形式,而不再是 一 条语句,它会返回发送给 send 泊数的元素(尽管它可以通过两种方式袚调用:包括 yield X 或 A = (yield X)) 。

表达式必须包含在括号中,除非它是赋值语句右边的唯一一项。例如, X = yield Y 是对的,

X

=

(yield Y) + 42 也是对的。

当使用这 一 额外的协议时,值可以通过调用 G.send(value) 发送给一 个生成器 G 。之后恢 复生成器代码的执行,并且生成器中的 yield 表达式返回了发送给 send 函数的值。如果提

前调用了正常的 G._next—()方法(或者其等价的 next (G)), yield 则返回 None, 例如:

注 I:

有趣的是,生成器函数也是一种“穷人的“多线程机制:它们通过把操作拆分到每一次的 yield 之间,从而将一个函数的执行交错地插入它的调用者的工作中 。 然而生成器并不是 线程.程序的运行仍旧在一个单线程的控制内,被显式地交给函数或从函数收走 。 从某种 意义上说,线程更加通用(新线程中的程序可以真正独立地运行并将结果提交到一个队列

中)

, 但是生成器则史易于编写 。 关于 Python 的多线程编程工具,参阅笫 17 幸的脚注 。

注意.因为控制权是显式地在 yield 和 next 讲用时传递的,生成器也同样不是回溯而是 与共行柱序联系紧密 一更详细的概念已经超出了本章的范围 。

推导和生成

I

s91

»> def gen(): for i in range(lO): X = yield i print(X)

>» G = gen() >» next(G)

#

Must call next() first, to start generator



>» G.send(77)

# Advance, and send value to yield expression

77 1

»> G.send(88) 88 2

>» next(G)



# next() and X. next_J) send None

None 3

例如,采用 send 方法可以编写一个能够被它的调用者终止的生成器,或是一个能够被重新 定位内部处理的数据的生成器。

此外,在 2.5 及之后的版本中,生成器还支持 一 个 throw(type) 方法,它将在生成器内 部最后一个 yield 时产生一个异常以及一个 close 方法,从而在生成器内部引发一个新的

GeneratorExit 异常来彻底终止迭代。这些都是我们这里不会深入学习的 一些高级特性 ; 请查看 Python 的标准库或继续阅读本书第七部分关千异常的内容来获得更多的细节。

注意,尽管 Python 3.X 提供了一个 next(X) 方便的内置函数,它会调用一个对象的 X. —

next_方法,但是,其他的生成器方法,例如 send ,必须直接作为生成器对象的方法来调用(例 如, G.send(X)) 。这么做是有意义的,因为这些额外的方法只是在内置的生成器对象上被

实现,而—next_方法适用千所有的可迭代对象(包括内仅类型和用户定义的类)。 同时注意 Python 3.3 引入了 yield 的一种扩展:一种 “from 分句"。它允许生成器来委托

嵌套的生成器。因为这是 一 个已经相当高级的话题,我们将给出 一个边栏来讨论它,并在 那里再介绍另一个跟它相似的工具。

生成器表达式:当可迭代对象遇见推导语法 因为生成器函数的推迟求值如此重要,以至千它最终扩展到了其他的工具。在 Python 2.X 和 3.X 中,可迭代对象和列表推导的概念孕育了一种新的工具:生成器表达式。从语法上

来讲,生成器表达式就像 一 般的列表推导一样,而且也支持所有列表推导的语法(包栝 if 过滤器和循环嵌套),但它们是包括在圆括号中而不是方括号中的(跟元组一样,它们的 圆栝号通常是可选的) :

>» [x ** 2 for x in range(4)] [o, 1, 4, 9]

s92

I

第 20 章

#

List comprehension:

加ild

a list

»> (x ** 2 for x in range(4)) # Generator expression: make an iterable » list(x ** 2 for x in range(4)) [o, 1, 4, 9]

# List comprehension equivalence

然而,从执行过程上来讲,生成器表达式很不相同:它不是在内存中构建结果,而是返回

一 个生成器对象-~一 个自动被创建的可迭代对象。这个可迭代对象会支持迭代协议,井 在任意的迭代语境中产生一个结果列表。可迭代对象在激活时也持有生成器的状态



就是上面代码中的变扯 x 和生成器的代码运行位置。 最终的作用很像生成器函数,但是是在列表推导表达式的上下文中:我们拿到了 一 个在每

次返回一个结果后还能记住它所生成位置的对象。同生成器函数 一 样,通过观察这些对象 在它们自动支持的协议中的运行,我们能更好地理解它们;与前面一 样,这里也不需要写 出 iter 调用:

»> G = (x ** 2 for x in range(4)) >» iter(G) is G True >» next(G)

#

iter(G) optional:

— iter_

returns self

# Generator objects: automatic methods



>» next(G) 1

>» next(G) 4

»> next(G) 9 »> next(G) Traceback (most recent call last): File "", line 1, in Stop Iteration »> G

同样,我们一 般不会在生成器表达式的底层看到 next 迭代器机制的使用,因为 for 循环会 自动触发:

>» for num in (x ** 2 for x in range(4)): print('%s, %s'% (num, num / 2.0))

#

Calls next() automatically

0149 0024 0505

,','

...

推导和生成

I

s93

如前所述,每一个迭代上下文都是如此,包括 for 循环, sum 、 map 和 sorted 等内置由数、 列表推导,以及第 14 章中的其他迭代上下文,例如 any 、 all 和 list 内置函数等。作为可 迭代对象,生成器表达式可以出现在以上的任何迭代上下文中,就像生成器函数调用的结 果那样。

例如,下面的代码将生成器表达式应用到字符串的 join 方法调用和元组赋值中,两者均为 迭代上下文。在第一 个尝试中, join 运行生成器,并把产生的子串直接拼接起来:

>»''.join(x.upper{) for x in'aaa,bbb,ccc'.split{',')) 'AAABBBCCC'

>» a, b, c = (x +'\n'for x in'aaa,bbb,ccc'.split(',')) »> a, C ('aaa\n','ccc\n') 注意,上面的 join 调用不需要在生成器外面额外套 一 层圆括号。从语法上说,如果生成器 表达式包在其他的括号之内,比如在函数调用之中,生成器自身的括号就不是必须的。然而,

在下面第 二个 sorted 调用中,还是需要额外的括号:

>» sum(x

**

2 for x in range(4))

#

Parens optional

14

»> sorted(x ** 2 for x in range(4)) [o, 1, 4, 9] »> sorted((x ** 2 for x in range(4)), reverse=True) [9, 4, 1, o]

# Parens optional # Parens optional

就像元组中可选的圆括号,生成器圆括号的使用也没有统一的规则。不过生成器表达式不 像元组那样的其他固定集合对象有着明确的角色,因而在这里使用额外的括号看上去会稍

显古怪。

为什么要使用生成器表达式 就像生成器函数,生成器表达式是一 种对内存空间的优化:它们不需要像方括号的列表推 导一样, 一 次构造出整个结果列表。与生成器函数一样,它们将生成结果的过程拆分成更 小的时间片:它们会 一 部分 一 部分地产生结果,而不是让调用者在 一 次调用中等待整个集 合被创建出来。

另 一 方面,生成器表达式在实际中运行起来可能比列表推导稍慢 一 些,所以它们可能只对 那些结果集合非常大的运算或者不能等待全部数据产生的应用来说是最优选择。关千性能 的更权威的评价,必须等到我们在下 一 章编写计时脚本的时候给出。

尽管这是个很主观的表述,但生成器表达式也提供了 一 种编码优势,如下一 小节所示。

生成器表达式 vs map 一种发现生成器表达式优点的方式是把它们和其他由数式工具进行比较,就像我们说明列

594

I

第 20 章

表推导时所做的那样。例如,生成器表达式通常等效千 3.X 版本中的 map 调用,因为它们 都是按需生成元素。不过与列表推导一样,生成器表达式在所应用的操作不是函数调用的

时候更易千编写。在 2 . X 版本中, map 会产生临时的列表,生成器却没有,不过我们仍能 进行比较 :

>» (1, >» (1, »> [2, »> [2,

list(map(abs, (-1, -2, 3, 4))) 2, 3, 4] list(abs(x) for x in (-1, -2, 3, 4)) 2, 3, 4] list(map(lambda x: x * 2, (1, 2, 3, 4))) 4, 6, 8] list(x * 2 for x in (1, 2, 3, 4)) 4, 6, 8]

#

Map function on tuple

II Generator expression #

No1if11nctio11 case

#

Simpler as generator?

同样的情况对我们之前遇到的 join 调用的文本处理例子也成立一—-一 个列表推导要为结果 创建额外的临时列表,这对不需要保存列表的本例来说,是没有意义的。而且在要被完成 的操作不是函数调用的情况下, map 调用相较千生成器表达式语法在简洁性上也会袚减分:

>» line ='aaa,bbb,ccc' »>''.join([x.upper() for x in line.split(',')])

# Makes a pointless list

'AAABBBCCC'

»>''.join(x.upper() for x in line.split(','))

-# Generates results

'AAABBBCCC'

»>''.join(map(str.upper, line.split(',')))

#

Generates results

#

Simpler as generator?

'AAABBBCCC'

»> ". join(x * 2 for x in line. split(',')) 'aaaaaabbbbbbcccccc' >»''.join(map(lambda x: x * 2, line.split(','))) 'aaaaaabbbbbbcccccc'

map 与生成器表达式都可以进行任意嵌套,从而支持程序中的更 一般使用需求,而且需要

一个 list 调用或者其他迭代上下文来开始产生值的过程。例如,下面的列表推导与 3.X 版 本中的 map 产生了相同的结果,但创建了两个实际列表;而后面两种写法每次只产生 一 个

整数,其中的嵌套生成器形式能更加清楚地表达其目的 :

>» [x * 2 for x in [abs(x) for x in (-1, -2, 3, 4)]] [2, 4, 6, 8]

#

Nested comprehensions

»> list(map(lambda x: x * 2, map(abs, (-1, -2, 3, 4)))) [2, 4, 6, 8]

If Nested maps

»> list(x * 2 for x in (abs(x) for x in (-1, -2, 3, 4))) [2, 4, 6, 8]

#

Nested generators

尽管这 三 者的效果都是将操作进行组合,但生成器却可以不用创建临时列表。在 3 . X 版本 中,下面的例子同时嵌套井结合了生成器~而 map 又 被 list 唯一 激活:

推导和生成

I

595

»> import math »> list(map(math.sqrt, (x [o.o, 1.0, 2.0, 3.0)

**

2 for x in range(4))))

#

Nested combinations

从技术上讲,上面右边的 range 在 3.X 版本中也是一个值生成器,被生成器表达式自身所 激活:每个值的产生要经过三个层次,由内向外按需产生,井且由千 Python 的迭代工具和 协议而能“恰好完成工作“。实际上,生成器嵌套可以任意混合,且深度不限,尽管其中

的一些看上去更加有效:

»> list(map(abs, map(abs, map(abs, ( -1, O, 1))))) # Nesting gone bad? (1, o, 1] >» list(abs(x) for x in (abs(x) for x in (abs(x) for x in (-1, o, 1)))) (1, o, 1] 最后的这几个例子展示了生成器的通用性,但也被故意编写成一种复杂的形式以强调生成 器表达式可能会像之前提到的列表推导一样被滥用一和往常一样,你应当尽可能使它们

保持简单,除非它们必须变得非常复杂,我们也将在本章末尾回顾该主题。 如果使用得当,生成器表达式可以将列表推导强有力的表达性与其他可迭代对象的时间空

间优势结合起来。例如在这里,非嵌套的方式提供了更简单的解决方案,却依然利用了生 成器的优势-一书妦召 Python 的格言,

"扁平胜千嵌套”

>» list(abs{x) * 2 for x in (-1, -2, 3, 4)) [2, 4, 6, 8] »> list(math.sqrt(x ** 2) for x in range(4)) [o.o, 1.0, 2.0, 3.0] >» list(abs(x) for x in (-1, o, 1)) [1, o, 1]

生成器表达式 vs

#

Unnested equivalents

# Flat is often better

filter

生成器表达式也支持所有常见列表推导的语法一—包括与之前遇到的 filter 函数调用类似 的 if 分句。因为 filter 在 3.X 版本中是 一个能够按需产生值的可迭代对象, 一个带有 if

分句的生成器表达式与它在操作上是等价的(在 2.X 中, filter 会产生一 个临时列表而生 成器却没有,不过两者仍然具有比较价值)。同样,下面的 join 调用足以使所有的形式都 返回它们的结果:

>» line ='aa bbb c' »> •'.join(x for x in line.split() if len(x) > 1) 'aabbb' >» ".join(filter(lambda x: len(x) > 1, line.split())) 'aabbb'

# Generator with'if #

Similanofilrer

在这里,生成器看上去比 filter 稍微简单。然而要对 filter 调用的结果进行操作的话,还 需要借助 map 调用,这一点使得 filter 比一个生成器表达式更加复杂:

»>''.join(x.upper() for x in line.split() if ~en(x) > 1) 'AABBB'

596

I

第 20 章

»>''.join(map(str.upper, filter(lambda x: len(x) > 1, line.split()))) 'AABBB'

最终效果是,生成器表达式对 3.X 中的可迭代对象的操作类似于 map 和 filter 调用,在 2.X

中则是由列表推导处理这些调用的列表产生形式。生成器提供了不依赖千函数的更通用的 代码结构,同时延迟了结果的产生 。 正如列表推导 ,一 个生成器表达式总是存在等价的基 千语句的形式,尽管后者常常比前者需要更多的代码来表述:

»>''.join(x.upper() for x in line.split() if len(x) > 1) 'AABBB'

>» res = ', »> for x in line.split(): if len(x) > 1: res += x.upper()

# #

Statement equivalent? This is also a join

>» res 'AABBB' 在这个例子中,表达式形式不完全和生成器表达式一样一一它不能每次产生 一 个元素,因

而它也模拟了 join 调用强制所有结果袚 一次产生的效果。而真正与一个生成器表达式等价 的代码,是下面一 节将要介绍的 一 个带有 yield 语句的生成器函数。

生成器函数 vs 生成器表达式 让我们回顾一下到目前为止本章所介绍的内容:

生成器函数 一 个使用了 yield 表达式的 def 语句是 一 个生成器函数。当被调用时,它返回 一 个 新的生成器对象,该对象能自动保存局部作用域和代码执行位置;一个自动创建的 _iter_方法能够返回自身; 一 个自动创建的_next_方法(在 2.X 中为 next) 用千 启动函数或从上次退出的地方继续执行,并在结果产生结束的时候引发 Stop Iteration 异常。

生成器表达式 一 个包括在圆括号中的列表推导表达式被称为一个生成器表达式。当它运行时,会返 回 一 个新的生成器对象,这个对象带有同样是被自动创建的方法接口和状态保持, 一

同作为生成器函数调用的结果一一包括 一 个返回自身_ iter _方法和 一 个启动隐式循 坏或从上次运行离开的地方重新开始的

next

方法(在 2.X 中是 next) ,并且在结

束产生结果的时候引发 Stop Iteration 异常。 最终的结果是,两者都可以在能主动调用这些接口的迭代上下文中自动按需产生结果。 如上 一章所述,同样的迭代可以同时用 一 个生成器函数或一个生成器表达式编 写 。例如,

如下的生成器表达式,把一 个字 符串中的每个字母重复 4 次 。

推导和生成

I

597

>» G = (c * 4 for c »> list(G)

in'SP店')

# #

Generaror expression Force generaror 10 produce all res11/rs

['5555','PPPP','AAAA','MMMM'] 等价的生成器函数需要稍微多 一 些的代码,不过作为 一 个含有多条语句的函数,如果需要

的话,它将能编写更多的逻辑并使用更多的状态信息。事实上,这与上一章中 lambda 和 def 语句之间的取舍 一 样,即在表达式的简洁性和语句的表现力之间进行选择:

»> def timesfour{S): for c in S: yield c * 4 >» G = times four('spam') »> list(G) ['ssss','pppp','aaaa ,'mmmm']

.

#

Generaror f1111crion

#Iterate automatically

对用户而 言 ,两者的相似性大于差异。表达式和函数都支持自动迭代和手动迭代:上面的 list 自动调用迭代,下面的迭代手动进行:

>» G = (c * 4 for c in'SPAM') »> I = iter(G) »> next(I) 'SSSS' >» next(I) 'PPPP' »> G = t运sfour('spam') >» I = iter(G) >» next(I) ssss »> next(I) pppp

#

Iterate manually (expression)

#

Iterate manually (junction)

在上面的两个例子中, Python 自动创建了一个生成器对象,该对象既含有迭代协议所要求

的方法,又能够存储生成器的局部变益和代码执行的位置。请注意我们是如何让这里的新 生成器再次进入迭代的一正如下一小节所介绍的,生成器是单遍迭代器。

下面的代码是上 一节末尾表达式例子的真正基千语句的等价品: 一 个产生 (yield) 值的函 数一一即使对 join 这种强制生成所有结果的操作而言,它与返回 (return) 值也一样。

>» line ='aa bbb c' »>''. join(x.upper() for x in line.split() if len(x) > 1)

# Expression

'AABBB' #

»>''.join(gensub(line))

# But why generate?

'AABBB'

598

Function

»> def gensub(line): for x in line.split(): if len(x) > 1: yield x.upper()

l

第 20 章

尽管生成器能扮演更加实用的角色,然而在这种情况下,它与前面的简单语句难分伯仲, 或者说只是风格上的差异。再者,用 一行代码换四行对多数人来说是难以抗拒的。

生成器是单遍迭代对象 个十分细微但是重要的注意点:生成器函数和生成器表达式自身都是迭代器,并因此只 支持 一 次活跃迭代一不像 一 些内置类型,我们无法拥有在结果集中位于不同位置的多个 迭代器。因此, 一 个生成器的迭代器是生成器自身;实际上,在一 个生成器上调用 iter 没 有任何实际效果 :

>» G = (c • 4 for c

in'SP肌')

>» iter(G) is G

# My iterator is myself: G has _next_

True 如果你尝试手动地使用多个迭代器来迭代结果流,它们将都指向相同的位置:

»> G = (c * 4 for c >» I1 = iter(G) »> next(I1)

in'SP肌')

# Make a new generator # Iterate manually

·ssss· »> next(I1) 'PPPP'

»> I2 = iter(G) »> next(I2)

# Second iterator at same position!

'AAAA'

此外, 一 旦任 一 迭代器运行结束,所有的迭代器都将用尽-我们必须产生一个新的生成

器以便重新开始:

>» list(I1)

#

Collect the rest of 11's items

#

Other iterators exhausted too

['MMMM'l

»> next(I2) Stoplteration

>» I3 = iter(G) »> next(I3)

# Dirto for new iterators

Stoplteration

>» 13 = iter(c * 4 for c »> next(I3)

in'SP肌')

#

New generator to start over

'SSSS' 对于生成器函数来说也是如此一一如下的基干语句的 def 生成器函数等价形式只支持 一 个

活跃的生成器井会在一次迭代之后用尽:

»> def timesfour(S}: for c in S: yield c * 4

推导和生成

I

s99

»> G = timesfour('spam') >» iter(G) is G

# Generator functions work the same way

True

>» Il, I2 = iter(G), iter(G) »> next(I1) ssss

»> next(I1) pppp

»> next(I2)

# 12 at same position as ll

aaaa 这与某些内置类型的行为不同。内置类型支持多个迭代器与多次迭代,并且在活跃迭代器 中传递并反映它们的原位置修改:

>» L = [1, 2, 3, 4] »> 11, 12 = iter(L), iter(L) »> next{I1) 1

»> next{l1) 2

>» next(I2)

#

Lists support multiple iterators

1

»> del L[2:] >» next{l1)

# Changes reflected in iterators

Stoplteration 尽管在一 些简单的例子中并不明显,但这种效果可能会影响你的代码:如果你希望多次扫 描一个生成器的值,你就必须为每次扫描创建一个生成器,或者为这些值建立一个能够重

复扫描的列表一—单个生成器产生的值会在一遍扫描后耗尽。参阅这一章的边栏“请留意: 单遍迭代”中的一个需要考虑到生成器这种性质的例子。

当我们在本书的第六部分开始编写基千类的迭代器时,我们也将看到我们能够决定为自己 的对象支持的迭代器的数量。通常,需要支持多次迭代的对象将返回一个辅助类的对象而 非它们自身。下一小节将对该模型进行更多的介绍。

Python 3.3 的 yield from 扩展 Python 3.3 为 yield 语句引入了扩展语法,从而支持通过 from generator 的分句委 托给一个子生成器。在简单情况下,这和 yield 的 for 彼环形式等价一一-下面的 list 调用会强制生成器产生所有它的值,并且圆括号中的列表推导是一个本章介绍过的生

成器表达式:

»> def both(N): for i in range(N): yield i for i in (x ** 2 for x in range(N)): yield i

600

I

第 20 章

>» list(both(S)) [ o, 1, 2, 3, 4, o, 1, 4, 9, 16 l 新的 3.3 版本语法让它变得更简洁和明显,并且支持所有常见的生成器使用上下文·

»> def both(N): yield from range(N) yield from (x ** 2 for x in range(N})

»> list(both(S)) (0, 1, 2, 3, 4, O, 1, 4, 9, 16]

»> · : ·.join(str(i) for i in both(S)) '0 : 1 : 2 : 3 : 4 : 0 : 1 : 4 : 9 : 16 然而在更高级的使用场景下,这种扩展允许子生成器直接与外层调用域之间,进行值

的接收和抛出,并向外部生成器返回一个最终的值 。最终 的效果是能让这种生成器被 分觥成多个子生成器,就像一个函数能够被分解成多个子函数那样。

因为这种特性只在 3.3 及之后的版本中支持,并且也超出了本章生成器所讨论的范围, 所以我们将把更多的细节留给 Python 3 . 3 的手册 。关 于其他 yield

from 的例子,也

讨参阅本书笫四部分习题的笫 11 题。

内置类型、 工具和类中的值生成 最后,尽管我们在本节中重点讨论自己编写的值生成器,别忘了很多内置的类型也以类似 的方式工作一一正如我们在第 14 章中看到的 一样,例如,字典是在每次迭代中产生键的可 迭代对象:

»> D = {'a':1,'b':2,'c':3} »> x = iter{D) »> next(x) , , C

>» next(x) 'b' 和手动编写的生成器所产生的值一样,字典键也可以手动迭代,或者使用包括 for 循环、 map 调用、列表推导和我们在第 14 章介绍的很多其他上下文等的自动迭代工具:

>» for key in D: print(key, D[key]) C

3

b

2

a 1 如前所述,在文件迭代器中, Python 直接按需载人一 个文件的行:

推导和生成

601

>» for line in open ('temp. txt') : print (line, end='') Tis but a flesh wound. 尽管内置类型可迭代对象注定是 一 类特定的值生成,但这 一 概念与我们使用表达式和函数 编写的生成器是类似的。像 for 循环这样的迭代上下文接受任何的实现了所需方法的可迭

代对象,不管该对象是用户定义的还是内置的。

生成器和库工具:目录遍历器 尽管这超出了本书的讨论范围,今天许多 Python 的标准库工具也能生成值,包括 email 解

析和标准的目录遍历器

它在文件树的每一层产生 一 个当前目录的元组,包含它的子目

录以及文件:

» > import os »> for (root, subs, files) in os.walk('.'): for name in files: if name.startswith('call'): print(root, name)

# Directory walk generator # A Python'find'operation

. callables.py .\dualpkg callables.py 事实上, os.walk 在 Python 中的 os.py 标准库文件中被编写为 一 个迭代函数,位千 Windows 平台的 C:\Python33\Lib 目录下。由千它使用 yield (以及 3 .3 版本中作为 for 循 环替代品的 yield from) 返回结果,因此它是一个正常的生成器函数,也是 一 个可迭代对象:

»> G = os.walk(r'C:\code\pkg') >» iter(G} is G

#

Single-scan iterator: iter(G) optional

True

>» I = iter(G) >» next(I) ('C:\\code\\pkg', ['_pycache—'],[' eggs. py','eggs. pyc','main. py',... etc... ])

»> next(!} ('C: \ \code\ \pkg\ \_pycache_', [], ['eggs. cpython- 33. pyc',... etc. . . ])

>» next(I) Stoplteration 通过在遍历时产生结果,遍历器不需要让它的用户等待整个文件树被扫描。关千该工具更 多的细节,请参阅 Python 手册和《Python 编程》这样的进阶读物。也请参阅第 14 章等关 千 os.popen 的 一 个相关的可迭代对象,用千运行命令行指令井读取其输出。

生成器和函数应用 在第 18 章中,我们注意到带”*”的参数可以将 一个可迭代对象解包成单独的参数。既然

已经见过了生成器,我们也能明白这在代码中意味着什么。在 3.X 及 2. X 版本(尽管 2.X

的 range 返回一个列表)中:

602

I

第 20 章

>» def f(a, b, c): print('%s, %s, and %s'% (a, b, c)) >» f(o, 1, 2) o, 1, and 2 »> f(*range(3)) o, 1, and 2 >» f{*(i for i in range(3))) o, 1, and 2

# Normal posi1io11als

# Unpack range values: iterable in 3.X # Unpack generator expression values

这对字典和视图也适用(尽管 diet .values 在 2 . X 中也是一个列表,而且桉位置传入值的 时候顺序是任意的) :

»> D = dict(a='Bob', b='dev', C=40.5); {'b':'dev','c': 40.s,'a':'Bob'} >» f{a='Bob', b='dev', C=40-5) Bob, dev, and 40.5 »> f(**D) Bob, dev, and 40.5 >» f(*D) b, c, and a >» f{*D.values()) dev, 40.s, and Bob

D #

Normal keywords

# Unpack diet: key=va/ue #

Unpack keys iterator

# Unpack view iterator: iterable i113.X

因为 3.X 中的内置 print 函数将打印其所有参数的值,这点使得下面的三种形式相互等

价-最后一种形式使用了一个“*”将结果从一个生成器表达式中强制解包(尽管第二种 形式也创建了一个返回值的列表,但第一种形式在某些命令行中将你的光标在输出行的末

尾,在 IDLE GUI 中则不会) :

»> for x in'spam': print(x.upper(), end='') S P A M

»> list(print(x.upper(), end='') for x in'spam') SP AM [None, None, None, None)

»> print(*(x.upper() for x in'spam')) S PA M

参阅第 14 章中的另一个例子,该例将文件的每一行从迭代器中解包成参数。

预习:类中用户定义的可迭代对象 尽管这超出了本章的讨论范围,但我们还是可以用遵守迭代协议的类来实现任意的用户定 义的生成器对象。这样的类定义了一个特殊的_iter—方法,它由内置的 iter 函数调用、

并将返回一个对象,该对象有一个_next_ (在 2. X 中为 next) 方法,该方法由 next 内置 函数调用:

class Somelterable: def —init —(...) : def next_( ... ):



# On iter(): return se/jor supplemental object # On next(): coded here, or in another class

推导和生成

I

603

如上一节所示,这些类通常直接返回它们的对象以用千单遍迭代行为,或是返回 一 个带有

每次扫描状态的辅助对象用千支持多次扫描。 另一方面,一个用户定义的可迭代类方法函数有时可以使用 yield 将它们自身变成迭代器, 这个迭代器带有 一 个自动创建的_next_方法一—我们将在第 30 章中看到 一 个既相当隐晦,

又十分有用的 yield 的一个常见应用。使用一个—get item _索引方法作为对迭代退而求

其次的选项也是可以的,尽管这通常不能像_iter_和—next

方案那样灵活(但对千编

写序列而言却有着优势)。 这样的类实例化的对象被认为是可迭代的,并且可以用在 for 循环和所有其他的迭代上下

文中。然而,借助类可以实现比其他生成器构造所能提供的更丰富的逻辑和数据结构可能(例 如继承)。通过编写方法,类也可以使迭代行为更加显式,相比于内置类型和生成器函数

与表达式所带有的一些“神奇”的生成器对象(尽管类本身也拥有一些它们自己的“魔法”)。 因此,迭代器和生成器的内容不会真正结束,直到我们了解了它如何映射到类。就目前来说, 我们必须推迟到第 30 章学习基千类的迭代器的时候再结束这一话题。

实例:生成乱序序列 为了展示迭代工具在实际使用时的强大功能,让我们看几个更加完整的例子。在第 18 章中,

我们编写了 一 个用于打乱参数顺序的测试函数,来测试一般性的交集和并集函数。在那里, 我提到编写一个值的生成器会使该功能更好地实现。现在我们已经学习了如何编写生成器,

这个例子可以用千展示一 个实际的应用。 提前说 一 句:因为本节中的所有例子都是用千把对象进行分片和拼接,所以它们(包括结 尾的排列置换)只适用千如字符串和列表的序列`而不适用千文件、 map 及其他生成器的 任意可迭代对象。也就是说,这些例子中的 一部分自身也是生成器,能够按需产生值,但

它们不能把生成器当作输入进行处理。对千更多类型的 一 般化依然是 一 个开放问题,而这 里的代码可以原封不动地被再次利用,只要你把非序列的生成器先包在一 个 list 调用中再 传入。

打乱序列的顺序 正如第 18 章中所编写的,我们可以用分片和拼接将 一个序列重新排序,在每次循环中将第 一个元素移到序列的末尾;分片使用“+”操作任意的序列类型,而不是用下标索引元素:

»> L, S = [1, 2, 3],'spam' >» for i in range(len(S)): S = S(l:] + S[:1] print(S, end='') pams amsp mspa spam

604

I

第 20 章

#

#

For repeat counts 0..3 Move front item to the end

»> for i in range(len(L)): L = L[1:] + L[:1] print(L, end='')

# Slice so any sequence type works

[2, 3, 1] [3, 1, 2] [1, 2, 3] 另一种可供替代的方案是我们在第 13 章中看到的将整个前面的部分移到末尾,这可以达到 大致相同的效果:

»> for i in range(len(S)): X = S[i:] + S[:i] print(X, end='')

#

For positions 0..3

# Rear part+ front part (same effect)

spam pams amsp mspa

简单函数 上述这些代码只能操作固定名称的变最。为了 一般化,我们可以把它改写成一 个简单函数, 从而能操作任何传人的对象并返回相应结果,因为下面的第一个例子中采用了经典列表推

导的形式,所以相比第二个例子能够节省一部分代码量:

>» def scramble(seq): res = [] for i in range(len(seq)): res.append(seq[i:] + seq[:i]) return res

>» scramble ('spam') ['spam','pams','amsp','mspa'J

»> def scramble(seq): return [seq[i:] + seq[:i] for i in range(len(seq))]

» > scramble('spam') ['spam','pams','amsp','mspa']

»> for x in scramble((l,

2, 3)):

print(x, end='') (1, 2, 3) (2, 3, 1) (3, 1, 2) 我们还可以使用递归,但在这一场景中显得大材小用了。

生成器函数 上 一节的简单方法虽然能奏效,但是必须在内存中立即创建一 个完整的结果列表(内存很 大的话也许不成问题),而且需要调用者等待到整个列表被完全建好(如果这一过程十分 耗时,那么情况就不够理想了)。将代码改 写 成下面的两种生成器函数方案之一(每次产 生一 个值),我们可以达到更好的效果:

»> def scramble(seq): for i in range(len(seq)): 推导和生成

I 6os

seq = seq[1:] + seq[:1] yield seq >» def scramble(seq): for i in range(len(seq)): yield seq[i:] + seq[:i] >» list(scramble('spam')) ['spam', ' pams','amsp','mspa'] »> list(scramble((1, 2, 3))) [(1, 2, 3), (2, 3, 1), (3, 1, 2)) »> »> for x in scramble((1, 2, 3)): print(x, end='')

# Generator ftmction # Assignments work here # Generator function # Yield one item per iteration # list()generates all results # Any sequence type works

# for loops generare resulrs

(1, 2, 3) (2, 3, 1) (3, 1, 2) 生成器函数在活跃时保持了它们本地作用域的状态,能够最小化内存空间需求,并把工作

切分到更短的时间片。作为完整的函数,它们也很常见。重要的是,不论是否遍历了 一 个 实际的列表或是值的生成器, for 循环和其他的迭代工具的工作方式是 一 样的一生成器函

数能在这两种方案之间灵活切换,甚至可以在未来改变策略。

生成器表达式 如前所述,生成器表达式(圆括号中的推导表达式,而不是方括号中的)也能按需生成值

井保存它们的局部状态。虽然它们不像完整的函数那样灵活,但是它们能自动地按需产生值, 表达式通常可以在如下的情况中变得更简洁:

»> s spam >» G = (S[i:] + S[:i] for i in range(len(S))) »> list(G) ['spam','pams','amsp','mspa']

#Generator expression equivalent

注意,我们在这里不能使用第 一 个生成器函数版本的赋值语句,因为生成器表达式不能包 括语句。这让它们的适用范围变得有些狭窄 1 然而在许多情况下,表达式能完成相似的工作,

如下所示。为了把生成器推广到任意的 一 个 主 体,你可以把它包在一 个简单函数中,让这 个简单函数接收 一个参数并返回 一个使用该参数的生成器:

»> F = lambda seq: (seq[i:] + seq[:i] for i in range(len(seq))) »> F{S)

»> >» list(F(S)) ['spam','pams','amsp','mspa'] »> list(F([1, 2, 3))) ((1, 2, 3], (2, 3, 1], [3, 1, 2]] >» for x in F((l, 2, 3)): print(x, end='')

606

I

第 20 章

(1, 2, 3) (2, 3, 1) (3, 1, 2)

测试客户程序 最后,我们既可以使用生成器函数,又可以使用在第 18 章中它的等价表达式产生随机的参

数---序列乱序函数成为了 一 个能袚用在其他上下文中的工具: # file

scramble.py

def scramble(seq): for i in range(len(seq)): yield seq[i:] + seq[:i]

If Generator function If Yield one item per iteration

scramble2: lambda seq: (seq[i:] + seq(:i] for i in range(len(seq))) 而且通过将值生成过程移出到 一个外部的工具中,测试程序将变得更加简单:

» > from scramble 加port scramble »> from inter2 import intersect, union »> »> def tester(func, items, trace=True): for args in scramble(items): if trace: print(args) print(sorted(func(*args)))

# Use generator (or: scrambfe2(items))

>» tester(intersect, ('aab','abcde','ababab')) ('aab','abcde','ababab') ['a','b') ('abcde','ababab','aab') ['a','b'] ('ababab','aab','abcde') ['a','b'J

»> tester(intersect, ([1, 2], [2, 3, 4], [1, 6, 2, 7, 3]), False}

i

f:

全排列:所有可能的组合 这些技术有很多其他现实中的应用一~或是在 一 个 GUI 中 绘制点。此外,其他类型的序列乱序化工具在其他的应用中扮演着重要的角色,这些应用

从搜索到数学。其实,我们的序列乱序化函数只是 一 个简单的重新排序,然而 一 些程序需 要在全排列的范围内穷举更加完整的可能集合~用下面模块文件中的列表构建器或生 成器形式的递归函数来实现: # File permute.py

def permute1(seq): if not seq: return [seq) else:

# Shuffle any sequena: list # Empty sequence

推导和生成

1

607

res=[] for i in range(len(seq)): rest= seq[:i] + seq[i+1:] for x in permute1(rest): res.append(seq[i:i+l] + x) return res def permute2(seq): if not seq: yield seq else: for i in range(len(seq)): rest= seq[:i] + seq[i+l:] for x in permute2(rest): yield seq[i:i+l] + x

# Delete current node # Permute the others # Add node at front

#

Shuffle any sequence: generator

# Empty sequence

# Delete current node

# Permute the others #

Add node at front

这两个函数会得到相同的结果,不过第二个函数会把它大部分的工作推迟到被要求产生一 个值的时候。这段代码稍微高级一些,尤其是第二个函数(对千一些 Python 新手而言有些

过于难了!)。然而,正如我马上要解释的,在有些情况下,生成器方法非常有用。 你可以进一 步研究和测试这段代码,并且添加打印来跟踪其执行。如果还是很难理解,你

可以试着先理解第一个版本;要记得生成器函数只是返回 一个能够处理每层 for 循环 next 操作函数的对象,而且在被迭代之前不会产生任何的结果;你也可以跟踪下面的一些例子

的来看看这些代码是如何处理的 。 全排列提供了比原始的乱序函数更多种的顺序一对千 N 个元素,我们有 N! (阶乘)种结 果而不是 N 种。事实上这也是为什么我们需要迭代的原因:嵌套循环的数量是任意的,而 且取决千被全排列序列的长度:

>>> from scramble import scramble »> from permute import permute1, permute2 >>> list(scramble('abc')) ['abc','bca','cab']

# Simple scrambles: N

>» permute1('abc')

# Permutations larger: NI

['abc','acb','bac','bca','cab','cba'] >» list(permute2('abc')) ['abc','acb','bac','bca','cab','cba']

# Generate all combinations

»> G = permute2('abc') >» next(G)

# Iterate (iter() not needed)

'abc'

»> next(G) 'acb'

»> for x in permute2('abc'): print(x)

# Automatic iteration

.. . prints six lines... 列表和生成器版本的结果都是一 样的,然而生成器版本既能最小化内存使用又能延迟结果

的产生。对千更长的输入序列,全排列的集合将比简单乱序函数大得多:

608

I

第 20 章

» > permutel('spam')

==

list(permute2 ('spam'))

True

»> len{list(permute2('spam'))), len(list(scramble{'spam'))) (24, 4) »> list(scramble{'spam')) ['spam','pams','amsp','mspa'] » > list (permute2 ('spam')) ['spam','spma','sapm','samp','smpa','smap','psam','psma','pasm','pams', 'pmsa','pmas','aspm','asmp','apsm','apms','amsp','amps','mspa','msap', 'mpsa','mpas','masp','maps'] 根据第 19 章,这里也存在非递归的解决方案(例如使用显式的栈或者队列),当然其他序

列顺序也很常见(例如,能够过滤掉重复顺序序列的固定大小子集与组合),但是这需要 超出这里的编程扩展。关于本主题的更多内容请参阅《Python 编程》 一书,或者你也可以 自己动手试试看。

不要过度使用生成器:明确胜千隐晦 (EIBTI) 生成器是 一 种相对高级的工具,因而可能更适合被当作一门可选的主题,由千它们在

Python 语言(尤其 3.X) 中几乎无处不在,因此我们在本章中特别进行说明。事实上,它

们相比于 Unicode 而言更加重要(本书第八部分将介绍 Unicode) 。如前所述,像 range 、 map 、字典键甚至文件这样的内置工具如今都属于生成器,因此你必须熟悉生成器的概念,

即使你自己不编写新的生成器。此外,用户定义的生成器在今天的 Python 代码中正变得日 益常见,比如在 Python 的标准库中。 照例,我在此对生成器给出与推导语法 一 样的提醒:在不必要的情况下,不要强行使用自

定义的生成器让你的代码变得复杂。尤其对千较小的程序和较少的数据集,基本上用不上 这些工具。在这些情况下,简单的结果列表就能满足了,而且相比之下更易千理解,能自

动进行垃圾回收,同时能更快地被编写(这也是今天推荐的做法:请参阅下一章)。类似 生成器高级工具所依赖的隐式“魔法“非常有趣,值得你去实验,但它们除非必要否则要

因照顾其他程序员而尽批少用。 或者,再次引述 Python 的) mport this 座右铭: 明确胜于隐晦 (Explicit is better than implicit) 。

这句话的缩写 EIBIT, 是 Python 的核心准则之一,并且有着很重要的原因:你的代码的行

为越明显,下 一 个经手的程序员就越容易理解它。这点直接适用于生成器,其隐式行为可 能对其他程序员来说难以理解。时刻牢记:除非必要,否则保持简单。

另一方面:空间与时间、简洁性、表现力 也就是说,在一 些使用场景中生成器能做得更好。它们能在一些程序中减少内存,还能在

推导和生成

I

609

另 一 些程序中减少延迟,偶尔还能让不可能成为可能。例如,假设一 个程序必须产生 一 个 非平凡序列的所有全排列。因为全排列的阶乘数拭存在指数爆炸.前面的 permutel 递归列

表构建函数要么将导致一 个明显的(甚至漫长的)等待,要么因为内存的耗尽而彻底失败, 然而 permute2 的递归生成器却不会如此一—-它将快速地返回每个单独的结果,从而能处理

非常大的结果集:

» > import math

»> math.factorial(10)

# 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 3628800 »> from permute import permute1, permute2 »> seq = list(range(10)) »> pl = permutel(seq) # 37 seconds 011(1 2GHz quad-core 111achi11e # Creates a list of 3.6M numbers »> len(p1), p1[0], p计 1) (3628800, [o, 1, 2, 3, 4, s, 6, 1, 8, 9], [o, 1, 2, 3, 4, s, 6, 1, 9, 8])

在这个例子中,列表构建器在我的计算机上停顿了 37 秒,井构建了 一 个含有 360 万元素的

列表,生成器却可以马上开始返回结果:

>» p2 = permute2(seq) »> next(p2)

# Returns generator immediately # And produces each result quickly 011 request

[o, 1, 2, 3, 4, s, 6, 1, s, 9] »> next(p2)

[o, 1, 2, 3, 4, s, 6, 1, 9, 8) )» p2 = list(permute2(seq))

# About 28 seconds, though still impractical

»> p1

# Same set of results generated

== p2

True 当然,我们也可以优化列表构建器的代码使它运行得更快(例如,使用一个显式的栈而不 是采用递归能够改变其性能),但对千更大的序列,这将变得完全不可能一只需要 50 个

元素,全排列的庞大数晕将抹除试图构建一个结果列表的可能性,其耗费的时间甚至超越 我们的生命(同时大批的数值将超越递归调用栈的深度极限而导致移除:参阅前面的章节)。 然而生成器依然可用一—它能立即产生单个的结果:

>» math. factorial (50) 30414093201713378043612608166064768844377641568960512000000000000 »> p3 = permute2(list(range(so))) »> next(p3) # permute} is not an option here! [o, 1, 2, 3, 4, s, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1s, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49] 更好玩的是,为了产生更多变且不确定的结果,我们还可以使用第 5 章中介绍的 Python 中 的 random 模块,在全排列函数开始工作之前随机打乱要进行全排列序列的顺序。

(事实上,

我们也可以 一 般化地使用随机乱序函数作为 一 个全排列生成器,只要我们能假设在我们使 用的时间范围内随机乱序函数不会产生重复的顺序,或者将产生的结果与之前的结果相对

610

I

第 20 章

比米避免重复

希望我们所生存的世界不是循环地产生“随机”的序列!)。下面的代码中,

每个 permute2 和 next 调用与之前 一样都将立即返回,但是 permute1 会因运行时间较长而 桂起:

>» import random »> math.factorial{20)

#

permute I is not an option here

2432902008176640000 »> seq = list(range(20))

»> random.shuffle(seq) »> p = permute2(seq) »> next(p)

# Shuffle sequence randomly first

[10, 17, 4, 14, 11, 3, 16, 19, 12, 8, 6,

s,

2, 15, 18, 7, 1, o, 13, 9)

»> next(p) [10, 17, 4, 14, 11, 3, 16, 19, 12, 8, 6, 5, 2, 15, 18, 7, 1, o, 9, 13)

»> random.shuffle(seq) >» p = permute2(seq) >» next(p) [16, 1, ·5, 14, 15, 12, o, 2, 6, 19, 10, 17, 11, 18, 13, 7, 4, 9, 8, 3)

>» next(p) [16, 1, 5, 14, 15, 12, o, 2, 6, 19, 10, 17, 11, 18, 13, 7, 4, 9, 3, 8] 这里的关键点是生成器有时候能从一个较大的解集中产生结果,而列表构建器不行。照例, 我们不清楚这种用例在实际中到底有多常见,同时这也并不能完全为与生成器函数和生成 器表达式相伴的值生成的隐式风格而正名。正如我们将在本书第六部分中看到的,值生成

也可以通过类编写成可迭代对象。基千类的可迭代对象也可以按需产生元素,而且比生成 器函数和表达式所产生的神奇对象和方法要来得更加显式。 编程的 一 大乐趣也来自这些利弊之间的权衡,而且这里也没有绝对的法则。尽管生成器的 优势有时候能为它们的使用正名,可维护性也应时刻作为高优先级的因素进行考虑。和推 导语法一样,生成器也是 一 个提供了表现力和代码简洁性的工具,一且你掌握它们的工作 原理就很难抗拒使用它们的冲动一一不过你总需要考虑其他没有掌握这些工具的同事们的

疑惑。

示例:用迭代工具模拟 zip 和 map 为了帮你更好地理解生成器的价值,我们来看一些高级用例。 一 旦你掌握了列表推导、生 成器和其他的迭代工具,你就能更加直接自然地模拟众多的 Python 的函数式内置工具。例如, 我们已经看到了内置的 zip 和 map 函数是如何组合可迭代对象和向可迭代对象映射函数的。

通过传入多个可迭代对象参数, map 以与 zip 配对元素相同的方式,把函数映射到取自每 个序列的元素:

>» S1 ='abc' »> S2 ='xyz123' »> list(zip(S1, S2))

# zip pairs items from iterables

推导和生成

I

611

[('a','x'), ('b','y'), ('c','z')] # zip pairs items, truncates at shortest >» list(zip( [丑, -1, o, 1, 2])) [ (-2,), ( -1,), (o,), (1,), (2,)] »> list(zip([1, 2, 3], [2, 3, 4, s])) [(1, 2), (2, 3), (3, 4)] # map passes paired itenms to ajunctio11, truncates >» list(map(abs, [-2, -1, o, 1, 2])) [2, 1, o, 1, 2] »> list(map(pow, [1, 2, 3], [2, 3, 4, 5])) [1, 8, 81]

Ii Single sequence: 1-ary tuples

# N sequences: N-ary tuples

# Single sequence: 1-ary junction # N sequences: N-ary function

# map and zip accept arbitrary iterables »> map(lambda x, y: x + y, open('script2.py'), open('script2.py')) ['import sys\nimport sys\n','print(sys.path)\nprint(sys.path)\n',... etc... ]

>» [x

+ y for (x, y) in zip(open('script2.py'), open('script2.py'))] ('import sys\nimport sys\n','print(sys.path)\nprint(sys.path)\n',... etc...)

尽管它们用千不同的目的,但如果你花足够长的时间研究这些示例,可能会注意到 zip 结 果和执行 map 的函数参数之间的一 种关系,下面的例子将利用这种关系。

编写自己的 map(func, …) 尽管 map 和 zip 内置函数快速而方便,但我们总是可以编写自己的代码来模拟它们。例如, 在上一章中,我们用一个函数来模拟使用单个序列(或者其他可迭代)参数的 map 内置函数。

对千多个序列而言,实现内置功能也不需要太多额外的工作:

#map(junc, seqs...) workalike with zip def mymap(func, *seqs): res=[] for args in zip(*seqs): res.append(func(*args)) return res print(mymap(abs, [-2, -1, o, 1, 2])) print(mymap(pow, [1, 2, 3], [2, 3, 4, SJ)) 这个版本很大程度上依赖千特殊的* args 参数传递语法一它会收集多个序列(实际上是

可迭代对象)参数,将其作为 zip 的参数解包以便组合,然后把成对的 zip 结果解包作 为参数以便传入函数中。也就是说,我们在承认这样的一个事实, zip 是 map 中的 一 个基

本的嵌套操作。最后的测试代码对单个序列和两个序列都应用了这个函数,以产生这一 输 入一一我们可以用内置的 map 得到同样的输出(如果你想试着运行它的话,这段代码位千 本书例程的 mymap.py 文件中)

[2, 1,

612

I

o,

1, 2)

第 20 章

[1, 8, 81] 然而,实际上前面的版本展示了经典的列表推导模式,并在一个 for 循环中构建操作结果 的 一 个列表。我们可以将我们的 map 编写成更简洁的等价单行列表推导表达式: # Using a list comprehension

def mymap(func, *seqs): return [func(*args) for args in zip(*seqs)] print(mymap(abs, (-2, -1, o, 1, 2])) print(mymap(pow, (1, 2, 3], (2, 3, 4, 5])) 当这段代码运行的时候.结果与前面相同,但它更加精炼井且可能运行得更快(关千性能

的更多讨论,请参阅下一章中的”计时迭代可选方案”小节)。之前的两个 mymap 版本都 将 一 次性构建出结果列表,然而对于较大的列表来说,这会浪费内存。既然我们学习了生 成器函数和表达式,重新编写这两种替代方案来按需产生结果是很容易的: # Using generators: yield and(...)

def mymap(func, *seqs): res=[] for args in zip(*seqs): yield func(*args) def mymap(func, *seqs): return (func(*args) for args in zip(*seqs)) 这些版本达到了同样的结果,但是返回了旨在支持迭代协议的生成器。第一 个版本每次产

生 一 个结果,第 二个版本返回 一 个生成器表达式的结果来完成同样的事情。如果我们把它 们包含到 一 个 list 调用中强制它们 一 次生成所有的值,它们会产生同样的结果:

print(list(mymap(abs, [-2, -1, o, 1, 2]))) print(list(mymap(pow, [1, 2, 3], [2, 3, 4, SJ))) 这里并没有做任何实际 工 作,直到 list 调用通过激活迭代协议,来强制生成器运行。由这

些函数以及 Python 3.X 中的 zip 内置函数返回的生成器,都能够按需产生结果。

编写自己的 zip( …)和 map(None,...) 当然,目前给出的示例中的很多魔法是因为它们使用了 zip 内置函数配对来自多个序列的

参数。我们也注意到 map 近似版确实是模拟了 Python 3.X 的 map 的行为,它们从最短序列 的长度处截断,并且,当长度不同时不支持补充结果的效果,这与 Python 2.X 中带有一个

None 参数的 map 的效果一样:

C:\code> c:\python27\python »> map(None, [1, 2, 3), [2, 3, 4, s]) ((1, 2), (2, 3), (3, 4), (None, S)] >» map{None,'abc','xyz123')

推导和生成

I

613

[('a','x'), ('b','y'), ('c','z'), {None,'1'), (None,'2'), (None,'3')] 使用迭代工具,我们可以编写近似版来模拟截断的 zip 和 Python 2.X 的补充的 map, 这些 在代码上几乎是相同的:

# zip(seqs… )and 2.X map(None, seqs...) workalikes

def myzip(*seqs): seqs = [list(S) for Sin seqs] res = [] while all(seqs): res.append(tuple(S.pop(o) for Sin seqs)) return res def mymapPad(*seqs, pad=None): seqs = [list(S) for Sin seqs] res = [] while any(seqs): res.append(tuple((S.pop(o) if S else pad) for Sin seqs)) return res s1, s2 ='abc','xyz123' print(myzip(S1, S2)) print(mymapPad(S1, S2)) print(mymapPad(S1, S2, pad=99)) 这里编写的两个函数都可以在任何类型的可迭代对象上运行,因为它们通过 list 内笠函数

运行自己的参数来强制结果生成(例如,文件可以像参数 一 样工作,此外,序列可以像字 符串 一 样)。注意这里的 all 和 any 内置函数的使用 一如果 一 个可迭代对象中的所有或 任何元素为 True (或者作为等价,为非空),它们分别返回 True 。当列表中的任何或所有 参数在删除后变成了空,这些内置函数将用来停止循环。 还要注意 Python 3 . X 的 keyword-only 参数 pad, 和 Python 2. X 的 map 不同,我们的版本将

允许指定任何补充的对象(如果你使用 Python 2.X, 使用 一 个** kargs 形式来支持这 一 选项, 参阅第 18 章了解详细内容)。当这些函数运行的时候,打印出如下的结果( 一个 zip 和两

个填补 map) :

[('a','x'), {'b','y'), ('c','z')] [('a','x'), ('b','y'), ('c','z'), {None,'1'), (None,'2'), (None,'3')] [ ('a','x'), ('b','y'), ('c','z'), (99,'1'), (99,'2'), (99,'3') ] 这些函数井不一定非要使用列表推导的写法,因为它们的循环太具体了。和之前 一 样,既 然 zip 和 map 近似版构建并返回列表,我们同样能非常方便地用 yield 将它们改写为生成 器以便它们每次只能返回结果中的 一项。结果和前面相同,但我们需要再次使用 list 来强

制该生成器产生其值以供显示: #

Using generators: yield

def myzip(*seqs): seqs = [list(S) for Sin seqs] while all(seqs):

614

I

第 20 章

yield tuple(S.pop(o) for Sin seqs) def mymapPad(*seqs, pad=None): seqs = [list(S) for Sin seqs] while any(seqs): yield tuple((S.pop(o) if S else pad) for Sin seqs) S1, S2 ='abc','xyz123' print(list(myzip(S1, S2))) print(list(mymapPad(S1, S2))) print(list(mymapPad(S1, S2, pad=99))) 最后,下面是我们的 zip 和 map 模拟器的另外一种实现。下面的版本不是使用 pop 方法从 列表中删除参数,而是通过计算最小和最大参数长度来完成其工作。有了这些长度,很容

易编写嵌套的列表推导来遍历参数索引范围: #

Alternate implementation with lengths

def myzip(*seqs}: minlen = min(len(S) for Sin seqs} return [tuple(S[i] for Sin seqs) for i in range(minlen)] def mymapPad(*seqs, pad=None): maxlen = max(len(S} for Sin seqs) index = range(maxlen) return [tuple((S[i] if len(S} > i else pad} for Sin seqs) for i in index] S1, S2 ='abc','xyz123' print(myzip(S1, S2)) print(mymapPad(S1, S2)) print(mymapPad(S1, S2, pad=99)) 由千这些代码使用了 len 函数和索引操作,它们假定参数是序列或类似的类型,而不是任

意的可迭代对象,这与我们之前的序列乱序函数和全排列函数很像。 这里的外层推导将遍历参数索引范围,内层的推导将(传入到 tuple 中的)遍历传入的序 列从而井行地提取参数。当它们运行时,结果和前面相同。 更有趣的是,生成器和迭代器似乎在这个例子中被泛滥了。传递给 min 和 max 的参数是生 成器表达式,它在嵌套的推导开始迭代之前运行完成。此外,嵌套的列表推导使用了两层

的延迟计算-Python 3.X 的 range 内置函数是 一个可迭代对象,

tuple 中的生成器表达

式参数是另 一 个可迭代对象。

实际上,这里没有产生任何结果,直到列表推导的方括号开始请求放人到结果列表中的值 时——它们强制推导和生成器运行。为了把这些函数自身转换为生成器而不是列表构建器, 你需要使用圆括号而不是方括号。 zip 的例子如下所示:

II Using generators:(...) def myzip(*seqs): minlen = min(len(S) for Sin seqs)

推导和生成

I

61s

return (tuple(S[i] for Sin seqs) for i in range(minlen)) s1, S2 ='abc','xyz123' print(list(myzip(S1, S2)))

#

Go!...[('a','x'),('b','y'),('c', 切

在这个例子中使用了一个 list 调用来激活生成器和迭代器以产生它们自己的结果。你可以 自己体验这些来了解更多内容。进一 步编写其他方案的代码留作 一 个建议练习(参阅下面 的“请留意:单遍迭代”来了解另一个可能的方案)。

注意:关千更多 yield 的例子请参考第 30 章,那里我们将把它和_iter_运算符重载方法结 合起来以实现用户定义的自动化可迭代对象。这 一 角色中的局部变址状态持有能作为对 类属性的一种替代品,这种思想类似千第 17 章的闭包函数;然而,正如我们将看到的,

这种技术是类与函数式工具的一种结合,而不是完全新的 一 套范式。

请留意:单遍迭代 在笫 14 章中,我们学习了一些内置函数(如 map) 是如何只支持一个单次遍历的,并 且在结束之后为空,我在那里提过以后会给出一个示例展示这在实际中是如何变得微 妙而重要的 。 现在,在学习了有关迭代的许多内容之后,我终于可以兑现这个承诺了 。 计看下面这段比本章 zip 模拟示例更聪明的代码,该示例从 Python 手册中的一个例

子改编而来 。

def myzip(*args): iters = map(iter, args) while iters: res= [next(i) for i in iters] yield tuple(res) 由于这段代码使用了 iter 和 next, 它对任何类型的可迭代对象都有效。注意,在任 何一个参数迭代器用尽时,你完全不需要捕荻由 next(it) 引发的 Stop Iteration一—

允许这个异常的传递会终止这个生成器函数,这和一条 return 语句的效果是一样的 。 只要传入一个参数 , while

iters :就足以运行,并且也避免了无限损环(列表推导

将总是返回一个空的列表) : 这段代码能在 Python 2.X 中正常工作,如下所示:

»> list(myzip('abc','lmnop'}) [('a','l'}, ('b','m'), ('c','n')] 但在 Python 3.X 下,它将陷入一个无限彼环中并失效,因为 Python 3 . X 的 map 返回

一个单次可迭代对象,而不是像 Python 2. X 中那样的一个列表。在 Python 3.X 中, 只要我们在彶环中运行了一次列表推导, iters 会被用尽但仍永远为 True (并且 res

616

I

第 20 章

将会是[]) 。 为了使它在 Python 3.X 下也能正常工作,我们需要使用 list 内置函数 来创这一个支持多次迭代的对象

def myzip(*args): iters = list(map(iter, args)) ... rest as is...

# Allow mulitple scans

你可以自己运行并跟踪它的操作。这里要记住的是在 Python 3.X 中把 map 调用放入 到 list 调用中不是仅仅为了显示!

推导语法总结 我们已经在本章中关注过列表推导和生成器,但别忘了,还有两种在 Python 3.X 中可用的 推导表达式形式:集合推导和字典推导。我们在第 5 章和第 8 章曾遇到过这两种形式,但是, 有了推导和生成器的知识,现在我们应该能全面地理解这些 Python 3.X 扩展了。



对千集合,新的字面最形式 {1,

3,

2} 等价千 set([1,

3,

2] ),并且新的集合推导

语法 {f(x) for x in S if P(x)} 等效千生成器表达式 set(f(x)

for x in S if

P(x) ),其中 f(x) 是一个任意的表达式。



对千字典,新的字典推导语法 {key:

val for

(key,val) in zip(keys, vals) }与

dict(zip(keys, vals) )的效果一样,并且 {x:f(x) for x in items} 等效千生成器 表达式 dict((x, f(x)) for x in items) 。 下面是 Python 3.X 和 2.7 中的所有可能推导方式的总结。最后两种是新增的,并且在

Python 2.6 和更早的版本中不可用: »> [x * x for x in range(10)] [o, 1, 4, 9, 16, 2s, 36, 49, 64, 81)

# #

List comprehension: builds list like list(generator expr)

»> (x * x for x in range(10))

#

Generator expression: produces items Parens are often optional

»> {x * x for x in range(10)} {o, 1, 4, 81, 64, 9, 16, 49, 25, 36}

# #

Set comprehension, 3.X and 2.7 y} is a set in these versions too

# {x,

»> {x: x * x for x in range(10)} # Dicti ictionary comprehension, new in 3.X {o: o, 1: 1, 2: 4, 3: 9, 4: 16, s: 2s, 6: 36, 7: 49, 8: 64, 9: 81}

作用域及推导变量 我们现在已经介绍过所有的推导形式,你也有必要复习一下第 17 章对这些表达式中循环变

批的局部作用域的知识。 Python 3.X 将循环变量进行了四种形式的局部化一生成器推导、 集合推导、字典推导以及列表推导,这四种形式中的临时循环变批名的作用域只局限千对

推导和生成

I

617

应的表达式。它们不会与外部的变址名冲突,同时在外部也是不可访问的,这和 for 循环 的迭代语句是不 一样的:

c:\code> py -3 »> (X for X in range(s))

>» X NameError: name'X'is not defined »> X = 99 >» [X for X in range(s)] [o, 1, 2, 3, 4] »> X 99 »> V = 99 >» for V in range(S): pass

#3.x.. generator, set, diet, and list localize

# But loop statements do not localize names

»> V 4

正如第 17 章提到的, 3.X 版本在推导语句中的变屈赋值实际上是 一 个进一步嵌套的特殊作

用域:这些表达式中其他变扯名的引用遵循通常的 LEGB 法则。例如,在下面的生成器中, z 的作用域局限在推导表达式中,但 Y 和 X 能够和往常 一 样在外层的局部和全局作用域中

被访问到:

>» X ='aaa »> def func(): Y ='bbb' print(''.join(Z for Z in X + Y))

# Z comprehension, Y local, X global

»> func() aaabbb Python 2.X 在这点上和 3.X 是一致的,除了列表推导变品不是局部化的以外 ,它们和在 for 循环一样能够持有它们最后一次迭代的值,但是也有和外部名称相冲突的潜在可能性。生 成器、集合和字典形式在 3.X 中的变最名是局部化的:

c:\code> py -2 »> (X for X in range(s))

»> X NameError: name'X'is not de什 ned »> X = 99 »> [X for X in range(S)] [o, 1, 2, 3, 4) »> X 4

»> Y = 99

618

I

第 20 章

# 2.X: list does not localize its names, like for

»> for V in range(S): pass »>

# for loops do not localize names in 2X or 3.X

V

4

如果你要顾及版本间的可移植性,以及和 for 循环之间的对比,经验法则是尽最在推导表 达式中使用单独的变扯名。考虑到一个生成器对象在其完成值产生之后就被舍弃的事实, 2.X

的行为是合乎逻辑的,不过列表推导和 for 循环是等价的——尽管这种类比在 Python 中并 不适用千集合和字典形式(因为它们的名称在是 2.X 与 3.X 中都是局部化的),这也恰好 是下一小节要讲述的主题。

理解集合推导和字典推导 从某种意义上讲,集合推导和字典推导只是把生成器表达式传递给类型名的语法糖。由于 两者都接受任何的可迭代对象,因此一个生成器在这里能工作得很好:

>» {x * x for x in range(10)} {o, 1, 4, 81, 64, 9, 16, 49, 2s, 36} »> set(x * x for x in range(10)) {o, 1, 4, 81, 64, 9, 16, 49, 25, 36}

# Comprehe11sion # Generator and type name

>» {x: x * x for x in range(10)} {o: o, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 1: 49, 8: 64, 9: 81} »> dict((x, x * x) for x in range(10)) {o: o, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81} >»

# Loop variable localized in 2.X + 3.X

X

NameError: name'x'is not defined 然而,对千列表推导来说,我们总是可以用手动编码来创建结果对象。这里是最后两个推 导的基千语句的等价形式(尽管它们在局部变址命名上有所不同) :

»> res = set() »> for x in range(10): res.add(x * x)

#

Set comprehension equivalent

»> res {o, 1, 4, 81, 64, 9, 16, 49, 2s, 36} >» res = {} >» for x in range(to): res[x] = x

*

# Diet comprehension equivalent

x

»> res {o: o, 1: 1, 2: 4, 3: 9, 4: 16, s: 2s, 6: 36, 7: 49, 8: 64, 9: 81}

»> x

# localized in comprehe11sion xpressions.bttt not i11 oop statements

9

推导和生成

I

619

注意,尽管集合推导和字典推导都能接受并扫描迭代器,它们没有按需产生结果的概念一一

两种形式都是一次性构建所有对象。如果你想按需产生键和值,生成器表达式会是更合适

的选择:

»> »> (o, »>

G = ((x, x * x) for x in range(lO)) next(G)

o)

next(G) (1, 1)

集合与字典的扩展推导语法 与列表推导及生成器表达式一样,集合和字典推导都支持嵌套相关的 if 分句来过滤结果元 素一一如下的代码收集一个范围内偶数的平方:

»> [x * x for x in range(10) if x % 2 == o]

[o, >» {o, »>

{o:

4, 16, 36, 64] {x * x for x in range(10) if x 为 2 == o} 16, 4, 64, 36} {x: x * x for x in range(10) if x % 2 == o} o, 8: 64, 2: 4, 4: 16, 6: 36}

#

Lists are ordered

#

But sets are not

#

Neither are diet keys

你也可以使用嵌套的 for 循环,尽管集合与字典本身的无序性和无重复性可能会让结果难 以一眼看出:

» > [ x + y for x in [ 1, 2, 3] for y in [ 4, 5, 6] ] [5, 6, 7, 6, 7, 8, 7, 8, 9] »> {x + y for x in [1, 2, 3] for y in [4, 5, 6]} {8, 9, 5, 6, 7} »> {x: y for x in [1, 2, 3) for y in [4, 5, 6]} {1: 6, 2: 6, 3: 6}

#

Lists keep duplicates

# But sets do not #

Neither do diet keys

和列表推导一 样,集合推导和字典推导也可以在任何类型的可迭代对象上迭代,包括列表、 字符串、文件、范围以及支持迭代协议的任何其他类型:

>» {x + y for x in'ab'for yin'cd'} {'bd','ac','ad','be'} »> {x + y: (ord{x), ord(y)) for x in'ab'for yin'cd'} {'bd': (98, 100),'ac': (97, 99),'ad': (97, 100),'be': (98, 99)} »> {k * 2 fork in ['spam','ham','sausage') if k[O] =='s'} {'sausagesausage','spamspam'} »> {k.upper(): k * 2 fork in ['spam','ham','sausage'] if k[o] =='s'} {'SAUSAGE':'sausagesausage','SPAM':'spamspam'}

620

I

第 20 章

关千更多细节,你可以自己实验这些工具。它们的性能可能比生成器或 for 循环的替代方

案好,也可能不如它们,不过我们可以通过计时它们的性能来确定一一由此我们也平稳地 过渡到下一章的内容。

本章小结 本章完成了我们对内置推导和迭代工具的介绍。本章在函数式工具的背景下探索了列表推 导,井且把生成器函数和生成器表达式作为另外的两种迭代协议工具进行介绍。作为结尾, 我们也总结了今天 Python 中的四种推导语法形式~生成器、集合和字典。至此我 们已经学习了所有的内置迭代工具,关千推导的讨论还将在第 30 章中关于用户定义可迭代 类对象的内容中继续。

下一章是本章主题的某种延续-我们将看到一个用计时工具刻画所学过的工具性能的案 例,这不仅将完成本书这一部分的学习,同时也在本书中间位置提供了一个更加实际的例子。 在我们继续对推导和生成器进行基准测试之前,本章的习题将帮你复习你在这里所学的知

识。

本章习题 1.

把列表推导放在方括号和圆括号中有什么区别?

2.

生成器和迭代器之间有什么关系?

3.

如何判断函数是否为生成器函数?

4.

yield 语句是做什么的?

5.

map 调用和列表推导有什么关系?请比较并区分两者。

习题解答 1.

方括号中的列表推导会一次性在内存中创建结果列表。当位于圆括号中时,实际上是

生成器表达式:它们有类似的意义,但不会 一 次性产生结果列表。相反,生成器表达 式会返回一个生成器对象,用在迭代上下文中每次产生结果中的一个元素。

2.

生成器是自动支持迭代协议的可迭代对象 :它们是拥有一个_next_ (在 2.X 中为

next) 方法的迭代器,重复步进到序列结果中的下 一 个元素,并在到达序列尾端时引 发异常。在 Python 中,我们可以用 def 和 yield 来编写生成器函数,用加圆括号的列

表推导来编写生成器表达式,用定义了特殊方法_ iter —的类来编写生成器对象(本 书之后将讨论)。

推导和生成

I

621

3.

生成器函数在其代码中的某处会有一条 yield 语句。除此之外,生成器函数和普通函 数在语法上相同,但是,它们会被 Python 采取特殊编译,以便在调用的时候返回一个

可迭代的生成器对象。该对象能在每次值产生之间保持内部状态。

4.

当有了 yield 语句时, Python 会把函数特殊地编译成生成器;当调用时,会返回 一

个支持迭代协议的生成器对象。在 yield 语句运行时,会把结果传回给调用者,并让

函数的状态挂起。然后,当调用者再调用—next —方法时,这个函数就可以重新在上 次 yield 语句执行的地方继续运行。生成器也可以扮演更高级的角色,它的 send 方法 在恢复生成器运行的同时还能向 yield 语句中传入 一 个值。生成器函数也可以有 一 个 return 语句,用来终止生成器的运行 。

5.

map 调用类似千 列表推导,两者都会收集对序列或其他可迭代对象中每个元素应用某种 运算后的结果(每次 一 个元素),从而产生一系列的值。其主要差异在于, map 会对每

个元素应用函数,而列表推导则是应用任意的表达式。因此,列表推导更通用 一 些, 可以像 map 那样应用函数调用表达式,但是, map 需要一个函数才能应用其他种类的表 达式。列表推导也支持扩展语法,例如嵌套 for 循环和 if 分句,从而可以包含内置函 数 filter 的功能。在 Python 3.X 中, map 的改进点是因为它产生了 一个生成器,而列 表推导会将结果全部载入内存中。在 2.X 中,两者都会将结果列表全部载入内存。

622

I

第 20 章

第 21 章

基准测试

既然我们了解了函数和迭代工具的编写,那么现在将开始一场短途的支线旅程来让它们 二 者投入使用。本章我们要研究 一 个大型案例,它将对之前的迭代工具相对性能进行计时测试, 以此结束本书的函数部分。 在学习这一案例的过程中,我们会介绍 Python 的代码计时 工具,讨论通用的基准测试技 巧,并探索比目前为止所见的大部分代码更实际和有用的代码。同时,我们也会测揽今天 Python 实现的运行速度,该因素的重要程度取决千所编 写代码的需求。

最后,由千这是本书这一部分的最后 一章,因此我们将给出常见的"陷阱“集锦与练习, 来帮助你开始编写实现一 些你已经读过的想法。不管怎样,先让我们用 一 个实际的例子来 小试牛刀吧。

计时迭代可选方案 本书已经介绍了很多迭代的可选方案。与编程中的其他情况一 样,它们反应了对利弊的权衡, 既要考虑如表达性的主观因素,又要考虑如性能等更客观的标准。程序员和工程师的工作 之一就是基于这些因素选择相应的工具。

从性能上讲,我多次提到列表推导有时比 for 循环语句有速度方面的优势,而且 map 调用 会依据调用模式的不同比这两者更快或更慢。上一节介绍的生成器函数和表达式虽然比列

表推导速度稍慢一些,但是它们不仅把内存需求降到了最小,还不会延迟结果的生成。 上面这些结论在现在的 Python 中 一 般都是正确的,但随着时间的推移相对性能也会发生变 化,因为 Python 的内部实现还在不断地改变和优化,而且代码结构也能任意地影响速度。

623

如果你要测试它们的性能,就需要在自己的计算机上用自己的 Python 版本计时这些可选方

案。

自己编写的计时模块 幸运的是,你可以在 Python 中非常方便地计时代码。例如,要得到(带任意基千位置参数的) 函数的多次调用花费的总时间,下面的初版函数也许就足够了:

File timerO.py import time def timer(func, *args): start = time.clock() for i in range(1000): func(*args) return time. clock() - start

#

#

Simplistic timing junction

#

Total elapsed time in seconds

这是可行的。它从 Python 的 time 模块获取系统时间值,并在用传入的参数执行传入的函

数 1000 次后用结束时间减去开始时间。在我计算机上的 Python 3.3 版本中:

» > from timero import timer »> timer(pow, 2, 1000) 0.00296260674205626 »> timer(str.upper,'spam') 0.0005165746166859719

#

Time to call pow(2, 1000) 1000 times

#

Time to call'spam'.upper() 1000 times

虽然简单,但这个计时器也是相当局限的,并有意暴露了 一 些在函数设计和基准测试上的

经典错误。其中,该函数:



在被测试函数调用中不支持关键字参数



硬编码了重复次数



额外计入了 range 的时间开销



总使用 time.clock, 在 Windows 系统以外并非是最好的



无法让调用者检测待测函数确实有效



只给出总时间,这在一些重负载的机器上可能会波动

换言之,代码计时要比想象中的更复杂!为了更具一般性和精确性,让我们将这段代码扩

展为依旧简单却更有效的计时器实用函数,从而不仅能在目前计时迭代可选方案,还可以 在未来计时其他程序。由千这些函数被编写在一个模块文件中,因此可以在各种程序中使用,

同时也具备文档字符串以给出 PyDoc 能展示的 一 些基本的细节(参阅第 15 章的图 15-2, 该 图是我们这里编写的计时模块所渲染的文档页面的一个屏幕截图)

# File timer.py

624

I

第 21 章

Homegrown timing tools for function calls. Does total time, best-of time, arid best-of-totals time import time, sys timer= time.clock if sys.platform(:3] =='win'else time.time def total(reps, func, *pargs, **kargs): """ Total time to run func() reps times. Returns (total time, last result) repslist = list(range(reps)) start = timer() for i in repslist: ret = func(*pargs, **kargs) elapsed = timer() - start return (elapsed, ret)

#

#

Hoist out, equalize 2.x, 3.x Or perf_counterlother in 3.3+

def bestof(reps, func, *pargs, **kargs):

""" Quickest func () among reps runs. Returns (best time, last result) """

best= 2 ** 32 for i in range(reps): start = timer() ret = func(*pargs, **kargs) elapsed = timer() - start if elapsed< best: best= elapsed return (best, ret)

# #

136 years seems large enough range usage not timed here

# Or call total() with reps= I Or add to list and take min()

#

def bestoftotal(repsl, reps2, func, *pargs, **kargs): """ Best of totals: (best of repsl runs of (total of reps2 runs of func)) """ return bestof(repsl, total, reps2, func, *pargs, **kargs) 从操作上讲,该模块同时记录了调用的总时间和最短时间,还有把两者嵌套结合起来的最

短总时间。在这三个函数中,都能计时使用任意基于位置参数和关键字参数的任意函数的 调用,也都通过获取开始时间、调用传入函数和用开始时间减停止时间来实现。下面来看

看这一 版是在哪些方面提升了上一版的不足:



Python 的 time 模块可以访问当前时间,其精度随运行平台而不同。在 Windows 上, clock 函数宣称有着毫秒级的颗粒度,因而非常精确。因为 time 函数在 UNIX 平台上 可能会更好,这段代码基千 sys 模块中的 platform 字符串自动地在它们之间切换,该

字符串在 Windows 下以 “win" 开始。也可参阅边栏 “3.3 版本中新的计时器调用“中 的其他在 3.3 及之后版本中可用的 time 选项,这里由千版本间可移植性并没有使用;

我们也能对这些新调用所不能使用的 Python 2.X 进行计时,对千所有的待计时事件, 它们在 Windows 下得到的结果与 3.3 版本中很相近。

基准测试

I

62s



由千把 total 函数中 range 调用从计时循环中脱离出来,因此在 Python 2.X 中不会将

创建 range 的开销计人函数上。因为在 3.X 中 range 是 一 个可迭代对象,所以这一 步 是无关紧要的,但是我们仍然使用 list 调用,从而使其遍历开销在 2.X 和 3.X 中相等。 不过这并不适用千 bestof 函数,因为在 bestof 中与 range 相关的因素并没有计人测 试的时间。



将 reps 计数作为参数传入(写在测试函数和测试参数之前),从而允许在调用之间指 定不同的重复次数。



使用带星号参数语法收集了任意多基干位隍参数和关键字参数,因此它们必须被独立

传人,而不是包在 一 个序列或字典中。如果需要,调用者可以在调用时使用星号将参 数集合解包为独立的参数,也就是最后的 bestoftotal 函数所做的那样。如果你不能

理解这段代码,参考井复习 一 下第 18 章吧。



该模块中的第一 个函数把所有调用总共花费的时间和最后 一 次调用的返回结果打包在 一个元组中返回,从而让调用者可以验证传入函数操作的正确性。



第 二 个函数和第 一 个类似,但返回的是所有调用中的最短时间而不是总时间一一比如你

想要过滤掉计算机中其他活动的影响时这就非常有用,但是对于运行得太快(运行时 间太短)的测试就不那么有用了。



考虑到上面这种情况,该文件中的最后一 个函数在最短时间测试中嵌套了总时间测试, 从而获取最短总时间。内嵌的总时间操作让测得的运行时间更具实际意义,但我们同 时也获得了过滤最短时间的效果。如果你还记得 Python 中所有函数都是可传递的对象

(甚至包括这些测试函数),那么这里函数的代码理解起来就很容易了。 从 一 个更大的视角看,由于这些函数被编写在 一个模块文件中,因此它们成为了更通用的 工具,可以在任何需要的地方被导入。我们在第 3 章介绍了模块和导入,而你将在本书的

下一部分中学习更多关千它们的知识。从现在起,你只需简单地导入模块并调用由数来使 用该文件中的计时器。在简单的使用场景下,该模块和之前的那个例子效果类似,但在更

大型的上下文中则显得更加健壮。照例在 Python 3.3 中:

» > import timer »> timer.total(1000, pow, 2, 1000)[0]

# Compare to timerO results above

0.0029542985410557776 »> timer.total(1000, str.upper,'spam') (o.000504845391709686,'SPAM')

# Returns (time, last ca/l's result)

>» timer.bestof(1000, str.upper, •spam')

# 1 I1000 as long as total time

(4.887177027512735e-07,'SPAM') >» timer.bestof(1000, pow, 2, 1000000)[0] 0.00393515497972885

>» timer.bestof(so, timer.total, 1000, str.upper,'spam') (0. 0005468751145372153, (0.0005004469323637295,'SPAM'))

»> timer.bestoftotal(so, 1000, str.upper,'spam') (0.000566912540591602, (0.0005195069228989269,'SPAM')) 626

I

第 21 章

这里的最后两次调用计算了最短总时间,即在 50 次运行中的最低时间,其中的每次都计算 了 l000 次 str .upper 调用的总时间(大致与这里前面的计时结果相同)。最后这次调用使

用的函数,实际上只是倒数第二 次调用的一种简单方便的映射,两者都返回最短结果元组, 并嵌入了最后总时间调用的结果元组。 我们可以对比最后的两个结果和下面基千生成器的可选方案:

>» min(timer. total{10的,

str. upper,'spam') for i in range(so)) (0.0005155971812769167,'SPAM')

这种对总时间迭代结果元组使用 min 函数的做法能起到相似的效果,因为结果元组中的时

间项(位千元组的最左边)主导了 min 所做的比较译注 1 。我们也能在自己的模块中使用这 种做法(同时也会在接下来的变体中使用),通过避免最短时间函数代码中一个非常小的 开销以及不嵌套结果元组,最后测得的时间结果会稍有不同,不过这两个结果都能满足相

对比较的需求。确实,最短时间函数必须选出 一个较高的初始最低时间值一-136 年的时间 很可能比你需要运行的绝大多数测试都长!

>» ((((2 ** 32) / 60) / 60) / 24) / 365 136.19251953323186 >» ((((2 ** 32) // 60) // 60) // 24) // 365 136

# Plus a few extra days # Floor: see Chapter 5

3.3 版本中新的计时器调用 本小节使用 time 模块的 clock 和 time 调用,因为它们适用于本书的所有读者 。

Python 3.3 在这个模块中引入新的接口,目的是实现更强的可移植性。具体来说,随 着平台的不同,这一模块的 clock 和 time 调用的行为也会变化,但是它的新的 perf_ counter 和 process_time 函数有着定义良好和平台无关的语义 :



time.perf_counter( )以包含小数部分的才少数返回一个性能计数器的值,其被定 义为测量较短时间段所能使用的最高可用分辨率叶钟。它包括在睡眠期间流逝的 时间 ,而且是系统范围的。



time. process_time( )以包含小数部分的秒数返回当前进程中系统和用户 CPU 执 行时间之和的值 。 它不包括睡眠期间流逝的时间,而且被定义为进程范围的。

译注[:注意,这里 min 的比较操作是作用在一个元组上,而不是单独的时间数字上的 。 权据官 方文档,例如元组、列表、字典这样的序列类型也支持按照字典序的比较 。 在该例中,因

为每个元组内的第二个元素均为字符串` SPAM'

,所以在比较中起决定性作用的是笫一

个元素,即总时间对应的数字 。 感兴趣的读者可以参考官方文档 https:/ldocs.python.org/3/ reference/expressions.html#va lue-comparisons 中关于序列比较的详细描述。

基准测试

I

627

对这两个调用来说 , 返回值的参照点是未定义的,所以只有连续调用结果间的差值才

是有意义的 。 可以把 perf_counter 调用想象成挂钟时间译注 2, 并且在 Python 3 .3 中 它被默认用作前面讨论过的 timeit 模块的基准测试 。 process_time 则可移柱地给出 CPU 时间 。 正如本书所示 , time.clock 调用在今天的 Windows 上仍然是可用的 。 在 3.3 的手册里 , 它被标记为弃用,但是在使用时不会引发任何警告.这意味着在之后的发行版本中它 可能会也可能不会被官方弃用 。 如果需要,你可以使用下面这样的代码来检测 Python 3.3 及之后的版本,出于简洁性和计时器间可比较性的考虑,我没有选择使用它:

if sys.version_info[o] >= 3 and sys.version_info[1] >= 3: # or process_time timer = time. perf_counter else: timer= time.clock if sys.platform[:3] =='win'else time.time 作为另一种选择,下面的代码也可以增加可移植性,并且能让你遇免使用未来可能被

弃用的工具 。 不过它依赖于我们目前尚未系统性学习过的异常主题,而且选择它也会 让跨版本间的速度比较变得毫无意义,因为计时器在分辨率方面会变得不同!

try: # or process_time timer = time. perf_counter except AttributeError: timer= time.clock if sys.platform[:3] == ' win'else time.time

假设我只为 Python 3.3 之后的读者写这本书,那么我会在这里使用较新的和经过显芳 改进的调用 , 如果情况允许你也应该在自己的工作中使用它们 。 然而新式的调用不会

为所有的其他 Python 早期版本的用户工作,即便这部分用户仍然占当今 Python 世界 的大多数 。 假装与过往划清界限能让一切变得轻松,但这既不现实也略显鲁莽 。

计时脚本 现在 , 为了计时迭代工具的速度(我们 一开始的目标),我们要运行如下的脚本 , 它使用 我们自己编写的计时器模块来计时之前学 过的列表创建技术的相对速度:

File timeseqs.py "Test the relative speed of iteration tool alternatives."

#

import sys, timer reps= 10000 repslist = list(range(reps))

译注 2:

I

Import timer functions

#

Hoist out, list in both 2.X/3.X

原文为 wall time, 指的是一项任务从开始到结束花赍的总时间,是 CPU 时间 、 I/0 时间 以及通信信道延迟之和 。

628

#

第 21 章

def forloop(): res=[] for x in repslist: res.append(abs(x)) return res def listComp{): return [abs(x) for x in repslist] def mapCall{): return list(map{abs, repslist)) # return map{abs, repslist)

#

Use list() here in 3.X only!

def genExpr(): return list{abs{x) for x in repslist)

#

list() required to force results

def genFunc(): def gen(): for x in repslist: yield abs(x) return list(gen())

#

list() required to force results

print(sys.version) for test in (forloop, listComp, mapCall, genExpr, genFunc): (bestof, (total, result)) = timer.bestoftotal(S, 1000, test) print ('%-9s: %.sf => [%s... %s]'% (test. name ,bestof, result[o], result[-1]))

— —

这段代码测试了五种创建结果列表的可选方法。如代码所示,它报告的时间反应了这五个 测试函数执行 1000 万步的相对快慢次序,换句话说,每个函数都创建了 1000 次带有 10000 个元素的列表。这 一过程被重复了 5 次来获得这 5 个测试函数每个的最短时间,因此整个

脚本产生了 2.5 亿之巨的总步数(在今天的计算机上这既令人震惊,又合情合理)。 注意我们是如何必须通过内置的 list 调用运行生成器表达式和函数的结果来强制它们产生 所有的值,如果不曾这样做,我们在 2.X 和 3 .X 版本中就只会得到绝不做任何工作的生成器。

仅当在 Python 3.X 中,我们必须对 map 结果做同样的事情,因为 map 在 3.X 中也是一个可 迭代对象了,对千 2.X 版本, map 外围的 list 必须手动地移除以避免每一 回测试都有额外

的列表创建开销(尽管在大多数测试中这部分时间是可以忽略的)。 用同样的方式,内层循环的 range 结果被提升至模块顶层,以从总时间中移除它的创建开销。

它被包装在 list 调用中,这样它的遍历消耗的时间不会因为仅在 3.X 中是生成器而出现偏 差 (这 与 我们在 timer 模块所做的十分类似) 。 这可能会被内层迭代循环消耗的时间所掩盖, 但我们最好尽可能多地移除变量。

基准测试

I

629

同时要注意最底下的代码是如何遍历 一 个有五个函数对象的元组,并逐个打印函数对象的

name

的:如前所述,这是 一 个给出函数名称的内置属性

注 l



计时结果 当上一 小节的脚本在 Python 3.3 下运行时,我在自己的 Windows 7 笔记本上得到了如下结

果: map 比列表推导稍微快一 些,而这两者都比 for 循环快,同时生成器表达式和函数速 度居中(这里的时间是以秒为单位的总时间) :

(:\code> c:\python33\python timeseqs.py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] forloop : 1. 33290 => [ o... 9999] listComp : o.69658 => [0... 9999] mapCall : 0.56483 => [o... 9999] genExpr : 1. 08457 => f O... 9999] genFunc : 1.07623 => [o. . . 9999] 如果你花足够的时间研究这段代码及其输出,就会注意到当今的生成器表达式比列表推导

运行得要慢。尽管把一 个生成器表达式包装到 一 个 list 调用中能使其功能上等效于 一 个方 括号括起的列表推导,但是这两个表达式的内部实现看上去有所不同(尽管我们也对生成

器测试有效地计时 list 调用) :

return [abs{x) for x in repslist] return list(abs(x) for x in repslist)

# #

0.69 seconds 1.08 seconds: differs internally

尽管确切的原因需要更加探人地分析(和必要的源代码调研),但考虑到生成器表达式在 值生产期间必须做额外的工作来保存和恢复它的状态,这 一 结果也就不难理解了,而列表

推导不会,它在这里和稍后的测试中运行起来会快一 个小的常最。 有趣的是,本书的第 4 版,我在 Windows Vista 的 Python 3.0 中运行它 1 本书第 3 版,我在

Windows XP 的 Python 2.5 中运行它,结果是相似的——列表推导几乎比等效的 for 循环语 句快 一 倍,而以这种方式映射 abs (绝对值)这样的内置函数时, map 比列表推导略快。

Python 2.5 的绝对时间是当前 3.3 版本的 4~5 倍,但是这很可能反映了更加快速的笔记本而 不是 Python 中的任何改进。

实际上,今天在同 一 台机器上对这 一 脚本的 Python 2.7 的大部分结果要比 3.3 的稍快-我

注 I:

作为预习:注意这里为何我们必须手动地将计时器传入函数中 。 在笫 39 章和笫 40 章 , 我 们将看到基于装饰器的计时器可选方案,有了它被计时的函数只常被正常调用,但是要求 在函数定义的位置添加额外的"@“语法 。 装饰器对于集成在大型项目中组件函数的计时

逻辑而言十分有用、但并不能简单地支持这里假设的相对孤立的浏试调用模式 - 当函数

被装饰后,对该函数的每一次讲用都运行计时逻辑,是好是坏取决于你的具体岔求 。

630

I

第 21 章

从下面代码的 map 测试中移除了 list 调用,避免在那个测试中创建结果列表两次,尽管如 果留下它的话只会增加一 个非常小的常量时间:

c:\code> c:\python27\python timeseqs.py 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)] for loop : 1. 24902 => [ o... 9999] listComp : 0.66970 => [0..,9999] mapCall : 0.57018 => [o... 9999] genExpr : 0.90339 => [o... 9999] genFunc : 0.90542 => [o... 9999] 为了便千比较,下面是在当前的 PyPy 下的相同测试的速度结果, PyPy 是第 2 章讨论过的 优化了的 Python 实现,它目前的 1.9 发行版实现 Python 2.7 语言。这里 PyPy 大体上要快

10 倍(一个数量级) ;我们在本章稍后使用带有不同代码结构的工具重新讨论 Python 版本 比较,它甚至会做得更好(尽管它也可能会在其他一些测试中输掉)

c:\code> c:\PyPy\pypy-1.9\pypy.exe timeseqs.py 2.7.2 (341e1e3821ff, Jun 07 2012, 15:43:00) [PyPy 1.9.0 with MSC v.1500 32 bit] forloop : 0.10106 => [o... 9999] listComp : 0.05629 => [o... 9999] mapCall : 0.10022 => [o... 9999] genExpr : 0.17234 => [o... 9999] genFunc : 0.17519 => [o... 9999] 仅在 PyPy 上,这一 测试中列表推导击败了 map, 但是现在所有的 PyPy 的结果都要快很多 这 一事实才是这里的重点。在 CPython 上, map 仍然是目前为止最快的。

函数调用的影响: map 注意,如果改变这个脚本使每次迭代执行 一 个内联操作(比如加法)而不是像 abs 的内置 函数的话,会发生什么(下面的文件省略的部分与之前是一样的,仅当在 3.3 中我才把 map

放回 list 调用中) : # File timeseqs2.py (differing parts)

def forloop(): res=[] for x in repslist: res.append(x + 10) return res def listComp(): return [x + 10 for x in repslist] def mapCall (): return list(map((lambda x: x + 10), repslist))

# list() in 3.X only

def genExpr(): return list(x + 10 for x in repslist)

# list() in 2.X + 3.X

基准测试

I

631

def gen Fune(): def gen(): for x in repslist: yield x + 10 return list (gen())

# list

in 2.X + 3.X

现在在 map 中调用 一 个用户定义函数这 一需求,使其比 for 循环语句要慢,尽管循环语句 版本在代码量上更大。换句话说,移除函数调用有可能使其他的方式更快(接下来的提示 中有更多相关的知识)。在 Python 3.3 上:

c:\code> c:\python33\python timeseqs2.py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] forloop : 1.35136 => (10 . . . 10009] listComp : 0.73730 => [10... 10009] mapCall : 1.68588 => [10... 10009] genExpr : 1.10963 => [10... 10009] genFunc : 1.11074 => [10... 10009] 这些结果在 CPython 中也是 一致的。在上一 版的 Python 3.0 中,运行在一台较慢的机器上,

也是类似的结果,尽管测试机器的差别要慢大约两倍(在 Python 2.5 中,运行在 一 台更慢 的机器上,是当前的结果的 4~5 倍)。 因为解释器内部进行了相当多的优化,分析这样的 Python 代码的性能是一件非常棘手的事。 不过,如果没有数字,要猜测哪个方法的运行最优几乎是不可能的。所以你能做的就是在

自己的计算机上,使用自己的 Python 版本,计时自己的代码。 在本例中,我们能确定的是在这个版本的 Python 上,在 map 调用中使用一个用户定义的函 数会显著地降低性能(可能比简单的 abs 还慢),而列表推导在本例中运行得最快(尽管 其他一些方式比 map 要慢)。列表推导似乎总是比 for 循环快两倍,不过这也是有条件的一

列表推导的相对速度可能会受它的扩展语法(例如, if 过滤器)、 Python 的变化以及这里 没有计时过的使用模式的影响。 正如我之前提过的,不论如何,在你编写 Python 代码时,性能都不应作为第一优先级。优 化 Python 代码的第一 步,就是不要去优化 Python 代码!首先写出具有可读性和简洁性的 代码,然后在需要时去优化。针对你的程序需要处理的数据集,这五个可选方案中的每一

个都有可能是最快的 1 因此,程序的清晰性应该成为第一优先因素。

注意:为了深人真相之中,你可以改写这段代码,在被计时的全部五个迭代技巧中都应用 一 个 简单的用户定义函数。例如(来自本书示例 timeseqs2B.py)

def F(x): return x def listComp(): return [F(x) for x in repslist] def mapCall(): return list(map(F, repslist)) 632

I

第 21 章

这些结果(在文件 timeseqs-results.txt 中)与使用像 abs 的内置函数非常相似一至少在

CPython 中, map 是最快的。更 一 般的情况是,对于现在的五个迭代技巧,如果这五个

都调用任意函数(无论是否内置)那么 map 是最快的,但是在不调用时则是最慢的。 也就是说, map 看起来慢些,因为它要求函数调用,而函数调用通常相对慢些。由千 map 不能避免调用函数,它只是因为与函数沾边就会轻易地输掉!其他的迭代工具获胜

的原因是它们可以不通过函数调用来完成操作。我们在接下来的 timeit 模块下运行的 测试中可以证明这一发现。

计时模块可选方案 上一节的计时模块可以工作,但是我们还可以把它修改得更加易用。最明显的是,其中的 函数需要将重复次数作为第一个参数传入,而没有为它准备默认值~或许还不太重要, 但作为 一 种多用途工具确实不太理想。我们也可以利用之前见过的 min 技巧来稍微简化返 回值井节省一段很小的时间开销。

下面的代码实现了另 一 个可选的计时器模块,它解决了这些问题,允许重复次数作为一个 名为_reps 的关键字参数传递进来: # File timer2.py (2.X and 3.XJ

'""' total(spam, 1, 2, a=3, b=4, _reps=lOOO) calls and times spam(l, 2, a=3, b=4) _reps times, and returns total time for all runs, with final result.

bestof(spam, 1, 2, a=3, b=4, _reps=S) runs best-of-N timer to attempt to filter out system load variation, and returns best time among _reps tests. bestoftotal(spam 1, 2, a=3, b=4, _repl=S, reps=lOOO) runs best-of-totals test, which takes the best among _reps1 runs of (the total of _reps runs); import time, sys timer= time.clock if sys.platform[:3] =='win'else time.time def total(func, *pargs, **kargs): _reps = kargs. pop ('_reps ', 1000) reps list = list(range(_reps)) start = timer() for i in repslist: ret = func(*pargs, **kargs) elapsed = timer() - start return (elapsed, ret)

# Passed-in or default reps # Hoist range out for 2.X lists

def bestof(func, *pargs, **kargs): _reps= kargs.pop('_reps', 5) best= 2 ** 32 for i in range(_reps): start = timer()

基准测试

I

633

ret = func(*pargs, **kargs) elapsed = timer() - start if elapsed< best: best= elapsed return (best, ret) def bestoftotal(func, *pargs, **kargs): _repsl = kargs.pop('_repsl", 5) return min(total(func, *pargs, **kargs) for i in range(_reps1)) 该模块在文件顶部的文档字符串描述了它设计的用法。它使用字典 pop 操作从测试函数传 入的参数中移除_ reps 参数,并为_ reps 提供了 一 个默认值(它的不常见的名字,可以避 免与被计时的由数上的真实关键字参数发生冲突)。 注意这里的最短总时间为什么使用了前面所述的 min 和生成器方案,而不是嵌套调用方案: 一 方面是因为这简化了结果,并避免了上 一 版本的 一 段较小的时间消耗(它的代码在总时

间被计算之后选取最短时间);另一方面也因为它必须支持两个不同重复次数的默认值参数, 也就是说 total 和 bestof 二者不能同时使用相同名字的重复次数。你可以在代码中添加参

数打印,来追踪其运行情况。

如果你想测试这个新的计时器模块,可以像下面这样修改计时脚本,或者使用本书示例文 件 timeseqs_timer2.py 中预先编写好的版本;结果本质上还是相同的(这主要是 一个 API 变

化),因此,这里不再列出它们:

import sys, timer2 for test in (forloop, listComp, mapCall, genExpr, genFunc): (total, result) = timer2.bestoftotal(test, _repsl=S, _reps=lOOO) # Or: # (total, result)= timer2.bestoftotal(test) # (total, result)= timer2.bestof(test, _reps=S) # (total, result)= timer2.total(test, _reps=lOOO) # (bestof, (total, result))= timer2.bestof(timer2.total, test, _reps=S)

print ('%-9s: %.Sf=> [%s... %s]'% (test. name_, total, result[o], result[-1]))



你也可以像之前那样运行 一 些交互式测试一同样,结果本质上是相同的,但是我们需要 将重复次数作为关键字参数传入,如果省略则会提供默认值,在 Python 3.3 中:

»> from timer2 import total, bestof, bestoftotal >» total(pow, 2, 1000)[0] 0. 0029562534118596773 >» total(pow, 2, 1000, _reps=1000)[0] 0.0029733585316193967 »> total(pow, 2, 1000, _reps=1000000)[0] 1. 2451676814889865 »> bestof(pow, 2, 100000)[0]

634

I

第 21 章

#

2 ** /000, 1K dflt reps

#

2 ** /000, JK reps

#

2 ** /000, JM reps

# 2 ** JOOK, 5 dflt reps

0.0007550688578703557 »> bestof(pow, 2, 1000000, _reps=30)[o] 0.004040229286800923

#

»> bestoftotal(str.upper,'spam', _reps1=30, _reps=lOOO)

It Best of 30, tot of I K

(0.0004945823198454491,'SPAM') >» bestof(total, str.upper,'spam', _reps=30) (0.0005463863968202531, (0.0004994694969298052,'SPAM'))

# Nested calls work too

2 ** IM, besr of 30

为了测试对关键字参数的支持,我们可以定义 一 个带有更多参数的函数,并通过名称传人

其中 一 些参数:

»> def spam(a, b, c, d): return a + b + c + d >» total(spam, 1, 2, c=3, d=4, _reps=lOOO) (0.0009730369554290519, 10)

»> bestof(spam, 1, 2, c=3, d=4, _reps=lOOO) (9.774353202374186e-01, 10)

»> bestoftotal(spam, 1, 2, c=3, d=4, _repsl=lOOO, _reps=lOOO) (0.00037289161070930277, 10)

>» bestoftotal(spam, *(1, 2), _repsl=lOOO, _reps=lOOO, **dict(c=3, d=4)) (0.00037289161070930277, 10)

在 3.X 中使用 keyword-only 参数 这段讲述中的最后 一 个要点:我们也可以在这里利用 Python 3.X 中的 keyword-only 参数来

简化计时器模块的代码。我们在第 18 章中学过, keyword-only 参数非常适合像这里函数的 _reps 参数的配置选项。它们必须编写在函数头部的“*”之后和“**”之前,在一 次函数 调用中它们必须通过关键字传人而且要放在“**”的前面(如果使用了“**”的话)。下

面的代码是前 一 个模块基千 keyword-only 的替代方案。尽管更简单了,但它只能在 Python 3.X 下编译和运行,

2 . X 中不支持:

# File timer3.py (3.X 011/y)

"""

Same usage as timer2.py, but uses 3.X keyword-only default arguments instead of diet pops for simpler code. No need to hoist range() out of tests in 3.X: always a generator in 3.X, and this can't run on 2.X. import time, sys timer= time.clock if sys.platform[:3] =='win'else time.time def total(func, *pargs, _reps =lOOO, **kargs): start= timer() for i in range(_reps): ret = func(*pargs, **kargs) elapsed = timer() - start return (elapsed, ret) def bestof(func, *pargs, _reps=S, **kargs): best = 2 ** 32

基准测试

I

635

for i in range(_reps): start = timer() ret = func(*pargs, **kargs) elapsed = timer() - start if elapsed< best: best= elapsed return (best, ret) def bestoftotal(func, *pargs, _repsl=S, **kargs): return min(total(func, *pargs, **kargs) for i in range(_repsl)) 这一版本同上一版本一样,使用相同的方式并得到相同的结果,所以这里就不再重复列出

相同测试下的输出结果,你可以自己亲自试验一下。如果你试过了,请注意调用中的参数 顺序规则。例如,像下面这样调用 total:

(elapsed, ret) = total(func, *pargs, _reps=l, **kargs) 参阅第 18 章了解更多 3.X 版本中 keyword-only 参数的知识;它们能用来简化这类可配置工 具的代码,却不能向后兼容 Python 2.X 。如果你想比较 2.X 和 3.X 的速度,或身为需要同

时支持这两种 Python 系列的程序员,那么前一个版本可能是更好的选择。 同时,要注意对千一般的简单由数(例如上面版本中测试的那些)而言,计时器代码本身 的运行时间与被测时间处千同一最级,因此你不必对计时器结果过千较真。不过,计时器 的结果总可帮助你评判编码可选方案之间的相对速度,而且对千那些运行时间更长或经常 重复的操作会更有意义。

其他建议 为了获得更深刻的理解,你可以试着修改这些模块所使用的重复次数,或探索 Python 标准 库中的可选的 timeit 模块。 timeit 模块自动化了代码计时,支持命令行使用模式,并巧 妙处理了一些平台相关的问题。事实上,我们在下一小节中就会用到它。

你也可以了解一下 profile 标准库模块,它提供了一套完整的源代码分析器工具。我们将在 第 36 章中的 一 个大型项目开发工具的情境下学习更多相关知识。通常,你在计时其他可选 方案之前,应该首先分析并定位出当前代码的性能瓶颈。 你可以试着修改或模拟计时脚本,来测量 3.X 和 2.7 版本中的集合与字典推导的速度,以 及它们对应的 for 循环等价形式。在 Python 程序中使用它们比直接创建结果列表要更不常

见,因此我们把这个任务留到推荐练习 一栏中(请别直接猜谁快谁慢)。下一小节会揭开 一些答案。

最后,请保存好我们这里编写的代码,以备之后的使用:在本章结尾的一道习题中,我们 会对它稍加修改来测匮另外的数值平方根操作的性能。如果你想深入这一主题,我 们也会

在习题中交互地进行字典推导与 for 循环方案的计时实验。

636

I

第 21 章

用 timeit 为迭代和各种 Python 计时 上一小节使用了我们自己编写的计时函数来比较代码速度。正如那里提到的,标准库也配 备一个用法相似的名为 timeit 的模块。不过这个 timeit 模块提供了更多的灵活性,也更

好地帮助用户避免一些平台差异。 照例,你必须理解上一小节中讲述的那些基本原理。 Python 的“电池依赖”方式意味着你 也可以找到预编写的选项,不过你仍要了解它们背后的思想来正确地使用它们。确实,该 模块就是一个典型的例子一—它曾常常被不明白它所蕴含原理的人们错误地使用。既然我 们已经学习了基础知识,那么就让我们接着学习一个能自动化很多工作的工具。

timeit 基础用法 在对大型脚本使用 time it 模块之前,先让我们从它的基础知识开始说起。有了 timeit,

测试就用可调用对象或是语句字符串所指定。如果使用语句字符串,则可以支持以分隔符”," 或换行符 “\n" 分开的多条语句,以及用空格或制表符(例如\ n\t) 表示的嵌套块中的缩

进语句。你也可以为测试指定初始化动作,井可通过命令行和 API 调用从脚本和交互式命 令行下启动。

交互式用法与 API 调用 举个例子, timeit 模块中的 repeat 调用运行若干组(由关键字参数 repeat 指定)相同测 试井返回 一个列表,列表中的各项分别表示运行各组测试的总时间,而每组测试则由若干

条(由关键字参数 number 指定)相同的测试语句组成。因此列表中的最小项(可通过 min 函数获取)就是各组运行的最佳时间,从而帮助过滤掉系统负载波动,否则计时结果会存 在较大的偏差。

下面的代码展示了实际中的该调用,计时了列表推导在 CPython 和 PyPy (它现在支持

Python 2.7 编程)在第 2 章中介绍过的 Python 实现上的运行。这里的结果以秒为单位给出 了五次运行中的最短总时间,其中每一 次都执行语句字符串 1000 遍,语句字符串自身每次 执行时又创建了 一个拥有 1000 个元素的整数列表(参阅附录 B 了解这里在 Windows 命令

行中启动不同 Python 版本的更多内容) :

c:\code> PY -3 Python 3.3.0 (v3 . 3. o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit... >» import timeit »> min(timeit.repeat(stmt="[x ** 2 for x in range(1000)]", number=1000, repeat=S)) 0.5062382371756811 c:\code> py -2 Python 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)) on win32 »> import timeit >» min(timeit.repeat(stmt="[x ** 2 for x in range(lOOO)]", number=1000, repeat=S)) 基准测试

I

637

0.0708020004193198 c:\code> c:\pypy\pypy-1.9\pypy.exe Python 2.7.2 (341e1e3821ff, Jun 07 2012, 15:43:00) [PyPy 1.9.0 with MSC v.1500 32 bit] on win32 »» import timeit >»> min(timeit.repeat(stmt="[x ** 2 for x in range(1000)]", number=lOOO, repeat=S)) 0.0059330329674303905 抛开 PyPy 在 32 位架构上可能会慢 一 点这个事实,你会发现 PyPy 记录的速度比这里的

CPython 2.7 快 JO 倍,比 CPython 3.3 快 100 倍之多。当然,这只是 一 个小型的刻意设计的 基准测试,却是令人震惊的,也反映了与本书中其他测试一 致的相对速度排名(不过我们

也将 看到,在某些种类的代码中 CPython 还是能击败 PyPy) 。

这个特别的测试测量了列表推导和整数运算的速度。后者随着 2.X 和 3.X 的不同而变化 :

CPython 3.X 只有一个单独的整数类型,而 CPython 2.X 则有短整数和长整数。这能部分地 解释差异的大小,但结果是正确的。非整数测试可以得到相似的排名(例如,在本部分习

题解答中的浮点数测试),同时整数运算结果本身也意义重大。这是因为这里对速度的 一 到两个数显级 (10 的幕)的提升可以影响很多实际的程序,鉴于整数和迭代在 Python 代码 中几乎无处不在。

另一方面,这里的结果和上一小节中对应版本的结果有所不同,那里的 CPython 2.7 只是稍 快千 3.3 ,而 PyPy 总体上要快 10 倍,这是一个被本书中其他大多数测试都确认过的数字。 除了这里被计时的各类型代码外, time it 内部的不同编码结构也会产生影响 :对千像这里

被测试的语句字符串, timeit 会构建、编译并执行一个函数 def 语句字符串,将测试字符 串嵌入在内,从而避免在每次内层循环中都进行函数调用。我们将在下 一 小节看到,就相 对速度而言这是没有影响的。

命令行用法 timeit 模块有 着合理的默认值,既可以通过显式文件名,又可以通过使用 Pyt hon 的- rn 标签自动地在模块搜索路径上定位作为脚本运行(参阅附录 A) 。下面所有的代码均在

Python

(又名 CPython) 3.3 下运行。在该模式下, timeit 得到了单次 -n 循环的平均时间,

以微秒(标为 usec) 、亳秒 (msec) 或是秒 (sec) 为单位;为了将这里的结果同其他测试 报告的总时间值相比较,你可以乘以循环运行的次数一一这里为 500 微秒乘以 1000 次循环 等干 500 毫秒,也就是半秒的总时间:

c:\code> C:\pythonB\Lib\timeit.py -n 1000 "[x 1000 loops, best of 3: 506 usec per loop

**

2 for x in range(1000)]"

c: \code> python -m timeit -n 1000 "[x ** 2 for x in range(1000)]" 1000 loops, best of 3: 504 usec per loop

638

I

第 21 章

c:\code> py -3 -m timeit -n 1000 -rs "[x 1000 loops, best of s: SOS usec per loop

**

2 for x in range(1000)]"

作为一个示例,我们可以用命令行来验证对不同计时器调用的选择并不会影响本章目前为 止运行的不同版本速度比较结果一3 . 3 默认使用它的新调用,如果计时精度差别很大这可 能需要注意。为证明计时器调用因素确实是无关的,下面的代码使用- c 标签迫使 timeit

在所有版本中都使用 time.clock, 这是被 3.3 手 册标记为弃用的选项,但为了和之前版本 进行公平比较就必须这么做(这里我把 PyPy 添加到系统路径中,从而使命令更加简洁)

c:\code> set

PATH=%PAT欣; C: \pypy\pypy-1. 9

c:\code> py -3 -m timeit -n 1000 -r 5 -c "[x ** 2 for x in range(1000)]" 1000 loops, best of s: 502 usec per loop c:\code> py -2 -m timeit -n 1000 -r 5 -c "[x ** 2 for x in range(1000)]" 1000 loops, best of s: 70.6 usec per loop c:\code> pypy -m timeit -n 1000 -r 5 -c "[x ** 2 for x in range(1000)]" 1000 loops, best of 5: 5.44 usec per loop C:\code> py -3 -m timeit -n 1000 -r 5 -c "[abs(x) for x in range(10000)]" 1000 loops, best of s: 815 usec per loop C:\code> py -2 -m timeit -n 1000 -r 5 -c "[abs(x) for x in range(10000)]" 1000 loops, best of 5: 700 usec per loop C:\code> pypy -m timeit -n 1000 -r 5 -c "[abs(x) for x in range(10000)]" 1000 loops, best of 5: 61. 7 usec per loop 这些结果与本章之前在同类代码上的那些测试本质上是相同的。当应用 X

**

2 时,

CPython 2.7 和 PyPy 同样分别比 CPython 3.3 快 10 倍和 LOO 倍,说明计时器选择会影响计 时结果。对于采用之前我们自己编写的计时器计时的 abs(x) 结果,这两个版本的 Python 分别比 3.3 快一个小常扯以及快 IO 倍,这意味着 timeit 的不同的编码结构不会影响相对

比较的结果一一被测试代码的类型完全地决定了速度差异的大小。 这里 一 个微妙的细节:注意这些测试结果中的最后 三 个,它们模拟了之前自己编写的计时

器的运行(而且儿乎是一模一样的),但是由千 range 用法的不同似乎导致了一个较小的 总时间消耗一一原先它是 一 个预创建好的列表,但是这里是一个 3.X 的生成器或是一个 2.X

的在每个内层循环上都重新创建的列表。换言之,虽然我们并没有计时完全相同的东西, 但是被测试的 Python 版本的相对速度是一样的。

计时多行语句 为了在 API 调用模式下计时大型的多行代码,你可以使用符合 Python 语法的换行符、制表 符或空格,或者从已经满足这些需求的源文件中读入的代码。因为你在该模式下将 Python

字符串对象传入 Pytho n 函数中,所以不 需要任何 she ll 方面的 考虑,不过你也需小心必要 的转义缩进字符。例如下面的例子,计时了第 13 章中的 Python 3 .3 的循环可选方案;你可

以使用相同的模式来计时第 l4 章中的文件逐行读取器可选方案:

基准测试

I

639

c:\code> PY -3 »> import timeit »> min(timeit.repeat(number=lOOOO, repeat=3, stmt="l = [1, 2, 3, 4, 5]\nfor i in range(len(L)): l[i] += 1")) 0.01397292797131814 »> min(timeit.repeat(number=toooo, repeat=3, stmt="l = [1, 2, 3, 4, 5]\ni=O\nwhile i < len(l):\n\tl[i] += 1\n\ti += 1")) 0.015452276471516813 »> min(timeit.repeat(number=lOOOO, repeat=3, stmt="L = [1, 2, 3, 4, 5]\nM = [x + 1 for x in L]")) 0.009464995838568635 为了在命令行模式下运行像这样的多行语句,你可以针对你的 shell 将每条语句作为 一个单 独的参数传入,并使用空白来缩进。而 timeit 会把所有的行拼接起来并在语句之间插入换

行符,之后根据其自身的语句进行重新缩进。在该模式下,用空格来缩进会比制表符更合适, 你要根据你的 shell 来确保是否将代码参数放入括号中:

c:\code> py -3 -m timeit -n 1000 -r 3 "L = [1,2,3,4,5)" "i=O" "while i < len(l):" " L[i] += 1" " i += 1" 1000 loops, best of 3: 1. 54 usec per loop c:\code> py -3 -m timeit -n 1000 -r 3 "L = [1,2,3,4,5)" "M = [x + 1 for x in L]" 1000 loops, best of 3: 0.959 usec per loop

其他使用模式:初始化、总时间和可运行对象 timeit 模块也允许你提供在主语句作用域下运行的初始化代码,但是其时间不会计人主语

句的总时间。这对千你可能想从总时间中排除初始化代码的需求是非常有用的,比如必需 模块的导入、测试函数的定义以及测试数据的创建。因为它们是在同 一 个作用域下运行, 任何初始化代码所创建的名称对千主测试语句都是可用的 l 但在交互式命令行下定义的名 称一般是不可用的。

为了指定初始化代码,你可以在命令行模式下使用- s 标签,或在 API 调用模式下使用

setup 参数字符串。这可以更为精确地指定测试,例如下面的代码将列表初始化分离出来 作为初始化语句,以便只计时迭代过程。不过按照经验,你在测试语句中包括越多的代码, 其结果对实际代码就具有更一般化的适用性:

c:\code> python -m timeit -n 1000 -r 3 nl = (1,2,3,4,5]" "M = [x + 1 for x in Lr 1000 loops, best of 3: 0.956 usec per loop c:\code> python -m timeit -n 1000 -r 3 -s "L = [1,2,3,4,5]" "M = [x + 1 for x in L]" 1000 loops, best of 3: 0.775 usec per loop 这里是一 个在 API 调用模式下的初始化示例:我使用了如下类型的代码来计时第 18 章的最 小值示例中的基干排序的方案一有序的范围要远比随机数字排序得 更快,在 3.3 版本的 示 例代码中,排序也要比线性扫描更快(这里相邻的字符串被拼接起来)

640

I

第 21 章

>» from timeit import repeat >» min(repeat(number=1000, repeat=3, setup='from mins import min1, rnin2, min3\n' 'vals=list(range(1000))', strnt='min3 (*vals)')) 0.0387865921275079 »> min(repeat(number=lOOO, repeat=3, setup='from mins import minl, min2, min3\n' 'import random\nvals=[random.random() for i in range(1000)]', stmt='min3(*vals)')) o. 275656482278373 有了 timeit, 你也可以只计时总时间,使用模块的类 API, 计时可调用对象而不是字符串, 传入自动的循环次数,以及使用这里没有篇幅进一步展开的基于类的技巧,其他的令行转

换和 API 参数选项。你可以查阅 Python 的库手册以获得更多细节:

c:\code> PY -3 >» import timeit »> timeit.timeit(stmt='[x ** 2 for x in range(tooo)J', number=tOOO) 0.5238125259325834 >» timeit.Timer(stmt='[x ** 2 for x in range(tooo)]').timeit(tooo) 0.5282652329644009

#

Total time

#

Class AP!

»> timeit.repeat(stn计:::'[ x ** 2 for x in range(1000)]', number=lOOO, repeat=3) [0.5299034147194845, 0.5082454007998365, 0.5095136232504416] » > def testcase(): y = [x •• 2 for x in range(1000)]

#

Callable objects or code strings

»> min(timeit.repeat(stmt=testcase, number=1000, repeat=3)) 0.507382814046337

基准测试模块和脚本: timeit 现在我们不再更进一步了解 timeit 模块,而是研究一个将该模块应用到计时编程可选方案 和各个 Python 版本的程序。下面的 pybench.py 文件,被编写为计时将一系列编写在脚本文 件中的语句导入并运行的过程,可以在各个 Python 下运行。它用到了后面会讲述的一些应

用层级的工具。因为它主要使用了我们已经学过的思想而且伴有详实的注释作为文档,所

以我将它作为一份自学性材料以及一个 Python 代码阅读练习译注 30

译注 3:

pybench .py 文件中文档字符串翻译如下 :

pybench.py, 用一系列简单的代码字符串基准刹试来计时一或多种 Python 版本。

(纨写了)

一个允许改变持浏语句 (stmt 这里是对 statement 的简写)的函数。该系统本身能同时 运行在 2.X 和 3 . X 上`以及衍生出的不同 Python 实现。

基准测试

I

641

pybench . py: Test speed of one or more Pythons on a set of simple code-string benchmarks. A function, to allow stmts to vary. This system itself runs on both 2.X and 3,X, and may spawn both. Uses timeit to test either the Python running this script by API calls, or a set of Pythons by reading spawned command-line outputs (os . popen) with Python's -m flag to find timeit on module search path. Replaces $listif3 with a list() around generators for 3 . X and an empty string for 2.X, so 3.X does same work as 2.X. In command-line mode only, must split multiline statements into one separate quoted argument per line so all will be run (else might run/time first line only), and replace all \tin indentation with 4 spaces for uniformity. Caveats: command-line mode (only) may fail if test stmt embeds double quotes, quoted stmt string is incompatible with shell in general, or command-line exceeds a length limit on platform's shell--use API call mode or homegrown timer; does not yet support a setup statement: as is, time of all statements in the test stmt are charged to the total time. import sys, os, timeit defnum, defrep= 1000, 5

#

May vary per stmt

def runner(stmts, pythons=None, tracecmd=False): """

Main logic: run tests per input lists, caller handles usage modes. stmts: [(number?, repeat?, stmt-string)], replaces $listif3 in stmt 峦 hons : None=this python only, or [(ispy3?, python-executable- path) ) print(sys.version) for (number, repeat, stmt) in stmts: number= number or defnum repeat= repeat or defrep # O=default if not pythons : # Run stml on this py1hon: AP/ call # No need to split lines or quote here ispy3 = sys.version[o] =='3' stmt = stmt. replace ('$listi f3','list'if ispy3 else' ' ) best= min(timeit.repeat(stmt=stmt, number=number, repeat=repeat)) print('%,4f [%r]'% (best, stmt[:70))) else: # Run stmt on all pythons: command line # Split lines into quoted arguments

print(' - '* 80) print('[%r]'% stmt) for (ispy3, python) in pythons: stmt1 = stmt. replace('$listif3','list'if ispy3 else' ' ) stmt1 = stmtl.replace('\t','' * 4) lines = stmt1.split('\n') args = ' ' . join ('"%s"' % line for line in lines) cmd ='%s -m timeit - n %s -r %s %s'% (python, number, repeat, args) print(python) if tracecmd: print(cmd) 642

I

第 21 章

print('\t'+ os. popen(cmd). read().rs trip()) 然而,这个文件只是全部图景的一 半。你可以用这个模块的函数测试脚本,可以传入实际 同时也是可变的语句列表、待测试 Python 版本、预期的使用模式。例如,下面的 pybench_ cases .py 脚本,测试了 一 些语句和 Python 版本 , 并允许命令行参数决定它的部分操作: - a 测试所有列出的 Python 版本而不是一个,额外的士能追踪构建的命令行,这样你就可以 查看多行语句和缩进是如何按照之前展示的命令行格式进行处理的(参阅这两个文件的文

档字符串以获得更多细节)译注 4:

pybench_cases.py: Run pybench on a set of pythons and statements. Select modes by editing this script or using command-line arguments (in sys.argv): e.g., run a "C:\python27\python pybench_cases.py" to test just one specific version on stmts, "pybench_cases.py -a" to test all pythons listed, or a "py -3 pybench_cases.py -a -t" to trace command lines too.

import pybench, sys pythons = [ (1,'C:\python33\python'), (o,'C:\python27\python'), (o,'C:\pypy\pypy-1.9\pypy')

# (ispy3?, path)

stmts = (O, (o, (o, (o, (o, (o,

# (num,rpr,stmt) # Iterations #\n=multistmt #\n\t=indent # $=list or" #Stringops

[ O, o, o, o, o, o,

" [ x ** 2 for x in range (1000)] "), "res=[]\nfor x in range(1000): res.append(x ** 2)"), "$listif3(map(lambda x: x ** 2, range(1000)))"), "list(x ** 2 for x in range(1000))"), "s ='spam'* 2500\nx = [s[i) for i in range(10000))"), "s ='?'\nfor i in range(10000): s +='?'"),

tracecmd ='-t'in sys.argv pythons = pythons if'-a'in sys.argv else None pybench.runner(stmts, pythons, tracecmd)

# -t: trace command lines? II -a: all in list, else one?

基准测试脚本结果 下面是该脚本测试 一 个特定版本(运行该脚本的 Python) 时的输出:该模式使用直接的 译注 4 :

pybench_cases.py 文件中文档字符串翻译如下:

pybench_cases.py :

在一系列 Python 版本和语句上运行 pybench 。 通过编辑该脚本或使

用命令行未数(在 sys . argv) 选择模式 . 例如 , 运行 “C:

\python27\python pybench_

cases.py" 在一个特定版本的 Python 上浏试语句 , 运行 “pybench_ cases.pu

-a " 测试

所有列出的 Python 版本 ` 运行 “ py-3 pybench_cases.py -q -t " 跟踪命令 行。

基准测试

I

643

API 调用而不是命令行,将总时间列在左边 一 栏,将被测试的语句列在右边 一 栏。在前两 个测试中,我再次使用 3.3 版本的 Windows 启动器来计时 CPython 3.3 和 2.7; 而在第三个 测试中,我运行了 PyPy 的 1.9 发行版:

c: \code> py -3 pybench_cases. py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] 0.5015 ['[x ** 2 for x in range(1000)]'] 0,5655 ['res=[]\nfor x in range(1000): res.append(x ** 2)'] o.6044 ['list(map(lambda x: x ** 2, range(1000)))'] 0.5425 ['list(x ** 2 for x in range(1000))'] 0.8746 ["s ='spam'* 2500\nx = [s[i] for i in range(10000)]"] 2. 8060 [ "s ='? ' \nfor i in range (10000): s +='? "'] c: \code> py -2 pybench_cases.py 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)] 0.0696 ['[x ** 2 for x in range(1000)]'] 0.1285 ['res=[]\nfor x in range(1000): res.append(x ** 2)'] 0.1636 ('(map(lambda x: x ** 2, range(1000)))'] 0.0952 ['list(x ** 2 for x in range(1000))'] o.6143 ["s ='spam'* 2500\nx = [s[i] for i in range(10000)]"] 2.0657 ["s ='?'\nfor i in range(10000): s +='?"'] c:\code> c:\pypy\pypy-1.9\pypy pybench_cases.py 2.7.2 (341ele3821ff, Jun 07 2012, 15:43:00) [PyPy 1.9,0 with MSC v.1500 32 bit] 0.0059 ['(x ** 2 for x in range(1000)]'] 0.0102 ('res=[]\nfor x in range(lOOO): res.append(x ** 2)'] 0.0099 0.0156 0.1298 5.5242

['(map(lambda x: x ** 2, range(1000)))'] ['list(x ** 2 for x in range(1000))'] ["s ='spam'* 2500\nx = [s[i] for i in range(10000)]"] ("s ='?'\nfor i in range(10000): s +='?"']

下面的代码展示了该脚本为每一个语句字符串测试多个 Python 版本时的输出。在该模式

下,脚本本身是通过 Python 3.3 运行的,但是它启动了 shell 命令行,从而启动其他版本的 Python 在测试字符串上运行 timeit 模块。为了在命令行下满足 timeit 的预期和 shell 的要 求,该模块必须分离、格式化,并且给多行语句加引号。 该模式也依赖千 -m 的 Python 命令行标签,从而当其作为脚本运行时在模块搜索路径上定

位 timeit 模块。同时也可以定位 os.popen 和 sys.argv 标准库工具分别执行 shell 命令和 检查命令行参数。参阅 Python 手册和其他资源以获得关千这些调用的更多知识 1 os.popen

曾在第 9 章对文件相关知识的介绍中被简要地提及,并在第 13 章对循环的介绍中被展示。 你可以在运行时使用标签 -t, 来观察命令行的运行情况:

c:\code> py -3 pybench_cases.py -a 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)]

--------------------------------['[x ** 2 for x in range(1000)]'] C:\python33\python

644

I

第 21 章

1000 loops, best of s: 499 usec per loop C:\python27\python 1000 loops, best of S: 71. 4 usec per loop C:\pypy\pypy-1.9\pypy 1000 loops, best of 5: s.71 usec per loop

---------------------·--------------------------['res=[]\nfor x in range(1000): res.append(x ** 2)'] C:\python33\python 1000 loops, best of s: 562 usec per loop C:\python27\python 1000 loops, best of 5: 130 usec per loop C: \pypy\pypy-1. 9\pypy 1000 loops, best of s: 9.81 usec per loop - - - - - - - - - - - - - - - - - •• ··一一一一- - -一- -一一一一一一一一·伽会...一一--一一一··

['$listif3(map(lambda x: C:\python33\python 1000 loops, best C:\python27\python 1000 loops, best C:\pypy\pypy-1 . 9\pypy 1000 loops, best 一··

--

- -

··一·伽一一-.呻-

•·

x ** 2, range(1000)))'] of 5: 599 usec per loop of 5: 161 usec per loop of s: 9.45 usec per loop

- -一··一·岫一一---------------------------

['list(x ** 2 for x in range(1000))'] C:\python33\python 1000 loops, best of 5: 540 usec per loop C:\python27\python 1000 loops, best of s: 92.3 usec per loop C: \pypy\pypy-1. 9\pypy 1000 loops, best of 5: 15.1 usec per loop

------------------------------------------------["s ='spam'* 2500\nx = C:\python33\python 1000 loops, best C:\python27\python 1000 loops, best C: \pypy\pypy-1. 9\pypy 1000 loops, best

[s[i] for i in range(10000)]"] of s: 873 usec per loop of s: 614 usec per loop of s: 118 usec per loop

------ -------------------- ----------------------[ "s ='?'\nfor i in range(10000): s C:\python33\python 1000 loops, best of s: 2.81 C:\python27\python 1000 loops, best of 5: 1.94 C:\pypy\pypy-1.9\pypy 1000 loops, best of 5: 5.68

+='? "'] msec per loop msec per loop msec per loop

如你所见,上面的大部分测试都表明 CPython 2.7 仍然比 CPython 3.3 要快,

PyPy 则明显

比这两者都快~ PyPy 的速度只有 CPython 的一半,大概是因为 内存管理的 差异。另一方面 ,计时结果最好是相对的。除了本章前面提到过的常见计时警

告外 :



timeit 可能会以超出我们探讨范围的方式使结果产生偏差(例如,垃圾回收)

基准测试

I

645



存在一个随 Python 版本的不同而变化的基本时间开销,这里将其忽略(不过看上去是

平凡的)。



该脚本执行的语句非常小,井不一定能反映实际中的代码(但是其结果是正确的)。



结果有时会以看上去随机的方式变化(这里使用进程时间可能会有所帮助).



这里列出的所有的结果很容易在未来发生改变(也就是,在每一 个新的 Python 发行版

中!) 换言之,你应该从这些数字中得出自己的结论,并在自己的机器上和自己的 Python 版本中 运行这些测试,以获得与自身需求更为相关的结果。为了测定每个版本 Python 的基础时间

消耗,你可以运行不带语句或者 pass 语句参数的 timeit 。

基准测试的更多乐趣 想要更深入地理解,你也可以试着在其他的 Python 版本上运行脚本,或是测试其他的语句

字符串。本书示例套件中的 pybench_cases2.py 文件增加了更多测试,从而能帮助你看出

CPython 3.3 与 3.2 之间的差异, PyPy 的 2.0 测试版与其当前版本的差异,以及其他的使用 案例。

Map 的胜利与 PyPy 的罕见失败 例如,下面在 pybench_cases2.py 中的测试计时了通过函数调用进行其他迭代操作的影响,

按照本章前面的提示,增加了 map 的获胜机会。一般来说, map 会因为与泊数调用的联系 在性能比拼中失败:

# pybench_cases2.py

pythons+= [ (1,'C:\python32\python'), (o,'C: \pypy\pypy-2. o-beta1 \pypy')] stmts += [ Use Junction calls: map wins (o, o, "(ord(x) for x in'spam'* 2500]"), (o, o, "res=[]\nfor x in'spam'* 2500: res.append(ord(x))"), (o, o, "$listif3(map(ord,'spam'* 2500))"), (o, o, "list(ord(x) for x in'spam'* 2500)"}, II Set and diets (o, o, "{x ** 2 for x in range(1000)}"), (0, o, "s=set()\nfor x in range(1000): s.add(x ** 2)"), (0, o, "{x: x ** 2 for x in range(1000)}"), (o, o, "d={}\nfor x in range(1000): d[x] = x ** 2"), # Pathological: 300k digits (1, 1, "len(str(2**1000000))")]/tPypy loses on this today

#

646

I

第 21 章

下面是在 CPython 3.X 上该脚本对这些语句测试的结果。这些结果展示了当函数调用使竞

争平等化后, map 是如何成为最快的 (map 在之前当其他方案运行一个内联的 X ** 2 时输 掉了) :

c:\code> py -3 pybench_cases2.py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) (MSC v.1600 64 bit (AMD64)] 0.7237 ["[ord(x) for x in'spam'* 2500]"] 1.3471 ["res=[]\nfor x in'spam'* 2500: res.append(ord(x))"] 0.6160 ["list(map(ord,'spam'* 2500))"] 1.1244 ["list(ord(x) for x in'spam'* 2500)"] 0.5446 ['{x ** 2 for x in range(1000)}'] o.6053 ['s=set()\nfor x in range(1000): s.add(x ** 2)'] 0.5278 ['{x: x ** 2 for x in range(1000)}'] 0.5414 ['d={}\nfor x in range(1000): d[x] = x ** 2'] 1.8933 ('len(str{2**1000000))'] 照例,在目前的这些测试上 2.X 工作起来比 3.X 要快,而 PyPy 在除最后一 个测试外的所有

测试上仍然是最快的,它在最后一个测试上慢了一整个数量级 (JO 倍),但是这里它以相 同的程度赢得了所有其他的测试。然而,如果你运行 pybench_cases2.py 文件中预编写好的

测试,就会发现在逐行读取文件时 PyPy 也输给了 CPython, 也就是下面这个位于 stmts 列

表中的测试元组:

(o, o, "f=open('C:/Python33/Lib/pdb.py')\nfor line in f: x=line\nf.close()"), 该测试使用文件迭代器打开并逐行读取 一 个 60K 大小、 1675 行的文本文件。它的输入循环

大概主导了总的测试时间。在这个测试上, CPython 2.7 比 3.3 快两倍,但是通常 PyPy 比 CPython 慢一个数蜇级。

你可以在 pybench_cases2 结果文件中找到这个案例,也可以交互式地或通过系统命令行来

验证(这正是 pybench 内部所做的) :

c:\code> py -3 -m timeit -n 1000 -r 5 "f=open('C:/Python33/Lib/pdb.py')" "for line in f: x=line" "f.close()"

» > import timeit >>>

min(timeit.repeat(number丑ooo, repeat=S, stmt="f=open('C: /Python33/Lib/pdb. py') \nfor line in f: x=line\nf. close()"))

参阅本书示例包中的 listcomp-speed.txt 文件,以获得另 一个关干同时测定列表推导和 PyPy

的当前文件速度的示例;它使用直接的 PyPy 命令行来运行第 14 章的代码,并得到相似的 结果:目前的 PyPy 行输人会慢上 JO 左右的 一 个因子。 这里我将省略其他 Python 版本的输出, 一是为了节省篇幅, 二是因为当你读到这里的这些 文字时,这些结果很可能已经改变。通常,不同类型的代码能表现出不同类型的相对性能

比较。虽然 PyPy 会优化很多算法相关的代码,但是它不 一定会优化你的代码。你可以在本

基准测试

I

647

书的示例包中找到其他的结果,但是你最好亲自运行这些测试,来验证目前的这些发现,

或是观察它们在未来可能的不同结果。

重新回顾函数调用的影响 如前所述,对于其他用户定义的函数 map 也会获胜。下面的测试证明了之前提示里的结论: 如果在所有可选方案中必须使用某 一 泊数的话,那么 CPython 下的 map 会在性能比赛中获胜

stmts = (o, (o, (o, (o,

[ o, o, o, o,

"def "def "def "def

f(x): f(x): f(x): f(x):

return return return return

x\n[f(x) for x in'spam'* 2500]"), x\nres=[]\nfor x in'spam'* 2500: res.append(f(x))"), x\n$listif3(map(f,'spam'* 2500))"), x\nlist(f(x) for x in'spam'* 2500)")]

c:\code> py -3 pybench_cases2.py 3,3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] 1.5400 ("def f(x): return x\n[f(x) for x in'spam'* 2500]"] 2.0506 ["def f(x): return x\nres=[]\nfor x in'spam'* 2500: res.append(f(x))"] 1.2489 ["def f(x): return x\nlist(map(f,'spam'* 2500))"] 1.6526 ("def f(x): return x\nlist(f(x) for x in'spam'* 2500)") 你可以比较上面的结果和前一 小节的 ord 测试 1 尽管用户定义的函数可能比内置函数慢些,

通常目前较大的速度瓶颈来自函数,无论它们是否是内置的。注意这里的总时间还包括创 建一 个辅助函数的开销,不过每经过 10 000 次内层循环才有一个。因此根据常识和其他测

试的结果,这是一个可以忽略的因素。

比较技术:自己编写的 vs 内置的 我们可以高屋建领地看看这一小节基千 time it 的结果和上一 小节基千自己编写计时器的结 果比较起来如何,这可以通过运行本书示例包中的文件 timeseqs3.py 实现一它使用自己编 写的计时器但是实现了相同的 X ** 2 运算,并使用了和 pybench_cases.py 一样的重复次数:

c:\code> py -3 timeseqs3.py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) (MSC v.1600 64 bit (AMD64)] forloop : 0.55022 => (0... 998001] listComp : 0.48787 => [o... 998001] mapCall : 0.59499 => (0... 998001] genExpr : 0.52773 => [o... 998001] genFunc : 0.52603 => (0... 998001] c:\code> py -3 pybench_cases.py 3.3.0 (v3.3.o:bd8afb9oebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] 0.5015 ['[x ** 2 for x in range(lOOO)]'] 0.5657 ['res=[]\nfor x in range(lOOO): res.append(x ** 2)'] 0.6025 ['list(map(lambda x: x ** 2, range(1000)))'] 0.5404 ['list(x ** 2 for x in range(1000))'] o.8711 ["s ='spam'* 2500\nx = [s[i] for i in range(10000)]"] 2.8009 ["s ='7'\nfor i in range(10000): s +='?'"]

648

I

第 21 章

自己编写的计时器的结果与本小节使用 timeit 基千 pybench 的结果非常相似,但是它们并 不完全一 样:基于自己编写的计时器的 timeseqs3.py 引人了 一 个在内部总循环中的函数调用,

以及一 个本身最短时间逻辑计时器中的较小时间开销,但同时 timeseqs3 .py 也在内层循环 上使用了 一 个预构建的列表而不是 3.X 的 range 生成器,这使得它在可比较的测试上总体

稍快 一 些。

(我把这个例子称为“明智检查”,但我不太确定这 一 术语在基准测试中是否

适用!)

可提升的空间:初始化 就像大多数软件,本小节的程序是开放式的并可以被任意地扩展。作为 一 个例子,本书示 例包中的 pybench2.py 和 pybench2_cases.py 文件添加了对之前讲过的 timeit 的初始化语句 选项的支持,井在 API 调用和命令行两种模式下都支持。 坦白地讲,该特征起初出千简洁性被省略了,因为我的测试曾看上去并不需要它:这是因为,

一 方面,计时更多的代码可以给不同 Python 版本间的比较提供更为完整的图像,另一方面, 对同 一 Python 版本上不同可选方案的初始化过程的开销是相同的。即便如此,有时在被测

代码作用域中提供只运行 一 次的初始化代码是有用的,不过初始化代码的时间并不计入语 句的总时间中。初始化代码的例子有模块导入、对象初始化和辅助函数定义等。 我不会完整地列出这两个文件,但这里它们发生的变化可以作为 一个实际中软件演化示例。

作为测试语句,初始化代码与在 API 调用模式下一 样地传入,但在命令行模式下却被分离、 用空格缩进并每行都带有一个- s 参数传入(这里没有使用 “$listif3", 因为初始化代码 不被计时) : # pybench2.py

...

def runner(stmts, pythons=None, tracecmd=False): for (number, repeat, setup, stmt) in stmts: if not pythons: best= min(timeit.repeat( setup=setup, stmt=stmt, number=number, repeat=repeat)) else: setup = setup.replace('\t', • ' * 4) setup=''.join('-s”为s"'% line for line in setup.split('\n •)) 于or

(ispy3, python) in pythons:

...

cmd ='%s -m timeit -n %s -r %s %s %s'% (python, number, repeat, setup, args) # pybench2_cases.py

import pybench2, sys stmts = [ # (num.rpt,setup,stmt) (O, o,'"', "[x ** 2 for x in range(1000)]"), (o, o, "", "res=[]\nfor x in range(1000): res.append(x ** 2)"),

基准测试

I

649

(o, o, "def f(x): \n\treturn x", "[f(x) for x in'spam'* 2500]"), (o, o, "def f(x):\n\treturn x", "res=[]\nfor x in'spam'* 2500:\n\tres.append(f(x))"), (o, o, "L = [1, 2, 3, 4, 5]", "for i in range(len(L)): L[i) += 1"), (o, o, "L = [1, 2, 3, 4, s]", "i=O\nwhile i < len(L):\n\tL[i) += 1\n\ti += 1")] pybench2.runner(stmts, pythons, tracecmd) 你可以使用- a 和- t 命令行标签运行这个脚本,了解初始化代码的命令行是如何被创建的。 例如,下面这个指定了测试的元组为 3.3 版本生成了命令行。也许看起来不太漂亮,但足 以将指令从 Windows 传人 timeit, 同时在行之间用换行符拼接井以适当的重新缩进插人到 生成的计时器函数中 :

(o, o, "def f(x):\n\treturn x", "res=[]\nfor x in'spam'* 2500:\n\tres.append(f(x))") C:\python33\python -m timeit -n 1000 -r 5 -s "def f(x):" -s " "for x in'spam'* 2500:" "res.append(f(x))"

return x" "res=[]"

在 API 调用模式下,代码字符串是被无改变地传入的,因为不需要考虑 shell, 所以也可以 使用嵌人的制表符和行结束符。你可以亲自试验来了解更多 Python 代码可选方案的速度。

在命令行模式下,对千大型的代码段落你也许最终会受到 shell 命令长度的限制,但是我们 自己编写的计时器和 pybench 中基千 timeit 的 API 调用模式都支持更自由的代码。基准测 试还可以有更多的用法,不过我们得把未来进 一步的改进留到椎荐习题中。

其他基准测试主题: pystones 本章集中在讲述可以在自己代码中使用的基础代码计时技术,也适用干 一 般化的 Python 基

准测试,同时本章中的例子也可作为本书的一 个大型开发案例。总而言之, Pytho11 中的基 准测试是一个比我们目前所介绍更为广阔而丰富的领域。如果你有兴趣进一步了解这 一主

题,可以搜索相关的网页。其中,你会发现:



pystone.py—设计能够测晕一 系列代码 Python 速度的程序,并在其 Lib\test 目录下配 有 Python



http://speed.python劝了一一集合了一般 Python 基准测试的项目站点



http://speed.pypy.org一模仿了上 一 条中网站的 PyPy 基准测试站点

例如, pystone 测试最早是由 Python 的创始人 Guido van Ros sum 从一个 C 语言的基准测试

程序翻译成 Python 语言的。它提供了另一种测试不同 Python 实现相对速度的方法,其结 果基本上与我们这里得到的相吻合 :

6so

I

第 21 章

c:\Python33\Lib\test> cd C:\python33\lib\test c:\Python33\Lib\test> py -3 pystone.py Pystone(1.1) time for 50000 passes : o.685303 This machine benchmarks at 72960.4 pystones/second

c: \Python33\Lib\test> cd c: \python27\lib\test c:\Python27\Lib\test> py -2 pystone.py Pystone(1.1) time for 50000 passes: 0.463547 This machine benchmarks at 107864 pystones/second

c: \Python27\Lib\test> c: \pypy\pypy-1.9\pypy pystone.py Pystone(1.1) time for 50000 passes~ 0.099975 This machine benchmarks at 500125 pystones/second 由千现在我们要结束本章了,这将足以作为我们测试结果的独立确认。分析 pysrone 结果的

含义留作推荐习题;其代码在 3.X 和 2.X 中是不同的,但今天看上去只在打印操作和全局 变屈初始化上有所不同。你也甜牢记基准测试只是 Python 中诸多代码分析方面中的 一 个:

要获得相关领域的备选项的指南(例如测试),参阅第 36 章对 Python 开发工具的复习。

函数陷阱 既然已经到了函数介绍的尾声,让我们复习 一 些常见的陷阱。函数有着一 些你想不到的边 缘地带,它们都相对晦涩,而且有些巳经在最新版本中从语言里完全消失,但多数都会让 新手吃亏。

局部变量是被静态检测的 正如我们所知道的 一 样, Python 默认把在一个函数中赋值的变址名归为局部变量,它们存 在千函数的作用域中并只在函数运行时存在。你需要注意的是, Python 是在编译 def 代码

时静态检测 Python 的局部变扯的,而不是在运行时通过发现赋值语句进行检测的。这也成 为了各大 Python 论坛中初学者最常提问的问题。

一 般来说,没有在函数中赋值的变篮名会在整个外围模块文件中查找:

»> X = 99 >» def selector(): print(X)

#

#

X used but 1101 assigned Xfound i11 global scope

» > selector() 99 这里,函数中的 X 被解析为模块中的 X 。但是如果在引用之后增加了 一 个赋值语句,看看 会发生什么。

»> def selector():

基准测试

I

6s1

print(X)

#

X = 88

# X classified as a local name (everywhere) # Can also 加ppenfor "import X", "def X"...

Does not yet exist!

»> selector() UnboundlocalError: local variable'X'referenced before assignment 你得到了一个未定义变最名的错误,但其原因十分微妙。在交互式命令行下输入或从一个

模块文件中导入时, Python 会读入并编译这段代码。在编译时, Python 看到了对 X 的赋值 语句,并且决定了 X 会在函数中的所有地方都是局部变扯名。但是,当函数实际运行时,

因为在 print 执行时赋值语句并没有发生,所以 Python 会告诉你正在使用一个未定义的变 址名。根据 Python 的变量名规则,确实应该有这一报错。也就是说,局部变量 x 的确是在

其被赋值前就被使用了。实际上,任何在函数体内的赋值都会使相应的变址成为局部变址。 import 、=、嵌套 def 、嵌套类等,都会受这种行为的影响。 产生这种问题的原因是被赋值的变最名在函数内部的所有位置都被当作局部变址来对待的, 而不是仅仅在赋值以后的语句中才被当作是局部变量。实际上,前一个例子是有二义性的:

是希望打印一个全局变量 X 并创建一个局部变最 X ?还是这真的是一个程序错误?因为 Python 会在齿数中所有的地方都把 X 作为局部变量,所以它是 一 个错误。如果你想要打印

全局变最 X, 就需要在一个 global 语句中声明这一点:

»> def selector(): global X print(X) X = 88

# Force X to be global (everywhere)

>» selector() 99

不过要记住,这里的赋值语句同样会改变全局变量 X, 而不是一 个局部变量。在函数中, 不可能把一个简单变最名同时作为局部变扯和全局变址。如果你真希望打印全局变扯,并 在之后设置一个有着相同变扯名的局部变址,那么你需要导人外围的模块,并使用模块的 属性标记来获得其全局变量:

»> X = 99 >» def selector() : import _main_ print(_main_.X) X = 88 print(X)

»> selector() 99 88

6s2

I

第 21 章

import enclosing module Qualify to get to global version of name # Unqualified X classified as local # Prints local version of name # #

点号运算 (.X 这部分)从一个命名空间对象中拿到了变址的值。交互式命令行下的命名空

间是一个名为—main一的模块,所以_main

• X 可以拿到全局变批版本的 X 。如果还不够

清楚的话,请查看第 17 章。 在 Python 最近的版本中,已经针对这种情况发布了更为专用的 "unbound local" 错误消息

来改进这一问题,如前面的示例打印所示(它用来直接引发一个通用的名称错误) ;不过, 这个陷阱仍然十分常见。

默认值参数和可变对象 第 17 章和第 18 章曾简单提过,用作默认值参数的可变值可以在调用之间保留状态,尽管 这通常不是我们所期望的。通常,当 def 语句运行的时候,默认值参数就被求值并保存, 而不是在每次该函数被调用的时候。从内部来讲, Python 会将每一个默认值参数保存成一 个对象,并附加到这个函数本身。 这也就是通常我们所期望的:因为默认值参数是在 def 时被求值的,如果需要的话,它能 让你在外层作用域中保存值(在循环内通过工广函数定义的函数甚至依赖于这个行为一一

复习之前章节的内容)。但是因为默认值参数在调用之间持有着同一个对象,你必须非常 小心对可变默认值参数的修改。例如,下面的函数使用一个空列表作为默认值,并在函数

每次调用时都对它进行原位置改变:

»> def saver(x=[]): x.append(1) print(x)

»> saver([2]) [2, 1] »> saver()

#

Saves away a list object

# Changes same object each time!

II Default not used #

Default used

[1)

»> saver()

# Grows on each call!

[1, 1) »> saver() (1, 1, 1] 有些人把这种行为作为一种特性。因为可变类型的默认值参数在函数调用之间保存了它们

的状态,从某种意义上讲它们能充当 C 语言中的静态局部函数变量的角色。在一 定程度上, 它们能像全局变量那样工作,但是它们的变量名对千函数来说是局部变扯,从而不会与程

序中的其他变量名发生冲突。 不过在另一些人看来这似乎是一个陷阱,尤其是第一次遇到这种的情况的时候。在 Python 中有其他更好的办法在调用之间保存状态(例如,使用我们在本部分见过的嵌套作用域闭

包以及将在第六部分学习的类).

基准测试

I

653

此外,可变类型默认值参数比较难以记忆(理解起来也不容易) 。 它们的值在默认值参数

对象创建的时候被确定。在上 一 个例子中,其中只有 一 个列表对象作为默认值,这个列表 对象是在 def 语句执行时被创建的。你将不会在函数每次调用时都得到 一 个新的列表,因此, 每当新元素加入后列表就会变大。换句话说,对千每次调用它都没有被重究为空列表。 如果这不是你想要的行为的话,你可以直接在函数体的开始处对就默认值参数进行复制, 或者将默认值参数的表达式移入函数体内。只要值是存在于在代码中,且这部分代码又在 函数每次运行时都会执行的话,你就会每次都得到 一 个新的对象。

»> def saver(x=None): if xis None: X

= [)

x.append(1) print(x)

»> saver([2]) [2, 1] »> saver()

ti No arg山11en1 passed? #

Run code ro make a new list each rime

# Changes new list object

/fDoesn 't grow here

[1]

»> saver() [1] 顺便提一 下,这个例子中的 if 语句基本上可以被赋值语句 x = x or[ ]替代,该赋值语 句会利用 Python 的 or 语句返回 一 个操作数对象:如果没有参数传入 , 那么 x 将默认为

None, 因此 or 会返回右边的空列表。 尽管如此,这并不是完全相同的。如果传入的是一 个空列表, or 表达式会让函数扩展井返 回 一 个新创建的列表,而不是像 if 版本那样扩展并返回被传入的列表。

(此时的表达式变

成 []or[ ],会为右边的空列表求值。如果你想不起来原因的话,请参看第 12 章中的相关 内容。)在实际程序中,这两种行为可能都会被用到。 如今,另 一 种更不容易令人混淆的可以实现可变默认值参数状态记忆效果的方式,是使用 我们在第 19 章讨论过的函数属性:

» > def saver() : saver.x.append(1) print(saver.x)

>» saver. x = []

»> saver() [ 1l

>» saver() [ 1, 1]

>» saver() [1, 1, 1]

654

I

第 21 章

该函数的名称对千函数自身而言是全局的,但它不需要声明,因为它在函数内部是不会被 直接修改的。这并不总是以完全相同的方式使用,但如果被这样编写代码的时候, 一 个对

象就会更显式地附加到函数上(而且肯定更容易被理解)。

没有 return 语句的函数 在 Python 函数中, return (以及 yield) 语句是可选的。如果 一 个函数没有显式的返回值 时,那么函数会在控制权从函数体脱离时退出。从技术上来讲,所有的函数都会返回 一 个值, 如果没有提供 return 语句,那么函数将自动返回 None 对象:

»> def proc(x): print(x)

# No retum is a None rerum

»> x = proc('testing 123...') testing 123... >» print(x) None Python 中没有 return 语句的函数等价于 一 些其他语言中的“过程”。它们常被当作语句使用,

井且一 般忽略 None 这个返回结果,就好像它们只是执行任务而不需要计算有用的结果一样。 你有必要了解这些知识,因为 Python 不会报告你正在尝试使用 一个没有返回语句的函数的

返回结果。例如,我们在第 11 章提到过,赋值 一 个列表 append 方法运行的结果并不会导 致错误,但是得到的会是 None, 而不是改变后的列表:

»> list = [1, 2, 3] >» list = list.append{4) »> print(list) None

fl append is a "procedure" #

append changes list in place

第 15 章的“常见代码编写陷阱”这 一 小节更广泛地讨论了这 一主题。通常,任何以执行任

务为主的函数,往往被设计作为语句来运行,而不是表达式。

其他函数陷阱 这里有另外两个函数相关的陷阱 一一虽然主要是作为复习,但是因其相当常见而有必要在 这里重申。

外层作用域和循环变量:工厂函数 我们在第 17 章对外层函数作用域的讨论中讲述过这 一 陷阱,但是作为 一 个提醒:当编写工

厂函数(又名闭包)时,要小心地使用在外层函数作用域中对被外层作用域修改的变址的

查找一--当一个生成的由数之后被调用 时,所有这些引用都会记住在外 层函数作用域中最 后一 次循环迭代的值。在此情况下,你必须使用默认值参数来保存循环变批的值,而不是

基准测试

I 6ss

依赖千在外层作用域中的自动查找。参阅第 17 章的"循环变址可能需要默认值参数,而不 是作用域”以获取关千这一主题的更多细节。

通过赋值隐藏内置名称:遮蔽 同样在第 17 章,我们见过在一个靠近的局部或全局作用域中如何重新赋值内置名称,对 千赋值语句身处的作用域剩下的部分,重新赋值有效地隐藏并替换了那个内置名称。这意 味着你将不能使用该名称的初始内置的值。只要你不需要正在被赋值的名称的内置值,这

就不成问题一许多名称都是内置的,而它们也可以被自由地重新使用。然而,如果你重 新赋值了 一 个代码依赖的内置名称,那么你就会有麻烦了。因此要么不做,要么就使用像 PyChecker 这样的工具,它会在你那么做的时候警告你。好消息是你会很自然地记住经常使

用的内置名称,而且 Python 的错误捕获会在较早的测试中警告你,如果你忘记了某些内置 名称的话。

本章小结 本章以 一 个大型的案例研究,完成了我们对函数和内置迭代工具的介绍。该案例侧扯了迭

代可选方案和不同 Python 版本的性能。本章以对常见的函数相关错误的回顾结束,来帮助 你避免诸多陷阱。有关迭代的内容在第六部分中还有 一些介绍,在第 30 章对运算符重载的

介绍中,我们将学习如何编写用户定义的使用类和_iter_生成值的可迭代对象。 到此,本书的由数部分宜告结束。在下 一部分,我们将扩展对模块已有的知识,模块是包

含各种工具的文件,是 Python 中最顶层的组织单元,也是函数总是位千其中的结构。在模 块之后,我们将探索类,也就是 一 系列带有特殊的第一位参数的函数的集合。我们将看到, 用户定义的类可以实现接入迭代协议的对象,就像我们在这里遇到过的生成器和可迭代对 象。实际上,当函数随后以类方法的形式出现的时候,我们在本书这 一 部分学到的所有内 容也都将适用。 不过在继续学习模块部分之前,记得通过本章的测试和本书这 一 部分的习题,来实践一 下 我们在这里所学习的关于函数的知识。

本章习题 l

从本章关千 Python 迭代工具的相对速度结果中你能得出什么结论?

2.

从本章关千被计时的不同 Python 版本的相对速度结果中你能得出什么结论?

习题解答 1.

656

一 般来讲,列表推导通常是这一批工 具中最快的; map 只有在所有的工具必须调用由数

I

第 21 章

时才会在 Python 中打败列表推导; for 循环倾向千比推导更慢;而生成器函数和表达

式比推导慢一个常数因子。在 PyPy 下,这些结论中的某些会有所改变;例如, map 通 常给出 一 个不同的相对性能,而列表推导也许是由千函数层面的优化似乎总是最快的。 至少目前在被测试的 Python 版本、使用的测试机器上、对千被计时的这类代码来说, 这些结论都是正确的。如果这三个变扯中有任一个不同,这些结果都可能发生变化。

你可以使用我们这里自己编写的 timer 或是标准库的 timeit 测试你的用例,以获取更 多相关结果。也要记住迭代只是程序时间开销的 一 部分:考虑更多的代码将给出更完

整的图像。

2.

一 般来讲, PyPyl.9 (实现了 Python 2.7) 通常比 CPython 2.7 要快,而 CPython 2.7 通 常比 CPytbon 3.3 要快。在绝大多数被计时的例子中, PyPy 比 CPython 快 10 倍左右, 而 CPython 2.7 比 CPython 3.3 快一个较小的常盐。对千整数运算来说, CPython 2.7 会

比 CPython 3.3 快 10 倍,而 PyPy 比 CPython 3.3 快 100 倍。在其他的情况下(例如,

字符串操作和文件迭代器), PyPy 比 CPytbon 慢 10 倍,尽管 timeit 和内存管理的差 异会影响 一些结果。 pystone 基准测试确认了这些相对排名,但是它报告的差异的程度

随着被计时代码的不同而变化。 至少今天在被测试的 Python 版本、使用的测试机器上、对千被计时的这类代码来说, 这些结论都是正确的。如果这三个变量中有任 一 个不同,这些结果都可能发生变化。

你可以使用我们这里自己编写的 timer 或是标准库的 timeit 来测试你的用例,以获取 更多相关结果。当计时不同 Python 实现时这点尤其正确,因为在所有发行版中都有着 不同程度的优化。

第四部分练习题 在这些练习中,你将开始编写更为成熟的程序。 一 定要查看附录 D 中的解答,而且一定要

把代码编写并保存在模块文件中。如果发生了错误的话,你一 定不会想从头输入这些练习的。

l.

基础知识。 在 Python 交互式命令行下,编 写一个函数接收并打印一个单独的参数,并 以交互模式进行调用,传人各种类型的对象:字符串、整数、列表和字典。然后,试

着不传递任何参数进行调用,发生了什么?当你传两个参数时,发生了什么?

2

参数。在 Python 模块文件中编写 一 个名为 adder 的函数。这个函数需要接受两个参数, 并返回两者的和(或拼接后的结果)。然后,在文件末尾增加代码,使用各种对象类

型调用 adder 函数(两个字符串、两个列表和两个浮点数),然后,将这个文件当作 脚本,从系统命令行运行。你是不是必须得通过打印调用语句的结果,才能在屏幕上

查看结果?

3

可变长参数。把上个练习题所编 写的 adder 函数通用化,来计算任意多的参数的和, 然后修改调用方式,来传入两个以上或以下的参数。返回值的类型是什么?

基准测试

(提示:

I

657

例如 5(:0] 的切片会返回和 S 相同类型的空序列,而 type 内置函数可以测试类型;不过,

你也可以参考第 18 章中 min 例子从而了解更简单的做法)如果你传入不同类型的参数 时,会发生什么?传入字典又是什么情况?

4.

关键字参数。修改练习题 2 的 adder 函数,使其可以接受 三 个参数,井求其和 I 合并值:

def adder(good,

bad, ugly) 。现在,为每个参数提供默认值,在交互式命令行下调

用这个函数进行实验。试着传入 一 个、两个、三个以及四个参数。然后,试着传人关

键字参数。 adder(ugly=1,

good=2) 这样的调用方式能用吗?为什么?最后,把新的

adder 再通用化,从而可以接受任意多的关键字参数,并求其和 I 合并值。这类似干你

在前面习题 3 所做的事,但是你需要遍历字典,而不是元组。

(提示: diet .keys 方法

会返回 一 个字典中所有键的列表,你可以用 for 或 while 遍历,但是确保在 Python 3.X

中将其放入到 一个 list 调用中: diet.values 在这里也很有用)。

5

字典工具。编 写一个名为 copyDict(dict) 的泊数,它复制其 字典参数。这个泊数应返 回 一 个新的字典,其中包含了其参数内所有的元素。使用字典的 keys 方法来进行迭代

(或者,在 Python 2.2 及之后的版本中,不调用 keys 遍历字典的键)。复制序列很简 单 (x[:] 完成了顶层复制) ;字典也可以这么做吗?正如本题答案所示,因为字典现 在有了相似的工具,本题和下 一题就只是编程练习,但仍然作为典型的函数示例,0

6

字典工具。编 写一个名为 addDict (diet 1,

dict2) 的函数,计算两个字典的并集。这

个函数应该返回新的字典,其中包含了它的两个参数(假设为字典)中的所有元素。

如果两参数中有相同的键,可以随意挑选其中之 一 作为这个键的值。在文件中编写函 数并将该文件作为脚本来执行,从而测试函数。如果你传入列表而不是字典的时候,

会发生什么?你怎么样把函数推广到能处理这种情况?

(提示:参考之前使用的 type

内置函数)传入参数的次序有影响吗?

7.

更多参数匹配示例。首先,定义下面的 6 个函数(在交互式命令行下或者在一 个可以

被导入的模块文件中) :

def f1(a, b): print(a, b) def f2(a, *b): print(a, b)

# #

Normal arg.1· Positional varargs

def f3(a, **b): print(a, b)

#

Keyword varargs

def f4(a, *b, **c): print(a, b, c)

#

Mixed modes

def fs(a, b=2, c=3) : print(a, b, c)

HDefaults

def f6(a, b=2, *c): print(a, b, c)

#

Defaults and positional varurgs

现在,使用交互式命令行测试下面的调用,并试着解释每个结果:在某些情况下,可 能需要用到第 18 章的匹配算法。你认为混合匹配模式是个好主意吗?你能举几个它所

适用的情况吗?

»> f1{1, 2)

658

I

第 21 章

»> fl(b=2, a=1) >» f2(1, 2, 3) »> f3(1, X=2, y=3) »> f4(1, 2, 3, X=2, y=3) »> f5(1) >>>伈 (1,

4)

>» f6(1) >» f6(1, 3, 4) 8.

重访质数。回顾第 13 章中下列代码片段,它过千简单地判断一 个正整数是否为质数:

x

=y

II 2

while x > 1: if y % X == 0: print(y,'has factor', x) break

II For some y > I #

Remainder

II Skip else

X -= 1

else: print(y,'is prime')

# Normal exir

把这个代码封装成一 个模块文件中可重用的泊数 (y 应成为 一 个传入参数),并在文件

末尾增加一些该函数的调用。写好后,把第一行的//运算符换为/来实验,看看真正 的除法是如何改变 Python 3.X 中的 I 运算符并破坏这段代码的。

(如果你需要提示,

请回顾第 5 章)。那负数应该怎么处理呢? 0 和 1 呢?如何加快其运行速度?你的输 出看上去应该像下面这样:

13 is prime 13.0 is prime 15 has factor 5 15 .0 has factor s.o 9.

迭代和推导。编 写 代码来创建 一 个新列表,新列表包含了下面这个列表中所有数字的

平方根:

[ 2, 4, 9, 16, 25] 。先将它写成 for 循环,然后是 map 调用,接着是列表推导,

最后是生成器表达式。使用内置 math 模块中的 sqrt 函数来进行计算(也就是,导入

math 并使用 math.sqrt(x)) 。这四种做法中,你最喜欢哪 一种? 10 计时工具。在第 5 章中,我们见过计算平方根的 3 种方式: math.sqrt(X), X **.s 和 pow(X,

. 5) 。如果你的程序有很多这样的运算,它们的相对性能就变得十分重要。

为了知道哪一 种最快,修改我们在本章中编写的 timerseqs.py 脚本的目的来对这 三种工 具中的每一种进行计时。使用本章的 timer 模块中的 bestof 或是 bestoftotal 函数来

测试(你可以使用原始版本仅在 3.X 中的 keyword-only 的变体或是 2.X/3.X 通用版本, 也可以使用 Python 的 timeit 模块)。你可能也希望把刹试代码重新包装到这个脚本

中以便更好地重用~通过向 一 个通用的测试由数传入 一个被测函数元组(对千 本题而言,你可以选择复制并在原有基础上修改)。 3 个平方根工具中的哪一 个 一 般在

基准测试

I

659

你的机器和 Python 中运行得最快?最后,当你交互式地计时并比较字典推导和 for 循

环的速度的时候,结果又是如何呢? 11. 递归函数。写一个名为 countdown 的简单递归函数,在计数下降为零的过程中打印数字。 例如,调用 countdown(S) 将打印: 5 4 3 2 1 stop 。你并不需要使用显式栈或队列来

编写它,但是怎样通过 一 种非函数方式来实现呢?在这里有必要使用生成器吗? 12. 计算阶乘。最后,来做一个计算机科学经典问题(虽然仍是说明性的)。我们在第 20 章对排列的介绍中引入了阶乘的概念: N!, 其值为 N*(N-1)*(N- 2)*... 1 。例如, 6! 是 6*5*4*3*2*1, 或 720 。编写并计时四个函数,调用 fact(N) ,其中的每一个都返回 N! 。 编写这四个函数 (1) 根据第 19 章的递归倒计时函数;

的 reduce 调用;

( 2) 根据第 19 章使用函数式

( 3) 根据第 13 章通过简单迭代的计数循环 I

(4) 根据第 20 章使用

math.factorial 的库工具。使用第 21 章的 timeit 计时这里的每个函数。你能从结果

中得出什么结论呢?

660

I

第 21 章

第五部分

模块和包

第 22 章

模块:宏伟蓝图

从这 一 章开始,我们将深入学习 Python 中的模块,模块是最高级别的程序组织单元,它将

程序代码和数据封装起来以便再利用,同时提供自包含的命名空间从而避免程序出现变最 名冲突。从实际的角度来看,模块往往对应千 Python 程序文件。每 一 个文件都是一个模块,

井且模块在导入其他模块之后就可以使用被导人模块中定义的名称。模块也可以是使用如 C 、 Java 或 C #等其他语言编写的扩展包,甚至还可以是在包导入时的文件路径。模块可以

用下面两个语句和一 个重要的内置函数进行处理:

import 使用户程序(导入者)以 一 个整体获取一个模块

from 允许用户程序从一 个模块文件中获取特定的名称 imp.reload (在 2.X 中是 reload) 提供了 一种在不终止 Python 程序的情况下重新载入模块文件代码的方法

第 3 章介绍了模块的基础知识,而且我们之前也已经用到过这些知识。本部分的目标是扩 展核心语言中的模块概念,然后开始探索更高级的模块用法。本部分的第 一章回顾了模块 的基础知识,并提供了模块在整个程序结构中所扮演角色的概览。在后面的章节中,我们 将探入到理论背后的代码编写的细节。 在此过程中,我们将充实曾经忽略的模块的细节:你将学到重新加载、

name



all

属性、包导人、相对导入语法、 3.3 中的命名空间包等。因为模块和类实际上就是两种增强 后的命名空间,我们也会在这里正式介绍关千命名空间的概念。

663

为什么使用模块 简而言之,模块是提供自包含的变般的包(也就是所谓的命名空间)从而将部件组织为系 统的 一 种可行方式。一个模块文件顶层定义的所有变量都变成了被导入的模块对象的屈性 。

正如我们本书中前一 部分中见到的那样,导入给予了对模块的全局作用域中名称的访问权。 也就是说,在模块导人时,模块文件的全局作用域变成了模块对象的属性命名空间。最终, Python 的模块允许将独立的文件连接成一 个更大的程序系统。

更确切地说,模块至少能扮演以下三 个角色: 代码重用

就像在第 3 章中讨论过的那样,模块可以在文件中永久保存代码。不像在 Python 交 互 式命令行下输入的代码,当退出 Python 时就会消失,模块文件中的代码是持续存在的。

你可以按照需要任意次数地重新载人和重新运行模块。同样重要的是、模块还是定义 变量名的空间,被认作是属性,可以被多个外部的用户程序引用。如果使用得当,模 块化编程设计能够将代码按照功能组织成可重用的单元。 系统命名空间的划分 模块也是在 Python 中最高级别的程序组织单元。尽管它们的本质是变輩名的软件包,

但作为这种软件包,它们是自足的。除非你显式地导入一个文件,否则你将不会在另 一 个文件中看到那里面的变址名。类似于函数的局部作用域,这能帮助我们避免程序 中的名字冲突。事实上,这一特性是不可避免的,因为所有的一切都”存在千“模块 文件中,无论是执行的代码还是创建的对象都隐式地封装在模块之中。正是由千这一 点, 模块是组织系统组件的原生工具。 实现共享的服务和数据 从操作层面来看,模块对实现系统内共享的组件是很方便的,而且只需要存在 一 份单

独的副本。例如,假设你需要 一 个袚多个函数或文件使用全局对象,那么你可以将它 编写在一个模块中以便能被多个用户程序导入。

以上是对模块知识的一个宏观把握,为了真正理解 Python 系统中模块的角色,我们需要偏 离一下主题,来探索一 下通用 Python 程序的结构。

Python 程序架构 到目前为止,在本书中我们简化了对 Python 程序描述的复杂性。实际上,程序通常涉及不 只一个文件。除了最简单的脚本之外,程序一 般将采用多文件系统的形式,上一 章介绍的

计时程序便是如此。即使能够自己编写单个文件,你也 一 定会使用到其他人已经写好的外 部文件。

664

I

第 22 章

这 一 节介绍了通用的 Python 程序架构:这种架构是将一 个程序分割为源代码文件(也就是 模块)的集合,并将这些集合连接成整体的方式。正如我们将要看到的那样, python 鼓励 模块化的程序结构,将功能相近的可重用单元组织在一个模块中,这种方式符合直觉,同 时也合乎直觉 。 在这个过程中,我们也会探索 Python 模块、导入以及对象属性这三个核心

概念。

如何组织一个程序 从本质上讲, 一 个 Python 程序包括了多个含有 Python 语句的文本文件。程序拥有一个主

体的顶层文件,辅以零个或多个被称为模块的支持文件。 以下是模块的工作原理。顶层文件(又称为脚本)包含了程序的主要控制流程:这就是你

用来启动应用程序的文件。而模块文件是工具库,这些文件中收集了顶层文件(或者其他 可能的地方)要使用的组件。顶层文件使用了在模块文件中定义的工具,而这些模块又有 可能使用了其他模块所定义的 工 具 。

尽管模块文件也是代码文件,但它们通常在运行时不需直接做任何事。作为替代,它们定 义的工具会在其他文件中使用。在 Python 中,一个文件通过导人 一 个模块来获得这个模块 定义的工具的访问权,这些工具被认为是这个模块的属性(即附加到模块对象的名称,例

如函数)。总而言之,我们导入了模块、获取它的属性从而使用其中的工具 。

导入和属性 下面进行更具体的介绍。图 22- ]是 一个包含三个文件的 Python 程序的草图: a.py 、 b.py 和 c.py 。 文件 a.py 是顶层文件,它是一 个由语句组成的简单文本文件,在运行时这些语句将从上至 下执行。文件 b.py 和 c.py 是模块,它们也是含有语句的简单文本文件,但是它们通常并不

是直接运行。就像之前解释的那样,取而代之的是,模块通常被想要使用它们的文件导入。 例如,图 22-] 中的文件 b.py 定义了 一 个名为 spam 的函数,供外部来使用。就像我们在第 四部分提到的那样, b.py 中包含 一 个 Python 的 def 语句来生成函数,这个函数会在之后通

过给函数名后的括号中传人零个或更多的值来运行:

def spam(text): print (text,'spam')

# File b.py

现在,假设 a.py 想要使用 spam 。为了实现这个目标, a.py 中也许要包含如下这样的 Python 语句:

import b b.spam('gumby')

# #

File a.py Prints "gumby spam"

模块宏伟蓝图

I

665



模块



标准库 模块

图 22-1: Python 的程序架构。一个程序是由一系列模块组成的系统。它有一个顶层脚本文件(用 千启动并运行该程序)以及多个模块文件(用来导入工具库)。脚本和模块都是包含了 Python 语句的文本文件,尽管在模块中的语句通常都是创建之后使用的对象。 Python 的标准库提供了 一系列的预先编写好的模块 第一条 Python 的 import 语句,给文件 a.py 提供了由文件 b .py 在顶层所定义的所有对象的

访问权限。代码 import b 可以大致理解为: 载入文件 b.py (除非它已经被载入了),井给我能通过变最名 b 获取它所有的屈性的权限。

为了达到这样的效果, import (以及我们之后见到的 from) 语句会按需运行并载入其他的 文件。更确切地说,在 Python 中,跨文件的模块链接在运行时 import 语句执行后才会进

行解析。实际效果就是, import 语句将模块名(可以简单地认为是变摄名)赋值了载人的

模块对象。事实上,在一个 import 语句中的模块名起到两个作用:识别加载的外部文件, 同时它也会变成赋值了被载入模块的变最。

类似的,模块中定义的对象也会在运行时被创建,即在 import 执行时 1 import 原则上会 逐行运行在目标文档中的语句从而构建其中的对象。与此同时,每个在文件顶层赋值的名

称都变成了模块的 一 个属性,这些属性可以袚导入者访问。例如, a .py 中的第 二 行语句通 过使用对象属性语法,调用了模块 b 中所定义的函数 spam (spam 在导入过程中通过运行 def 语句而创建)。代码 b.spam 可以理解为: 取出存储对象 b 中名称为 spam 的值。 在这个例子中, spam 碰巧是个可调用的函数,所以我们可以在小括号内传入字符串 ('gumby' )。如果你亲自编写了这些文件,并在保存之后执行 a.py, 那么 字符串 “gumby spam" 就会被打印出来。

如前所述,在 Python 脚本中随处可见 object.attribute 这种表示法:多数对象都有一 些 可用的属性,可以通过“.”运算符取出。有些是像函数这样可调用的对象(例如, 一 个 工

资计算器),而其他的则是用来表示静态对象和属性的简单数据数值(例如, 一 个人的名称)。 666

I

第 22 章

导入的概念在 Python 之中贯穿始终。任何文件都能从任何其他文件中导人其工具。例如, 文件 a.py 可以导入 b.py 从而调用其函数,而 b.py 也可能导入 c.py 以利用定义在其中的不

固工具。导入链要多深就有多深:在这个例子中,模块 a 可导人 b, 而 b 可导入 C, C 可以 再导入 b, 诸如此类。 除了作为最高级别的组织结构外,模块(以及将在第 24 章中提到的模块包)也是 Python

中程序代码重用的最高层次。在模块文件中编写组件,可让原有的程序以及任何其他之后 可能编写的程序得以使用。例如,编写图 22-1 中的程序后,我们发现函数 b.spam 是通用 的工具,可在完全不同的程序中再次使用。而我们所需要做的,只是从其他程序文件中再 次导入文件 b.py 。

标准库模块 注意图 22-1 最右侧的部分。程序导入的模块有一些是由 Python 自身提供的,而不是你所需 要编写的。 Python 自带了很多实用的模块,称为标准库。根据最近的统计,这个集合体有超过 200 个

模块,包含与平台无关的常见程序设计任务:操作系统接口、对象持久化、文本模式匹配、 网络和 Internet 脚本、 GUI 建构等。虽然这些工具都不是 Python 语言的组成部分,但是你

可以在任何安装了标准 Python 的环境下,通过导入适当的模块来使用它们。因为这些都是

标准库模块,所以你可以理所当然地认为它们一定可用,而且在执行 Python 的绝大多数平 台上都可运行。 在本书例子中,你会看到一些标准库模块的运用(例如上一章中用到的 time it 、 sys 和

OS) ,但是我们只 看到了标准库的冰山一角。为了更全面地了解,你应该查看 Python 标准 库参考手册,这份手册在 Python 安装后就可以看到(通过一些 Windows 版本上的 IDLE 或 Python 在开始菜单的选项),或者也可以使用 http://www.python .org 的在线版本 。第 15 章 中介绍的 PyDoc 工具是另一种探索标准库模块的方式。

因为有如此繁多的模块,这是唯一了解有哪些工具可以使用的方式。你也可以在涉及应用 级程序设计的商业书籍中找到 Python 库工具的教程,例如《Python 编程》,不过手册是 免费的,可以用任何网页浏览器查看(属于 HTML 格式),也可以使用其他方式(例如 Windows 中的帮助功能),而且每次 Python 发行时都会更新。更多内容请参阅第 15 章。

import 如何工作 上一节谈到了导人模块,然而并没有解释当你这么做时会发生什么。因为导人是 Python 中 程序结构的核心,所以本节要探入讨论导人这个操作,让这个流程尽批不再那么抽象。

模块宏伟蓝图

I

667

有些 C 程序设计者喜欢把 Python 的模块导入操作比作 C 语言中的# include, 但其实不应

该这么比较:在 Python 中,导入并非只是把一个文件文本插入另 一 个文件。导入其实是运

行时的操作,程序第一次导入指定文件时,会执行 三 个步骤:

l.

找到模块文件。

2.

编译成字节码(如果需要的话)。

3.

执行模块的代码来创建其所定义的对象。

要深入理解模块导入,我们将逐 一 探索这些步骤。记住,这 三 个步骤只在程序执行期间模

块第 一 次导入时才会进行。在这之后导入相同模块时,会跳过这 三个步骤,而只提取内存

中已加载的模块对象。事实上, Python 把载入的模块存储到 一个名为 sys.modules 的表中, 并在每次导入操作的开始首先检查该表。如果模块不存在,则启动这个 三 个步骤的过程。

1. 搜索 首先, Python 必须查找到 import 语句所引用的模块文件。注意:上一节例子中的 import 语句所使用的文件名中没有 .PY 的扩展名,也没有目录路径,只有 import

b, 而不是

import c: \dirl \b. py 。路径和后缀是刻意省略掉的,因为 Python 使用了标准模块搜索路

径来找出 import 语句所对应的模块文件注 l 。因为这是程序员对千 import 操作所必须了解 的 主要部分,我们之后会再详细讨论这个话题。

2 .编译(可选) 在遍历模块搜索路径找到符合 import 语句的源代码文件后,如果需要的话, Python 接下来

会将其编译成字节码。我们在第 2 章讨论过字节码,但这里我们要更详细地讨论它 。 在导 入操作发生时, Python 会同时检查文件最近一 次的修改时间和生成的字节码对应的 Python

版本 号 ,从而决定接下来的行为。前者使用文件的”时间戳",后者使用字节文件内嵌的“魔 数”或是字节文件的文件名(取决千使用的 Python 版本)。这一步的两种选择如下 : 编译 如果发现字节码文件比源代码文件旧(例如,如果你修改过源文件),或者是由不同

注 I:

在标准 import 语句中包含目录和扩展信息是违反语法的 。 然而我们将在笫 24 章中讨论包 导入,允许 import 语句包含以一组用“.“隔开的名宇 , 来指向一个文件的部分路径。包

导入还是依赖正常的模块查找路径 , 来定位在一个包目录中的最左路径(也就是说 , 它们 相对搜索目录中的一个路径) 。 它们也不能在 import 语句中利用任何平台相关的路径语法;

这样的语法只在搜索目录下起作用 。 同样,注意模块文件查找在你运行冻结可执行文件(笫

2 章中讨论过)时不起作用,因为后者将字节码嵌入了 二 进制镜像中 。

668

I

第 22 章

的 Python 版本编译的,就会在程序运行时自动重新生成字节代码。

如前所述,这一 模型在 Python 3.2 及之后的版本中有相应改动一字节码文件被集中存 放在_pycache—子目录中,并以编译它们的 Python 版本来命名,从而避免跨版本使 用 Python 带来的竞争和重复编译。这种新模型的设计避免了在字节码文件中检查版本 号,但是时间戳检查仍被保留下来用千检查源文件是否发生改变。 不编译

另一方面,如果发现.pyc 字节码文件不比对应的.PY 源代码文件旧,而且是由同一 Python 版本所编译的,那么 Python 就会跳过源代码到字节码的编译步骤。 此外,如果 Python 在搜索路径上只发现了字节码文件,而没有源代码,就会直接加载

字节码(这意味着你可以把一个程序只作为字节码文件发布,而避免发送源代码)。 换句话说,如果不会影响程序的正常运行,就会跳过编译步骤从而加速程序启动。

注意当文件导入时,就会进行编译。因此,你通常不会看见程序顶层文件的.pyc 字节码文件, 除非这个文件也被其他文件导入:只有被导入的文件才会在机器上留下.pyc 文件。顶层文 件的字节码是在内部使用后就丢弃了的,被导入文件的字节码则保存在文件中从而可以提

高之后导入的速度。 顶层文件通常是设计成直接执行,而不是被导人的。稍后我们会看到,将一个文件设计成程 序的顶层文件,以及被导人的模块工具是可行的。这类文件既能执行也能被导入,因此会产

生pyc 文件。要了解其运作方式,可参考第 25 章中关于特殊的_name_属性以及—main 的讨论。

3. 运行 import 操作的最后步骤是执行模块的字节码。文件中所有语句会从头至尾依次执行,而此 步骤中任何对名称的赋值运算,都会产生所得到的模块对象的属性。这就是创建模块代码

所定义的工具的方式。例如,文件中的 def 语句会在导入时执行,来创建函数并将它们赋 值给模块对象的属性名称。之后,函数就能被程序中这个文件的导入者调用了。 因为这最后的导入步骤实际上是执行被导入文件的程序代码,所以如果模块文件中的任何 顶层代码做了什么实际的工作,那么你就会在导入时看见其结果。例如当 一个模块导入时, 该模块内顶层的 print 语句就会显示其输出。函数的 def 语句只是简单地定义了稍后使用

的由数对象。

模块宏伟蓝图

I

669

如你所见, import 操作包括了不少的操作:搜索文件、可能运行一个编译器以及执行

Python 代码。因此,任何给定的模块默认悄况下在每个进程中只会导入 一次。之后的导人

会跳过导入的这 三 个步骤,井重用已加载到内存中的模块。如果你在模块已加载后还需要 再次导入(例如,为了支持终端用户的动态定制),那么你就得通过调用 imp.reload !'fl 数

(下 一章我们将会学到的 一个工具)来强制处理这个问题注 2.

字节码文件: Python 3.2 及以上版本的

—pycache— 简而言之, Python 存储编译后获得的字节码文件的方式从 Python 3.2 版本开始有所改变。 首先,如果 Python 因为某些原因不能在你的计算机上写入并保存这个文件,你的程序将照

常运行一一Python 在内存中为宇节码创建了空间并运行它,同时在退出时将其删除。为了 加快启动, Python 将尽可能地将字节码存储在一 个文件中从而在下一次运行的时候跳过编 译过程。 Python 完成这一操作的行为随着版本的不同而变化:

在 Python 3. ]及之前的版本中(包括所有的 Python 2.X 版本)

字节码存储在与源文件相同的路径下,通常带有 . pyc 的文件扩展名(例如, module . pyc) 。字节码文件同时在内部被打上生成它们的 Python 的版本戳(被开发者们称为“魔

数”字段),因此 Python 能获知所运行的 Python 版本的改变从而及时重新编译。例如, 如果你升级到 一个新的拥有不同字节码的 Python 版本.那么你的所有字节码文件都会 由千版本号的不匹配而披自动重新编译、即使你没有更改过源文件。

在 Python 3.2 及之后的版本中 字节码转而被存放在一个名为_pycache _的子路径下。 Python 在需要时会在存放源

文件的路径下创建这个子路径。这能通过将字节码文件分离到它们独自的路径中,帮 助我们避免文件路径中的拥挤。此外,尽管字节码文件像之前 一 样拥有.pyc 扩展名, 但它们拥有了更具描述性的文件名。新的文件名包含了生成该字节码文件的 Python 版 本信息的文本(例如, module.cpython-32.pyc) 。这避免了不同版本间的竞争和重新

编译:因为安装的每个 Python 版本在 ___pycache—子路径下,都有它自己独特的字节 码文件的命名,所以在一个版本下运行不会覆盖其他版本的字节码,从而也就节省了

不必要的重新编译。事实上,字节码文件名也包括了生成它们的 Python 的名字,因而 CPython 、 Jython 与其他在前言和第 2 章中提到的 Python 实现可以在同一台机器上共

存而不会互相影响对方的工作(只要它们支持这种模式)。

,主 2

如前所述, Python 将已经被导入的模块储存在内置的 sys.modules 宇典中,从而跟踪己 导入的模块 。

事实上,如果你想查看已导入的模块,也可以导入 sys 并打印 list(sys.

modules.keys( )) 。 在笫 25 章中将会讨论更多关于该内部表的使用方式 。

670I

第 22 章

在两种换型中, Python 总会在你修改源文件之后重新生成字节码文件,但是版本差异的处

理方式却有所不同一一在 3.2 以前的版本中使用魔数,在之后的版本中使用支持保存多个 版本的文件名。

实际应用中的字节码文件模型 以下是这两种文件模型在 2.X 和 3.3 中的例子。为了节省空间,我已经省略了 Windows 下 大多数由 dir 路径列举所展示出来的文本。井且这里所用到的脚本没有被列出来,因为它

与本讨论不相关(这个脚本来自第 2 章,它仅仅打印两个值)。在 Py thon 3.2 版本以前, 字节码文件在导人操作完成后就被生成出来,并与它们的源文件存放在同 一 目录下:

c: \code\py2x> dir 10/31/2012 10:58 AM

39 scripto.py

c:\code\py2x> C:\python27\python >» import scri.pto hello world 1267650600228229401496703205376 >>>

^Z

c: \code\py2x> dir 10/31/2012 10:58 AM 10/31/2012 11:00 AM

39 scripto.py 154 scripto.pyc

然而,在 3.2 及以后的版本也具有这种导入后生成字节码的功能,但不同的是它们生成的 字节码会被存储在_ pycache_子目录下,并在文件名中包含版本信息以及 Python 实现的

细节,从而避免你的计莽机上安装的不同 Python 版本之间的竞争:

c: \code\py2x> cd.. \py3x c:\code\py3x> dir 10/31/2012 10:58 AM

39 scripto.py

c: \code\py3x> C: \python33\python >» import scripto hello world 1267650600228229401496703205376 >>> ^Z c: \code\py3x> dir 10/31/2012 10: 58 AM 39 scripto. py 10/31/2012 11:00 AM _pycache_ c:\code\py3x> dir _pycache_ 10/31/2012 11: oo AM 184 scripto. cpython-33. pyc 关键是,在 Python 3.2 及之后版本采用的导人模型中,使用不同的 Python 导入相同的文件

将创造 一 个不同的字节码文件,而不是像 3.2 之前的版本那样直接覆盖单个的 字 节码文件 。 因此在新的模型中,每个 Pyt hon 版本和 实现都有它们各自的 字节码文件,从而能在下一次

程序运行时被载入(更早的 Python 版本则能够相 安无事地在同 一 台机器上继续使用它们之 前的方案) :

模块宏伟蓝图

I

671

c:\code\py3x> C:\python32\python » > import scripto hello world 1267650600228229401496703205376 »> "Z c:\code\py3x> dir_pycache_ 10/31/2012 12:28 PM 178 scripto . cpython-32.pyc 10/31/2012 11 :oo AM 184 scripto. cpython- 33. pyc Python 3.2 的新字节码文件模型更优秀,因为它避免了不同 Python 版本之间所导致的重复 编译一目前 Python 2.X/3.X 版本交错的现象十分普遍。另一方面,对千某些依赖文件和

路径结构的程序而言,这种字节码文件模型的不同可能会引发不兼容性。尽管绝大多数维 护良好的工具能够在不同版本之间保持一致,但这可能对其他的某些工具程序而言是一种

兼容性问题。关千更多细节,参阅 Python 3.2 的 "What's New?" 文档。

同样要注意字节码生成的过程是完全自动的(这是程序运行的副产品),并且大多数桯序 员除了这带来的避免重复编译而节省的启动时间之外,可能不太关心甚至不会注意到这种 区别。

模块搜索路径 如前所述,通常对程序员来说,导人过程最重要的部分是其中的第一个步骤,也就是定位 要导入的文件(搜索部分)。因为我们要告诉 Python 在哪找到要导入的文件,所以我们应

该知道如何利用其搜索路径以扩展它。

多数情况下,我们可以依赖模块导入搜索路径的自动特性,而完全不需要配置这些路径。 不过,如果你想在整个目录的边界中都能导入用户定义的文件,那么你就蒂要知道搜索路 径的工作原理并进行定制。概括地讲, Python 的模块搜索路径是下面这些主要组件拼接而

成的结果。其中有些进行了预先定义,而其中有些你可以进行调整来告诉 Python 去哪里搜索

I.

程序的主目录

2.

PYTHONPATH 目录(如果设置了的话)

3.

标准库目录

4.

任何.p巾文件中的内容(如果存在的话)

5.

第三方扩展应用的 site-packages 主目录

最终,这五个组件组合起来就变成了 sys.path, 它是一个本节稍后将详细介绍的可变的目录

名称字符串的列表。上面搜索路径的第一和第三组件是自动被定义的。由千 Python 会从头 到尾搜索这些组件拼接后的结果,第 二 和第四组件就可以用千拓展路径,从而包含你自己 的源代码目录。以下是 Python 使用这些路径组件的方式:

672

I

第 22 章

主目录(自动的)

Python 首先会在主目录内搜索被导人文件 。主目录的含义与你如何运行代码相关。当 你运行 一 个程序的时候,主目录就是包含程序的顶层脚本文件的目录。当在交互式命

令行下工作时,主目录就是你当前工作的目录。 因为这个目录总是优先被搜索,如果 一 个程序完全位千单独一个目录下,那么所有导 人都会自动工作而无需进行路径配置。另 一 方面,由干这个目录是优先搜索的,其文

件也将覆盖路径上其他目录中具有同样名称的模块。因此如果你需要在自己的程序中 使用库模块,小心不要以这种方式意外地隐藏了库模块,或者选择使用我们之后将介 绍的能部分回避这一问题的包工具。

PYTHONPATH 目 录(可配置的) 之后, Python 会从左至右搜索 PYTHON PATH 环境变址(前提是你已经设置了, Python

不会为你预先设置它)设置中罗列出的所有目录。简而言之, PYTHON PATH 是设置包含 Python 程序文件的目录列表,这些目录可以是用户定义的或平台特定的目录名。你可

以把想导人的目录都加进来,而 Python 会使用你的设置来扩展模块搜索的路径。 因为 Python 会先搜索主目录,所以当导入的文件跨目录时,这个设置才显得格外重要:

也就是说,如果你需要被导入的文件与进行导入的文件处在不同目录时。 一 旦你开始 编写大址程序时,你就应当设置 PYTHON PA TH 变益,但是刚 开始编写时,只要把所有 模块文件放在交互式命令行所使用的目录下就行了(即主目录,比如本书例子中的 C:\

code) ,如此导入便可正常工作,而你无需担心这个设置。 标准库目录(自动的) 接着, Python 会自动搜索安装在机器上的标准库模块目录。因为标准库目录 一 定会被

搜索,所以通常是不需要添加到 PYTHONPATH 之中或包含到路径文件(接下来介绍)中的。 .pth 文件目录(可配置的)

之后, Python 有个相对不常用的功能,允许用户把需要的目录添加到模块搜索路径中去, 也就是在后缀名为.pth (路径 path 的意思)的文本文件中一行一行地列出目录。这些 路径配置文件是和安装相关的高级功能,我们不会在这里进行全面的讨论,但它们提 供了 PYTHONPATH 设置的一种替代方案。 简而 言 之,当把内含目录名称的文本文件放在适当的目录中时,它也可以基本上扮演

与 PYTHON PATH 环境变 址设置相同的角色。例如,如果你在运行 Wi ndows 和 Python 3.3, 一 个名为 myconfig .pth 的文件可以放在 Python 安装目 录的顶层 (C:\Python33) 或 者在标准库所在位置的 sitepackages 子目录中 (C:\Python33\Lib\sitepackages) ,用来 扩展模块搜索路径。在类 UNIX 系统中,该文件可能位千 usrllocal/lib/Python 3.3/sitepackages 或/usrllocal/liblsite-python 中。

当存在该.pth 文件的时候, Python 会把文件每行所罗列的目录从头至尾地添加到模块 搜索路径列表中-—+门节YTHONPATH 与标准库之后,在第 三方库 site-package 路径之前。

模块宏伟蓝 00

I

673

实际上, Python 将收集它所找到的所有 pth 路径文件中的目录名,井且会过滤掉任何

重复的和不存在的目录。因为它们是文件而不是 she ll 设置值,所以路径文件可以适用

干所安装系统的所有用户,并非仅限于一个用户或者 一个 shell 。此外对千某些用户和 应用而言,文本文件可能比环境更容易进行设置。 这个特性比这里介绍的更为复杂。相关细节可参考 Python 标准库手册,尤其是标准库 模块 site 的说明文档。这个模块允许配登 Python 库和路径文件的位置,并且其文档

描述了一般的预期位置。我建议初学者使用 PYTHONPATH 或单个的.pth 文件,并且只在 必须进行跨目录的导人时才使用它们。路径文件经常被第 三 方库使用,这些库通常在

Python 的 site-packages 目录下创建 一 个安装路径,详情请继续往下阅读。 第三方扩展应用的 Lib\site-packages 目录(自动的) 最后, Python 会自动将标准库的 site-packages 子目录添加到模块搜索路径。按照惯例, 这是大多数第三方扩展安装的地方,自动会被在下一个边栏中要介绍的 distutils 工 具所管理。因为它们的安装路径总是模块搜索路径的一部分,所以用户程序可以直接 导人这些扩展包而无需额外的路径设置。

配置搜索路径 所有这些的最终效果是,作为搜索路径构成部分的 PYTHON PA TH 环境变 量和 .p 山路径文件

两者都允许我们定制导入查找文件的地方。设置坏境变撮的方法以及存储路径文件的位 置会随着每种平台而变化。例如在 Wi ndows 上,我们可能使用控制面板的系统图标来把

PYTHONPATH 环境变 量设置为分号隔开的一串目录: c: \pycode\utilities; d: \pycode\package1 或者,你可以创建一个名为 C:\Python33\pydirs.pth 的文本文件,其内容如下所示.

c: \pycode\utilities d:\pycode\packagel 这些设置在其他平台上也是类似的,但是细节可能有很大变化,无法在本章中 一一 介绍。 参考附录 A 有关 PYTHONPATH 或.pth 文件在各种平台上扩展模块搜索路径的常见方式。

搜索路径的变化 这里对模块搜索路径的说明已经很准确了,但只算 一 般性的说明;实际中的搜索路径的配 置可能随平台、 Python 版本以及 Python 的不同实现而变化。取决千你所使用的平台,其他 的目录也可能被自动加人模块搜索路径。

例如,某些 Python 版本可能会把当前工作目录也加进来(也就是启动程序所在的目录),

放在搜索路径 PYTHON PATH 之前 。在从命令行启动时,当前工作目录和顶层文件的主目录(也

674

I

第 22 章

就是程序文件所在的目录)可能会有所不同,而且每次都会被添加。因为每次程序执行时 当前工作目录都可能会不同,所以 一 般来说不应该依赖这个值进行导入。更多内容请参考

第 3 章有关从命令行启动程序注 30 如果你想知道 Python 在你所用的平台上配笠的模块搜索路径,可以试着查看 sys.path, 这

也是下 一 小节的主题。

sys.path 列表 如果你想看看你的机器上实际的模块搜索路径配笠,可以通过打印内胃的 sys.path 列表(也

就是标准库模块 sys 的 path 屈性)来查看这些路径。这个目录名称字符串列表就是 Python 内部实际的搜索路径:在导入时, Python 会由左至右搜索这个列表中的每个目录,并使用 第 一 个能够匹配的文件。 其实, sys.path 就是模块搜索的路径。 Python 在程序启动时配置 sys.path,

自动将顶层

文件的主目录(或者是在交互命令行模式下,代指当前工作目录的 一 个空字符串)、所有 的 PYTHONPATH 目录、已经创建的任何.pth 文件路径的内容以及标准库目录合并。其结果是

一 个 Python 在每次导入新文件的时候查找目录名字符串的列表。 Python 暴露了该列表的访问权限,原因有二 。其 一 ,这提供一 种方式来确认你所做的搜索

路径的设置值一一如果你在列表中看不到某个设置值,就需要重新检查你的设置。例如, 以下是我的模块搜索路径在 Windows 的 Python 3.3 中的样子,其中我的 PYTHON PATH 设置 为 C:\code 和一 个 C:\Python33\mypath.th 路径文件(该文件中列出了 C:\users\mark) 。

前面的空字符串表示当前工作路径译注 1, 并且我的两个设笠都被合并了进去,其余的部分 是标准库目录及文件和第 三方扩展的 site-packages 主目录:

>» import sys »> sys.path ['','C: \\code','C: \\Windows\ \system32\ \python33. zip','C: \ \Python33\ \DLLs', 'C: \ \Python33\\lib','C: \\Python33','C: \\Users\\mark', 'C:\\Python33\\lib\\site-packages'] 其 二 ,如果你清楚自己在做什么,那么这个列表也提供一种让脚本手动定制其搜索路径的 方式。你在本书该部分的后面就会知道,通过修改 sys.path 这个列表,你可以在程序运 行 一 段时间后修改要导入的搜索路径。然而,这种修改只会在脚本执行期间保持而已。

注 3

你也可以参阅笫 24 章关于新增的相对导入语法的讨论和 Python 3 . X 中的搜索规则 ;

当使

用了".“路径符号后,这种相对导入语法会修改传给 from 语句的搜索路径(例如, from

• import string) 。 欢认情况下,一个包自己的路径在 Python3.X 中不会自动被搜索, 除非在该包本身的文件中使用了这种相对导入 . 译注]

因为这是在 交 互命令行下 .

模块宏伟蓝图

I

67s

PYTHONPATH 和.pth 文件则提供了更持久的路径修改方法-前者以程序运行为限,后者以

Python 安装为限。

另 一方面,有些程序确实需要改变 sys.path 列表。例如,在 Web 服务器上的脚本通常以 匿名用户的身份来运行从而限制机器的权限。由千这样的脚本通常不能依赖某个用户去按 需设置 PYTHONPATH 环境变最,因此它们通常都采取设竖手动更改 sys.path 列表的形式,

在导入语句之前添加必要的源文件目录。采用 sys.path.append 或 sys.path.insert 的方 式在这种情况下就足以胜任,尽管它们的效果只能在一次运行内持续。

模块文件选择 记住,文件名的后缀(例如, PY) 是刻意从 import 语句中省略的。 Python 会选择在搜索 路径中第 一 个能够匹配导入名称的文件。事实上,导入语句的本质是外部组件暴露的接

口—源文件、多种类型的字节码、编译扩展包等。 Python 会自动选择所有能够匹配某一 个模块名称的类型。

模块源文件: 例如,具有 import b 形式的 import 语句如今可能会加载或解析为:



源代码文件 b .py



字节码文件 b.pyc



优化字节码文件 b.pyo ( 一种相对不常见的形式)



目录 b, 对千包导入而言(在第 24 章说明)



编译扩展模块(通常用 C 或 C++编写),导入时使用动态链接(例如, Linux 的 b.so 、 Cygwin 和 Windows 的 b.dll 或 b.pyd)



用 C 编写的编译好的内置模块,并被静态链接至 Python



ZIP 文件组件,导人时会自动解压缩



内存内镜像,对千冻结可执行文件而言



Java 类,在 Jython 版本的 Python 中



NET 组件,在 IronPython 版本的 Python 中

C 扩展、 Jython 以及包导入都是对简单文件导入机制的延伸。不过对导入者来说,需要加

载的文件类型之间的差异却是完全不相关的,无论是在导入时或者是在读取模块的属性时 都是这样。例如, import b 就是读取模块 b ,根据模块搜索路径, b 是什么就是什么。而 b.attr

676

I

第 22 章

的形式能取出模块中的一 个名为 attr 的元素,这个元素可以是 Python 变益也可以是链入的

C 函数。本书所用的某些标准模块实际上是用 C 编写的而不是 Python 。正是因为它们看起 来与用 Python 编写的模块文件无异,所以用户程序不必在乎文件具体是什么。

选择优先级 如果在不同目录中同时有 b.py 和 b.so 文件,那么 Python 总是在由左至右搜索 sys .path 时, 加载模块搜索那些目录中最先出现(最左边的)的相匹配文件。但如果是在相同目录中找

到 b.py 和 b.so, 会发生什么事情呢?在这种情况下, Python 遵循一个标准的挑选顺序,不 过这种顺序不保证永远保持不变。通常来说,你不应该依赖 Python 在给定的目录中选择何

种文件类型:让模块名独特一 些,或者设置模块搜索路径来显式确定模块选择的偏好。

导入钩子和 ZIP 文件 一 般来说,导人的工作方式就像这一 节所介绍的那样:在机器上搜索并载入文件。然而,

你也可以重新定义 Python 中 import 操作的行为,也就是使用所谓的导人钓子 (import hook) 。这些钓子可以让导人做各种有用的事,例如从压缩文件中加载文件、执行解密等。 事实上, Python 自身使用这些钩子让文件可直接从 ZIP 压缩文件中导入:当选择了一个搜

索路径中的 zip 文件后,压缩后的文件会在导人时自动解压缩。例如,之前用 sys.path 列 出的 一个标准库路径如今就是一 个. zip 文件。更多细节,可以参考 Python 标准库手册中关

千内置的_import_函数的说明,这个由数是 import 语句实际执行的可定制工具。 注意:关于这一话题的最新进展,请参阅 Python 3.3 的 “What's New?" 文档。简言之,在 3.3

及之后的版本,_import_函数被 importlib._import_齿数的实现所替代,目的是 统一并规范其接口 。 importlib._import_被打包在 importlib.import_module 内。根据 Python 目前的手册, 一 般在直接用名称字符串调用导入(我们将在第 25 章讨论这 一 技术)时 , 更推荐使用

importlib.import_module 模块而不是—import_。目前这两种调用方式都能正常工作,

尽管—import_由数支持替换标准内置作用域的定制化导入(见第 17 章),而其他技 术支持类似的功能。更多信息参阅 Python 库手册。

优化的字节码文件 最后, Python 也支持优化字节码文件 pyo, 这种文件在创建和执行时要加上-0 这个 Python 标志位,之后便会自动地被 一 些安装过的工具所生成。尽管这些文件执行时会比普通的.pyc

文件快 一 点( 一 般快 5%) ,然而,它们并没有频繁地被使用。 PyPy 系统(参考第 2 章和 第 21 章)提供更实质性的加速效果。更多.pyo 文件的内容参阅附录 A 和第 36 章。

模块宏伟蓝图

I

677

第三方工具: distutils 本章对模块搜索路径设置的说明,主要是针对你自己编写的用户定义的源代码。

Python 的笫三方扩展,通常使用标准库中的 distutils 工具来自动安装,所以不需要 路径设置就能使用它们的代码。

使用 distutils 的系统一般都附带 setup.py 脚本,执行这个脚本可以进行程序的安装 。

这个脚本会导入并使用 distutils 模块,将这类系统放在属于模块自动搜索路径一部 分的目录内(通常是在 Python 安装目录树下的 Lib\site-packages 子目录中,而不管

Python 在目标机器上的安装位置)。 更多关于 distutils 分发和安装的细节,可参考 Python 的标准手册集 。 它的使用不 在本书范围之内(例如,此工具还提供一些方式,可在目标机器上自动编译 C 所编写

的扩展)。此外,参考笫三方开源的 eggs 系统,它增加了对已安装的 Python 软件依 赖关系的检查。

注意:就在本书笫 5 版写作的时候,出现了一些关于在 Python 标准库中废除 distutils 并代之以 distutils2 的讨论。这一可能性现状不明:本来预期在 3.3 版本 中实现但是最终没有,所以诗在本书出版后持续关注 Python 的 “What's New" 文档

以跟进这一事件的最新进展译注 2 0

本章小结 在这一章中,我们介绍了模块、属性以及导入的基础知识,并探索了 import 语句的操作。 我们也学到,导入会在模块搜索路径上寻找指定的文件,将其编译成字节码,并执行其中

的所有语句从而产生其内容。我们也学到如何配置搜索路径,以便于从主目录和标准库目 录以外的其他目录进行导入,主要是通过对 PYTHONPATH 的设 置来实现的。

如本章所示, import 操作和模块是 Python 中程序架构的核心。较大的程序可分成儿个文件, 在运行时利用导入链接在一 起。而导入会使用模块搜索路径来寻找文件,井且模块定义了 供外部使用的属性。

当然,导入和模块的意义就是为程序提供结构,让程序将其逻辑分割成 一 些独立完备的软 件组件。一个模块中的程序代码和另 一 个的程序代码彼此隔离。事实上,没有文件可以看

译注 2:

在本书翻译时, Python 官方推出的版本为 3.6.1, 而 3.7 版处于开发中,其中 distutil 模

块仍继续被使用。然而原书中提到的 distutils2 的开发却被废弃了。 Python 的 What's New 文档网址为 https ://docs.python.org/3.6/wliatsnew/3.6.html 。

678

I

第 22 章

到另一个文件中定义的名称,除非显式地运行 import 语句。因此,模块最小化了程序内不 同部分之间的名称冲突。 下一章从实际代码的角度介绍,帮助你了解其中的含义。但在继续之前,我们先做完本章 的习题吧。

本章习题 I.

模块源代码文件是怎样变成模块对象的?

2.

为什么需要设置 PYTHONPATH 环境变最?

3.

举出模块导入搜索路径的五个主要组成部分。

4.

举出 Python 可能载入的能够响应 import 操作的四种文件类型。

5

什么是命名空间?模块的命名空间包含了什么?

习题解答 1.

模块的源代码文件在模块导入时,会自动生成模块对象。事实上,模块的源代码会在 导入时逐条运行,而在这个过程中赋值的所有名称都会生成模块对象的属性。

2.

只需设置 PYTHON PATH, 便可以从当前工作目录(也就是在交互式命令行下使用的当前 目录,或者包含顶层文件的目录)之外的其他目录进行导入。大部分实际的 Python 代 码都会用到这种手段。

3

模块导入搜索路径的五个主要组件是顶层脚本的主目录(包含该文件的目录)、列在 PYTHONPATH 环境变扯中的所有目录、标准库目录、位千标准位置中.pth 路径文件中的 所有目录以及存放安装后的第三方扩展的 site-packages 目录。其中,程序员可以定制 PYTHONPATH 和.pth 文件。

4.

Python 可能载人源代码文件 (.py) 、字节码文件 (.pyc 或.pyo) 、 C 扩展模块(例如, Linux 的.so 文件,以及 Windows 的.di[或.pyd 文件)以及相同名称的目录(用于包导入)。 导入也可以加载更罕见的事物,例如, ZIP 文件组件、 Jython 的 Java 类、 IronPython 的 NET 组件以及根本没有文件形式的静态连接 C 扩展。事实上,有了导入钩子,导入可以加 载任何东西。

5

命名空间是一种独立完备的变量包,而其中的变址就是命名空间对象的属性。模块的

命名空间包含了代码在模块文件顶层赋值的所有名称(也就是没有嵌套在 def 或 class 语句中的那些变批)。事实上,模块的全局作用域会变成模块对象的属性命名空间。 模块的命名空间也会随导入它的其他文件中所做的赋值运算而发生变化,不过通常不

推荐这么做(关千跨文件改变的弊端,参阅第 17 章的相关内容)。

模块宏伟蓝图

I

679

第 23 章

模块代码缩写基础

现在我们已经接触了 一 些模块的重要概念,下面我们来看 一 些简单的模块实例吧 。 尽管对 于已经在上 一章 中学习过模块应用示例的读者来讲,本章开始的 一 些主题会是一个复习,

但是我们会发觉它们迅速带领我们了解我们至今还没有见过的围绕 Python 模块的更深人的 细节,比如嵌套、重新加载、作用域以及更多。

你可以很容易地创建 Python 模块,它们只是用文本编辑器创建的 Python 程序代码文件而已。 你不需要编写特殊语法去告诉 Python 现在正在编写模块,而且几乎任何文本文件都可以。

因为 Python 会处理寻找并加载模块的所有细节,所以模块很容易使用。用户程序只需导入 模块,就能使用模块中定义的名称以及名称所引用的对象。

模块的创建 为了定义模块,你只需使用文本编辑器把一些 Python 代码输入至文本文件中,然后以 “.py" 为后缀名进行保存,这样创建出的任何文件都被自动认为是 Python 的模块。在模块顶层指

定的所有名称都会变成其属性(与模块对象相关联的名称),并且可以导出供用户程序使 用一它们会自动地从变量变为模块对象属性。 例如,如果在名为 modulel.py 的文件中输入下面的 def 语句并导入该文件,你将创建一个 模块对象,该对象拥有一个属性名称 printer, 而 printer 引用了一个函数对象:

def printer(x): print(x)

#

Module attribute

模块文件名 在继续学习之前,我们应该对模块文件名再多介绍 一下。模块怎么命名都可以,但是如果

680

打算将其导入,模块文件名就应该以.PY 结尾。对千会执行但不会被导入的顶层文件而言, .PY 从技术上来讲是可有可无的,但加上去总是可以确保文件类型更醒目,井且可以在任何文

件中被导入。 因为模块名在 Python 程序中会变成变最名(没有.py) 。因此,应该遵循第 11 章所述的 一

般变篮名的命名规则。例如,你可以建立名为 if.py 的模块文件,但无法将其导入,因为 if 是保留字。当你尝试执行 import

if 时,就会得到 一 个语法错误。事实上,包导人中所用

模块的文件名和目录名(下一章讨论),都必须遵循第 11 章介绍的变批名规则。例如,只 能包含字母、数字以及下划线。包的目录也不能包含平台特定的语法,例如名称中有空格。

当一个模块被导入时, Python 会把内部模块名映射到外部文件名,也就是通过把模块搜索

路径中的目录路径加在前边,而.PY 或其他后缀名添加在后边。例如,名为 M 的模块最后会 映射到某个包含模块程序代码的外部文件 \M. 。

其他种类的模块 正像上 一章所提到的那样,你也可以使用像 C 、 C++以及其他的(例如 Java, 对千 Python 语言的 Jython 实现)外部语言编写代码来创建 Python 模块。这类模块称为扩展模块,一般 都是在 Python 脚本中作为包含外部扩展库来使用的。当被 Python 代码导入时,扩展模块

的外观和用法都与 Python 源代码文件所编写的模块一 样:它们通过 import 语句获取,并 提供函数和对象作为模块属性。扩展模块不在本书讨论范围之内。参考 Python 的标准手册, 或者更高级的书籍,如《Python 编程》来获得更多细节。

模块的使用 用户程序可以执行 import 或 from 语句使用我们刚才编写的简单模块文件。如果模块还没有

被加载,那么这两个语句就会去搜索、编译以及执行模块文件代码。主要的差别是 import

会整体读取一个模块,所以你之后必须借助点号”.”才能获取其中的名称 1 而 from 将从

模块中取出(或者说复制)特定的名称。 让我们借助代码来看看这些描述的含义。下面所有的示例最后都是调用上一节中 module].py

模块文件所定义的 printer 函数,但却使用了不同的方式。

import 语句 在第一 个例子中,名称 modulel 有两个不同的作用:识别要被载入的外部文件,同时会成 为脚本中的变量,在文件加载后可用千引用模块对象:

»> import modulel » > modulel. printer('Hello world I')

# Get module as a whole (one or more) #

Qualify to get names

Hello world! 模块代码编写基础

I

681

import 语句直接列出 一个或多个需要加载的模块的名称,以逗号分隔。因为它用一个名

称引用整个模块对象,所以我们必须通过模块名称来获取该模块的属性(例如, module1.

printer) 。

from 语句 相比之下,因为 from 会把特定的名称从一个文件复制到另 一个作用域,所以它可以让我们

直接在脚本中使用复制后的名称,而不需要通过模块(例如, printer) :

»> from modulel import printer » > printer('Hello world I') Hello world!

# #

Copy out a variable (one or more) No need ro qualify name

这种形式的 from 允许我们列出一个或多个被复制的名称,以逗号分隔。这里,它和上 一个 例子有着相同的效果,但由千被导入的名称被直接复制到了 from 语句所在的作用域中,因 此在使用该名称时就可少打一些字。也就是说我们可直接使用名称,而无须指明包含该名 称的模块名。实际上,我们必须直接使用名称,因为 from 本身并不会赋值模块的名称。 稍后我们会更详细地介绍, from 语句其实只是稍稍扩展了 import 语句而已。它同样导人 了模块文件(运行上一章中完整的三步过程),但是多了 一 个额外的步骤,将文件中的 一

个或多个名称(而不是对象)从文件中复制出来。整个文件都被加载了,而你可以通过复 制出的名称对它的某些部分进行更直接的访问。

from *语句 最后,下面的例子使用特殊的 from 形式:当我们使用*代替特定的名称时,会取得模块顶

层被赋值的所有名称的副本。在这里,我们还是在脚本中使用复制的名称 printer, 而不 需要通过模块名:

» > from module1 import * » > printer ('Hello 叩rld!')

#

Copy out _all_ variables

Hello world! 从技术角度来说, import 和 from 语句都采用相同的导入操作。 from

*形式只是多加了 一

个步骤,即把模块中所有名称复制到了进行导入的作用域中。本质上这就是把 一 个模块的 命名空间纳入另一个模块之中;同样,最终效果就是可以让我们少输入 一 些。注意只有* 可以在这个上下文中工作;你不能使用模式匹配来挑选出名称的子集(但是你可以通过更

多的手动编写和对模块_diet

的循环来挑选,下面将会讨论)。

就这样,模块是很容易使用的。不过,为了进一步了解定义和使用模块时究竟会发生什么, 我们详细地看一 下它们的某些特性吧。

682

I

第 23 章

注意:在 Python 3.X 中,这里所描述的 from... *语句形式只能用在一个模块文件的顶部,

不能用于一个由数 中。 Python 2. X 允许它用在 一 个函数中,但会给出 一 个警告。在实际 中将它用在函数中是很罕见的,当出现在函数中时,它让 Python 不能在由数运行之前静 态地检查变批。在所有 Python 中的最佳实践都推荐在 一 个橾块文件的顶部列出所有的导 入 1 这不是必须的,但这让导入更容易被识别出来。

导入只发生一次 使用模块时,初学者最常问的问题之 一 似乎就是:

“为什么我的导入不是 一直有效?”他们

时常报 告说,第一次导 入能够成功,但是在交互式命令行(或程序)运行期间,之 后 的导

入似乎没有效果。事实上,本来就应该如此,原因如下。 模块会在第一次 import 或 from 时披载人并执行,井且只在第一 次是如此。这是有意而为的, 因为导入是一个开销较大的操作。在默认的情况下, Python 只对每个进程中的每个文件做 一 次导入。之后的导入操作都只会取出已加载的模块对象。

初始化的代码 结果,因为模块文件中的顶层程序代码通常只执行一次,你可以借此对变址进行初始化。 例如,考虑下面的文件 simple.py:

print ('hello') spam= 1

#

lnitiali.::e variable

此例中, print 和=语句在模块第一 次被导入时执行,而变量 spam 也在导入时被初始化: % python

»> import simple hello »> simple.spam

#

First import: loads and runs file's code

# Assignment makes an attribute

1

第 二次及之后的导入并不会重新执行该模块的代码,只是从 Python 内部模块表中取出己创 建的模块对象。因此,变量 spam 不会被再次初始化:

>» simple.spam = 2 »> import simple »> simple.spam

# Change attribute in module

# Just fetches already loaded module #

Code wasn't rerun: attribute unchanged

2

当然 ,有时你确实需要 一 个模块的代码通过某种导入后再一次运行 。 我们将在本章稍后介 绍如何使用内置函数 reload 实现这种操作。

模块代码编写基础

I

683

import 和 from 是赋值语句 就像 def 一样, import 和 from 是可执行的语句,而不是编译时的声明。而且它们可以嵌套 在 if 测试中,在选项中挑选出现在函数 def 之中,只有在调用时才被加载(依照上一条提示);

在 try 语句中使用,提供默认值;以及其他各种方式。直到执行程序时 Python 到达这些语句, 它们才会进行解析和运行。换句话说,被导入的模块和名称,只有在它们所对应的 import 或 from 语句执行后,才可以使用。

在模块中改变可变对象 此外,就像 def 一样, import 和 from 都是隐式的赋值语句:



import 将整个模块对象赋值给 一 个单独的名称。



from 将一个或多个名称赋值给另一个模块中 的同名对象。

所有我们讨论过的关千赋值语句的内容,也适用千模块的读取。例如,以 from 复制的名称 会变成对共享对象的引用。就像函数的参数,对已复制的名称的重新赋值,并不会影响它 复制而来的模块,但是通过复制的名称修改一个共享的可变对象,则会影响导人的模块内 的对象。为了解释清楚,思考下面的文件 small.py, X = 1

y = [ 1, 2 l 当使用 from 导人时,我们将名称复制到导入者的作用域,并一 开始通过模块的名称来共享 被引用的对象: % python

» > from small >» X = 42 »> y[o] = 42

垃port

x, y

Copy two names out Changes local x only # Changes shared mutable in place

# #

在这里, x 并不是一个共享的可变对象,但 y 是。导入者和被导入者中的名称 y 都引用同

一个列表对象,所以在其中一处的修改,也会影响另一处该对象的值 : >>>加port

small

»> small.x

# Get module name(jrom doesn't) # Small's x is not my x

1

»> small.y

# But we share a changed mutable

[42, 2] 参阅第 6 章获得关干此的更多背景知识。对千 from 赋值操作和引用的关系的图示,你可以 翻回去看看图 18-1 (函数参数传递),只要在心中把“调用者”和“函数”换成”被导入模块“ 和“导入者”即可。实际效果是相同的,只不过我们现在面对的是模块的名称,而不是函

数的名称。 Python 中所有赋值操作的工作原理都是一 样的。

684

I

第 23 章

跨文件的名称修改 回忆前边的例子中,在交互式命令行下对 x 的赋值运算,只会修改该作用域内的变量 x,

而不是这个文件内的 x 。以 from 复制而来的名称和其来源的文件之间并没有联系。为了实 际修改另一个文件中的全局变量名,你必须使用 import: % python

>» from small import x, y »> X = 42

# #

>» import small »> small.x = 42

Copy two names out Changes my x only

# Get module name #

Changes x in other module

这种现象已在第 17 章中介绍过。因为像这样修改其他模块内的变最是常常困惑开发人员的 原因之一(通常也是坏的设计选择),所以本书这一部分稍后会再谈到这个技巧。请注意 前一个会话中对 y[o] 的修改是不一样的,区别是这会修改一个对象而不是一个名称,同时 两个模块中的这一名称会引用着同一个的修改后的对象。

import 和 from 的等价性 要注意在上一个例子中,我们需要在 from 后执行 import 语句,来获取 small 模块的名称。 from 只是把名称从 一个模块复制到另一个模块,但不会对模块名本身进行赋值。至少从概

念上来说, 一 个像这样的 from 语旬: from module import name1, name2

# Copy these two names out (only)

与下面这些语句是等效的:

import module namel = module.namel name2 = module.name2 del module

# Fetch the module object #

Copy names out by assignment

# Get rid of the module name

就像所有赋值语句一样, from 语句会在导入者中创建新的变最,而那些变量在初始化时引 用了被导人文件中的同名对象。不过,只有名称被复制出来,而非它们引用的对象以及模 块本身的名称。当我们使用 from

*这种语句形式时 (from module

import *),以上的

等效写法是一样的,只不过是模块中所有的顶层名称都会以这种方式袚复制到进行导入的

作用域中。 请注意 from 的第一步只是运行了 一 次普通的 import 操作,其中的所有语义都在上一章中 讲述过。因此, from 总是会把整个模块导入到内存中(如果还没被导入的话),无论是从 这个文件中复制出多少名称。只加载模块文件的 一部分(例如,一个函数)是不可能的, 但是因为模块在 Python 之中是字节码而不是机器码,所以通常可以忽略性能的问题。

模块代码编写基础

1

685

from 语句潜在的陷阱 因为 from 语句会让变篮的位置更隐式和模糊(与 module .name 相比, name 的意义就不那 么明确了),所以有些 Python 用户多数时候推荐使用 import 而不是 from 。不过我不确定 这种建议是否有根据;毕竟 from 得到了广泛的应用,也没太多可怕的结果。在实际的程序 中 , 每次想使用模块的工具时,省略了模块名称的输人通常是很方便的。对千提供很多属 性的大型模块而 言 更是如此。例如,标准库中的 Tkinter GUI 模块。 不过 from 语句确实有破坏命名空间的可能性,至少从理论上讲是这样的。如果使用 from

导入的名称碰巧和作用域中现有的名称同名,那么该名称就会被悄悄地覆盖掉。而使用简 单 imp ort 语句时就不存在这种问题,因为你必须通过模块名才能获取其内容 (module.

attr 不会和你的作用域内名为 attr 的变批相冲突)。不过,只要你在使用 from 时了解井预 料到可能发生这种情况,在实际中这就不是 一 个大问题了,尤其当你显式列出导入的名称

时(例如, from module import x,

y,

z) 。

另 一方面, 当 和 reload 调用同时使用时, from 语句有比较严重的问题,因为被导入的名

称可能会引用之前版本的对象。此外, from module import *形式的确可能破坏命名空间, 让变最名难以理解,尤其是在导入 一个以上的文件时。在这种情况下,不能看出 一 个名称 是来自哪个模块的,只能搜索外部的源代码文件。事实上, from

*形式会把 一 个命名 空 间

纳入另 一 个之中,所以这会使得模块的命名空间的正交特性失效。我们会在本书这 一 部分

最后的“模块陷阱"中再探人探讨这些话题(参考第 25 章)。 在这里,也许真正务实的建议就是对于简单模块一般倾向于使用 import 而不是 from 。多 数的 from 语句是用千显式列举出想要的变批,而且限制在每个文件中只用 一 次 from *形式。 这样一来,任何未定义的名称都可认为是存在千 from * 所引用的模块内。使用 from 语句时,

的确要小心一 点,但只需掌握少量相关的知识,多数程序员都会认为这是一 种相当便捷的 获取模块的方式。

必须使用 import 的场景 当你必须使用两个不同模块中定义的同名变扯时,就必须使用 import 而不能使用 from 。 例如,如果两个文件以不同方式定义了同 一 名称: #M.py

def func(): ... do something... # N.py

def func(): ... do something else... 你必须在程序中使用这两个版本的名称时, from 语句就难以胜任了,因为作用域内的一 个 名称只能对应 一 次赋值语句:

686

I

第 23 章

#0.py from M import func from N import func func()

II This overwites the one we fetched from M II Calls N.fu11c only!

不过,只使用 一 个 import 也可以奏效,因为加入了所在的模块名称后,两个名称将都是唯 一的: # O.py

import M, N M.func() N.func()

# Get the whole modules, not their names # We can call both names now # The module names make them unique

这种情况很少见,在实际中你应该很难碰到。如果你确实遇到了话,那么 import 可以帮助 你避免名称冲突。另 一 种解决这 一 困境的方法是使用 as 扩展(我们将在第 25 章讨论它), 不过它很简单,下面是一 个例子:

#0.py from M import func as mfunc from N import func as nfunc mfunc(); nfunc()

# Rename uniquely with "as" # Calls one or the orher

as 扩展作为 一 个简单的重命名 工 具在 import 和 from 二者中都工作(它也用来给出 import 中长模块名称的较短的同义名称),第 25 章有关千此内容的更多细节。

模块命名空间 一种最佳的理解模块的方式是把它看作名称的封装,也就是你定义想让系统其余部分都能 看见的名称的位置。从技术上来讲,模块通常对应千文件,而 Python 会建立模块对象,以

包含模块文件内赋值的所有名称。但简而 言之,模块就是命名空间(名称所创建的位置), 存在千 一 个模块内的名称被称为模块对象的属性。我们会在本节展开讨论这一 模型背后的

细节。

文件产生命名空间 我已提到过文件会变成命名空间,但实际这是如何发生的呢?简单来说,在模块文件顶层(也 就是不在函数或类的 主 体内)赋值的所有名称都会成为该模块的属性。 例如,假设模块文件 M.py 的顶层有 一 个像 X = 1 这样的赋值语句,那么名称 X 就会变成 M

的属性,我们可在模块外以 M.X 的方式对它进行引用。名称 x 对 M.py 内其他代码而 言 也会 变成全局变 批 ,但我们需要更正式地考虑模块加载和作用域的概念以了解其原因:



模块语句会在首次导入时执行。在 一 个系统中任何模块第 一 次被导人的位置, Python 都会 建立空 的模块对象,并逐 一执行该模块文件内的语句 , 依照文件从头到尾的顺序 。

模块代码编写基础

I

687



顶层的赋值语句会创建模块属性。在导入时,文件顶层(不在 def 或 class 之内)赋

值名称的语句(例如,=和 def) ,会建立模块对象的属性,赋值的名称会存储在模块 的命名空间内。



模块的命名空间可以通过属性_diet_或 dir(M) 获取。由导人而建立的模块的命名空 间是字典;可通过模块对象相关联的内置的_diet _属性来读取,而且能通过 dir 拯 数查看。 dir 函数大致与对象的_diet—属性的键排序后的列表相等,但是它还包含了 类继承的变量名,也许不太完整,而且会随版本而异。



模块是一个独立的作用域(局部变量就是全局变量)。正如第 17 章所显示的,模块 顶层的名称遵循和函数内名称相同的引用/赋值规则,但局部作用域和全局作用域相

同一或者更正式的说法是,遵循我们在第 17 章提及的 LEGB 范围规则,但是没有 L 和 E 搜索层次。

不过很重要的一点是,模块的全局作用域会在模块加载后变成模块对象的属性字典 。 和函数作用域不同的是,函数的局部命名空间只在函数执行时才存在,而模块文件的 作用域在模块导入后就成为模块对象属性的命名空间,井在之后持续存在,从而为导

入者提供了一个工具来源。 以下是这些概念的示例。假设我们在文本编辑器中建立了如下的模块文件,并将其命名为

module2.py: print('starting to load...') import sys name= 42 def func(): pass class klass: pass print ('done loading.') 这个模块首次导入时(或者作为程序执行时), Python 会从头到尾执行其中的语句。有些

语句的副作用是在模块命名空间内创建名称,其他的语句则会在导人进行时做些实际工作。

例如,此文件中的两个 print 语句会在导人时执行:

» > import module2 starting to load... done loading. 当模块被加载后,它的作用域就成为 import 语句所返回的模块对象的一 个属性命名空间。 然后,我们可以通过在模块名称后面使用点号”.”运算符来访问此命名空间内的属性:

»> module2.sys

» > module2. name 42

688

I

第 23 章

>» module2. func

>>> module2.klass

此处, sys 、 name 、 func 以及 klass 都是在模块语句执行时赋值的,所以在导入后都变成 了属性。我们会在第六部分讨论类,但是请注意 sys 属性: import 语句其实是把模块对象

赋值给名称,而且文件顶层各种赋值得到的名称,都会成为模块属性。

命名空间字典:—diet— 事实上,在内部,模块命名空间被存储为字典对象。它们只是普通字典,拥有所有一般的

字典方法。当需要时(例如,我们将在第 25 章中编写一般性地列出模块内容的工具),我

们可以通过模块的—diet _属性获取模块命名空间字典。继续上一小节的例子(别忘了在

Python 3 . X 中将其包含到一个 list 调用中—一它在 3.X 中是 一个视图对象,而使用 3.3 之 外的版本也可能得到与这里不同的内容) :



»> list(module2._dict .keys()) ['_loader_','func','klass','_builtins_','_doc_','_file_','_name_' ,'func','klass','_builtins_','_doc_','_file_','_name_', 'name','_package_','sys','_initializing—',' _cached_'] 我们在模块文件中赋值的名称,在内部会成为字典的键。因此这里的一些名称反映了文件 中顶层的赋值语句。然而, Python 也会为我们在模块命名空间内加人一些名称。例如,

_file_指明模块是从哪个文件加载的,_name_则指明导入者的名称(没有.PY 扩展名和 目录路径)。如果只想看你自己代码赋值的名称,你可以像之前一样(在第 15 章 dir 介绍 和第 17 章内置作用域介绍中所做的那样)过滤掉双下划线名称:

>» list(name for name in module2._dict_.keys() if not name.startswith{'_')) ['func','klass',' ,'name','sys'] »> list(name for name in module2._dict_ if not name.startswith('_')) ['func','sys','name','klass'] 这里我们使用一个生成器来过滤而不是一个列表推导,并且可省略. keys( ),因为字典会 隐式但自动地生成它们的键,所以效果是相同的。在第六部分中类相关的对象上,我们也

会见到相似的_diet

字典。在这两种情况下,属性获取类似千字典索引,尽管只有前者

会出发类的继承搜索:

>» module2. name, module2._diet_['name'] (42, 42)

属性名称的点号运算 提到属性获取,现在你已经更熟悉模块的基本知识了,那么我们应该更为正式地巩固名称

模块代码编写基础

I

689

的点号运算 (qualification) 的概念。在 Python 之中,可以使用点号运算(又名属性获取)

语法 object.attribute 访问任何(拥有属性的)对象的属性。 点号运算其实就是表达式,它会返回和对象相关联的属性名的值。例如在上一个例子中, 表达式 module2.sys 会取出 module2 中赋值给 sys 的值。同样,如果我们有一个内置的列

表对象 L, 那么 L.append 会返回和该列表相关联的 append 方法对象。 我们关键要记住,属性的点号运算和第 l7 章学过的作用域法则没有任何关系;它是 一 个独

立的概念。当使用点号运算来读取名称时,你给 Python 提供了 一 个显式的对象来从中获取 指定的名称。 LEGB 作用域规则只适用于无点号运算的纯名称一它可能被用在一 个名称路

径中最左边的名称,在点号之后的名称则会搜索特定的对象。以下是其规则: 简单变量 X 是指在 当前作用域内搜索名称 X (遵循第 l7 章的 LEGB 规则)。

点号运算 X.Y 是指在当前所用千内搜索 X, 然后搜索对象 X 中的属性 Y (而非在作用域内)。 多层点号运算 X.Y.Z 指的是在对象 x 中寻找名称 Y, 然后在对象 X.Y 中 寻找名称 Z 。 通用性

点号运算可用于任何具有属性的对象:模块、类、 C 扩展类型等。 在第六部分中,我们会看到属性点号运算对类的意义还要再多 一 些(类也是继承发生的地 方),但 一 般而言,这里列举的规则适用于 Python 中所有的名称。

导入 vs 作用域 如前所述,如果不导入 一 个文件,就无法获取该文件内所定义的名称。也就是说,你不可 能自动看见另 一 个文件内的名称,无论程序中的导入结构或函数调用的结构是怎么样的。

变量的意义 一 定是由源代码中的赋值语句的位置决定的,而 一个对象的属性总是被显式地 获取。 例如,考虑以下两个简单模块。第一个模块 moda.py 定义了一个只对其文件是全局的名称 X, 以及一个可修改该全局 X 的函数:

X = 88

#

def f() : global X

#

X = 99

My X: global to this file only

Change this file's X # Cannot see names in other modules

第 二个模块 modb.py 定义自己的全局变 显 X, 导入并调用了第 一 个模块的函数。

690

I

第 23 章

X = 11

# My X: global to this file only

import moda moda.f() print(X, moda.X)

# Gain access to names in moda # Sets moda.X, not this file's X

执行时, moda.f 修改了 moda 中的 X, 而不是 modb 中的 X 。 moda.f 的全局作用域一定是其 所在的文件,无论这个函数是由哪个文件调用的:

% python modb.py 11 99 换句话说,导入操作不会赋予被导入文件中的代码对上层代码的可见性:被导入文件无法

看见进行导入的文件内的名称。更确切的说法是:



函数绝对无法看见其他函数内的名称,除非它们从物理上处千这个函数内。



模块程序代码绝对无法看见其他模块内的名称,除非被显式地导入了。

这样的行为是词法作用域搜索 (lexical scoping) 概念的一部分:在 Python 中,一段代码的 作用域完全由该代码在文件中所处的实际位置决定。作用域绝不会被函数调用或模块导入

影响

注 1

命名空间的嵌套 就某种意义而言,虽然导入不会使命名空间发生向上的嵌套,但确实会发生向下的嵌套。 也就是说,虽然 一 个袚导入的模块绝不会直接访问导入它的文件中的名称,但是利用属性

的点号运算路径,你可以探入到任意内嵌的模块中并读取其属性。例如,考虑下列三个文件。 mud3.py 以赋值语句定义了 一个全局名称和属性: X = 3

接着, mod2.py 定义了自己的 X, 然后导人 mod3, 使用点号运算来获取被导人的 mod3 模块 的属性: X = 2

import mod3 print(X, end='') print(mod3.X)

# My global X # mod3's X

modl .py 也定义了自己的 X, 然后导人 mod2, 并获取第 一 和第二个文件内的属性:

注 I:

其他一些编程语言的行为有所不同并提供动态作用域令在那些语言中作用域具正依赖于运

行时的调用。不过这会令代码变得史复杂,因为一个变量的意义可能随时间而变化。在 Python 中,作用域更多地只是对应于你的程序的文本。

模块代码编写基础

I

691

X = 1

import mod2 print(X, end='') print(mod2.X, end='') print(mod2.mod3.X)

# My global X # mod2 's X # Nested mod3's X

实际上,当这里的 modl 导入 mod2 时,会创建一个两层的命名空间的嵌套。利用 mod2. mod3.X 的名称路径,就可深入到被导入的 mod2 内嵌的 mod3 。结果就是 modl 可以看见全部 三个文件中 的 X, 也就是可以读取这 三个全局范围: % python 2 3 1 2 3

mod1.py

然而,反过来却不是这样了: mod3 无法看见 mod2 内的名称,而 mod2 也无法看见 modl 内 的名称。如果你不用命名空间和作用域的观点思考,而是把注意力集中在牵涉到的对象, 那么这个例子就比较容易掌握。在 modl 中, mod2 只是引用了带有属性的对象的名称,而 该对象的某些属性又可能引用其他带有属性的对象 (import 是赋值语句)。对千 mod2.

mod3.X 这样的路径而言, Python 只会从左到右地进行计算,并沿着这样的路径取出对象的 属性。 要注意在 modl 可以编写 import mod2, 然后使用 mod2.mod3.X, 却不能编写 import

mod2.

mod3 。这一语法会启用所谓的包(目录)导入,这将在下一章 介绍。包导入也可以导致模 块命名空间嵌套,但它们的 import 语句将用千反映目录树结构,而非简单的文件导入链。

重新加载模块 如前所述, 一个模块的代码默认只为每个进程执行 一 次。要强制使模块代码重新载入并重 新运行,那么你必须显式地要求 Python 这么做,也就是调用 reload 内置函数。本节我们

要探索如何使用 reload 让系统变得更加动态。简而言之:



当模块第一次在进程中袚导入时,才加载(通过 import 或 from 语句)和执行该模块 的代码。



之后的导入只会使用已加载的模块对象,而不会重新加载或重新执行文件的代码。



reload 函数会强制已加载模块的代码重新载入并重新执行。文件中新的代码的赋值语 句会在原位置修改现有的模块对象。

为什么我们要关注重新加载模块?简单地说,是为了动态定制化: reload 函数允许在整体 程序不停止的情况下修改程序的一部分。利用 reload, 可以立即看到对组件修改后的效果。

重新加载不一定适用千所有情况,但当它可以使用时能够缩短开发的周期。例如,想象一

692

I

第 23 章

下,数据库程序必须在启动时连接服务器,因为程序修改或调整可在重新加载后立即测试, 所以在调试时只需连接一 次就可以了。长时间运行的服务器可以用这种方式自我更新。

因为 Python 是解释性的(或多或少)语言,所以已经避免了类似 C 语言程序执行时所需的 编译/链接步骤:在被 一 个正在运行的程序导入时,模块会被动态地加载。重新加载进一 步地提供了性能优势,让你可以修改执行中的程序的一部分,而无帣中止。 尽管超出了本书的范围,但请注意 reload 目前只能用于 Python 编写的模块;用 C 这类语

言编写的编译后的扩展模块也可在执行中动态加载,但无法重新加载(不过大多数用户还 是会喜欢用 Python 来编写定制化代码!)。

注意:版本差异提示:在 Python 2 .X 中,披 reload 作为 一个内笠函数使用。在 Python 3.X 中,

它已经移入 imp 标准库模块中一一在 Python 3.X 中称为 imp.reload 。这直接意味着, 需要一 条额外的 import 语句或 from 语句来载人该工具(仅在 Python 3.X 中)。使用

Python 2.X 的读者可以在本书的示例中忽略这些导入,或者总是使用它们-—-Python 2.X 在其 imp 模块中也有一个 reload, 以便更容易地迁移到 Python 3.X 。无论其如何封装, 重新加载都会 一样地工作。

reload 基础 与 import 和 from 不同的是:



reload 在 Python 中是一个函数,而不是一条语句。



reload 传入的参数是一个已经存在的模块对象,而不是一个新的名称。



reload 在 Python 3.X 中位千模块之中、井且必须导入才能使用。

因为 reload 期望得到的是一个对象,所以在重新加载之前,模块 一 定是已经预先成功导入 的(如果因为语法或其他错误使导入没有成功,你得继续试下去,否则将无法重新加载)。 此外, import 语句和 reload 调用的语法并不相同:

reload 作为一个函数需要使用小括号,

但 import 语句不需要。抽象地说,重新加载看起来如下所示:

import module ... use module. attributes...

# lniria/ import #

from imp import reload reload(module) ... use module. attributes...

Now, go change the module file

# Get reload itse/f(in 3.X) #

Get updated exports

一 般的用法是导人一个模块,在文本编辑器内修改其原代码,然后将其重新加载。这会在 你通过交互式命令行工作时发生,但也会在周期性重新加载的较大型程序里发生。

模块代码编写基础

I

693

当调用 reload 时, Python 会重读模块文件的源代码,重新执行其顶层语句。也许有关

reload 所需要知道的最重要的事情就是, reload 会在原位置修改模块对象, reload 并不 会删除并重新创建模块对象。因此,程序中任何引用整个该模块对象的地方,都自动会受 到 reload 的影响。下面是一 些细节。



reload 会在模块当前命名空间内执行模块文件的新代码。重新执行模块文件的代码会 覆盖其现有的命名空间,而不是删除并重建。



文件中顶层赋值语句会将名称替换成新的值。例如,重新执行的 def 语句会因重新赋 值函数名称而替换模块命名空间内该函数之前的版本。



重新加载会影响所有使用 import 读取了模块的用户程序。因为使用 import 的用户程 序需要通过点号运算取出属性,在重新加载后,它们会发现模块对象中变成了新的值。



重新加载只会对以后使用 from 的用户程序造成影响。之前使用 from 来读取属性的用 户程序并不会受到重新加载的影响,那些用户程序引用的依然是重新加载前所取出的 旧对象。



重新加载只适用千单一的模块。你必须在希望更新的每个模块上面重新加载,除非你 使用了能够传递式地应用重新加载的代码或工具。

reload 示例 这里是 一个更具体的 reload 的例子。在下面这个例子中,我们要修改并重新加载一个模块

文件,但是并不终止 Python 的交互式会话。重新加载也可以在很多场景中使用(参考下面 的边栏内容“请留意:模块重新加载“),但出千解释的目的,这里我们举一 个简单的例子。

首先,在你的文本编辑器中,编写一个名为 changer.py 的模块文件,其内容如下:

message= "First version" def printer(): print(message) 这个模块会创建并导出两个名称 : 一个绑定到字符串,另一个绑定到函数。现在,启动 Python 解释器,导入该模块,然后调用其导出的函数。此函数会打印出全局变量 message 的值:

% python

» > import changer » > changer. printer() First version 不要关掉解释器,现在在另 一 个窗口中编辑该模块文件。

. . . modify changer.py without stopping Python... % notepad changer.py

694

I

第 23 章

改变 message 变盘和 printer 函数体:

message= "After editing" def printer(): print('reloaded:', message) 然后,回到 Python 窗口,重新加载该模块从而获得新的代码。注意下面的交互式命令行内容,

再次采用导人并没有效果。即使文件已经被修改过了,我们得到的也是原先的 message 。 我们只有调用 reload, 才能获取新的版本:

... back to the Python interpreter... » > import changer » > changer. printer() # No effect: uses loaded module First version >» from imp import reload »> reload(changer) # Forces new code to load/run

>» changer. printer() # Runs the new version now reloaded: After editing 要注意 reload 实际为我们返回了模块对象:其返回结果通常会被忽略,但因为表达式结果 会在交互式命令行下被打印出来,所以 Python 会打印默认的 展示形式。

这里最后的两个提示:首先,如果使用 reload, 你很可能想令其与 import 配对而不是

from, 因为后者不会被重新加载操作更新~我们 将推迟到本部分第 25 章结尾的"陷阱"一节再予以讨论。其次, reload 本身只更新单一 的模块,但是你可以直接编写 一 个函数,传递式地应用它到相关模块-我们会把这个扩

展留到第 25 章快结束时的一个案例研究中再讲解。

请留意:模块重新加载 除了可以在交互式命令行下重新加载(以及重新执行)模块外,模块重新加载在较大

系统中也十分有用,尤其在重新启动整个应用程序的代价太大的情况下。例如,必须 在启动时通过网络连接服务器的浒戏服务器和系统,就是动态重新加载的一个非常重 要的应用场景。

重新加载在 GUI 工作中也很有用(组件的回调行为可以在 GUI 保持活跃的状态下进

行修改) 。 此外,当 Python 作为 C 或 C+ +程序的嵌入式语言时,也相当有用(外围 的程序可以讨求重新加载其所执行的 Python 代码而无需停止)。参考《 Python 编程》 有关重新加载 GUI 回调函数和嵌入式 Python 程序代码的更多内容。 通常情况下,重新加载使程序能够提供高度动态的接口。例如, Python 通常作为较大

系统的定制语言:用户可以在系统运作时通过编写 Python 程序定制产品,而不用重

模块代码编写基础

I

69s

新编译埜个产品(甚至荻取整个源代码) 。 这样, Python 程序代码本身又增加了一层

动态的本质。 不过为了更具动态性,这样的系统可以在执行期间定期自动重新加载 Python 定制的 程序代码 。 这样一来,当系统正在执行时,就可采用用户的修改;每次 Python 代码

修改时,都不需要停止并重启。并非所有系统都需要这种动态的实现,但对那些需要 的系统而言,模块重新加载就提供了一种易于使用的动态定制工具。

本章小结 本章深入讨论了模块编码工具的必备知识: import 和 from 语句,以及 reload 调用。我们 知道 from 语句只多加了一个步骤,在文件导入之后,将文件的名称复制出来,也学习了 reload 如何不停止和重启 Python 而使文件再次导入。我们还研究了命名空间的概念, 学习 了当导人在嵌套时会发生什么,探索了文件转换为模块的命名空间的过程,并学习了 from

语句潜在的一些陷阱。 虽然我们已在程序中见过一些处理模块文件的例子,但下一章要通过介绍包导入来扩展导 入模块的相关内容。包导人是 import 语句指定部分的目录路径,从而获取所需模块的方式。

正如我们将看到的,包导入给予我们一 种层次架构,能够用千较大型的系统中,而且可以

避免同名模块间的冲突。不过,让我们先做一做习题来复习本章介绍的概念。

本章习题 l.

怎样创建模块?

2.

from 语句和 import 语句有什么关系?

3.

reload 函数和导入有什么关系?

4.

在什么情况下必须使用 import, 而不是 from?

5.

请列举出 from 语句的三种潜在陷阱。

习题解答 1.

要创建模块,你只需编写 一 个包含 Python 语句的文本文件就可以了 1 每个源代码文件 都会自动成为模块,而且也不存在声明模块的语法。导入操作会把模块文件加载到内 存中使其成为模块对象。你也可以用 C 或 Java 这样的外部语言编写代码来创建模块,

但是这类扩展模块不在本书讨论的范围之内。

696

I

第 23 章

2.

from 语句是导入整个模块,就像 import 语句那样,但 from 还有一个额外的步骤,就 是会从被导人的模块中,复制一个或多个变量到 from 语句所在的作用域中。这样可以 让你直接使用被导入的名称 (name) ,而不是通过模块来使用 (module.name) 。

3.

默认情况下,模块在每个进程中只导人一次。 reload 函数会强制一个模块再次被导入。 这基本上都是用千开发过程中获取模块源代码的新版本,或是用在动态定制的场景中。

4.

当需要读取两个不同模块中的同名变量 时,就必须使用 import, 而不能用 from, 因为 你必须指定变量所在模块,从而保证这两个名称是独特的。这一场景下的 as 扩展会使 得 from 也能使用。

5.

from 语句会让变量的意义模糊化(究竟是哪个模块定义的),通过 reload 调用时会有

问题(名称还是引用对象之前的版本),而且会破坏命名空间(可能悄悄覆盖正在作 用域中使用的名称)。 from *形式在多数情况下都很糟糕:它会严重地污染命名空间, 让变量意义变得模糊,少用为妙。

模块代码编写基础

I

697

第 24 章

模块包

到目前为止,我们已导入过模块,加载了文件。这是 一 般性的模块用法,也可能是你早期 Python 职业生涯中大多数导人会使用的技巧。然而,模块导入的故事比目前所提到的还要 丰富一点。 除了模块名之外,导人还可以指定目录路径。 Python 代码的目录被称为包,因此这样的导 入就称为包导入。事实上,包导入是把计算机上的目录变成另一个 Python 命名空间,其属 性则对应千目录中所包含的子目录和模块文件。 这是一个有点高级的特性,但是它所提供的层次,对千组织大型系统内的文件会很方便,

而且可以简化模块搜索路径的设置。我们将看到,当多个相同名称的程序文件安装在某一 机器上时,包导人也可以偶尔用来解决导入的不确定性。 由于包导入只与包中的代码相关,因此在这里我们还将介绍 Python 最近的相对导入模型和 语法。正如我们将看到的,这种方法在 3.X 版本中修改了搜索路径井且扩展了在 2.X 和 3 . X

版本中包中用千导入的 from 语句。这一模型让这类包内导人变得更显式和简洁,但同时带 来一 些可能影响到你的程序的折中。 最后,对使用 Python 3.3 及之后版本的读者而言,本章也将介绍新增的”命名空间包“模型(允

许 一 个包横跨多个路径而无需初始化文件)。这种新式的包模型是可选的同时可以与原有 包模型(现在也称“常规“包模型)配合使用,但它颠覆了 一 些原有包模型的基本思想和 规则。因此,我们将首先介绍常规包模型,而将命名空间包放在最后作为 一个可选主题。

包导入基础 包导入在基础水平是很直观的,在 import 语句中列举简单文件名的地方 , 也可以改成彼此 以点号相隔的路径名称:

698

import dir1.dir2.mod from 语句也是 一样的:

from dir1.dir2.mod import x 这些语句中“带点号”的路径对应千机器上目录层次的路径,通过这个路径可以获得到文

件 mod.py (或类似文件,扩展名可能会有变化)。也就是说,上面的语句表明了机器上有 个目录 dirl, 而加1 里有子目录 dir2, 以及 dir2 内包含有一个名 为 mod.py (或类似文件) 的模块文件。 此外,这些导人意味着小rl 在某个容器目录 dirO 中,而山rO 目录可以在 Python 模块搜索

路径中找到。换句话说,这两个 import 语句代表了这样的目录结构(以 Windows 反斜线 分隔字符显示)。

diro\dir1\dir2\mod.py

#

Or mod.pyc, mod.so. etc.

容器目录 dirO 需要被添加到模块搜索路径中,除非它是顶层文件的主目录,就好像 dir] 是

一 个简单模块文件那样。 更严格地说,包导入路径中最左边的部分仍然是相对千我们在第 21 章所介绍的 sys.path 模块搜索路径列表中的一个目录。从最左边的目录出发,脚本内的 import 语句需要显式地

指出找到模块的目录路径 。

包和搜索路径设置 如果你使用了这个功能,要记住 import 语句中的目录路径只能是以点号间隔的变量。你不 能在 import 语句中使用任何平台相关的路径语法。例如,

“C: \dirl"

"My Documents.

dir2" 或“../ dirl" :这些从语法上讲是行不通的。你要做的就是在模块搜索路径设笠中 使用这类平台相关的语法来声明容器目录。 例如,上一个例子中, dirO (加在模块搜索路径中的目录名)可以是任意长度而且是与平台 相关的目录路径,并在其下能够找到 dirl 。而不是使用像这样的无效的语句:

import C:\mycode\dir1\dir2\mod

#

Error: illegal syntax

但你可以将 C:\mycode 添加到 PYTHON PATH 系统变批中或是 pth 文件中,井且在代码中这样

编写:

import din.dir2.mod 实际上,模块搜索路径上的各项都提供了平台相关的目录路径前缀,其效果是在 import 和

模块包

I

699

from 语句的路径左边添加了这些路径前缀。这些导入语句本身以与平台不相关的方式,提

供了前缀右边目录路径的写法让 IO 对千简单文件导入,如果容器路径 dirO 已经存在千搜索路径中了,那么你无需添加它。由 第 22 章可知,它可以是顶层文件所在路径、交互式命令行当前的工作路径、标准库路径或

者第 三 方扩展的 site-packages 路径。你的模块搜索路径或多或少会包含以上儿类,作为你 的代码中包导入语句左侧的前缀组成。

—init_.py 包文件 如果选择使用包导人,就必须多遴循 一 条约束:至少到 Python 3.3 版本为止,包导人语句

的路径中的每个目录内都必须有_init_.py 这个文件,否则包导入会失败。也就是说,在 我们所采用的例子中, dirl 和小 r2 内都必须包含—init—. PY 文件。容器目录小 r0 不需要 —init_.py 文件,因为其本身没列在 import 语句之中。更正式的用法请看下面这样的目录

结构:

diro\dir1\dir2\mod.py 以及对应形式的 import 语句:

import dir1.dir2.mod 必须遵循下列规则:



dirl 和 dir2 中都必须含有 一 个—init—.PY 文件。



dirO 是容器,不需要_init_.py 文件;如果有的话,这个文件也会被忽略。



dirO (而非如叭iirl) 必须列在模块搜索路径的 sys.path 列表中。

为了满足前两条规则,包的创建者必须创建我们这里所提到的_init—.PY 文件。为了满足 后面的条件, dirO 必须是自动搜索路径的 一部分(主目录、标准库或 site-packages 中的目录),

或者列在 PYTHONPATH 或.pth 文件之中,亦或是手动修改 sys.path 列表。 最终结果就是,这个例子的目录结构应该如下所示(以缩进表示目录的嵌套)

Python 选择点号语法的原因一部分是出于平台无关性,另外也是因为 import 语句中的路

注 I:

径将成为代码中实际的嵌套对象属性路径 。 这种语法也意味着如果你忘记在 import 语句

中去掉文件名的.PY 后级的话 ,就会碰到奇怪的错误信息。例如, im port 是导入的文件路径

它通过佴析 mod.py,

mod. PY 被 认为

尝试载入一个 mod\py.py 文件,并最终引发一

个可能是 “No module named py" 的错误信息。在 Python3.3 中,这个错误信息被改进为 “No

module named ' m.py'; m is not a package" 。 700

I

第 24 章

dirO\ dir1\

#

Container on module search path

— init— .PY dir2\ _init_.py mod.py

—init—.PY 可以包含 Python 程序代码,就像普通模块文件那样。它们的名字很特殊是因为 它们的代码将在 Python 第一 次导入一个路径的时候被自动运行,所以它们也被用作执行包 的初始化步骤的钓子。这些文件可以完全是空的,但有时也可以扮演额外的角色,就如下

一节中的例子所介绍的那样。

注意:正如在本章末尾我们将看到的那样,在 Python 3.3 及以后的版本中,包中要含有 —init_.py 文件的要求被放宽了。在 3 . 3 及之后的发行版中,没有这一 文件的模块路径 会被当作单路径命名空间包而导人,其工作方式相同,但是没有初始化时代码的执行。 而在 Python 3.3 之前以及所有的 2.X 版本中,包依然需要_init_.py 文件。如前所述, 在 3.3 及后面的版本中,_init_.py 文件的出现能够提高运行时的性能。

包初始化文件的角色 更详细地说.—init_.py 文件可以用作包初始化的钓子,将目录声明成一个 Python 包,替

目录生成一 个模块命名空间以及在目录导入时实现 from *(也就是 from... import *) 语句行为的角色。 包的初始化

Python 在首次导入某个目录时,会自动执行该目录下_init_.py 文件中的所有程序代码。 因此,—init_.py 文件很自然地成为了放置包内文件所需要初始化代码的场所。例如, 包可以通过其初始化文件来创建所需要的数据文件、连接数据库等。 一 般而言,直接 执行_init_.py 文件并没什么用;它们在包首次读取时会自动运行。 模块使用的声明

包的_init_.py 文件从某种程度上讲就是声明 一个路径是 Python 包。在扮演这个角色 的时候,_init_.py 文件可以防止有相同名称的目录不小心隐藏了出现在模块搜索路径

后面的真正模块。没有这层保护, Python 可能会挑选出和你的程序完全无关的目录, 只是因为有一个同名的目录刚好出现在搜索路径上位置较前的目录内。我们之后将看

到, Python 3.3 的命名空间包消除了该角色大部分的必要性,但也通过在路径前搜索之 后文件的方式从算法角度获得了一个相似的效果。

模块命名空间的初始化

在包导入的模型中,你脚本中的目录路径在导入后会变成真实的嵌套对象路径。例如, 在上一 个例子中,导入表达式 di r1. di r2 运行后 , 会返回 一 个模块对象,而此对象的

模块包

I

701

命名空间包含了山r2 的_init_.py 文件中赋值的所有名称。_init_.py 文件为目录(没 有实际对应的模块文件)创建的模块对象提供了命名空间。 from*语句的行为

作为一个高级功能,你可以在_init_.py 文件内定义—all—列表来规定目录以 from*

语句形式导入时,需要导出什么。在—in“—.PY 文件中,—all —列表是指当包(目 录)名称使用 from *的时候,应该导入的子模块的名称清单。如果没有设定_all_,

from *语句不会自动加载嵌套千该目录内的子模块;取而代之的是,只加载该目录的 _init—.PY 文件中赋值语旬定义的名称,包括该文件中程序代码显式导入的任何子模块。

例如,某目录中_init_.py 内的语句 from submodule import X, 会让名称 X 在该目录 的命名空间内变得可用(我们将在第 25 章看到_all_的另 一种用法 :它让 from *语

法也可以用千简单文件)。 如果你不使用_init_.py 文件,也可以让它们保持空白(在实际中,它们多数情况下确实

是空的)。不过为了让目录导入至少可以工作,—init_.py 文件必须要存在。

注意:不要把包_ini/—.PY 文件和我们将在本书下一部分中介绍的类_init_构造函数方法混 淆起来。前者是当导入初次遍历一个包目录时所运行代码的文件,而后者是在创建 一 个 实例对象时才调用的函数。它们都具有初始化的作用,但它们有着很大的区别。

包导入示例 让我们实际编写刚才介绍的例子,来说明初始化文件和路径的工作方式。下列 三 个文件分 别位千目录 dirl 以及 dirl 的子目录 dir2 中,这些文件的路径名在注释中给出:

# dirl\_init_.py print('dirl init') X =

1

#dirl\dir2\_init_.py print ('dir2 init') y = 2

#dirl\dir2\mod.py print('in mod.py') z = 3

这里, dirl 要么是我们当前工作所在目录(也就是主目录)的子目录,要么就是位千模块 搜索路径中(实际上就是 sys. path) 的一个目录的子目录。无论哪一种, dirl 的容器都不 需要_init_.py 文件。 当 Python 向 下搜索路径的时候 , import 语句 会在每个目录首次遍历时执行该目录的初始化

文件。这里使用 print 语句来跟踪它们的执行:

702

I

第 24 章

c: / code> python >>>垃port

Run in dirf's container direcorry First imports run inir files

dir1.dir2.mod

# #

dir1.dir2.mod

# later imports do not

dirt init dir2 init in mod.py >>>



i叩ort

就像模块文件一样,任何已导入的目录也可以传递给 reload, 来强制该项目重新执行。就

像这里展示的那样, reload 可以接受点号路径名称来重新载人嵌套的目录和文件:

>» from imp import reload # from needed in 3.X only »> reload(dirl) dirl init

»> »> reload(dir1.dir2) dir2 init

一且导 人后 ,

import 语句内的路径会变成脚本中的 一条嵌套对象路径。在这里, mod 是 一

个嵌套在对象 dir2 中的子对象,而 dir2 又嵌套在对象 dir1 中:

»> dirl

>» dir1.dir2

>» dir1.dir2.mod



实际上,路径中的每个目录名称都会变成赋值了模块对象的变量,而该模块对象的命名 空间则是由该目录内的_init—.PY 文件中所有赋值语句进行初始化的。 di r1. x 引用了在

dirl\—init—.PY 中赋值的 x 变址, mod.z 则引用了在 mod.py 内赋值的变批 Z:

>» dirl.x 1

»> dir1.dir2.y 2

>» dir1.dir2.mod.z 3

包的 from 语句 vs 包的 import 语句 当 import 语句和包 一起使用时可能有些不便,因为你必须经常在程序中重新输入路径。例 如,上一节的例子中,每次要得到 z 时,就得从 d ir1 开始重新输入完整的路径,并且每次

都要重新执行整个路径。如果你想要尝试直接读取 dir2 或 mod 、就会得到一个错误:

>» dir2.mod NameError: name'dir2'is not defined

模块包

1

703

»> mod.z NameError: name'mod'is not defined 因此,我们可以让包使用 from 语句,来避免每次读取时都要重新输入路径,通常这样比较 方便。也许更重要的是,如果你重新改变目录树结构,那么 from 语句只需在程序代码中更

新一次路径, import 则需要修改很多地方。 import 作为一个扩展功能(下 一章会详细讨论), 在这里也有 一 定的帮助,它提供一个完整路径较短的同义词,并在出现多个同名模块的时 候充当重新命名的工具:

C:\code> python »> from dir1.dir2 import mod dir1 init dir2 init in mod.py »> mod.z 3 »> from dir1.dir2.mod import z >»Z 3 »> import dir1.dir2.mod as mod »> mod.z

# Code path here only

# Don't repeat path

# Use shorter name (see Chapter 24)

3

»> from dir1.dir2.mod import z as modz »> modz 3

#

Ditto if names clash (see Chapter 25)

为什么要使用包导入 如果你刚开始学习 Python, 必须确保已经精通了简单的模块,才能继续进入包的领域,因

为包是相对高级的功能。然而,包也扮演了重要的角色,尤其是在较大程序中:包让导入 包含了更多信息,并可以作为组织工具来简化模块的搜索路径,同时还可以解决模糊性。 首先,因为包导人提供了程序文件的目录信息,所以可以轻松地找到文件,从而作为组织 工具来使用。假设没有包导入路径,那么通常得通过查看模块搜索路径才能找出文件。再者,

如果根据功能把文件组织成子目录,那么包导入会让模块扮演的角色更为明显,也使代码 更具可读性。例如,正常导入模块搜索路径上某个目录内的文件时,就像这样:

import utilities 与下面包含路径的导入相比,提供的信息就少很多了:

import database. client. utilities 包导入也可以大幅简化 PYTHONPATH 和.pth 文件搜索路径设置。实际上,如果对所有跨目录 的导入都使用包导人,并且让这些包导入都相对于一个共同的根目录,以及把所有 Python

程序代码都存在其中,那么在搜索路径上就只需 一 个单独的接入点:通用的根目录。最后,

704

I

第 24 章

包导入能让你想导入的文件更明确,从而解决了模糊性。同时还能解决同一模块在多处被 导人所引发的冲突。下一节要深入探索包导人所扮演的这一角色。

三个系统的故事 实际中必须借助包导入的场景,就是解决当多个同名程序文件安装在同一个机器上时,所

引发的模糊性 。 这是一 个安装方面的问题,但通常也是实际中所要留意的地方——尤其是 考虑到程序员倾向千使用更加简单且彼此相似的名字来命名模块文件的事实。下面让我们

用 一 个假设的场景来说明。

假设 一位程序员开发了 一 个 Python 程序,它包含了 一个文件 utilities.py, 其中包含了通用 的工具代码,还有 一 个顶层文件 main.py 让用户来启动程序。在这整个程序的所有文件中 会以 import

utilities 加载井使用共用的代码。当软件交付的时候,采用的是.tar 或 zip

文件的形式,其中包含了该程序的所有文件。而当它安装时、会把所有文件解压放进目标 机器上的某一个名为 system] 的目录中:

systeml\ utilities.py main.py other.py

Common utility functions, classes Launch this to start the program # Import utilities to load my tools

# #

现在,假设有第二位程序员开发了另一个不同的程序,其文件也命名为 utilities.py 和 main PY, 并且同样也在全部的程序文件中使用 import utilities 来加载共用的代码文件。当第 二 个系统安装到和第一个系统相同的计算机上时,它的文件会解压并安装至接收机器上某 处文件夹名为 system2 的新目录内,从而不会覆盖第一个系统的同名文件:

system2\ utilities.py main.py other.py

#

Common uriliries

# Launch this to run #

Imports utilities

目前为止,一切顺利:两个系统可以在同 一 台机器上共存并运行。而实际上,你甚至不需

要配置模块搜索路径,就能在你的计算机上使用这些程序。因为 Python 总是先搜索主目录(也 就是包含顶层文件的目录),在这两个系统的文件内的导入,都会自动看见它们各自系统 目录内的所有文件。例如,如果点击 system]\main.py, 所有的导人都会先搜索 system] 。同 样,如果启动 system2\main.py, 则会改为先搜索 system2 。 记住,只有在跨目录进行导入时

才需要模块搜索路径的设置 。 尽管如此,假设在机器上安装这两套程序之后,你又决定在自己的系统中使用这两个

utilities .py 文件内各自的一些程序代码。毕竟,这是通用工具的代码,而且 Python 代码的

本质是提高复用度。因此,你想在第 三 个目录内编 写 的文件中使用下面的代码,来载入两 个文件中的一个:

模块包

1 705

import utilities utilities. func ('spam') 现在,问题开始出现了。为了让这能够工作,需要设置模块搜索路径,引人包含 utilities.py 文件的目录。但是,要在路径内先放哪个目录呢: system] 还是 system2?

这个问题在千搜索路径本质上是线性的。因为搜索总是从左至右扫描的,所以不管你纠结 这个困境多久,一定只会得到一个搜索路径上最左侧(最先列出)的目录内的 utilities.py 。

也就是说,永远无法导入另 一 个目录的那个文件。 每次导入操作时,可以试着在脚本内修改 sys.path, 但那是额外的工作,而且很容易出错。 更不用说在每次 Python 程序运行前手动地修改 PYTHON PATH 环境变量这种繁复的方式,这

么做同样不能让你在一个文件中同时使用这两个版本的 utilities.py 。在默认情况下,可以说 你走到了一个死胡同。 这个问题正是包能够解决的。除了把程序安装到模块搜索路径中的独立的目录内,你还可 以将它们打包并将它们安装成同 一 根目录下的子目录。例如,你可能想组织这个例子中的

所有代码,以变成下面这样的安装路径层次:

root\ system1\ init_.py utilities.py main.py other.py system2\ .. init_.py utilities. py main.py other.py system3\ _init .PY myfile.py

— —



# # #

Here or elsewhere Need _ init_ .py here only if imported elsewhere Your new code here

现在,你只需把共同根目录添加到搜索路径中。如果你程序中所有的导人都相对千这个通 用的根目录,那么你就可以通过包导人来导人任何一个子路径中的 utilities.py 文件:该文 件所在的目录名称使其路径具有唯一性(因此,引用的模块也将成为唯一的)。事实上,

只要使用 import 语句,就可以在同 一个模块内导入这两个工具文件,而每次引用工具模块 时,都要重复其完整的路径:

import systeml. utilities import system2.utilities systeml. utilities . function('spam') system2. utilities. function ('eggs') 在这里,所在目录名称让模块的引用变得具有唯 一性。

706

I

第 24 章

注意,如果需要读取两个或两个以上路径内的同名属性时,就必须使用 import, 而不能用 from 。如果被调用的函数名称在每个路径内都不同, from 语句就可以避免每当调用其中 一 个函数时,就要重复写出完整包路径的问题 1 from 语句的 as 扩展子句也可以用千提供具 有唯 一性的别名。 此外,要注意在前面所示的安装层次中,虽然 system I 和 system2 目录中已经加入了 _init_.py 文件来使其工作,但是根目录中却不常要—/nit_.py 文件。只有程序中 import

语句所列出的目录才需要这些文件。 Python 首次通过包的目录处理导入时,这些文件就会 自动运行了。

事实上,在这种情况下 system3 目录不需要放在 root 目录下:只有被导入的代码包需要。然而,

因为你自己的程序可能在未来会披其他程序用到,所以你也许还是希望将其放在通用的根 目录下,以避免之后类似的变批名冲突问题。 最后,注意两个原来系统中的导入仍旧能正常运作。因为它们的主目录都会被优先搜 索,而且新增的 root 目录井不影响 system! 和 system2 内的代码;它们仍然只需写 import

utilities, 就可以找到自己的文件,尽管在下一节里 Python 3 . X 中的包并非如此。如果 你小心地在共同的根目录下放置这些 Python 系统,那么路径配置就会变得很简单:只需要 添加 一次共同根路径即可。

请留意:模块包 因为包是 Python 的标准组成部分,所以较大的笫三方扩展通常是以包目录的形式交

付给用户,而不是一系列扁平的模块列表。例如, win32al) 这个 Python 的 Windows 扩展包,就是首先采用包这种风格的扩展之一。它的许多工具模块都位于包内,并且 通过路径未导入 。 例如,为了加载客户端的 COM 工具,你可以使用如下的语句:

from win32com.client import constants, Dispatch 这一行代码会从 win32com 包(一个安装后的子目录)的 client 模块取出其中的名称。

包导入在 Jython (Python 的 Java 实现版本)所运行的代码中也很普逼,因为 Java 库 也是组织为层次结构的。在最近的 Python 发行版中, emai) 和 XML 工具,也类似地

被组织成了标准库中的子目录,并且 Python 3.X 甚至将更多相关的模块组织为包(包 括 tkinter GUI 工具、 HTTP 网络相关工具等)。下面的导入语句访问了 Python 3.X (2.X 中的用法可能有所不同)中的各种标准库工具:

from email.message import Message from tkinter.filedialog import askopenfilename from http.server import CGIHTTPRequestHandler 无论你是否需要创建包目录,你总归还是会导入并使用它们 。

模块包

I

707

包相对导入 到目前为止,对包导入的介绍主要集中在从包的外部导入包文件。在包自身的内部,包文 件导入同一个包中的内容时,可以使用和外部导人相同的完整路径语法,但它们还可以利

用特殊的包内搜索规则来简化 import 语句。也就是说,包内的导入可以相对千包,而不需 要列出包导入路径。 这种机制的工作方式与版本有关: Python 2.X 会隐式地在导入时首先搜索包目录,而

Python 3 . X 需要显式地使用相对导入语法。这种 Python 3.X 的变化让同 一 包内的导入更为 明显,从而增强了代码的可读性,但同时也造成了与 2.X 版本的不兼容而导致 一 些程序不 能在 3.X 版本下正常运行。

如果你刚开始使用 Python 3.X, 本节中你的关注点可能是其新的导入语法和模型。而如果 你之前已经用过 2.X 中的 Python 包,那么你同时还可能会对 Python 3.X 导入模式的区别感 兴趣。让我们带着第二种作比较的目标来开启旅程。

注意:正如我们在这一节中将学到的,包相对导入的使用实际上会限制你的文件的功能。简而 言之,它们在 2.X 和 3.X 中将不再被用作可执行程序文件。因此,正常的包导入路径在

很多情况下可能是 一种更好的选择。然而,相对导入这一特性也有着广泛的应用,它值 得大多数的程序员来重温以更好地理解其中蕴含的折中和动机。

Python 3.X 中的变化 包中的导入操作的工作方式在 Python 3.X 中略有改变。这种变化只适用千我们本章中已经 学习过的文件作为包路径的一部分的情况;其他的导入方式像以前一样工作。对千包中的 导入, Python 3.X 引入了两个变化:



它修改了模块导入搜索路径语义,从而默认地跳过包自己的目录。导入只检查 sys.

path 列表中的搜索路径。这称为绝对导入。



它扩展了 from 语句的语法,以允许显式地要求导入只搜索包的目录(以点号开始)。 这称为相对导入语法。

这些变化在 Python 3.X 中全部可用。新的 from 语句相对语法在 Python 2.X 中也可以使用, 但如果要默认采用绝对路径的搜索方式,则必须作为一 个选项进行开启。 一 且开启这 一选项, 就可能会破坏 2.X 版本的程序,但它仍可用于 3.X 版本的向前兼容性。

这一 变化的影响是,在 Python 3.X (及相关的 2.X) 中,我们通常必须使用特殊的 from 语 法来导入与导入者位 于 同一包中的模块,否则你需要列出一个从 sys.path 路径出发的包的

708

I

第 24 章

根目录的完整的路径;或者你的导入是相对千程序顶层文件而言会一直被搜索到的路径(这

通常是当前的工作路径)。 不过默认情况下,你的包路径是不会被自动搜索的,而且对千作为包使用的路径中的文件,

如果缺少了特殊的 from 语法,那么这些文件的包内导人将会失败。下面我们将看到,在 3.X 中这将会影响到你设计导人和路径的方式,尤其对千那些同时被顶层程序和可导入包所使

用的模块。首先,我们还是深入了解一下相对导入的工作机制。

相对导入基础知识 目前在 Python 3 . X 和 Python 2.X 中, from 语句可以使用以点号(".")开头的子句来导

人位于同 一 包中的模块(也就是包相对导入),而不是位干模块导人搜索路径上某处的模 块(称为绝对导入)。也就是说:



以点号开头的导人:在 Python 3.X 和 Python 2.X 中,你可以在 from 语句中使用以点号 开头的模块名,来表示导入应该只相对千外围的包一一这样的导入将只是在包的内部目

录进行搜索,而不会搜索到位干导入搜索路径 (sys.path) 上某处的同名模块。最终 效果是包模块覆盖了外部的模块。



不以点号开头的导入:在 Python 2.X 中,包的代码中的常规导入(没有以点号开头), 目前默认为一种先相对再绝对的搜索路径顺序,也就是说,它们首先搜索包自己的路径。 然而,在 Python 3.X 中,在 一个包中的导入默认是仅绝对的-~在缺少特殊的点号开

头语法的时候,导人忽略了外围包自身,转而在 sys.path 搜索路径上的其他位置查找。 例如,在 Python 3.X 和 Python 2.X 中,如下形式的一条语句:

from. import spam

#

Relative to this package

告诉 Python 把位千与语句中给出的文件相同包路径中的名为 spam 的 一 个模块导入。类似的, 语句:

from.spam import name 意味着“在包含这条语句的文件所位于的包中,找到名为 spam 的模块并导人其中的变量 name 。

"

前面没有点号开头的那条语句的行为,取决于你使用的 Python 的版本。在 Python 2.X 中, 这样的 一 条 import 仍然会默认采用原本的先相对再绝对的搜索路径顺序(也就是说,先搜 索包的目录),除非在导人文件中包含了如下形式的 一 条语句(作为该文件的第 一 条可执 行语句) :

from _future_ import absolute_import

# Use 3.X relative import model in 2.X

模块包

1

709

如果你使用了上面这条语句,那么它将启用 Python 3.X 的仅绝对搜索路径策略 。

在 Python 3.X 和开启状态的 2.X 中,不以点号开头的 一 个 import 总是会使 Python 略过模 导入搜索路径的相对部分,并且在 sys.path 所包含的绝对目录中查找。例如,在 Python 3.X

的模型中,下面形式的一 条语句将总是在 sys.path 上的某处查找一个 string 模块,而不 会查找该包中的同名模块:

import string

II Skip this package's version

然而,没有了 Python 2.X 中的 from _future_语句,那么假如包中有 一 个本地的 string 模块,就会导入这个模块。要在 Python 3.X 和开启了仅绝对导入的 Python 2.X 中得到相同 的行为,你可以运行下面的语句形式来强制进行相对导入:

from. import string

# Searches this package only

如今,这一语句在 Python 2.X 和 Python 3.X 中都能工作。 Python 3.X 模型中方式的唯一区 别是,如果要载入与当前文件在同一个包目录下的另一个文件,那么就必须使用该语句(或

者你也可以写出整个包的路径)。 注意,点号开头的形式只能对 from 语句强制使用相对导入,而不能对 import 语句这样使用。

在 Python 3.X 中, import modname 语句形式总是仅绝对导入的,并且会跳过内部的包路径 。 在 2.X 中这一语句却仍然采取相对导人,井首先搜索包的路径。无点号开头的 from 语句与

import 语句的行为相同,在 Python 3.X 中是仅绝对的(略过包目录),并且在 Python 2.X

中是先相对再绝对(先搜索包目录)。 其 他基千点号的相对引用模式也是可能的。 在一个名为 myp 蚐的包目录下的 一 个模块文件 中,下面的可替代 import 形式可以像注释中所述地那样工作:

from.string import namel, name2 from. import string from.. import string

# # #

Imports names from mypkg.string Imports mypkg.stri11g Imports string sibling of mypkg

要更好地理解后一种形式,并且解释这种额外的复杂性,我们需要花一点时间来解释这一 变化背后的原理。

为什么使用相对导入 除了让包内导人更加显式,这个功能的一部分设计初衷是,为了帮助脚本解决同名文件出

现在模块搜索路径上多个不同位置时的 二 义性。考虑下面的包目录:

mypkg\ init .PY main.py string.py

— —

710

1

第 24 章

这定义了 一 个名为 mypkg 的包,其中含有名为 mypkg.main 和 mypkg.string 的模块。现在, 假设模块 main 试图导入名为 string 的模块。在 Python 2.X 和更早版本中, Python 会先 寻找 mypkg 目录以执行相对导入。这会找到并导入位千该处的 string.py 文件,将其赋值给

mypkg.main 模块命名空间内的名称 string 。 不过,这 一导入的本意可能是要导入 Python 标准库的 string 模块。可惜的是,在这些

Python 版本中,无法直接忽略 mypkg.string 去寻找位千模块搜索路径更右侧的标准库中 的 string 模块。此外,我们无法使用完整包导入路径来解决这个问题,因为我们无法依赖 在每台机器上的标准链接库路径。

换句话说,包中的简单导人可能具有 二 义性而且容易出错。在包内,我们无法确定 import spam 语句指的是包内的模块还是包外的模块。一种可能的后果是,一个局部的模块或包会

在不经意间隐藏了 sys.path 上的另一个模块。 在实践中, Python 使用者可以避免为他们自己的模块重复使用标准库模块的名称(如果需 要标准 string 库,就不要把新的模块命名为 string) 。但是,一个包还是有可能意外地 隐藏标准库模块。再者, Python 以后可能新增标准库模块,而其名称可能刚好就和自己的

一 个模块同名。而依赖于没有点号开头相对导入的程序代码同样也不容易理解,因为读者 可能对希望使用哪个模块而感到困惑。所以我们最好能在代码中显式地指出导人的解析过

程。

Python 3.X 中的相对导入解决方案 要解决这一 困境,在 Python 3.X 中包内部运行的导人已经改为仅绝对的(这种方式也成为 了 Python 2. X 中的 一个可选项)。在这种导人模型下,示例文件 mypkglmain.py 中一条如

下形式的 import 语句将总是在包之外找到 一个 string, 井通过对 sys.path 的一 次绝对导 入搜索来实现:

import string

# Imports

string outside package (absolute)

没有点号开头的一 条 from 语句也将被看作是绝对的 :

from string import name

#

Imports name from string outside package

如果你确实想要从包中导入 一 个模块 , 且不必给出从包根目录的完整路径,那么可以在 from 语句中使用点号语法来进行相对导入:

from. import string

#

Imports mypkg.string here (relative)

这种形式只会导入相对千当前包的 string 模块,而且是前面导入示例中绝对形式的等价相

对形式(两者都会整体导人 一 个模块)。当使用这一 特殊的相对语法的时候,包的目录是 唯一被搜索的目录。

模块包

I

711

我们也可以使用相对语法从一个模块中复制特定的名称:

from.string import namel, name2

#

Imports names from mypkg.string

这条语句再次相对千当前包来引用 string 模块。如果这段代码出现在我们的 mypkg.main

模块中,它将从 mypkg.string 中导入 namel 和 name2 。 实际上,相对导入中的".“用来表示包含当前文件的包目录。如果前面再增加一个点,将 执行从当前包的父目录的相对导入。例如,下面的语句:

from.. import spam

#

Imports a sibling of mypkg

将导人 mypkg 的一个同级模块,也就是位千 mypkg 父目录中紧挨着的 spam 模块。更 一 般化

的情况是,位千某个模块 A.B.C 中的代码可以进行下面的几种导人:

from. import D from.. import E

# Imports A.E (.. means A)

from.D import X from.. E import X

# Imports A.B.D.X (. means A.BJ # Imports A.E.X (.. means A)

#

Imports A.B.D (. means A.BJ

相对导入 vs 绝对包路径 此外, 一 个文件有时候也可以在 一 条绝对导入语句中显式地指定它自己的包,而该包的路

径需要相对千 sys.path 中的一个目录。例如,下面的语句会在 sys.path 的一个绝对路径 中找到 mypkg:

from mypkg import string

# Imports mypkg.string (absolute)

然而,这依赖千环境配置以及模块搜索路径设置的顺序,与此相反的相对导入点号语法则

不会依赖它们。实际上,这种绝对导入方式要求模块搜索路径必须包括 mypkg 的直接父目

录。而且 mypkg 很可能就是包的根目录(否则包一开始就不可能在外部被引用!),不过 mypkg 也可能嵌套在一个更大的包树中。假如 mypkg 不是包的根目录,那么当需要显式地 指定包的时候,就必须像下面这样在绝对导入语句中列出到达 mypkg 所有的包根目录下的 路径:

from system.section.mypkg import string

# system container on sys.path only

在较大或较深的包中,这可能比点号语法要写更多的字符:

from. import string

#

Relative import syntax

如果你使用了绝对导入,那么不论搜索路径设置、搜索路径顺序以及路径嵌套如何,导入 都会自动搜索父级包。另一方面,不论文件是否作为程序或包的 一 部分被使用,全路径绝

对导入形式都将正常工作。我们在下面将详细介绍。

712

I

第 24 章

相对导入的适用情况 相对导入乍 一 看可能有些令人困惑,但如果你记住一 些关键点,就能更好地理解它:



相对导入只适用千在包内部的导入。记住,这种功能的模块搜索路径修改只适用千位 千包内的模块文件中的 import 语句,即包内导入。位于包文件之外的普通导入也会像

前面介绍的那样工作,即首先自动搜索包含当前顶级脚本的目录。



相对导入只能用千 from 语句。还要记住,这一 功能的新的语法只能用于 from 语句, 而不能用千 import 语句。相对导入的特征是在一个 from 中的模块名前面有一个或多

个点号。有些模块名中本身嵌有点号,如果没有 一 个开头的点号的话,那么该导入是 包导人,而不是相对导入。

换句话说, Python 3.X 中的“包相对导人“实际上只是删除了 Python 2.X 针对包的内部

搜索路径行为,并且添加了特殊的 from 语法来显式地指定相对导入行为。如果你之前在

Python 2.X 时编写的包导入没有依赖 Python 2.X 的特殊隐式相对查找(例如,总是通过拼 写出从 一 个包根目录出发的完整路径),那么这种版本间的语言改变就不太会影响你的代

码在新版本中的运行。如果你使用了这种特殊隐式相对查找,就需要修改包文件,使其采 用针对本地包文件的新的 from 语法或者采用完整的绝对路径。

模块查找规则总结 对于包导入和相对导人来说,我们目前见过的 Python 3.X 中模块查找可以概括为如下几条:



基础模块的简单名称(例如 A) 通过搜索 sys.path 列表上的各个目录,从左到右被查找。 这个列表由系统默认设置和第 22 章中介绍的用户配置设置构成。



包是带有一个特殊的_init_.py 文件的 Python 模块的目录,该文件允许导入中可以使

用 A.B.C 目录路径语法。例如在 A.B.C 的 一 次导人中,名为 A 的目录通过常规模块导 入搜索在 sys.path 中被找到, B 是 A 中的另一个包子目录, C 是 B 中的 一 个模块或其 他可导人项。



在一 个包文件内,常规的 import 语句使用和其他地方的导入是 一样的 sys.path 搜索 规则。包内导入使用 from 语句和点号开头的模块名,而且它是相对千包的;也就是说,

Python 只会检查包目录,而不会采用常规的 sys.path 搜索。例如,在 from.import A 中, 模块搜索袚限制在包含了该语句出现的文件的目录之中。

Python 2.X 的工作方式类似,唯一 不同的是不带点号的普通导入在开始搜索 sys.path 之前, 会首先自动搜索包的目录。

总的来说, Python 的导人能够通过以下方式,在相对(外围的目录内)和绝对 (sys.path 的目录中)之间切换:

模块包

I

713

以点号开头的导入: from. import m 在 2.X 和 3.X 中都是仅相对的

不以点号开头的导人: import m 、 from m import x 在 2.X 中是先相对再绝对的,在 3 .X 中仅是绝对的

正如我们后面将看到的, Python 3.3 为模块添加了另一种方式,即命名空间包。那种方式与

这里提到的包相对导入并不相关。命名空间包模型也支持包相对导入,而且提供了另一种 构建一个包的方式。它增强了导入搜索过程,从而允许包的内容分别位千多个简单目录, 这也是包导入解析留的“最后一手”。然而,这种复合包的行为,就相对导入规则而言是

一样的。

相对导入的实际应用 我们已经对理论做了足够多的介绍,现在让我们来运行一些简单的代码,以演示相对导入

背后的概念。

包外导入 首先,如前所述,这 一 功能不会影响到 一个包之外的导入。因此,下面的代码能按照预期 找到标准库的 string 模块:

C:\code> c:\Python33\python >>>扛port string »> string

但是,如果我们在当前工作目录中添加一个同名的模块,那么 Python 会选择该模块,因为

模块搜索路径的第一 项就是当前工作目录 (Current Working Directory, CWD)

#code\string.py print('string'* 8) C:\code> c:\Python33\python »> import string stringstringstringstringstringstringstringstring »> string

换句话说,常规导人仍然相对千“主”目录(即包含顶级脚本的目录,或者当前的工作目录)。

实际上,如果不是位千作为一个包一部分的一个文件中,就不会允许代码使用相对导入语法:

»> from. import string SystemError: Parent module''not loaded, cannot perform relative import 在本小节中,在交互式命令行中输入的代码和将它们放入 一 个顶层脚本中运行的行为是相

714

1

第 24 章

同的,因为 sys.path 上的第一项要么是命令行的当前工作目录,要么是包含顶层文件的目

录。唯 一的区别是, sys.path 的开头是一个绝对路径,而不是一 个空字符串。 # code\main.py

import string print(string)

# Same code but in a file

(:\code> C:\python33\python main.py stringstringstringstringstringstringstringstring

#

Equivalent results in 2.X

类似地,在这个非包文件中的 一 条 from. import string 语句会同该语句在命令行中 一样 执行失败,这是因为程序和包是两种不同的文件使用模式。

包内导入 现在,让我们删除在 CWD 中编写的本地 string 模块,并创建带有两个模块的 一 个包目录, 同时创建必须存在但是为空的 test\pkg\_init—.PY 文件。本小节中包的根目录位千袚自动添 加到 sys.path 内的 CWD 中,因此我们不需要设置 PYTHON PATH 。 我也会出干篇幅原因忽

略空的_init_.py 文件以及大部分的错误信息文本(对千非 Windows 用户的读者可能要将

这里的 shell 命令,转换成对应平台的内容) :

C:\code> del string* (:\code> mkdir pkg c: \code> notepad pkg\_init_.py code\pkg\spam.py import eggs print(eggs.X)

#

de/ _pycache~\string* for bytecode in 3.2+

#

# c:\Python27\python »> import pkg.spam

99999

C:\code> c:\Python33\python >» import pkg. spam ImportError: No module named'eggs'

模块包

I

71s

为了使这段代码在 Python 2.X 和 Python 3.X 下都能运行,你可以修改 spam.py 文件使之采

用特殊的相对导入语法,以便其导人也能在 Python 3.X 中搜索包目录: # code\pkg\spam.py from. import eggs print(eggs.X)

If c:\Python27\python >» import pkg. spam

99999

C:\code> c:\Python33\python >» import pkg. spam

99999

导入仍然是相对千 CWD 的 注意,在前面的示例中,包模块仍然能访问 string 这样的标准库模块。它们的常规导人仍 然相对千模块搜索路径上的项目。实际上,如果你再次向 CWD 添加一个 string 模块,那

么包中的导入将找到你新添的这个 string, 而不是标准模块库中的 string 。尽管你可以在

Python 3.X 中通过使用绝对导入跳过对包目录的搜索,但你仍然不能跳过对发起导入的程 序所在主目录的搜索:

code\string.py print('string'* 8)

#

# code\pkg\spam.py from . import eggs print(eggs.X)

# code\pkg\eggs.py X = 99999

import string print(string)

# c: \Python33\python # Same result in 2.X >>> import pkg.spam stringstringstringstringstringstringstringstring

99999

使用相对导入和绝对导入选择模块 为了展示这是如何应用千标准模块库的导人,再一次重新设翌包。去除本地的 string 模块, 并在包自身之中定义 一 个新的:

716

I

第 24 章

C:\code> del string*

# del _pycache—\string* for bytecode in 3.2+

# code\pkg\!'pam.py import string print(string)

# python pkg\main.py EggsEggsEggsEggs c:\code> python >» import pkg.spam Eggs Eggs Eggs Eggs

# From main script: Same result in 2.X and 3.X

# From elsewhere: Same result in 2.X and 3.X

与子路径修正不同,像这样的完整路径绝对导入能让你独立运行并测试你的模块:

c: \code> python pkg\spam.py

# Individual modules are runnable too in 2.X and 3.X

Eggs Eggs Eggs Eggs

示例:模块自测试代码的应用(预习) 作为总结,下面是另 一 个该问题的典 型 例子及其完整路径解决方案。这个例子使用了 一 种

我们会在下一 章中展开讲述的通用的技术,由千其思路十分简单因而我们在此可以对它进 行预习(尽管你后面还可以回顾这个例子,把该例安排在这里是很恰当的) 。

考虑下面一 个包目录中的两个模块,其中第 二 个包含自测试代码。简而 言 之, 当一 个模块

722

I

第 24 章

作为顶级脚本被运行时,它的_name _属性的值将变为 “_main_" 字符串,但是当它被 导入时却不是,这一事实允许它可以同时被用作模块和脚本: # codeviua/pkg\n11.py

def somefunc(): print ('ml. somefunc') # codeviualpkg\Jn2.py

# Replace me with a real import statement

... import m1 here... def somefunc(): ml. somefu:,c () print ('m2. somefunc') if

name ==' == · main somefunc()

,

# Self-test or top-level script usage mode code

其中的第 二 个模块需要导入第一 个模块,也就是占位行

“...import ml here …”所指代的。

用 一 条相对导入语句替换这 一 占位行能让该文件在被用作包时正常工作,但在 Python 2.X

和 3 .X 中都不能在非包模式下 工 作(这里出千篇幅忽略了结果和错误信息;关千完整信息, 参见本书例子中的 dualpkg\result.txt 文件)

code\dualpkg\m2.py from. import ml

#

c:\code> PY 多 3 >» import dualpkg.m2 C:\code> py -2 >» import dualpkg.m2 c:\code> py -3 dualpkg\m2.py c:\code> py -2 dualpkg\m2.py

# OK #

OK

# Fails! # Fails!

相反, 一 条简单导人语句在 2.X 和 3.X 中的非包模式下都能工作 , 但是只会在 3.X 的包模 式下失败,因为简单导入语句在 3 . X 中不会搜索包目录: # codeviualpkg\m2.py import ml

c:\code> py -3 >» import dualpkg.m2 c: \code> py -2 >» import dualpkg.m2 c:\code> py -3 dualpkg\m2.py c: \code> py -2 dualpkg\m2.py

# Fails! # OK #

OK

# OK

最后,只要包的根目录在模块搜索路径中(只有这样它才能在其他位置被使用),那么使 用完整包路径对所有的使用模式和所有的 Python 版本都能工作: # code\dualpkg\m2.PY i叩ort

dualpkg.m1 as ml

#

And: set PYTHONPATH=c:\code

模块包

I

723

c: \code> py -3 >» import dualpkg.m2 C:\code> py -2 »> import dualpkg.m2 c:\c.ode> py -3 dualpkg\m2.py c:\code> py -2 dualpkg\m2.py

# OK # OK # #

OK OK

总的来说,除非你希望并且能够将你的模块单独放到脚本文件下的 一 个子目录中,完整包

路径导人将是比包相对导入更加推荐的一种做法。尽管它们需要更多的手动输入,它们能 处理所有的情况,而且在 2.X 和 3.X 中的使用效果是一致的。当然也存在着需要其他付出

的可能解决方案(比如,在代码中手动设置 sys.path) ,但我们将忽略它们,因为它们更 难以理解而且依赖千导入的语义,同时也是易千出错的;完整路径包导人只依赖于基础的

包机制。 当然,这种方式对你的模块的影响程度取决千具体的包 ;绝对导入也可能需要在包目 录被

重新组织的时候进行相应的改变,而相对导入也可能在一个本地模块被移动的时候失效。

注意:你也需要持续关注 Python 未来在这一 方面的变化。尽管本书只涉及 Python 3.3 的内容, 在本书创作时也出现了探讨解决 Python 3.4 中 一 些包相关问题的 PEP, 可能会在程序模 式下允许使用相对导入。另 一 方面,这 一 倡议的影响范围和最终结果尚不清楚,而且只

会在 3.4 及之后的版本中才有效 1 同时,这里介绍的完整路径解决方案与版本无关 1 而

且 3.4 的发布距现在还有一年时间译注 2。也就是说,你可以选择等待一个只适用千 3.X 的功能上较为局限的改变,或者直接使用被长期实践验证过的完整包路径。

Python 3.3 中的命名空间包 现在你已经学习了所有包和包相对导入的知识,接下来我会介绍 一 种对我们上面学过的思 路进行改造的一种新选择。至少抽象地来说,这种选择可以作为 Python 3.3 发行版中的第 四种导入模型。 Python 3.3 的所有四种导入模型按照出现的时间如下依次列出: 基础模块导入: import mod 、 from mod import attr

最初的模型:对文件及其内容的导入,相对千 sys.path 模块搜索路径

包导入: import dir1.dir2.mod 、 from dir1.mod import attr 这 一 类导入提供了相对千 sys.path 模块搜索路径的目录路径扩展,在这一 模型中每个 包位千一个单独的目录中并含有 一个 _init_.py 包初始化文件,适用千 Python 2.X 和 3.X

包相对导人: from. import mod (相对)、 import mod (绝对) 这是前 一 小节中包内导入所使用的模型,它分为以点号开头和不以点号开头相应的相 对或绝对的查找机制,在 Python 2.X 和 3 .X 中均能使用,但有所不同 译注 2

724

I

本书英文版出版于 2013 年 , 而 Python 3.4 发布于 2014 年 。

第 24 章

命名空间包: import

splitdir.mod

我们马上要学习的新命名空间包模型,该模型允许包横跨多个目录,并且不要求—

init—.py 初始化文件,在 Python 3.3 中被引入 这里的前两种模型是自包含的,但第三种收紧了包内导入的搜索路径和扩展语法,第四种

则颠覆了原有包模型的一些核心概念和要求。事实上, Python 3 .3 (以及之后的版本)现在 拥有两种风格的包:



原始的模型,现在称为常规包



可选的模型,称为命名空间包

这与我们在本书后面将遇到的“经典类”和“新式类”之间的区分是相似的,尽管这里的“新” 要更“新”一些。原来的包模型和新的包模型并不是相互排斥的,你可以在一个程序中同

时使用它们。事实上,新的命名空间包模型常常被作为一种后备选项进行使用,它只在该 名称的普通模块和常规包在模块搜索路径上都找不到的时候才被识别。 命名空间包的理论基础根植千包安装目标中,所以如果你不是负责这一类工作的话就比较

难以理解这一概念,同时这一特性的 PEP 文档也对它进行了很好的说明。不过简单来说, 它们解决了一种在包的各部分进行合并时,存在多个_init_.py 文件的潜在冲突,解决的 方式是彻底地移除这些文件。此外,对千 一 些各个部分分散在多个不同目录下或位千多个

sys.path 项目中的包,命名空间包能够为它们提供标准支持 。同时命名空间包增强了安装 的灵活性,并提供了 一 种通用的机制,从而替代多种为了达到同样目的互相之间不兼容的 解决方案。 尽管现在评判它们的使用还为时过早,一般的 Python 用户可能会觉得命名空间包是对常规 包模型的 一 种有用的替代解决方案。命名空间包不要求初始化文件,而且允许任何的代码

目录可被用作一个可导入的模块。让我们继续了解更多的细节来一探究竟。

命名空间包的语义 命名空间包与普通的包并没有本质的区别:它只不过是另一种创建包的方式。此外,它们

在顶层仍然相对千 sys.path: 一个带点号的命名空间包路径的最左边组件同样必须在常规 包搜索路径的 一个项目中被定位。

然而,就物理结构而言两者有着本质上的区别。常规包仍然必须拥有 一 个能自动运行的— init_.py 文件,而且必须位千 一个独立的目录中 。相反,新式的命名空间包不可以含有一个 _init_.py 文件,而且可以横跨多个路径,这些路径将在导人时被收集。事实上,所有能够

成为一个命名空间包组成部分的目录都不能包含一个 —in“—.PY 文件,但是嵌套 在它们中 的内容可以被当作 一个单独的包。

模块包

1

725

命名空间包导入算法 为了真正理解命名空间包,我们需要从底层去学习包导入操作在 Python 3.3 中是如何完成 的。在导入过程中, 3.3 版本与 3 . 2 和之前的版本一样,依旧会遍历模块搜索路径 sys.path

中的每个目录。然而,在 3.3 版本中,当对每个模块搜索路径中的 directory 搜索名为 spam 的被导人包时, Python 会按照下面的顺序测试 一系列更广的匹配条件 :

1.

如果找到 directory\spam\_init—.py ,便会导入 一个常规包井返回。

2

如果找到 directory\spam. {py,

pyc,

或其他模块扩展名},便会导入一个简单模块并

返回。

3.

如果找到文件夹 directory\spam, 便会将其记录下来,而扫描将从搜索路径中的下 一 个目录继续。

4.

如果上述的所有都没有找到,扫描将从搜索路径中的下一个目录继续。

如果搜索路径扫描结束后没有从上述步骤 1 和步骤 2 中返回一个模块或包,而同时在上述

步骤 3 中至少记录了 一 个路径,那么就会创建一个命名空间包。 命名空间包的创建会立即发生,而且不会推迟到 一 个子层级的导入发生之时。新的命名空

间包有一个_path _属性,该属性被设置为在上述步骤 3 中扫描井记录的目录路径字符串 的可迭代对象,但是没有_file_属性。

__path—属性在随后更探的访问过程中用千搜索所有包组件。命名空间包的_path _中每 个被记录的项目,都会在进一步嵌套的项目被请求时进行搜索,这很像一个常规包的单独 路径。

从另一方面看,命名空间包的_path 一属性对千更低层次组件的关系,和 sys.path 对于 顶层最左侧的包导入路径的关系是一 样的。命名空间包成为了访问更低层次项目的"父路 径",这一访问也使用了上面介绍的四个步骤。

最终的结果是 一 个命名空间包是 一 种对多个目录的虚拟拼接,这些目录可以位千多个 sys. path 项目中。然而一且一个命名空间包被 创建,它和 一个常规包之间并没有功能上的区别, 它能够支持我们所学过的常规包的 一 切功能,包括包相对导人语法。

对常规包的影响:可选的—init—.PY 作为这种新的导入过程的结果,由千 Python 3.3 包不再需要 _init_.py 文 件-如果一个单

独目录包没有该文件,它将被当作一个单独目录命名空间包,而且不会引发任何警告。这 不但放宽了前面介绍的规则,而且是 一 个常常被用到的变化 1 许多包不要求初始化代码,

而且在这些情况下创建一个空的初始化文件显得有些多余。这种多余的行为在 3.3 中终千 不再是必需的了。

726

I

第 24 章

同时,原始的常规包模型目前仍被全面支持,而且作为一个初始化钓子会自动运行—init_ PY 文件中的代码。此外,当 Python 清楚一个包肯定不会是另一个命名空间包的组成部分时,

将其编写成一个带有_init_.py 的常规包有着性能上的优势。常规包的创建和加载会在它 被定位到路径的时候就立即发生。一旦使用了命名空间包,在这个包袚创造之前,路径上 的所有项目都必须披扫描。更正式地说,常规包会在前一小节介绍的算法的步骤 l 处完成, 命名空间包则不会。

根据这一语言变化的 PEP ,并没有移除对常规包支持的计划,至少,这是目前的情况。不 过开源项目的改变总是可能的(事实上,本书前一版引述的在 2.X 中的格式化字符串和相

对导入的变更计划事后被放弃了),和之前一样,你需要留心这一特性在未来的发展。不过, 考虑到常规包的性能优势和自动初始化代码,它们不太可能被完全移除。

命名空间包的实际应用 为了了解命名空间包是如何工作的,考虑下面的两个模块和嵌套目录结构。在这一结构中, 两个名为 sub 的子目录位于两个不同的父目录 dirl 和 dir2 中:

C:\code\ns\dirl\sub\modl.py C:\code\ns\dir2\sub\mod2.py 如果我们将 dir1 和 d ir2 都添入模块搜索路径, sub 将成为一个横跨这两个目录的命名空 间包。该命名空间包含有 modi.py 和 mod2.py 这两个模块文件,尽管它们位千物理上分离 的两个目录。下面是文件的内容和在 Windows 下的必要路径设置:这里没有—init—.PY 文 件一事实上,在命名空间包中不可能存在_init_.py 文件,因为这是它们最主要的物理上

的差异:

c:\code> mkdir ns\din\sub c:\code> mkdir ns\dir2\sub

#

c: \code> type ns\dirl \sub\modl. py print(r'dir1\sub\mod1')

# Module files in different directories

Two dirs of same name in different dirs II And similar outside Windows

c: \code> type ns\dir2\sub\mod2.py print(r'dir2\sub\mod2') c:\code> set PYTHONPATH=C:\code\ns\dir1;C:\code\ns\dir2 现在,如果在 3.3 以及之后的版本中进行直接导入,那么命名空间包将是它的独立目录组件 的虚拟拼接,并且允许使用常规导入的单独或复合名称,来访问进一步内嵌的组件:

c:\code> C:\Python33\python » > import sub »> sub II Namespace packages: nested search paths

»> sub._path_ _NamespacePath(['C: \ \code\ \ns\ \din\ \sub','C: \ \code\ \ns \ \dir2\ \sub'])

模块包

1

727

»> from sub import modl dir1\sub\mod1 »> import sub.mod2 dir2\sub\mod2

# Content from two different directories

»> mod1

» > sub. mod2

如果我们通过命名空间包名称直接导入的话,也是成立的一因为命名空间包会在首次访

问时被创建,所以路径扩展发生的时间点是没有影响的:

c: \code> C: \Python33 \python »> import sub.modl dir1\sub\mod1 »> import sub.mod2 dir2\sub\mod2

# One package spanning two direcrorie.\,

>» sub. modi

»> sub.mod2

»> sub

»> sub._path_ NamespacePath{ ['C: \\code\\ns \\dirt \\sub','C: \\code\\ns\\dir2\\sub' ]) 有趣的是,相对导人也适用千命名空间包一在下面的代码中,相对导入语句引用了包中 的一个文件,尽管被引用的文件位千另一个不同的目录中:

c:\code> type ns\dir1\sub\mod1.py from. import mod2 print(r'dir1\sub\mod1')

# And "from. import string" still fails

c:\code> C:\Python33\python »> import sub.modi # Relative import of mod2 in another dir dir2\sub\mod2 dir1\sub\mod1 »> import sub.mod2 # Already imported module not rerun »> sub.mod2

正如你所看到的,命名空间包在各个方面都和普通的单目录包相似,除了存储上的分离一

这便是为什么单目录命名空间包没有—init_.py 文件也照样和常规包相似的原因,但是它 却没有可以运行的初始化逻辑。

728

1

第 24 章

命名空间包嵌套 命名空间包甚至支持任意的嵌套。 一且一 个命名空间包被创建,它将在它的层次上扮演和 sys.path 一样的角色,并成为低层级的"父路径"。继续前一小节的例子:

c:\code> mkdir ns\dir2\sub\lo情er c:\code> type ns\dir2\sub\lo钏er\mod3.py print(r'dir2\sub\lower\mod3') c: \code> C: \Python33\python >» import sub. lower. mod3 dir2\sub\lower\mod3 c:\code> C:\Python33\python » > import sub » > import sub. mod2 dir2\sub\mod2 >>>加port sub.lower.mod3 dir2\sub\lower\mod3

#

Further nested components

# Namespace pkg nested in namespace pkg

# Same effect if accessed incrementally

»> sub.lower It A single-directory namespace pkg

» > sub. lower. Jath_ _NamespacePath(('C:\\code\\ns\\diI2\\sub\\lower']) 在上面的代码中, sub 是一个横跨两个路径的命名空间包,而 sub.lower 是一个物理上位

千 dir2 中且嵌套在 sub 的一部分中的单目录命名空间包。 sub.lower 也是一个常规包的不 带_init_.py 的命名空间包等价物。 不论较低层的组件是一个模块、常规包或者另 一个命名空间包,这里的嵌套行为都能成立。

通过作为新的导入搜索路径,命名空间包允许所有的这三种包和模块能自由地嵌套在其中:

c: \code> mkdir ns\dirl\sub\pkg C:\code> type ns\dirl\sub\pkg\_init_.py print(r'dir1\sub\pkg\_init_.py') c:\code> C:\Python33\python » > import sub.mod2 dir2\sub\mod2 »> import sub.pkg dir1 \sub\pkg\_init_. PY » > import sub. lower. mod3 diT2\sub\lower\mod3

# Nested module # Nested regular package #

Nested namespace package

# Modules, packages.and namespaces >» sub

>» sub.mod2

»> sub.pkg

» > sub. lower

模块包

1

729

»> sub.lower.mod3

你可以跟踪这一例子中的文件和目录进一步思考和体会。如你所见,命名空间包可以无缝 集成到先前的导人模型中,而且添加了新的功能上的扩展。

文件仍然优先千路径 如前所述,常规包中_init_.py 文件的一个意义是将该目录声明为一个包。_init_.py 文件 告诉 Python 使用这个目录,而不是跳过该目录去使用搜索路径之后的一个可能的文件。这 能够避免无意地选取 一个意外出现在搜索路径上的非代码的子目录,进而覆盖了我们希望 导入的同名目录。

因为命名空间包不需要这些特殊的文件,它们看上去可能违背了这一保护措施。然而,事 实并非如此,因为前面介绍的命名空间算法会在寻找一个命名空间包之前先进行搜索路径

的扫描,搜索路径上靠后的路径依然会优先于靠前的没有_init_.py 文件的路径。 例如, 考虑下面的目录和模块:

c:\code> mkdir ns2 c:\code> mkdir ns3 c:\code> mkdir ns3\dir c:\code> notepad ns3\dir\ns2.py c:\code> type ns3\dir\ns2.py print(r'ns3\dir\ns2.pyl') 在 Python 3.2 以及更早的版本中不能导人这里的 ns2 目录,这是由千 ns2 不是一个常规包(它 缺少一个—init_.py 初始化文件)。不过,这个路径能在 3.3 下被导入——它是 一个当前工 作目录中的命名空间包目录,而当前工作目录又是 sys.path 模块搜索路径中的第一个项目, 不论 PYTHONPATH 的配置如何:

c:\code> set PYTHONPATH= c:\code> py -3.2 » > import ns2 ImportError: No module named ns2 c:\code> PY -3.3 >» import ns2 »> ns2

>» ns2._path_ NamespacePath(['. (['.\\ns2'])

#

A single也rectory namespace package in CWD

不过要注意接下来的这种情况,我们现在通过 PYTHONPATH 设置,将一 个与之前的命名空间 目录 ns2 有着相同名称的 ns2.py 文件的父目录,添加到模块搜索路径的后面: Python 会选

取文件而不是命名空间目录,因为 Python 在一个命名空间包被找到之后,会继续搜索之后

730

I

第 24 章

的路径项目。 Python 只会在 一 个模块或常规包被找到的时候,或者整个路径已经被完全扫

描过以后才停止搜索。命名空间包只有在整个过程中没有找到其他同名的模块、包或文件 才会被返回:

c:\code> set PYTHONPATH=C:\code\ns3\dir c:\code> PY -3.3 »> import ns2 # Use later module file, not same-named directory! ns3\dir\ns2.py! »> ns2

»> import sys »> sys.path[ :2) #First " means current working directory. CWD ['','C: \\code\\ns3\\dir'] 事实上,将路径设置为包含一 个模块和在之前版本的 Python 中达到的效果是 一 样的,即使 一 个同名的命名空间目录出现在搜索路径的前面;命名空间包只可以在 3.3 中使用,而它适 用的情况对之前版本的 Python 又会引发错误:

c:\code> PY -3.2 » > import ns2 ns3\dir\ns2.py! »> ns2

这也是为什么一个命名空间包的所有目录都不允许包含一个—/nit_.py 文件的原因:只要

导人算法找到 一 个包含_init_.py 的目录,就会立即返回 一个常规包,并放弃路径搜索和命 名空间包。更正式地说,导入算法只在路径扫描的末尾才会选择 一 个命名空间包,井且只 要找到 一 个常规包或者模块文件就停止在步骤 l 或步骤 2 。 最终的效果是模块搜索路径上任意位置的模块文件和常规包都优先于命名空间包目录。在

下面的例子中,一个名为 sub 的命名空间包是分别位千搜索路径 dir1 和 dir2 下的同名目 录的拼接:

c:\code> mkdir ns4\dir1\sub c:\code> mkdir ns4\dir2\sub c: \code> set PYTHONPATH=c: \code\ns4\dirt;c: \code\ns4\dir2 c:\code> py -3 » > import sub »> sub

>» sub._path_ _NamespacePath(['c:\\code\\ns4\\dir1\\sub','c:\\code\\ns 4\\dir2\\sub']) 不过,与 一 个模块文件很像的是, 一 个在最右路径项目的常规包也优先千同名的命名空间 包目录。这是因为导入路径扫描和之前 一 样在 dirl 中开始记录 一 个命名空间包,但是在

dir2 中找到 一 个常规包的时候就停止了记录过程:

模块包

I

731

c:\code> notepad ns4\dir2\sub\_init_.py c:\code> PY -3 »> import sub # Use later reg. package, not same-named directory! >» sub

尽管命名空间包是一个有用的扩展,由千它只适用千 Python 3.3 (及之后的版本),我将把 这一 主题的更多细节留给 Python 的官方手册。尤其请参见这一 改变的 PEP 文档中说明的理

论基础、更多细节和更多易干理解的例子。

本章小结 本章介绍了 Python 的包导入模型:这是 一种可选但也相当有用的方式,可以帮助你显式列

出指向你的模块的目录路径。包导入依然是相对千模块导入搜索路径上的一个目录的,不 过你的脚本需要显式补全指向模块的剩余路径。

如前说述,包不仅赋予了导入在较大系统中的意义,同时也简化了导入搜索路径的设置(否 则如果所有跨目录的导入都在共同根目录下的话,那将是 一场悲剧),而且当存在 一 个以

上的同名的模块时,也可解决 二 义性(通过在包导入中指明要导入模块所在的外围的目录 来进行区分)。 由千新的相对导人模型只与包内代码相关,因此我们在这里也对它进行了介绍一一这种方

式在一个 from 中以点号开头,从而让包文件的导入能够选择同一包中的模块,而不是依赖 千较旧且易出错的隐式包搜索规则。最后,我们探索了 Python 3.3 中的命名空间包,它允

许 一个逻辑包横跨多个物理路径,并且作为导人搜索的 一 种后备选项,同时还去除了之前 模型中的初始化文件需求。

在下一章中,我们要研究 一 些更高级的模块的相关话题,例如,_name _使用模式变量以 及名称字符串导入。不过和往常 一样,我们要以习题结束本章,来测试你在本章所学到的

内容。

本章习题 I.

模块包目录内的_init—.PY 文件有何用途?

2.

你应如何避免在每次引用包的内容时,重复包的完整路径?

3

哪些目录需要—init—.PY 文件?

4.

在什么情况下必须通过 import 而不能通过 from 使用包?

5.

from mypkg import spam 和 from. import spam 有什么 差 别?

6.

732

什么是一 个命名空间包?

I

第 24 章

习题解答 1.

_init_.py 文件用千声明和初始化一个常规模块包。当你第一次在进程中通过目录导入 时, Python 会自动运行这个文件中的代码。该文件赋值的变址会变成对应千该目录在 内存中所创建的模块对象的属性。它在 Python 3.3 之前的版本中都是必须使用的:如 果一个目录中没有包含这个文件,是无法通过包语法导入目录的。

2.

你可以使用 from 语句并带上包的名称,直接把包的名称复制出来,或者使用 import

语句的 as 扩展功能,把路径改为较短的别名。在这种情况下,路径只出现在一个地方, 就在 from 或 import 语句中。

3.

在 Python 3.2 以及更早的版本中,一条被执行的 import 或 from 语句中所列出的每个 目录都必须含有—init_.py 文件。其他目录则不需要包含这个文件,包括含有包路径最 左侧组件的目录也不需要。

4.

只有在你需要访问定义在一个以上路径的相同名称时,才必须通过 import 来使用包, 而不能使用 from 。使用了 import 就可以通过路径进行区分,然而 from 却让任何名称 只有一个版本(除非你同时使用了 as 扩展来重新命名)。

5

在 Python 3.X 中, from mypkg import spam 是绝对导入: mypkg 的搜索跳过包路径并 且 mypkg 位千 sy s.path 中的 一个绝对目录中。另一方面, from

.

import spam 是相

对导入: spam 的查找只会是相对千该语句所在的包。然而在 Python 2. X 中,绝对导入 会在搜索 sys.path 之前,先搜索包目录;而相对导人的情况和 3.X 中一样。

6

命名空间包是对导入模型的一种扩展,能够在 Python 3.3 和之后的版本中使用,能够 利用一个或多个没有_init_.py 文件的目录。当 Python 在一 个导入搜索过程中找到这

些目录,并且没有找到一个简单模块或常规包的时候,就会创建一个命名空间包。这 个命名空间包是所有找到的同名路径的虚拟拼接。 Python 会在所有命名空间包的目录 中搜索进 一 步嵌套的组件。其效果和常规包类似,但是内容可以横跨多个目录。

模块包

I

733

第 25 章

高级模块话题

本章以一些更高级的模块相关内容作为第五部分的结尾:数据隐藏、_future —模块、

—name_变量、 sys.path 修改、展示工具、通过名称字符串来导入模块和传递性模块重载等, 此外还有本书这一 部分所提到的相关陷阱和练习题。

在本章中,我们将构建 一 些比之前见到的更大和更有用的工具,并在其中组合使用函数和

模块。与函数一样,如果我们能恰当地定义模块的接口,就能让它们使用起来更加高效。 因此,本章也简单地回顾了模块设计概念,其中的 一 些概念与本书之前介绍的诸多思想 一 脉相承。 尽管出千规整的目的本章标题用到了“高级”二字,但本章基本上是对其他剩余模块主题

的枚举介绍。因为这里所讨论的有些主题得到了广泛使用(尤其是

name

技巧),所以

在学习本书下一部分内容(类)之前,要确保学过了这些内容。

模块设计概念 与像函数 一 样,模块体现了设计上的些许折中:你需要思考哪个函数应该分在哪个模块中, 模块的通信机制,等等。当你开始写更大的 Python 系统时,所有这些都会变得明了,但是

有一 些通用的思想需要记住:



在 Python 中你总是位千某个模块内。你绝不可能写出不存活于任何模块中的代码。就 像第 17 章和第 21 章简单提到的,即使是在交互式命令行下输入的代码实际上也存在 于一个名为_main _的内置模块中,关于交互式命令行唯一不同的地方是代码会披运 行并立即抛弃.以及表达式结果会自动地打印。



734

最小化模块耦合:全局变量。与函数 一 样,当模块被编写得像 一个封闭的箱 子 时它才

工作得最好。作为一条经验法则,除了主动导入的函数和类之外,模块应该尽可能地

独立于其他模块内使用的全局变品。榄块唯一应该和外部世界共享的事物就是它使用 的工具,以及它定义的工具。 最大化模块内聚:统一的目标。你可以通过最大化一个模块的内聚来最小化它的耦合 1 如果模块的所有组件都拥有 一 个共同的目标,你就不太可能依赖千外部名称。

.

模块应尽可能不去更改其他模块的变量。我们在第 17 章用代码阐明了这 一点,但是这 里值得再次重申:使用在另一个模块内定义的全局名称是完全没问题的(毕竞,那就 是客户程序导入服务的方式),但是更改另一个模块内的全局名称通常是有问题的设 计的早期症状。当然也存在例外,但是你应该试着通过诸如函数参数和返回值来实现

通信,而不是跨模块的更改。否则,你的全局名称的值就会变得依赖千在其他文件中 的任意远程赋值的顺序,而你的模块也会变得难以理解和重用。

作为小结,图 25-1 描绘了模块工作的环境。模块包含变址、函数、类以及其他模块(如果 导入过)。函数有自己的局部变址,类也如此。类是存活干模块内的对象,我们将在下一 章开始学习类。就像我们在第四部分见过的,函数也能够嵌套,但是所有函数最终还是由 顶层的模块包含。

导入

`

, ;, 函数

--"、

'

t己已勹丿 ~-

1

`~- - ,

1

-.飞亘

(类

- -

-

-

全` 匕-- 千--

.

[三 1

丘: 、. -. ~丛'-

-

__、-.-

~ -

/

图 25-1: 模块执行环境。模块不仅可以被导入,而且还可以导入和使用其他用 Python 或诸 如 C 的其他语言编写的模块。另一方面,模块可以包含变量、滔数和类来完成它们的工作,而

这些类和函数也会包含变量和模块自身的其他元素。总而言之,从顶层来看,程序只是模块的 集合

高级模块话题

735

模块中的数据隐藏 如前说述, Python 模块会导出其文件顶层所赋值的所有名称。在 Python 中不存在对某一个 名称进行声明,从而使其在模块内可见或不可见的这种概念 。 实际上,只要客户程序想的话,

你完全没有办法阻止客户程序修改模块内的名称。 在 Python 中,模块内的数据隐藏是一种惯例,而不是 一种语法约束。你当然可以通过破坏

模块内的名称来破坏这个模块,但庆幸的是,我还没遇到过以此作为毕生奋斗目标的程序员。 有些纯粹主义者对 Python 数据隐藏采取的这种开放态度颇有微词,并宣称这表明 Python

无法实现封装。然而, Python 的封装更像是打包,而不是约束。我们将在下 一 部分对类的 介绍中看到对这一思想的延续:类也没有隐藏语法,但你却能够通过编写代码来模拟隐藏

的效果。

使*的破坏最小化: —X 和_all— 作为 一种特例,你可以在名称前面加上 一个单独的下划线(例如_ X), 从而防止客户程序

使 from *语句导入模块名时,把这些加下划线的名称复制出来。这其实只是为了最小化对 命名空间的破坏。因为 from *会复制出所有的名称,所以导入者很可能得到超出所需的部

分(包括会覆盖导入者自身的名称)。下划线不是“私有“声明:你仍然可以通过其他形 式的导入来获取并修改这类名称。例如,使用 import 语句 : #

unders.py

a, _b, c, _d = 1, 2, 3, 4

» > from unders import * »> a, C (1, 3) »> _b NameError: name'b'is not defined

#

Load non _X names only

» > import unders »> unders._b

#

But other importers get every name

2

此外,你也可以通过在模块顶层把变量名的字符串列表赋值给变量_all—,从而达到类似 千_x 命名惯例的隐藏效果。当使用此功能时, from *语句只会把列在_all—列表中的这 些名称复制出来。事实上,这刚好和_x 命名惯例的效果相反: _all_是指明要复制的名称,

而_x 是指明不被复制的名称。 Python 会先检查模块中是否存在—all—列表,并复制其中 所有的名称,包括那些以下划线开头的名称,如果没有定义_all

, from *则会复制出

所有开头没有单下划线的名称: # alls.py _ all_ = ['a',' _c'] a, b, _c, _d = 1, 2, 3, 4

736

I

第 25 章

#

_all_ has precedence over _X

>» from alls import * >» a, _c

#切ad

_all_ names only

(1, 3)

»> b NameError: name'b'is not defined >» from alls import a, b, _c, _d >» a, b, _c, _d

#

But other importers get every name

(1, 2, 3, 4)

>» import alls »> alls.a, alls.b, alls._c, alls._d (1, 2, 3, 4) 就像_x 命名惯例一样,_all_列表只对 from *语句这一种形式有效,所以并不能算是私

有声明:其他导入语句仍能访问全部的名称,如上述最后两个测试所示。即便如此,模块 编写者可以灵活使用这两种方法来控制模块针对 from *形式导入的行为。你也可以参考第

24 章中在_init_.py 文件中使用_all_列表的相关内容,在那里_all_列表为模块声明 了在 from *语句运行后被自动加载的子模块。

启用未来语言特性:—future— 在 Python 语言发展的过程中,逐渐引入了许多有可能会破坏现存代码的一些语言变化。它 们在一开始通常以可选扩展功能的形式出现,并默认是关闭的。你可以使用以下形式的特 定 import 语句来开启这些扩展功能:

from _future_ import featurename 当在脚本中使用时,这条语句必须作为文件的第一行可执行语句出现(可以放在文档字符

串或注释后),因为它将以模块为范围开启特殊的代码编译。此外,你也可以在交互式命 令行下使用这条语句来试验一些未来的语言变化。一且运行,你就可以在随后的交互式会 话过程中使用这些功能。

例如,在本书中我们见到过在 Python 2.X 中如何使用这一语句激活第 5 章中介绍的 3.X 的 真除法,第 11 章中介绍的 3.X 的 print 调用,以及第 24 章中介绍的 3.X 的包绝对导入。 本书之前的版本使用了这条语句来示范生成器函数,因为那时候还缺少一个必要的关键字 (在那里我们用 generators 来替代上面语句中的 featurename) 。 由千所有这些变动都有可能潜在地影响 Python 2.X 中现有的程序代码,因此它们需要被逐

渐引入或是作为可选的扩展,并通过这种特殊的导入被开启。与此同时,也正是它们的存 在允许你写出向前兼容的代码,并在有朝一日让你能转向这些未来的版本。

如果你想列举出能够通过这种方式获得的未来特性,可以在导入—future—模块后,对它 执行 dir 调用,或是参阅它的库手册条目。根据文档中的描述,它的任何特性名称都绝不

高级模块话题

I

737

会被移除,因此,在代码中留下 一 个_future

导人是安全的,即使代码之后在已普遍使

用某 一 特性的新 Python 版本上运行。

混合使用模式: _name_和—main— 下面 一 个与模块相关的技巧,允许你既可以把文件作为模块导入,又可以把它 当 作独立程 序的形式运行,因此也在 Python 文件中被广泛使用。其实该技巧是如此简单,以至千有些 人 一 开始都没有抓住要点:每个模块都有 一 个名为—name_的内置属性, Python 会遵循下 面的规则自动创建并赋值该属性 :



如果文件作为顶层程序文件执行,在启动时

"

ma1n



name

就会被设置为 字 符串"



如果文件被导入,_name_就会改设成客户程序所了解的模块名。

结果就是模块可以检测自己的— name_,来确定它是在执行还是在导入。例如,假设我们 创建下面的模块文件,名为 runme.py, 它只暴露了 一 个名为 tester 的函数:

def tester(): print("It's Christmas in Heaven... ") ' ma1n name =='maj tester()

if

,

#

Only when run

# Not when imported

这个模块定义了 一 个函数,让用户可以正常地导入并使用:

c:\code> python >>>泣port runme >» runme. tester() It's Christmas in Heaven... 然而,这个模块也在末尾包含了当此文件作为程序执行时,就会自动调用该函数的代码:

c:\code> python runme.py It's Christmas in Heaven... 实际上,模块的—name —变批充当了 一 个使用模式标志符,允许模块被同时编 写 成一个可

导人的库和一 个顶层脚本。尽管简单,但你会在你能遇到的大部分 Python 程序文件中发现 这 一钓子的身影-~也是为了测试和双重使用模式。

例如,也许使用—name _测试最常见的就是自我测试代码。简而言 之,你可以在文件末尾

加个—name —测试,把模块暴露接口的测试程序代码封装在其中。如此 一 来,你不但可以 在客户程序中通过导人来使用该文件,还可以在系统 shell 或其他启 动方案中通过运行来测

试其逻辑。

738

I

第 25 章

在文件末端编写_name_测试中的自我测试程序代码,也许是 Python 中最常见且最简单的 单元测试协议。这比你在交互式命令行下重新输入所有的测试代码要简单得多。

(第 36 章

会讨论其他用千测试 Python 程序代码的常用方式。你将看到, unittest 和 doctest 标准 库模块提供了更高级的测试工具。)

除此以外,当你编写既可以作为命令行工具也可以作为工具库使用的文件时,— name _技 巧也很好用。例如,假设你用 Python 编写了 一 个文件查询脚本。你可以将其打包成 一些函 数井在文件中加入_name_测试,因此当该文件独立执行时就自动调用这些函数,从而提 高代码的利用效率。这样一来,脚本的代码就可以在其他程序中再利用了。

以—name—进行单元测试 实际上,我们已经在本书的 一 个示例中看到了_name_检查的有用之处。在第 18 章讨论参 数那一节 ,我们编写了 一 个计算一组传入参数最小值的脚本(这就是“最小唤醒调用!” 中的文件 minm釭py) :

def minmax(test, *args): res= args[o] for arg in args[1:): if test(arg, res): res= arg return res def lessthan(x, y): return x < y def grtrthan(x, y): return x > y print(minmax{lessthan, 4, 2, 1, 5, 6, 3)) print{minmax(grtrthan, 4, 2, 1, 5, 6, 3))

#

Self-test code

这个脚本在底部包含了自我测试程序代码。所以我们不必每次执行时都在交互式命令行中

重新输入所有代码,而是可以直接进行测试。然而现在这种写法的问题是每次这个文件袚 另一个文件作为工具导入时,都会出现调用自我测试所得到的输出:这可不是 一 个用户友

好的特性!作为改进,我们可以把自我测试封装到—name _检查中,这样测试脚本就只会 在文件作为顶层脚本运行时才会执行,而不会在导人时披执行(该模块的这一新版本被重 命名为 minm釭2.py) :

print('I am:', _name_) def minmax(test, *args): res= args[o] for arg in args[1:]: if test(arg, res): res= arg return res def lessthan(x, y): return x < y def grtrthan(x, y): return x > y 高级模块话题

I

739

maln name == print(minmax(lessthan, 4, 2, 1, s, 6, 3)) print(minmax(grtrthan, 4, 2, 1, 5, 6, 3))

if

# Self-test code

这里我们也在顶端打印_ name _的值井进行跟踪。 Python 会在开始加载文件时就创建

井赋值这个—name _使用模式的变县。当我们把该文件当作顶层脚本执行时,它的名称 (_name_) 就会被设置为_main_ ,因而自我测试程序代码就会自动执行:

c:\code> python minmax2.py I am: main 1

6 然而当我们导人这个文件时,其名称就不是

main

了,因而我们必须显式地调用这个函

数来执行它:

c:\code> python » > import minmax2 I am: minmax2 '-''..''-''-' ,'s','p','a','a') »> m].nmax2.minmax(minmax2.lessthan,'s a 同样,无论这是否用千测试,最终的结果都是让代码扮演两种不同的角色:作为工具的库 模块,或是作为可执行的程序。

注意:根据第 24 章中包相对导入的讨论,本小节介绍的技巧也可以用于那些在 3 .X 中作为包 组件的文件的导人,此外也可以与绝对包路径导入等其他技巧一并使用。参看前一章的 讨论和示例获得更多细节。

示例:双模式代码 下面是 一 个更为真实的模块示例,它展示了另 一 种常见的— name _技巧使用方式。模块 formats.py 不但为导人者定义了字符串格式化工具,而且检查自身名称判断是否作为 一个顶 层脚本在运行 1 如果作为顶层脚本在运行,那么 formats.py 将测试并使用系统命令行上列 出的参数来运行一个自带的或传入的测试。在 Python 中, sys.argv 列表包含命令行参数, 它是一 个包含了在命令行上录入的单词的字符串列表,其中的第 一 项被规定为将要运行的 脚本的名称。在第 21 章的基准测试工具中我们将其用作开关,但是在这里将其用作通用的 输入机制:

#!python ”“” File: formats.py (2.X and 3.X) Various specialized string display formatting utilities. Test me with canned self-test or command- line arguments.

740

I

第 25 章

To do: add parens for negative money, add more features. def commas(N): Format positive integer-like N for display with commas between digit groupings: "xxx,yyy,zzz". '""'

digits= str(N) assert{digits. isdigit()) result ='' while digits: digits, last3 = digits[:-3), digits[-3:) result= (last3 +','+ result) if result else last3 return result def money(N, numwidth=O, currency='$'): `','"

Format number N for display with commas, 2 decimal digits, leading$ and sign, and optional padding: "$ -xxx,yyy.zz", numwidth=O for no space padding, currency=''to omit symbol, and non-ASCII for others (e.g., pound=u'\xA3'or u'\uOOA3').

.... "

sign='-'if N < o else'' N = abs{N) whole= commas(int{N)) fract = ('为 2f'% N)[-2:] number='%s%s.%s'% (sign, whole, fract) return'%s%*s'% (currency, numwidth, number) if

,. name == main , def selftest(): tests = o, 1 # fails: -1, 1.23 tests+= 12, 123, 1234, 12345, 123456, 1234567 tests+= 2 ** 32, 2 ** 100 for test in tests: print(commas(test))

print('') tests = o, 1, -1, 1.23, 1., 1.2, 3.14159 tests+= 12.34, 12.344, 12.345, 12.346 tests+= 2 ** 32, (2 ** 32 +.2345) tests += 1.2345, 1.2, 0.2345 tests += -1.2345, -1.2, -0.2345 tests+= -(2 ** 32), -(2**32 +.2345) tests+= (2 ** 100), -(2 ** 100) for test in tests: print('%s [%s]'% (money(test, 17), test)) import sys if len(sys.argv) == 1: selftest() else: print(money(float(sys.argv[l]), int(sys.argv[2])))

高级模块话题

I

741

该文件在 Python 2.X 和 Python 3.X 中的工作效果相同。当直接运行时,它和之前 一样会进

行自我测试,但是它使用命令行上的选项来控制测试行为。你可以尝试自己直接运行这个 文件而不带命令行参数来看看它的自测试代码打印出什么一~这里出千篇幅考虑只列举了 一部分的结果:

c: \code> python formats.py 。

1

12 123 1,234 12,345 123,456 1,234,567 ... etc... 如果你想测试其他特定的字符串,也可以将这些字符串连同 一 个最小字段宽一起从命令行 传入;脚本的_main_部分代码会将它们传入闭数 money, 而函数 money 将运行 commas:

C:\code> python formats.py 999999999 o $999,999,999.00 C:\code> python formats.py -999999999 o $-999,999,999 . 00 C:\code> python formats.py 123456789012345 o $123,456,789,012,345.00 (:\code> python formats.py -123456789012345 25 $ -123,456,789,012,345.00 C:\code> python formats.py 123.456 O $123.46 C:\code> python formats.py -123.454 o $ -123.45 和前面一样,由千这段代码针对双模式用法编写,因此,我们也可以正常地导入它的工具, 把它用作脚本、模块和交互式命令行下的库组件:

>» from formats import money, commas >» money(123.456) '$123.46' >» money(-9999999.99, 15) '$ -9,999,999.99' >>> X = 99999999999999999999 >»'%s (%s)'% (commas(X), X) '99,999,999,999,999,999,999 (99999999999999999999)' 你可以学习本例中通过命令行参数向脚本提供输人的方式,此外脚本也可以同时将它们的 代码封装成函数和类以供其他文件导人。关于更多命令行处理的高级内容,你可以参阅 附录 A 中的 “Python 命令行参数”,以及 Python 标准库手册中的 getopt 、 optparse 和

argparse 模块的文档说明。在某些情景下,你也可能用到内置的 input 函数(第 3 章和第

10 章中用过)来向 shell 用户弹出输入提示,而非从命令行中提取。 742

1

第 25 章

注意:你还可以参考第 7 章对 Python 2.7 和 Python 3.1 中添加的新 {,d} 字符串格式的讨论,七 d} 的格式扩展同样可以完成对数字按照每 三位加逗号隔开的效果。当然,上述添加了货币 格式化的模块文件也可以进一步修改,使之成为在更早的 Py小on 版本中向数字手动插入 逗号的另 一种替代方案 。

货币符号: Unicode 的应用 该模块中 money 函数的默认参数为美元,但是它也允许传人非 ASCII 的 Unicode 字符来支 持其他货币符号。例如, Unicode 编码中十六进制的 OOA3 代表英镑符号,而 OOA5 代表人 民币符号。你可以采用多种方法来编写它,例如:



使用 Unicode 或十六进制转义,将字符的 Unicode 编码表示为一个文本字符串(为了兼 容 2.X, 对 Python 3.3 中这样的字符串字面址使用开头的 u) 。



使用十六进制转义,将字符串的原始编码形式表示为 一 个字节字节串,井在传人前袚 解码(为了兼容 3 .X, 对 Python 2.X 中这样的字符串字面址使用开头的 b) 。



使用源代码编码声明,并在程序中使用该字符本身。

我们在第 4 章中预习了 Unicode 并将在第 37 章中深入学习更多的细节,不过这里对 Unicode 的要求相当基础,因此也是 Unicode 的 一个很好的例子。为了测试其他货币符号,

我将下面的代码输入到文件 jormats_currency.py 中,这样可以省去接下来在交互式命令行 中重复输人的麻烦:



from _future import print_function # 2.X from formats import money X = 54321.987 print(money(X), money(X, o,'')) print(money(X, currency=u'\xA3'), money(X, currency=u'\uOOAS')) print(money(X, currency=b'\xA3'.decode('latin-1'))) print(money(X, currency=u'\u20AC'), money(X, o, b'\xM'.decode('iso-8859-15'))) print(money(X, currency=b'\xM'.decode('latin-1'))) 下面给出了该测试文件在 Python 3.3 的 IDLE 下的输出,以及在其他适当配置过的环境下

的输出 。 该文件在 2.X 下同样能够工作,因为它对字符串的打印和编码具有可移植性。按

照第 11 章的介绍,_future_导入可以在 2.X 下使用 3.X 的 print 调用。如第 4 章所述, 3.X 的 b'... ' 字节字面最在 2 .X 中袚 当 作简单字 符串,而 2.X 的 u'... • Unicode 字面最在 3.X

中被作为普通字符串对待:

$54,321.99 54,321.99 £54,321.99 ¥ 54,321.99 £54,321.99 €54,321.99 €54,321.99 口 54,321.99

高级模块话题

I

743

如果这在你的计算机上行得通,那么你可以跳过接下来的几段话。不过取决千你的接口和

系统设置,使上述代码运行并正确显示或许需要额外的步骤。在我的机器上,当 Python 和

显示媒体同步的时候它能够正确显示,但是最后两行的欧元和通用货币符号在 Windows 命 令行窗口下却显示失败。 更具体地说,该测试脚本将总是运行和产生在 3.X 和 2.X 的 IDLE GUI 中显示的输出,因 为 Unicode 到象形文字的映射能被正确处理。如果你在 Windows 上将输出重定向到 一个文 件并用记事本打开,那么你也将得到理想的结果。因为 Windows 上的 3 . X 采用了 一 种记事

本能理解的默认 Windows 格式编码内容:

c:\code> formats_currency.py > temp c:\code> notepad temp 然而,这在 2. X 中却行不通,因为 2.X 的 Python 会默认将打印文本编码为 ASCII 。为了 在 Windows 命令行窗口中直接显示所有的非 ASCII 字符,在一 些计算机上你可能需要将

Windows 的编码设置(用千渲染字符)和 Python 的 PYTHONIOENCODING 环境变量(用千 标准流中的文本的编码,包括在打印时字符到字节的翻译)变成 一 个诸如 UTF-8 的常见 Unicode 格式:

c:\code> c:\code> c:\code> c:\code> c:\code>

chcp 65001 set PYTHONIOENCOOING=utf-8 formats_currency.py > temp type temp notepad te叩

# Console matches Python # Python matches console # Both 3.X and 2.X write VTF-8 text

# Console displays it properly # Notepad recognizes UTF-8 too

在其他一 些操作系统和部分 Windows 发行版上,你也许不需要做这些步骤。由千我自己笔 记本的编码设置是 437 (U.S ,字符),因此我需要进行上面的设置,但你的编码设置可能会 有所不同。 很微妙的是,该测试能在 Python 2. X 上工作的唯一原因是, 2.X 允许混合使用普通字符串 和 Unicode 字符串,只要普通字符串全部是 7 位的 ASCII 字符。在 3.3 上, 2.X 的 u

I•••

t

Unicode 字面量会出千兼容性被支持,但它们将和普通的'...'字符串 一起被同等地对待, 而且 3.X 中的普通字符串总是 Unicode 的(移除开头的 u 使测试能在 3.0~3.2 的版本下运行, 但是破坏了 2.X 兼容性) :

c:\code> PY -2 >» print u'\xA5'+'1','%s2'% u'\uOOA3' ¥1 £2

# 2.X: unicodelstr mix/or ASCII str

c: \code> PY -3 >» ¥1 >» ¥1

744

I

print(u'\xA5'+'1','%s2'% u'\uOOA3') £2 print('\xA5'+'1','%s2'%'\uOOA3') £2

第 25 章

# 3.X: str is Unicode, u" optional

照旧,第 37 章讲述了关千 Unicode 的更多知识一一很多人将其视为边缘主题,但它却很有

可能突然出现在像这样相对简单的场景中!这里需要掌握的一点是抛开运算问题,一个仔 细编写的脚本总是能够做到在 3.X 和 2.X 中同时支持 Unicode 。

文档字符串:模块文档的应用 最后,由千这个示例的主文件使用了第 15 章介绍的文档字符串功能,我们也可以使用 help 函数或 PyDoc 的 GUI/ 浏览器模式来探索其中的工具一一大部分的模块都披设计为通用目 的工具。下面是实际使用中的 help, 图 25-2 给出了文件 formats.py 上的 PyDoc 视图:

I) Pydoc: module form, "

M砒血sBoo_ 矿山叩. Malt.L..,.“nProgram一. O'妇llyM如a 飞· 恤加一

- ·一

内如lO 3.3.0 (v3 3 O:bd8或汛闷心, MSCv.1600" 比(心)J

M呻回·位心 : 酝叩

二 囡 厂二卫气

WIDdows习

I

|formats m1e:

^

fom血亡麝. P'/

12.x .心 !.X)

Var"口叩心 1 斗"己..,心g”耳心y Co...红比o

o.:illcu,.

五立= ”cb -立ed s已t-工.吓 0工叮- m-丘ne”叩“`

To

do: add 严-s

ror

R句“”它酝:mey,

add

more

te.~工工

,0-“po,“”它丘亡”“一五比”“工中叩lay 让心

c_,比~e”“”c g~~,江gs:

romm;

·-工 ,m,“z人

m删王工 ”toz dl叩江y vi中 r-…..

比“血.$心A m叩,

2 心尸旧 1

lll>dopc幻皿l p叩巾叨:

·$

d1”“'

士立,切...气

四皿lid心-O to,凹叩~ m心 ing.一· ·口编吐 •Y""l, 心d oon-矗SCll

丘江 0口`

(e.o. .一.,.

0工...仁).

圈 25-2 :文件 formats.py 的 PyDoc 视图,在 3 .2 及以后版本中通过运行 “py -3 -m pydoc

-b" 命令行,并点击该文件的索引项得到(参见第 15 章)

>>> import formats » > help(formats) Help on module formats: NAME

formats DESCRIPTION

File: formats.py (2.X and 3.X) Various specialized string display formatting utilities. Test me with canned self-test or command-line arguments. To do: add parens for negative money, add more features. FUNCTIONS

高级模块话题

I

745

commas(N) Format positive integer-like N for display with commas between digit groupings: "xxx,yyy,zzz". money(N, numwidth=O, currency='$') Format number N for display with commas, 2 decimal digits, leading$ and sign, and optional padding: "$ -xxx,yyy.zz". numwidth=O for no space padding, currency=''to omit symbol, and non-ASCII for others (e.g., pound=u'f'or u' 王'). FILE c:\code\formats.py

修改模块搜索路径 让我们回到更加通用的模块主题。第 22 章曾介绍过,模块搜索路径是一个目录列表,可以 通过环境变扯 PYTHONPATH 以及可能的.pth 文件进行定制。在那里我没有介绍的是, Python 程序本身其实也能够修改搜索路径,也就是修改内置的 sys.path 列表。按照第 22 章的讲解,

sys.path 在程序启动时就会初始化,但在那之后你可以随意对其组件进行删除、添加和 重设:

»> import sys » > sys. path ['','c: \\temp','C: \\Windows \\system32\\python33. zip',... more deleted.. . ] »> sys.path.append{'C:\\sourcedi工') >» import string

# Extend module search path # All imports search the new dir last

一 且你做了这些修改,就会对该次 Python 程序运行中任意地方未来全部的导入产生影响,

因为所有导入者都共享了相同的单 一 sys.path 列表(程序运行时内存中只有给定模块的 一 个副本,这也是 reload 存在的原因)。事实上,这个列表可以被任意修改:

»> sys.path = [r'd:\temp'] »> sys.path.append('c:\\lpse\\examples') »> sys.path.insert(o,'..') »> sys.path ['..','d: \\temp','c: \\lpse\\examples'] >» import string Traceback (most recent call last): File "", line 1, in ImportError: No module named'string

# Change module search pa小 #

For this run (process) only

因此,你可以用该技巧在 Python 程序中动态地配置搜索路径。不过,要小心:如果从路径 中删除了某个重要的目录,就无法获取 一 些关键的工具了。例如在上面的例子中,假设我

们从路径中删除了 Python 源代码库目录,那么我们就再也无法获取 string 模块了!

此外,你要记住 sys.path 的设笠只存在千修改发生的 Python 会话 或程序(即进程)中。 在 Python 结束后,修改并不会被保留下来。相比之下, PYTHON PATH 和 .pth 文件路径配置

746

I

第 25 章

却能够保存在操作系统中,而不是执行的 Python 程序中。因此使用这种配置方法将更全面

一些:机器上的每个程序都会去查找 PATHONPATH 和.pth, 而且在程序结束后配置还将继续 存在。在一 些操作系统上,前者可以是基千用户的,后者可以是基于 Python 安装的。

import 语句和 from 语句的 as 扩展 import 和 from 语句最终都可以扩展,从而在脚本中给予被导人名称一个不同的名字。之 前我们使用过这个扩展,但是这里有一些额外的细节,请看下面的 import 语句:

import modulename as name

II And use name, not modulename

它等价于下面的代码,只在导入者的作用域中重命名模块(对千其他的文件,它仍然是原

来的名称) :

import modulename name= modulename del modulename

H Don't keep original name

在这类 import 之后,你可以(其实是必须)使用列在 as 之后的名称引用该模块。 from 语 句也是如此,你可以把从某个文件导入的名称,赋值给导入者作用域中另一个不同的名称 1 同理你只获得你提供的新名称,而不是原来的:

from modulename import attrnarne as name

# And use name, not attrname

正如第 23 章所述,这个扩展功能常用千为变量名较长的变益提供简短的别名,同时避免 import 语句授盖脚本中已有名称而引发的命名冲突:

import reallylongmodulename as name name. func()

# Use shorter nickname

from modulel import utility as utill from module2 import utility as util2 uti11(); util2 ()

# Can have only I "utility"

此外,使用第 24 章所提到 的包导入功能时,也能方便地为整个目录路径提供简洁、易懂的

名称并避免命名冲突:

import din.dir2.mod as mod mod.func()

# Only List f,11/ parh once

from din.dir2.mod import func as modfunc modfunc()

# Rename to make unique

if needed

这有点像是预防名称改变的手段:如果某个代码库的新版本重命名了你代码中使用的模块

工具,或是提供了 一 个新的替代方案,那么你就可以简单地在导人时将其重命名为先前的 名称来避免代码崩溃:

高级模块话题

I

747

import newname as oldname from library import newname as oldname ... and keep happily using oldname until you have time to update all your code... 举例来说,该方法可以应对 一些 3.X 的库变更(例如 3.X 的 tkinter 和 2.X 的 Tkinter), 尽管它们通常远不只是 一个新名称!

示例:模块即是对象 因为模块通过内置属性的形式暴露了许多它们有用的性质,因此你可以很容易地编写程序 来管理其他程序。我们通常称这类管理程序为元程序 (metaprogram)

,因为它们是在其他

系统之上工作。这也称为内省 (introspection) ,因为程序能看见和处理对象的内部。内省 是稍微高级的功能,但它可以用千构建编程工具。

例如,假设你想获取 M 模块内名为 name 的属性,既可以使用属性点号运算,也可以索引 模块的属性字典(第 23 章中的内置_diet—属性)。 Python 也会在 sys.modules 字典中

暴露出所有已加载模块的列表,并提供一个内置函数 getattr, 让我们以字符串名取出属 性一就好像是 object.attr, 但 attr 是运行时生成字符串的表达式。因此,下列所有表

达式都会得到相同的属性和对象注 1 : M.name M. _diet_['name'] sys.modules['M'].name getattr(M,'name')

# Qualify object by attribute

Index namespace dictionary manually Index loaded-modules table manually # Call built-in fetch function #

#

通过像这样暴露出模块的内部, Python 能让你构建关千程序的程序。例如,下面是名为 mydir.py 的模块,其中运用了这些概念来实现定制版本的内置 dir 函数。它定义并导出了 一个名为 listing 的函数,该函数传人一个模块对象作为参数,并打印按名称排序的格式 化过的该模块命名空间列表:

#! python ',',', mydir.py: a module that lists the namespaces of other modules ,','`` from future_ import print_function # 2.X compatibility



注 1 :

就像我们在第 17 章的“其他访问全局变量的方式”节中简要介绍的那样,因为函数可

以通过遍历像这样的 sys.modules 表来访问它的外围模块,它也可以用来模拟 global

语句的效果。例如,语句 global

X;X=O 的效果能够通过在函数内部编写 import

sys;

glob=sys. modules [_name_]; glob. X=O 来模拟(尽管需要史多的轮入!) 。 记住,每 个模块都有一个可任意使用的_name—属性,它在模块中的函数内部作为全局名称可见 。 这一小技巧提供了在函数内部修改有着相同名称的局部和全局变量的另一种方式 。

748

I

第 25 章

seplen = 60 sepchr ='-' def listing(module, verbose-True): sepline = sepchr * seplen if verbose: print(sepline) print('name:', module._name_,'file:', module._file_) print(sepline) count= o for attr in sorted(module._dict_): # Scan 11amespace keys (or enumerate) print('%02d) %s'% (count, attr), end='') if attr. startswith(' —') : print('') # Skip_Jile~ etc. else: print(getattr(module, attr)) # Same as . dict_/attrJ count+= 1



if verbose: print(sepline) print(module._name_,'has %d names'% count) print(sepline) if

,

name == ma1n import mydir listing(mydir)

,.

. # Se/f'.resr code: /isr myself

注意顶部的文档字符串(就像在前面的 forma ts.py 示例中 一样),由千它会作为一个通用 工具被使用,因此我们可以用文档字符串通过 help 和 PyDoc ( 一 款同样是借助内省的工具) GUI/ 浏览器模式的形式,来提供有用的功能信息。该模块的底部也提供了自测试代码,其 中导人和罗列了它自己。下面是在 Python 3.3 中产生的输出;该脚本也能在 2.X 版本上工

作(那里它也许会列出更少的名称),因为它从_future_中导入了 3.X 的打印:

c:\code> py -3 mydir.py - -- - - - - - - - - - - - - - - - - - - - -

name: mydir 什 le: c:\code\mydir.py ------------------------------ - -oo) _ builtins_ 01) _cached 02) _doc_ 03) _ file_ 04) _initializing_ os) _loader_ 06) _ name_ 07)___package_ 08) listing 09) print_function _Feature((2, 6, o,'alpha', 2), (3, o, o,'alpha', o), 65536) 10) sepchr 11) seplen 60 -----------------mydir has 12 names



高级模块话题

I

749

如果你想用该工具列举其他模块,也可以直接将模块对象传入该文件的面数。下面使用该 由数列举了标准库 tkinter GUI 模块的属性 (Python 2 . X 中名为 Tkinter) ;从技术上讲,

该面数可用千任何带有—name—、—file_和_diet

属性的对象:

» > import mydir >» import tkinter »> mydir.listing(tkinter) ------------------------name: tkinter file: C: \Python33\lib\ tkinter\_init_. py

-----------------------------------------------------· oo) ACTIVE active 01) ALL all 02) ANCHOR anchor 03) ARC arc 04) At ... many more names omitted... 156) image_types 157) mainloop 158) sys 159) wantobjects 1 160) warnings

--------------------tkinter has 161 names

我们在本书后面会详细介绍与 get at tr 相关的一组函数。这里的重点在千, mydir 是一个

可以浏览其他程序的程序。由千 Python 暴露了其内部,因此你可以泛化地处理对象注 20

用名称字符串导入模块 在 import 或 from 语句中的模块名是被硬编码的变量名。有时候,我们的程序也可以在运

行时以 一个字符串的形式获取要导入的模块名称-f91J 如,按照用户在 GUI 中做出的选择, 或是根据 XML 文档中解析出的导入。遗憾的是,我们无法使用 import 语句直接载入以字

符串形式给出其名称的 一 个模块。因为 Python 在 import 中需要 一 个字面量形式的变量名,

而非计算出的变撬名称(字符串或表达式都不可以)。例如: >>>垃port'string'

File "", line 1 import "string"

^

SyntaxError: invalid syntax

注 2:

你可以通过将类似 mydir.listing 的工具(以及稍后见到的重加载器)导入到环境变量 PYTHONSTARTUP 引用的文件 , 从而把它们预加载到交互式命名空间中。由于启动文件中的

代码会在交互式命名空间(模块—main 以节石给入 。 史多细节诗参阅附录 A 。

75O

I

第 25 章

)中运行,因此,在启动文件中导入常用工具可

直接把该字符串赋给 一 个变量名也是无效的:

x ='string' import x 这里, Python 将尝试导入一个文件 x.py, 而不是 string 模块: import 语句中的名称既赋

值了被载入模块的变批,也从字面上指定了该外部文件。

运行代码字符串 为了解决这个问题,我们需要使用特殊的工具,以便从运行时生成的一个字符串来动态地

载入一个模块。最通用的方法是,自己构造 一 条 Python 导入语句代码的字符串,然后把它 传入 exec 内置函数运行 (exec 在 Python 2. X 中是一 条语句,但它在 2.X 中的用法与下面

基本一致--f,尔只需去掉圆括号即可)

»> modname = "string" >» exec("import " + modname) # Run a string of code >» string # Imported in this namespace

我们曾在第 3 章和第 10 章中学习过 exec 函数(以及和它很像的针对表达式的 eva 1) 。 exec 会编译一个代码字符串,井且把它传给 Python 解释器执行。在 Python 中,由千字节

码编译器在运行时可用,因此我们能像这样编写可以构建和运行其他程序的程序。默认情 况下, exec 会在当前作用域中运行代码,但你也可以传入可选的命名空间字典来指定其他

特定的作用域。虽然 exec 也存在之前提过的安全问题,但如果是你自己构造的代码字符串 则无须顾虑。

直接调用:两种方式 这里 exec 唯一真正的缺点是,每次运行时它必须编译 import 语句,而编译过程可能会很慢。

对千那些多次运行的字符串,你也许可以使用 compile 内置工具将其预编译为字节码来提 速,但在绝大多数情况下(就像在第 22 章提到的)如果使用内置的_import_函数来从一

个名称字符串载入,那么代码会更简单同时运行得更快。虽然效果类似,但_import _返 回模块对象,因而下面把它赋给一个名称来持有它:

>» modname ='string' >» string = _import_(modname) >» string

第 22 章还讲过,较新的 importlib.import_module 可以实现相同的效果,对千通过名称 字 符串导入的直接调用而言 ,这种方式在较新的 Python 版本中通常更受欢迎一至少它是 目前 Python 文档中所述的“ 官 方“机制:

高级模块话题

1

751

>» import importlib » > modname ='string' >» string= importlib.import_module{modname) >» string

import_module 调用会传入 一 个模块名称字符串,以及一个可选的第二参数(这是一个 包)用来作为解析相对导入的描点,默认值为 None 。该调用就其基本角色而言用起来和

—import—相同,但更多细节还请参阅 Python 文档。 尽管两种调用都能工作,但在那些两者都可用的 Python 版本中,原本的—import_ 一 般更 推荐用于对内置作用域重新赋值的导入定制(而“官方“机制在未来可能的变化都超出了 本书的范围)。

示例:传递性模块重载译注 1 本节将开发一个模块工具,把之前所学的内容融会贯通并加以应用,同时作为本章和本部 分结束的一个大型案例研究。我们在第 23 章曾学过模块重载,它能帮助我们在不停止或重 新运行程序的情况下,载入被导入模块发生的修改。当我们重载一个模块时, Python 只重 新载入这个指定模块的文件,而不会自动递归地重载被该文件导入的其他模块。 例如,假设你要重载某个模块 A, 并且 A 导人模块 B 和 C, 重载只适用千 A, 而不适用于

B 和 C 。 A 中导入 B 和 C 的语句在重载的时候会重新运行,但它们只是获取了已经载入的 B 和 C 模块对象(假设它们之前已经导人了) 。在实际代码中,文件 A.py 如下:

#A.py

import B import C

# Not reloaded when A is! #

Jusr an imporr of an already loaded module:no-ops

% python > >> . . .

» > from imp import reload »> reload(A) 默认情况下,这意味着你不能依赖千重载来传递性地获得程序中所有模块的修改;相反, 你必须多次调用 reload 来独立地更新各个子部分。对千进行交互测试的大型系统而言,这

会导致极大的工作量。你也可以通过在 A 这样的父模块中添加 re load 调用,从而将系统 设计成能够自动重载它们的子部分,但这也相应会使模块代码变得复杂。

递归重载器 一种更好的办法是编写一个通用工具来自动完成传递性重载,即通过扫描模块的 译注 I :

752

I

diet

这里的 “ 重载 “ 是指“模块的重新加载" , 读者朋友要注意与后面的 ” 运算符重载 “ 加以区分 。

第 25 章

命名空间属性并检查每一项的 type 以找到要重新载入的嵌套模块。这样的 一 个工具函数应 该递归地调用自己,来遍历任意构型和任意深度的导入依赖链。第 23 章及本章开头都介绍

并使用了模块的_diet—属性,井且在第 9 章中介绍了 type 调用;我们只需要把两种工具 组合起来 。 下面的模块 reloadall.py 定义一个 reload_all 函数自动地重载一个模块,该模块导入的所 有模块,以及所有通往各条导入链最底端的通路。它用 一 个字典来记录已经重载的模块,

递归地遍历导入链及标准库的 types 模块 (types 模块中预定义了内置类型的 type 结果)。 visited 字典的技术在这里用来在导入是递归或冗余的时候避免循环,由千模块对象是不 可变的,因此可以作为字典键;正如第 5 章和第 8 章所述,如果我们使用集合而不是字典 的话,那么只需修改为 visited.add(module) 来插入元素:

#!python '""'

reloadall.py: transitively reload nested modules (2.X + 3.X). Call reload_all with one or more imported module module objects. import types from imp import reload

# from required in 3.X

def status(module): print('reloading'+ module._name_) def tryreload(module): try: reload(module) except: print('FAILED: %s'% module) def transitive_reload(module, visited): if not module in visited: status(module) tryreload(module) visited[module] = True for attrobj in module. _diet_. values(): if type(attrobj) == types.ModuleType: transitive_reload(attrobj, visited) def reload_all(*args): visited = {} for arg in args: if type(arg) == types.ModuleType: transitive_reload(arg, visited) def tester(reloader, modname): import importlib, sys if len(sys.argv) > 1: modname = sys.argv[1] module= importlib.import_module(modname) reloader(module)

# 3.3 (011/y?)fails 011 some

# Trap cycles, duplicates # Reload this module # And visit children # For all attrs # Recur if module

# Main entry poinr # For all passed in

# Self-test code # Import on tests only # command line (or passed) # Import by name string # Test passed-in reloader

高级模块话题

I

753

if

ma1n name == tester(reload_all,'reloadall')

#

7七,\,r:

reload myself)

除了命名空间字典之外,该脚本还利用了我们前面学过的共他工具:作为顶层脚本运行时 才会执行的_name_测试以启动自测试代码,使用 sys.argv 来检查命令行参数的 tester 函数,以及使用 import lib 通过以函数或命令行参数传入的名称字符串来导入模块。有趣

的 一 点:注意这段代码是如何把基本的 reload 调用封装在 一 个 try 语句中从而捕获异常的, 因为在 Python 3.3 中,重载有时候会因为导入机制的重新写入而失败。我们在第 10 章中预

习过 try 语旬`并会在本书第七部分中详细介绍它。

测试递归重载 现在,如果要使用这一工具进行常规工作,就要导入其中的 reload_all 函数并将 一个已载 入的模块对象传人 reload_all, 这和你使用内置 reload 函数是一 样的。当 reloadall.py 文 件单独运行时,它的自测试代码会自动调用 reload_all, 如果没有使用任何命令行参数,

则默认情况下重新加载自身的 reload_all 模块。在这 一 模式下,模块必须导入自己,如果

没有导入,其本身的名称就不会出现在文件中译注 2 。这段代码在 Python 3.X 和 Python 2.X 中都有效,因为我们已经在 print 中使用了+和%而不是逗号,尽管所涉及的模块可能有 所不同:

C:\code> c:\Pythonn\python reloadall.py reloading reloadall reloading types c: \code> C: \Python27\python reloadall. py reloading reloadall reloading types 如果使用了命令行参数,那么测试函数会通过命令行参数给出的名称字符串来重新加载指 定模块。下面用到的 pybench, 是我们在第 21 章中编写的基准测试模块。要注意在该模式 下我们需要给出模块名称,而不是文件名称(这和导入语句不需要包含.PY 扩展名是 一 样的); reloadall.py 依然会使用模块搜索路径导入 pybench:

c: \code> reloadall. py pybench reloading pybench reloading timeit reloading itertools reloading sys reloading time reloading gc reloading os reloading errno reloading ntpath

译注 2

754

I

这里对 reloadall 自身的导入发生在上面代码的 tester 函数中 。

第 25 章

reloading stat reloading genericpath reloading copyreg 也许最为常见的是,这里我们也可以在交互式命令行下应用这一槐块 ,下面对 3 . 3 中的一 些标准库模块进行调用。要注意 tkinter 导入了 OS, 但 tkinter 在 OS 之前就已经导人了

sys (如果你想在 Python 2.X 下测试这段代码.要将 tkinter 改成 Tkinter) :

» > from reloadall 垃port reload_all >» import os, tkinter >» reload_all(os) reloading os reloading ntpath reloading stat reloading sys reloading genericpath reloading errno reloading copyreg

# Normal usage mode

>» reload_all(tkinter) reloading tkinter reloading _tkinter reloading warnings reloading sys reloading linecache reloading tokenize reloading builtins FAILED: reloading re ... etc... reloading os reloading ntpath reloading stat reloading genericpath reloading errno . . . etc . .. 最终,下面的会话展示了普通重载和传递性重载的对比效果一一除非使用传递性重载,否

则普通重载不会获得对两个嵌套文件的修改:

import b

# File a.py

X = 1

import c

#

File b.py

#

File c.py

y = 2

Z = 3

C:\code> PY -3 >» import a »> a.X, a.b.Y, a.b.c.Z (1, 2, 3) II Wilhour stopping Python, change all three files'assigmnenr values and save 高级模块话题

1

755

»> from imp import reload »> reload(a)

>» a,X, a.b.Y, a.b.c.Z (111, 2, 3)

# Bui/1-in reload is top level only

>» from reloadall import reload_all >» reload_all(a) reloading a reloading b reloading c »> a.X, a.b.Y, a.b.c.Z (111, 222, 333)

# Normal usage mode

#

Reloads all nested modules too

你可以进 一 步研究这个传递性重载器的代码和运行结果来理解其中操作的更多知识。下一 小节会改写传递性重载器的工具。

另外的代码 对千熟练使用递归的读者朋友,下面会给出上一节中函数的另一种递归代码一一它使用集 合而不是字典来检查循环,由千消除了顶层循环,因此这会稍微更直接一些,并且可用于 展示 一 般的递归函数技巧(与原来的代码比较来看看有何不同)。该版本也从原先版本中 直接获取了一些工作,不过如果命名空间字典的顺序发生了变换,那么它重载模块的顺序

也会相 应改变译注 3 :

reloadall2.py: transitively reload nested modules (alternative coding) import types from imp import reload from reloadall import status, tryreload, tester

# from required in 3.X

def transitive_reload(objects, visited): for obj in objects: if type(obj) == types.ModuleType and obj not in visited: status(obj) tryreload (obj) # Reload this, recur to attrs visited.add(obj) transitive_reload(obj. _diet .values(), visited)



def reload_all(*args): transitive_reload(args, set()) 译注 3: 细心的读者朋友会发现,上一个版本中也使用了递归调用 。 两者的不同在于.原先版本的 递归函数传入的是单个对象,因此在函数体中要用 for 徙环把命名空间字典中的值一个个 拿出来,然后分别传入递归函数;而新版本直接传入了多个对象的列表,所以只需把整个

命名空间字典的值列表传入递归函数,然后对传入列表使用 for 徙环 。 两者只是 for 循环 编写的位置不同,本质都使用了递归的编程方式 。

756

I

第 25 章

if

ma1n name == tester(reload_all,'reloadal12')

# Test code: reload myself?

正如我们在第 19 章所学,对于绝大多数递归函数,通常存在与之等价的 一 个显式栈或队列

的编程方式,而这在一些情境下会更适合使用。下面就是一个这样的传递性重载器;它使 用 一 个生成器表达式来过滤掉非模块对象及已在当前模块命名空间中访问过的模块。因为 它在其列表尾部同时弹出和添加了元素,所以它基千栈,而压人顺序和字典值顺序都会影

响它重载模块的顺序一它在命名空间字典中从右至左地访问子模块,与递归版本的从左 至右的顺序不同(细心追踪代码看看为何)。我们可以改变这 一 点,不过字典顺序无论如 何都是任意的:

reloadall3.py: transitively reload nested modules (explicit stack) "'"'

import types from imp import reload from reloadall import status, tryreload, tester

# from

required in 3.X

def transitive_reload(modules, visited): while modules: next = modules. pop() # Delete next irem ar end status (next) # Reload this. push attrs tryreload(next) visited.add(next) modules.extend(x for x in next._dict_.values() if type(x) == types .ModuleType and x not in visited) def reload_all(*modules): transitive_reload(list(modules), set()) if

main name == tester(reload_all,'reloadall3' )

#

Test code: reload myse/f?

如果你对本例中使用的递归和非递归感到困惑,可以参考第 19 章关千递归函数的讨论来了 解更多相关的背景知识。

测试不同的重载方案 为证明这些方案的效果 一致,让我们测试以上 三 种不同的重载器。多亏了它们共同的测试 凶数,我们既可以从 一 个命令行不带任何参数地运行这 三 个文件测试模块重载它自身,也

可以在命令行上(即在 sys.argv 中)列出希望重载的模块:

c:\code> reloadall.py reloading reloadall reloading types c:\code> reloadal12.py reloading reloadall2 reloading types

高级模块话题

I

757

c:\code> reloadall3,py reloading reloadall3 reloading types 尽管测试自身很难看出异同,但我们确实是在测试不同的重载器方案一虽然所有测试共 用同 一个 tester 函数,但是各个文件都传入了各自的 reload_all 函数。下面展示了这 二金 种方案亟载 3.X 中 tkinter GUl 校块以及它通过导入可以达到的所有模块:

c: \code> reloadall.py tkinter reloading tkinter reloading _tkinter reloading tkinter. _fix ... etc... c:\code> reloadal12.py tkinter reloading tkinter reloading tkinter.constants reloading tkinter. _fix ... etc... c:\code> reloadall3.py tkinter reloading tkinter reloading sys reloading tkinter.constants ... etc... 所有这 三种方案都可以在 Python 3.X 和 2.X 下工作一它们都非常小心地采用格式化来统 一打印,井避免使用版本特定的工具(不过你必须使用像 Tkinter 这样的 2.X 版本的模块

名称,而且我在这里使用了附录 B 中介绍的 3.3 版本的 Windows 启动器来运行)

c: \code> py -2 reloadall.py reloading reloadall reloading types c: \code> py -2 reloadal12.py Tkinter reloading Tkinter reloading _tkinter reloading FixTk ... etc... 我们照例也可以交互式地测试,通过传入 一 个模块对象米导入和调用 一 个模块的主亚载入

口,或是向 tester 函数中传入 一 个重载器函数和模块名称宇符串:

C:\code> PY -3 >» import reloadall, reloadall2, reloadall3 >» import tkinter >» reloadall. reload_all(tkinter) reloading tkinter reloading tkinter. _fix reloading os ... etc... >» reloadall. tester(reloadall2. reload_all,'tkinter' ) reloading tkinter reloading tkinter._fix 758

I

第 25 章

# Normal use case

# Testing

utili灯

reloading os . .. etc... » > reloadall. tester(reloadall3 . rel oad_all, ' reloadall3') re loadi ng reloadall3 re l oading t ypes

#

Mimic xelj-test code

最后,如果你观察前面 tk int e r 的重载输出结果,就会发现三 个版本都会以不同的顺序产

生结果,它们都依赖命名空间字典的顺序,而最后 一 个版本还依赖元素添加到栈的顺序。 事实上,在 P yt h o n 3 . 3 下,对于同 一 个给定的重载器,重载顺序在每次运行中可能都各不

相同。为了确保 三个版本重载了相同的一批模块,我们可以使用集合(或进行排序)来测 试它们的打印信息在不考虑顺序的情况下是乔相互等价 。 这里通过使用在第 13 章和第 21

章学过的 os.popen 工 具运行命令行指令来实现:

» > >» >» »> »>

import os resl = os.popen('reloadall.py tkinter').read{} res2 = os . popen('reloadall2.py tkinter ' ).read() res3 = os.popen( ' reloadall3.py tkinter ' ).read() res1[ : 75] 'reloading tki nter\nreloading tkinter .constants \nreloading tkinter. _fix\nreload'

»> resl == res2 , res2 == res3 (Fa l se, Fal se)

»> set(res1) == set(res2), set(res2) == set(res3) (True, True) 你可以运行这些脚本,研究它们的代码,在自己的机器上试验 、 进而获得更深刻的体会; 它们正是你希望添加到自己的源代码库中的那种可导人工具。等到在第 3 l 章介绍类树列举 器 的 时候,我 们 还会看到 一 个使用了相 似 测试技术 的 例子 , 到那 时我 们 会 把 它应用千传入 的类对象上并进 一 步扩展。 你还要记住全部三个版本都只重载那些已被 im port 语句载入的模块一一由千使用 fr om 语

句复制的名称不会使 一 个模块在导入者的命名空间中被嵌套和引用,因此只被 fr om 语句涉 及的模块是不会被重载的。更为基本的是,由千传递性重载器依赖于模块重载在原位置更

新模块对象的事实,因此在所有作用域中对那些模块的全部引用都会自动地看到更新后的 版本 。 因为 f r o m 语句只是将名称复制出来,所以 f ro m 导人者不会被重载更新(无论是否

是传递性的)。如果你想支持 fr o m 语句的重载功能,就需要进行源代码分析或是定制导入

操作(更多思路参阅第 22 章 )。 受此影响的 工具也许是很多人偏爱 i mpo r t 而不是 fr om 的另 一 个原因 一~将我们领向了 本章和本部分的末尾,以及本部分主题的标准警示录 。

模块陷阱 本节让我们看看一 些 会 让 Py th on 初 学 者感到有趣的常见边缘案例 。其 中 一 些是 复 习,另 一

高级模块话题

I

759

些则罕见到连举出有代表性的例子都十分困难,但它们大都阐述了 Python 语言中相对本质 和关键的内容。

模块名称冲突:包和包相对导入 如果你有两个同名的模块,那么你只能导入它们中的一个~ Python 总是会 选择在模块搜索路径 sys.path 中最左边的那一项。如果你偏爱的模块和顶层脚本在同一目 录下,那就不成问题;由千顶层脚本的主目录总是模块搜索路径中的第一项,因此它的内

容总是会首先被自动定位。然而对千跨目录的导入,模块搜索路径的线性本质意味着同名 的文件会产生冲突。

要修复这一冲突,要么避免同名文件,要么使用第 24 章中的包导入功能。如果你需要同时 访问两个同名的文件,那么就要把两个源文件分别放入子目录中,这样包导入目录名称将 使得模块引用唯 一 。只要外围的包目录名称是唯 一 的,你就能访问同名模块中的任意 一 个, 或是全部的两个。 注意,如果你不小心为自己的模块使用了 一 个名称,而它碰巧和你需要使用的标准库模块 的名称相同,那么也会出现这一问题。这是因为程序主目录(或是模块路径中靠前的另 一

个目录)下的本地模块会隐藏和替换标准库模块。

要修复这种覆盖,要么避免使用和你需要的另 一 模块相同的名称,要么把模块放到 一 个包 目录下然后使用 Python 3.X 的包相对导入模型(包相对导人在 2.X 版本中是一个可选的功

能)。在包相对导入模型下,普通导入会跳过包目录,因此你可以获取标准库版本,但在 必要时特殊的点号开头导入语句仍然可以选取同名模块的本地版本。

顶层代码中语句次序很重要 如前所述,当模块首次袚导人(或重载)时, Python 会从头到尾执行它的语句。这里有些 和前向引用 (forward reference) 相关的细节影响,值得在此强调 :



在导入时,模块文件顶部的程序代码(不在函数内),在 Python 运行到的时候就会立 刻执行。因此,这些语句无法引用文件底部位置赋值的变最名。



位千函数体内的代码直到函数被调用后才会运行。因为函数内的变量名在函数实际执 行前都不会被解析,所以在函数体中常常可以引用文件中任意位置的变量。

一般来说,前向引用只对立即执行的顶层模块代码有影响;而函数则可以任意地引用变撮名。 下面的文件展示了前向引用的相关须知:

76O

func1()

# Error:''Jlme I" nor yer assigned

def func1(): print(func2())

#OK:''func2" looked up later

I

第 25 章

funcl()

#Error:''junc2" not yet assigned

def func2(): return "Hello" func1()

#OK:''funcl" and''func2" assigned

当该文件被导人时(或者作为独立程序运行时), Python 会从头到尾执行它的语句。第一 行对 func1 的调用会失败,因为 func1 的 def 语句尚未执行。只要 func1 被调用时 func2 的 def 语句己运行过, func1 中对 func2 的调用就不会出错。也就是说,第四行对 func1 的调用会失败的原因是此时 f u nc2 还未被定义。最后一行对 func1 的调用能成功,因为 func1 和 func2 都已被赋值了。

将顶层代码与 def 交错编写会导致代码难以阅读,也造成了对语句顺序的依赖性。作为一 条准则,如果要在一个文件中同时编写立即执行代码和 def, 则需把 def 放在文件前面, 把顶层代码放在后面。这样,当你的函数在 Python 运行到使用它们的代码的时候,就可以

保证它们都已被定义并赋值了。

from 复制名称,而不是链接 尽管 from 语句很常用,但它也是 Python 中各种潜在陷阱的源头。如前所述, from 语句其 实是在导入者的作用域内对名称赋值,也就是名称复制运算,不是名称的别名机制。它的 含义和 Python 所有赋值运算都一 样,但考虑到共用对象的代码位于不同文件中,因此 from 语句会更加微妙。例如,假设我们定义了下列模块文件 nested].py:

# nestedl.py X = 99 def printer(): print(X) 如果我们在另一个模块文件 nested2.py 中使用 from 导入 nested].py 的两个名称,就会得到

这两个名称的副本,而不是指向它们的链接。在 nested2.py 中修改某一名称,只会重设该 名称在本地作用域版本的绑定值,而不是 nestedl.py 中的名称:

# 11ested2.py from nestedl import X, printer X = 88

printer()

# Copy names out # Changes my "X" only! # nestedl's X is still 99

% python nestedz.py

99 如果使用 import 获取整个模块,并对 一 个点号运算得到的名称赋值,就会修改 nested].py 中的名称。属性点号运算会让 Python 定向到模块对象内的名称,而不是导入者的名称,如

下面的 nested3.py 所示:

高级模块话题

I

761

# nested3.py

import nestedl nestedl.X = 88 nestedl.printer()

#

Get module as a whole

# OK: change nested l 's X

% python nested3.py

88

from *会让变量含义模糊化 我之前提到过这 一 点,但把细节留到现在来描述。由千在使用于rom module import *语句 形式时,你不会列出想要的变益,因此可能会意外地狡盖作用域内已使用的名称。更糟的是, 这将很难确认变址来自何处。对 一 个以上的披导入文件使用 from *形式,会让悄况变得 更糟。 例如,假设你像下面这样在 三 个模块上使用 from* ,就没有办法知道 一个简单的函数调用 的真正含义,除非去搜索这 三个外部模块文件一而这 三个文件很可能都在其他目录中:

» > from

module1 加port

*

» > from module2 import *

» > from module3 >>>...

加port

# Bad: may overwrite my names silently # Worse: no way to tell what we get!

*

>» func()

#Huh???

解决的办法就是避免这么做:试着在 from 语句中明确地列出想要的属性,而且限制在每个 文件中最多只有一个被导人的模块使用 from *这种形式。如此 一 来,任何未定义的名称一 定可以被归为那个 from *所对应的模块中。如果你总是使用 import 而不是 from, 那就可 以完全避开这个问题,但这样的建议过千苛刻。就像其他许多程序设计工具 一 样,如果合 理使用的话, from 也是一 种很方便的工具。甚至在这个例子中也不 一 定就是绝对不好,只

要你了解所有的名称,你也可以很方便地使用 from *将这些名称收集到同 一 个命名空间中。

reload 不能作用千 from 导入 这是另一个与 from 相关的陷阱:如前所述,因为 from 会在执行时复制(赋值)名称,所 以不会链接到名称所在的模块。通过 from 导人的名称就直接成为对象的引用,当 from 运 行时这个对象恰巧在被导人者内由相同的名称引用。

正是由千这种行为,重载被导入模块不能作用 于 通过 from 导人模块名称的客户程序 。 也就 是说,客户程序的名称依然引用了通过 from 取出的原始对象,即使原始模块中的名称之后 进行了重置:

from module import X

762

I

第 25 章

# X may not rej7ect any module reloads!

from imp import reload reload(module)

# Changes module,

X

# Still references old object

加r

not my namt•s

为了让重载更有效,可以使用 import 以及点号运算来取代 from 。因为点号运算总是会回 到揆块中的名称,所以会找到重载在原位置更新模块的内容后模块名称的新绑定值:

import module

# Get module.

11.01

names

... from imp import reload reload(module) module.X

# Changes module in pluce #

Ger current X: ref/eel.\· module reloads

作为 一 个相关的结果,本章之前的传递性重载器不再适用于通过 from 获取的名称,而只适 用千 import; 因此,如果你准备使用重新加载,那么最好使用 import 。

reload 、 from 以及交互式测试 事实上,前 一 个陷阱比看上去的要更加微妙 。 第 3 章曾提过,通常情况下,最好不要通过

导入或重载来启动程序,因为其中牵涉许多复杂的问题。当引入 from 之后,情况甚至变得 更糟。 Python 初学者经常会受困于如下的陷阱 : 假设在文本编辑窗口开启 一 个模块文件后,

你启动 一 个交互式命令行会话,通过 from 加载并测试模块:

from module import function function(1, 2, 3) 这时发现了 一 个 bug, 你跳回编辑窗口,做了修改,并试着重载模块:

from imp import reload reload(module) 但是这样行不通,因为 from 语句只是赋值了名称 function, 而不是 module 。要在 reload 中引用模块,你必须至少先通过 import 语句绑定它的名称一次:

from imp import reload import module reload(module) function(l, 2, 3) 然而,这样也无法运行:原因是 reload 在原位置更新模块对象,但如上 一 节所述,像

function 这样从模块复制出来的名称仍然会引用旧的对象;在这个例子中, function 仍然 是该函数原先的版本 。 要确实获得新的 function, 必须在 reload 后通过 module.function 来调用函数,或者重新执行 from:

高级模块话题

I

763

from imp import reload import module reload(module) from module import function function(1, 2, 3)

# Or give up and use module.function()

现在,新版本的 function 终千可以执行了,但是做到这点似乎需要可怕的大批工作。

如你所见,问题的本质在千同时使用了 reload 和 from: 你不但得记住在导人后要重载, 还得记住在重载后要重新执行 from 语句。其复杂程度有时甚至会让 Python 专家也栽跟头。 实际上,这种情形在 Python 3.X 中甚至变得更糟糕,因为你还必须记住要导入 reload 本身! 总之,不要对 reload 和 from 能完美合作抱有幻想。再次强调,最佳的原则就是不要将它

们结合起来使用:使用 reload 和 import ,或者以其他方式启动程序,如第 3 章的建议,例如, 使用 IDLE 中的 “Run" 一 “Run Module" 菜单选项点击文件图标、系统命令行或是内置 的 exec 函数)。

递归形式的 from 导入可能无法工作 我把最诡异(值得庆幸的是,这也是最罕见)的陷阱留到最后。因为导入会从头到尾执 行一个文件的语句,所以在使用相互导入的模块时要格外小心。这常常被称为递归导入

(recursive import) ,但是递归并不会实际发生(事实上,在这里循环 (circular) 可能会 是一个更好的术语) ~不过,因为一个模块内 的语句在其导入另 一 个模块之前不会全都执行,因此有些名称可能还尚不存在。 如果使用 import 获取整个模块,这可能没有什么影响;模块的名称在稍后使用点号运算获

取其值之前都不会被访问,而在使用点号运算的时候很可能就已经完成初始化了。如果你 采用 from 来取出特定的名称,就必须意识到你只能获取到在递归导入发生时刻之前 from

袚导人模块中被赋值了的名称。

例如,考虑下列模块 recur1 和 recur2 。 recur1 赋值了一个名称 X, 然后在赋值名称 Y 之

前导入 recur2 。这时 recur2 可以用 import 把 recurl 整个取出一recurl 已经存在于 Python 内部的模块表中了,这使其变得可导入,也阻止了来自循环的导人。但是,如果

recur2 使用 from, 就只能看见名称 X 。名称 Y 的赋值在 recur1 中发生千导人 recur2 之后, 目前尚不存在,因此会引发错误:

# recurl.py X= 1

import recur2 y = 2 # recur2.py from recun import X from recun import Y

764

I

第 25 章

# Run recur2 now if it doesn't exist

# OK: "X" already assigned #

Error: "Y" not yet assigned

C:\code> py -3 » > import recurt Traceback (most recent call last): File "", line 1, in File " . \recur1.py", line 2, in import recur2 File ".\recur2.py", line 2, in from recur1 import Y ImportError: cannot import name Y Python 会在 recur2 导入 recur1 的时候,避免重新运行 recur1 中的语句(否则导人会让

脚本变成死循环,那将会需要一个 Ctrl-C 来解决,甚至更糟),但在被 rec u r2 导入时, recurl 的命名空间尚不完整 。 解决办法是什么呢?不要在递归导入中使用 from (真的,不要)。如果这么做, Python 虽 然不会卡在死循环中,但是程序会依赖千模块中语句的顺序。实际上,有两种方式可避开 这个陷阱:



小心设计,通常可以消除这种导入循环:第 一 步就是最大化模块的内聚性,同时最小 化模块间的耦合性。



如果无法完全断开循环,就应使用 import 和属性点号运算(而不是 from 和直接变量名)

来推迟模块名称的访问,或者要么在函数内部(而不是在模块顶层),要么在文件底 部附近运行 from 语句,以延迟 from 语句的执行。

本章结尾的练习中有关千此问题的更多观点,不过我们目前已经正式接触过它了译注 40

本章小结 本章研究了 一 些模块相关的高级概念。我们研究了数据隐藏的技巧,通过_future—模块

启用新的语言特性,_ name _使用模式变最,传递性重载,由名称字符串导人等。我们也 探索和总结了模块设计的话题,并见到了模块相关的常见错误,从而在代码中避免发生类

似的错误。 下一 章要开始讨论 Python 的面向对象程序设计工具:类。前几章我们所涉及的内容多数也 都适用于类。类存在千模块内,同时类也是 一 种命名空间。但是类在属性查找时多加了 一 个额外的组件,称为继承搜索。不过,因为这是本书此部分最后一章,在深入下 一个主题 之前,要确保已经做过这一部分的练习题。让我们先做做本章习题来复习 一 下这里所讨论 的话题 。

译注 4 :

作为补充,强烈建议读者朋友阅读附录 D 中笫五部分习题 7 的轩答和译注 。

高级模块话题

I

76s

本章习题 I.

在模块顶层以单个下划线开头的变 盐 名具有哪些重要性?

2

当模块的_name_变且是字符串 “_main_" 时,代表什么疤思 ?

3.

要让用户通过交互式命令行输入想要的模块名,你的代码该怎样利用用户输人的模块 名进行导入?

4. 5.

改变 sys.path 和设置 PYTHONPATH 来修改模块搜索路径有什么区别?

如果模块_future_可让我们导入未来,那我们也能导人过去吗?

习题解答 I.

模块顶层以单个下划线开头的变址,在使用 from

*语句形式导入时不会被复制到进行

导入的作用域中 。 不过,它们还是可通过 import 或者普通的 from 语句形式来导人的 。

使用_all—列表与之类似,但是逻辑相反, _all_列表中的内容是在一 个 from* 语 句上唯 一 复制出来的名称。

2.

如果模块的—name—变量是字符串 "_main_", 那么说明该文件是作为顶层脚本运行 的,而不是被程序中的另 一 个文件所导入。也就是说,这个文件在作为程序使用,而 不是库。_name_使用模式变最能让我们支持“脚本+库“双模式编程和测试。

3.

用户输入通常以字符串的形式传入程序中。要通过字符串名称导入所引用的模块,可

以构造 import 语句并通过 exec 执行,或把字符串名传给—import _或 importlib. import_module 进行调用。

4.

修改 sys.path 只会影响正在运行的程序(进程),而且是暂时的一当程序结束时, 修改就会消失。 PYTHONPATH 设置是存在于操作系统中的,即机器上所有程序都会使用, 而且对 PYTHONPATH 设置的修改在程序离开后还是会保存。

5.

不行,我们无法在 Python 中导入过去。不过我们可以安装(或顽固地使用)这门语 言

的旧版本,但是,最新的 Python 往往是最好的 Python (至少在 3.X 或 2.X 系列内是这 样一参考 2. X 的长寿命!)。

第五部分练习题 参考附录 D 的第五部分以获得解答。

I.

导入基础。编 写一 个程序计算文件中的行数和字符数(思路上有点像 UNIX 系统上的

WC 指令的行为)。使用文本编辑器,编写 一 个名为 mymod.py 的 Python 模块,使其导

出 三 个顶层名称:

766

I

第 25 章



countlines(name) 函数:读取输入文件,计算其中的行数(提示: file. read lines

能帮你完成大部分的 工作, 而剩下的事可以交给 len 来做,尽管你也可以用 for 循 坏和文件迭代器来支持大型文件)。



countChars(name) 函数:读取输入文件,计算其中的字符数(提示: file.read 会 逐行返回单行的字符串,解法与之前类似)。



test(name) 函数 :用 一个指定的输人文件名调用上面的两个计数函数 。该文件名 通常是被传入的 、 硬编码的、通过 input 内茬函数输人的或是通过本章中 formats . PY 和 re/oada/l.py 示例展示的 sys.argv 列表从命令行中拿到的。就本题而言,你 可以假设这是传人的函数参数。

上述这三个函数都应预期传人一个文件名字符串。如果每个函数的代码多千两三行, 那就太费劲了:记得看看括号中的提示! 接着,在交互式命令行下测试你所编写的模块,使用 import 和属性点号运算来获取你 导出的名称。思考一下, PYTHON PATH 需不需要填人创建的 mymod.py 所在的目录呢? 试若让该模块针对自己的源代码文件运行:例如, test("mymod.py”) 。注意: test 函

数会打开 一 个文件两次;如果你有余力的话,可以重复利用 一 个已开启的文件对象,

并传入那两个计算函数来进行改进(提示: file. seek(O) 能让文件回 该到头部)。

2.

from/from

* 。在命令行下使用 from 导入来测试你在习题 l 中编写的 mymod 模块的函

数,首先通过函数名,之后通过 from *版本来获取所有函数。

3.

_main_ 。在 mymod 模块中加入一行,使得 test 函数仅 在模块作为脚本时(而不是被导 入时)被自动运行。你添加的代码应该会检测

name

的值是 否为字符串 "

main ",

就像本章所演示的那样。试着从系统命令行运行模块;然后导入该模块,在交互式命 令行下测试其函数。两种模式下 test 函数都会自动运行吗?

4.

嵌套导入。编 写另一个模块 myclien t .py 来导入 mymod , 井测试其函数。然后,从系统

命令行执行 myclient 。如果 myclient 使用 from 取出 mymod, 那么 mymod 的由数可 不可以从 my client 的顶 层存取 呢?如 果改用 import 导入会是什么情况呢?试着在

my client 中编写这两种版本,导入 myclient, 井在交互式命令行下查看 myclient 的 diet 5.

属性。

包导入 。从 一 个包中导入你的模块。在模块导入搜索路径上的一个目录中创建名为 mypkg 的子目录,把习题 l 或题 3 中创建的 mymod.py 模块文件复制或移动到这个新目 录下 、然 后试着以 import

my pkg. mymod

形式的包 导入方式导入它并调用它的函数。也

试着使用 from 语句来获取你的计数器由数。 你需要在模块移入的目录中添加 _init—PY 文件才行,而这可以让它在所有 主 流的

Python 平台上工作(这 也是 Pyt hon 使用“.”作为路径分隔字符的原因之一)。你所 创建的包目录可以是正在运行的主目录底下的子目录。如果是这样,包目录就可通过

高级模块话题

I

767

模块搜索路径的主目录组件找到,而不必配置路径。试着向—init_.py 中添加一些代码,

然后观察这些代码在每次导入时是不是都会运行。

6

重新加载。用模块来实验一下重载:运行第 23 章 changer.py 例子的测试,反复修改 被调用的函数的消息和行为,同时不要停止 Python 解释器。取决千你的操作系统,你

可以在另一个窗口编辑 changer, 或者暂停 Python 解释器,在相同的窗口中编辑(在

UNIX 上, Ctrl-Z 通常会挂起当前的进程,而 fg 命令可以使其稍后重新恢复,不过文 本编辑窗口也可以工作得很好)。

7.

循环导入。在递归导人(又名 ,循环导入)陷阱那一节中,导人 recurl 会引发错误。但是, 如果重启 Python, 通过交互式命令行导入 recur2, 则不会发生错误,自行测试并查看

结果。为什么导入 recur2 正常工作,而 recurl 则行不通?译注 5 (提示: Python 在运 行新模块的代码前,会先将新模块保存在内置的 sys.modules 表内( 一 个字典) ;稍

后的导入会先从这个表中取出该模块,无论该模块是否“完整"一。)现在,试着以顶 层脚本执行 re curl:

同的错误?为什么?

python recurl. py。你是否得到和交互式命令行导入

recur1 时相

(提示:当模块以程序执行时,不是被导入,所以这种情况下和

通过交互式命令行导入 recur2 是一样的效果 1

rec u r2 是最先导入的模块。)当你把

rec u r2 当成脚本运行时,发生了什么?循环导入并不常见,实践中极少用到这 一奇异 方式。另一方面,如果你能够理解它们为什么是潜在的问题,你对 Python 导入语义知

识的了解就更进一步了。

译注 5:

这道题目比较"绕",达议读者朋友直接阅读附录 D 中的斛答,并且不必在此花赍大量时间 . 参阅附录 D 中本题的译注。

768

I

第 25 章

Python/ Programming Languages

Python 学习手册(原书第5版) 本书将帮助你使用 Python 编写出高质量、高效的并且易于与其他语言

飞.对千那些想要开始使用 Python

和工具集成的代码。本书根据 Python 专家 Ma 「 k Lutz 的著名培训课程编

编程的朋友 , 本书是我所推荐

写而成,适合自学且易于掌握 。

图书中的首选 。"

-

本书各章对 Python 语言的关键内容依次进行讲解,并配有大量带注释

Doug Hellmann

Racern1 公司高级软件工程师

的示例代码以及图表,同时还配有章后习题、单元练习及详尽的解

答,便于你学习新的技能并巩固加深自己的理解。本书基千 Python 2.7

和 3.3 版本编写而成,同时也适用于其他 Python 版本。无论你是编程新 手抑或是其他编程语言的资深开发者,本书都将是你学习 Python 的理

想选择 。 本书主要内容 :

Mar k

Lutz 是—位世界级的

Python 培训讲师 。 他是 Python

· 学习 Python 的主要内 置 对象类型 , 如数字 、 列表和字典 。

畅销书籍的作者,同时从 1992

· 使用 Python语句创建和处理对象 , 井学习 Python 的通用语法

年起就成为引领 Python 社区的

模型 。

先驱人物。 Mark 有着 30 余年的 软件开发经验,也是《 Python

· 使用函数减少代码冗余 , 使用包代码结构实现代码 重 用 。

编程》

· 学习 Python模块 , 从而封装语句 、 函数以及其他工具 , 以便

《 Python 袖珍指南》等

书的作者。

构建大型组件 。

· 学习类 , 即 Python用千组织代码的面向对象编程工具。 · 使用 Python 的异常处理模型和开发工具编写大型程序 。

.

,

· 学习高级 Python 工具,包括装饰器 、 描述符 、 元 类 和 Unicode处理等 。

O' REILl丫®



oreilly.com

令 中 、

W

Inc 授权机械工业出版社出版

e4 、

O'Reilly Med 旧 ,

`

..

|

I SBN 978 - 7- 111 - 60366 -5 — — — (

此简体中文版仅限千在中华人民共和国境内(但不允许在中国香港 . 澳门特别行政区和中国台湾地区)销售发行

This Authorized Edition fo「 sale only in the territory of People's Repul:ilic of China (excluding Hong Kong, Macao and Taiwan)

9 7 8 7 111 6 3 665 > 投橘热线 : 客服热线 : 购 书 热线 :

(010) 88379604

(010) 88379426 88361066 (010)68326294 88379649 68995259

华章网站 www.hzbook.com 网上购书: www.china-pub.com 数字阅读: www.hzmedia.com.cn

定价 : 219.00元