第 5 章 Python 盒子:模块、包和程序
在自底向上的学习过程中,我们已经学完内置的数据类型,以及构建较大的数据和代码结构。本章落到实质问题,学习如何写出实用的大型 Python 程序。
5.1 独立的程序
到目前为止,我们已经学会在 Python 的交互式解释器中编写和运行类似下面的代码:
>>> print("This interactive snippet works.")
This interactive snippet works.
现在编写你的第一个独立程序。在你的计算机中,创建一个文件 test1.py,包含下面的单行 Python 代码:
print("This standalone program works!")
注意,代码中没有 >>>
提示符,只有一行 Python 代码,而且要保证在 print
之前没有缩进。
如果要在文本终端或者终端窗口运行 Python,需要键入 Python 程序名,后面跟上程序的文件名:
$ python test1.py
This standalone program works!
你可以把在本书中已经看到的终端可交互的代码片段保存到文件中,然后直接运行。如果剪切或者粘贴,不要忘记删除
>>>
提示符和…
(包括最后的空格)。
5.2 命令行参数
在你的计算机中,创建文件 test2.py,包含下面两行:
import sys
print('Program arguments:',sys.argv)
现在,使用 Python 运行这段程序。下面是在 Linux 或者 Mac OS X 系统的标准 shell 程序下的运行结果:
$ python test2.py
Program arguments: ['test2.py']
$ python test2.py tra la la
Program arguments: ['test2.py', 'tra', 'la', 'la']
5.3 模块和import
语句
继续进入下一个阶段:在多个文件之间创建和使用 Python 代码。一个模块仅仅是 Python 代码的一个文件。
本书的内容按照这样的层次组织:单词、句子、段落以及章。否则,超过一两页后就没有很好的可读性了。代码也有类似的自底向上的组织层次:数据类型类似于单词,语句类似于句子,函数类似于段落,模块类似于章。以此类推,当我说某个内容会在第 8 章中说明时,就像是在其他模块中引用代码。
引用其他模块的代码时使用 import
语句,被引用模块中的代码和变量对该程序可见。
5.3.1 导入模块
import
语句最简单的用法是 import
模块,模块是不带 .py 扩展的另外一个 Python 文件的文件名。现在来模拟一个气象站,并输出天气预报。其中一个主程序输出报告,一个单独的具有单个函数的模块返回天气的描述。
下面是主程序(命名为 weatherman.py):
import report
description = report.get_description()
print("Today's weather:", description)
以下是天气模块的代码(report.py):
def get_description(): #看到下面的文档字符串了吗?
"""Return random weather, just like the pros"""
from random import choice
possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
return choice(possibilities)
如果上述两个文件在同一个目录下,通过 Python 运行主程序 weatherman.py,会引用 report
模块,执行函数 get_description()
。函数 get_description()
从字符串列表中返回一个随机结果。下面就是主程序可能返回和输出的结果:
$ python weatherman.py
Today's weather: who knows
$ python weatherman.py
Today's weather: sun
$ python weatherman.py
Today's weather: sleet
我们在两个不同的地方使用了 import
:
主程序 weatherman.py 导入模块 report;
在模块文件 report.py 中,函数
get_description()
从 Python 标准模块random
导入函数choice
。
同样,我们以两种不同的方式使用了 import
:
主程序调用
import report
,然后运行report.get_description()
;report.py 中的
get_description()
函数调用from random import choice
,然后运行choice(possibilities)
。
第一种情况下,我们导入了整个 report
模块,但是需要把 report.
作为 get_description()
的前缀。在这个 import
语句之后,只要在名称前加 report.
,report.py 的所有内容(代码和变量)就会对主程序可见。通过模块名称限定模块的内容,可以避免命名冲突。其他模块可能也有函数 get_description()
,这样做不会被错误地调用。
第二种情况下,所有代码都在同一个函数下,并且没有其他名为 choice
的函数,所以我们直接从 random
模块导入函数 choice()
。我们也可以编写类似于下面的函数,返回随机结果:
def get_description():
import random
possibilities = ['rain', 'snow', 'sleet', fog', 'sun', 'who knows']
return random.choice(possibilities)
同编程的其他方面一样,选择你所能理解的最清晰的风格。符合模块规范的命名(random.choice
)更安全,但输入量略大。
这些 get_description()
的例子介绍了各种各样的导入内容,但没有涉及在什么地方进行导入——它们都在函数内部调用 import
。我们也可以在函数外部导入 random
:
>>> import random
>>> def get_description():
... possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
... return random.choice(possibilities)
...
>>> get_description()
'who knows'
>>> get_description()
'rain'
如果被导入的代码被多次使用,就应该考虑在函数外部导入;如果被导入的代码使用有限,就在函数内部导入。一些人更喜欢把所有的 import
都放在文件的开头,从而使代码之间的依赖关系清晰。两种方法都是可行的。
5.3.2 使用别名导入模块
在主程序 weatherman.py 中,我们调用了 import report
。但是,如果存在同名的另一个模块或者你想使用更短更好记的名字,该如何做呢?在这种情况下,可以使用别名 wr
进行导入:
import report as wr
description = wr.get_description()
print("Today's weather:", description)
5.3.3 导入模块的一部分
在 Python 中,可以导入一个模块的若干部分。每一部分都有自己的原始名字或者你起的别名。首先,从 report
模块中用原始名字导入函数 get_description()
:
from report import get_description
description = get_description()
print("Today's weather:", description)
用它的别名 do_it
导入:
from report import get_description as do_it
description = do_it()
print("Today's weather:", description)
5.3.4 模块搜索路径
Python 会在什么地方寻找文件来导入模块?它使用存储在标准 sys
模块下的目录名和ZIP压缩文件列表作为变量 path
。你可以读取和修改这个列表。下面是在我的 Mac 上 Python 3.3 的 sys.path
的内容:
>>> import sys
>>> for place in sys.path:
... print(place)
...
LibraryFrameworks/Python.framework/Versions/3.3/lib/python33.zip
LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3
LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3/plat-darwin
LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3/lib-dynload
LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages
最开始的空白输出行是空字符串 ''
,代表当前目录。如果空字符串是在 sys.path
的开始位置,Python 会先搜索当前目录:import report
会寻找文件 report.py。
第一个匹配到的模块会先被使用,这也就意味着如果你在标准库之前的搜索路径上定义一个模块 random
,就不会导入标准库中的 random
模块。
5.4 包
我们已使用过单行代码、多行函数、独立程序以及同一目录下的多个模块。为了使 Python 应用更具可扩展性,你可以把多个模块组织成文件层次,称之为包。
也许我们需要两种类型的天气预报:一种是次日的,一种是下周的。一种可行的方式是新建目录 sources,在该目录中新建两个模块 daily.py 和 weekly.py。每一个模块都有一个函数 forecast
。每天的版本返回一个字符串,每周的版本返回包含 7 个字符串的列表。
下面是主程序和两个模块(函数 enumerate()
拆分一个列表,并对列表中的每一项通过 for
循环增加数字下标)。
主程序是 boxes/weather.py:
from sources import daily, weekly
print("Daily forecast:", daily.forecast())
print("Weekly forecast:")
for number, outlook in enumerate(weekly.forecast(), 1):
print(number, outlook)
模块 1 是 boxes/sources/daily.py:
def forecast():
'fake daily forecast'
return 'like yesterday'
模块 2 是 boxes/sources/weekly.py:
def forecast():
"""Fake weekly forecast"""
return ['snow', 'more snow', 'sleet',
'freezing rain', 'rain', 'fog', 'hail']
还需要在 sources 目录下添加一个文件:init.py。这个文件可以是空的,但是 Python 需要它,以便把该目录作为一个包。
运行主程序 weather.py:
$ python weather.py
Daily forecast: like yesterday
Weekly forecast:
1 snow
2 more snow
3 sleet
4 freezing rain
5 rain
6 fog
7 hail
5.5 Python标准库
Python 的一个显著特点是具有庞大的模块标准库,这些模块可以执行很多有用的任务,并且和核心 Python 语言分开以避免臃肿。当我们开始写代码时,首先要检查是否存在想要的标准模块。在标准库中你会经常碰到一些“珍宝”! Python 同时提供了模块的官网文档(http://docs.python.org/3/library)以及使用指南(https://docs.python.org/3.3/tutorial/stdlib.html)。Doug Hellmann 的网站 Python Module of the Week(http://pymotw.com/2/contents.html)和他的书 The Python Standard Library by Example 都是非常有帮助的指南。
接下来的几章会着重介绍关于网络、系统、数据库等的标准模块。本节讨论一些常用的标准模块。
5.5.1 使用setdefault()
和defaultdict()
处理缺失的键
读取字典中不存在的键的值会抛出异常。使用字典函数 get()
返回一个默认值会避免异常发生。函数 setdefault()
类似于 get()
,但当键不存在时它会在字典中添加一项:
>>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
>>> print(periodic_table)
{'Helium': 2, 'Hydrogen': 1}
如果键不在字典中,新的默认值会被添加进去:
>>> carbon = periodic_table.setdefault('Carbon', 12)
>>> carbon
12
>>> periodic_table
{'Helium': 2, 'Carbon': 12, 'Hydrogen': 1}
如果试图把一个不同的默认值赋给已经存在的键,不会改变原来的值,仍将返回初始值:
>>> helium = periodic_table.setdefault('Helium', 947)
>>> helium
2
>>> periodic_table
{'Helium': 2, 'Carbon': 12, 'Hydrogen': 1}
defaultdict()
也有同样的用法,但是在创建字典时,对每个新的键都会指定默认值。它的参数是一个函数。在本例中,把函数 int
作为参数传入,会按照 int()
调用,返回整数 0
:
>>> from collections import defaultdict
>>> periodic_table = defaultdict(int)
现在,任何缺失的值将被赋为整数 0
:
>>> periodic_table['Hydrogen'] = 1
>>> periodic_table['Lead']
0
>>> periodic_table
defaultdict(<class 'int'>, {'Lead': 0, 'Hydrogen': 1})
函数 defaultdict()
的参数是一个函数,它返回赋给缺失键的值。在下面的例子中,no_idea()
在需要时会被执行,返回一个值:
>>> from collections import defaultdict
>>>
>>> def no_idea():
... return 'Huh?'
...
>>> bestiary = defaultdict(no_idea)
>>> bestiary['A'] = 'Abominable Snowman'
>>> bestiary['B'] = 'Basilisk'
>>> bestiary['A']
'Abominable Snowman'
>>> bestiary['B']
'Basilisk'
>>> bestiary['C']
'Huh?'
同样,可以使用函数 int()
、list()
或者 dict()
返回默认空的值:int()
返回 0
,list()
返回空列表([]
),dict()
返回空字典({}
)。如果你删掉该函数参数,新键的初始值会被设置为 None
。
顺便提一下,也可以使用 lambda
来定义你的默认值函数:
>>> bestiary = defaultdict(lambda: 'Huh?')
>>> bestiary['E']
'Huh?'
使用 int
是一种定义计数器的方式:
>>> from collections import defaultdict
>>> food_counter = defaultdict(int)
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
... food_counter[food] += 1
...
>>> for food, count in food_counter.items():
... print(food, count)
...
eggs 1
spam 3
上面的例子中,如果 food_counter
已经是一个普通的字典而不是 defaultdict
默认字典,那每次试图自增字典元素 food_counter[food]
值时,Python 会抛出一个异常,因为我们没有对它进行初始化。在普通字典中,需要做额外的工作,如下所示:
>>> dict_counter = {}
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
... if not food in dict_counter:
... dict_counter[food] = 0
... dict_counter[food] += 1
...
>>> for food, count in dict_counter.items():
... print(food, count)
...
spam 3
eggs 1
5.5.2 使用Counter()
计数
说起计数器,标准库有一个计数器,它可以胜任之前或者更多示例所做的工作:
>>> from collections import Counter
>>> breakfast = ['spam', 'spam', 'eggs', 'spam']
>>> breakfast_counter = Counter(breakfast)
>>> breakfast_counter
Counter({'spam': 3, 'eggs': 1})
函数 most_common()
以降序返回所有元素,或者如果给定一个数字,会返回该数字前的元素:
>>> breakfast_counter.most_common()
[('spam', 3), ('eggs', 1)]
>>> breakfast_counter.most_common(1)
[('spam', 3)]
也可以组合计数器。首先来看一下 breakfast_counter
:
>>> breakfast_counter
>>> Counter({'spam': 3, 'eggs': 1})
这一次,新建一个列表 lunch
和一个计数器 lunch_counter
:
>>> lunch = ['eggs', 'eggs', 'bacon']
>>> lunch_counter = Counter(lunch)
>>> lunch_counter
Counter({'eggs': 2, 'bacon': 1})
第一种组合计数器的方式是使用 +
:
>>> breakfast_counter + lunch_counter
Counter({'spam': 3, 'eggs': 3, 'bacon': 1})
你也可能想到,从一个计数器去掉另一个,可以使用 -
。什么是早餐有的而午餐没有的呢?
>>> breakfast_counter - lunch_counter
Counter({'spam': 3})
那么什么又是午餐有的而早餐没有的呢 ?
>>> lunch_counter - breakfast_counter
Counter({'bacon': 1, 'eggs': 1})
和第 4 章中的集合类似,可以使用交集运算符 &
得到二者共有的项:
>>> breakfast_counter & lunch_counter
Counter({'eggs': 1})
两者的交集通过取两者中的较小计数,得到共同元素 'eggs'
。这合情合理:早餐仅提供一个鸡蛋,因此也是共有的计数。
最后,使用并集运算符 |
得到所有元素:
>>> breakfast_counter | lunch_counter
Counter({'spam': 3, 'eggs': 2, 'bacon': 1})
'eggs'
又是两者共有的项。不同于合并,并集没有把计数加起来,而是取其中较大的值。
5.5.3 使用有序字典OrderedDict()
按键排序
在前面几章的代码示例中可以看出,一个字典中键的顺序是不可预知的:你可以按照顺序添加键 a
、b
和 c
,但函数 keys()
可能返回 c
、a
和 b
。下面是第 1 章用过的一个例子:
>>> quotes = {
... 'Moe': 'A wise guy, huh?',
... 'Larry': 'Ow!',
... 'Curly': 'Nyuk nyuk!',
... }
>>> for stooge in quotes:
... print(stooge)
...
Larry
Curly
Moe
有序字典 OrderedDict()
记忆字典键添加的顺序,然后从一个迭代器按照相同的顺序返回。试着用元组(键,值)创建一个有序字典:
>>> from collections import OrderedDict
>>> quotes = OrderedDict([
... ('Moe', 'A wise guy, huh?'),
... ('Larry', 'Ow!'),
... ('Curly', 'Nyuk nyuk!'),
... ])
>>>
>>> for stooge in quotes:
... print(stooge)
...
Moe
Larry
Curly
5.5.4 双端队列:栈+队列
deque
是一种双端队列,同时具有栈和队列的特征。它可以从序列的任何一端添加和删除项。现在,我们从一个词的两端扫向中间,判断是否为回文。函数 popleft()
去掉最左边的项并返回该项,pop()
去掉最右边的项并返回该项。从两边一直向中间扫描,只要两端的字符匹配,一直弹出直到到达中间:
>>> def palindrome(word):
... from collections import deque
... dq = deque(word)
... while len(dq) > 1:
... if dq.popleft() != dq.pop():
... return False
... return True
...
...
>>> palindrome('a')
True
>>> palindrome('racecar')
True
>>> palindrome('')
True
>>> palindrome('radar')
True
>>> palindrome('halibut')
False
这里把判断回文作为双端队列的一个简单说明。如果想要写一个快速的判断回文的程序,只需要把字符串反转和原字符串进行比较。Python 没有对字符串进行反转的函数 reverse()
,但还是可以利用反向切片的方式进行反转,如下所示:
>>> def another_palindrome(word):
... return word == word[::-1]
...
>>> another_palindrome('radar')
True
>>> another_palindrome('halibut')
False
5.5.5 使用itertools
迭代代码结构
itertools
(https://docs.python.org/3/library/itertools.html)包含特殊用途的迭代器函数。在for … in
循环中调用迭代函数,每次会返回一项,并记住当前调用的状态。
即使 chain()
的参数只是单个迭代对象,它也会使用参数进行迭代:
>>> import itertools
>>> for item in itertools.chain([1, 2], ['a', 'b']):
... print(item)
...
1
2
a
b
cycle()
是一个在它的参数之间循环的无限迭代器:
>>> import itertools
>>> for item in itertools.cycle([1, 2]):
... print(item)
...
1
2
1
2
.
.
.
accumulate()
计算累积的值。默认的话,它计算的是累加和:
>>> import itertools
>>> for item in itertools.accumulate([1, 2, 3, 4]):
... print(item)
...
1
3
6
10
你可以把一个函数作为 accumulate()
的第二个参数,代替默认的加法函数。这个参数函数应该接受两个参数,返回单个结果。下面的例子计算的是乘积:
>>> import itertools
>>> def multiply(a, b):
... return a * b
...
>>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
... print(item)
...
1
2
6
24
itertools
模块有很多其他的函数,有一些可以用在需要节省时间的组合和排列问题上。
5.5.6 使用pprint()
友好输出
我们见到的所有示例都用 print()
(或者在交互式解释器中用变量名)打印输出。有时输出结果的可读性较差。我们需要一个友好输出函数,比如 pprint()
:
>>> from pprint import pprint
>>> quotes = OrderedDict([
... ('Moe', 'A wise guy, huh?'),
... ('Larry', 'Ow!'),
... ('Curly', 'Nyuk nyuk!'),
... ])
>>>
普通的 print()
直接列出所有结果:
>>> print(quotes)
OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'), ('Curly', 'Nyuk nyuk!')])
但是,pprint()
尽量排列输出元素从而增加可读性:
>>> pprint(quotes)
{'Moe': 'A wise guy, huh?',
'Larry': 'Ow!',
'Curly': 'Nyuk nyuk!'}
5.6 获取更多Python代码
有时标准库没有你需要的代码,或者不能很好地满足需求。有很多开源第三方 Python 软件供参考,如下所示:
PyPi(http://pypi.python.org,也称为 Cheese Shop,名称源自 Monty Python1 的滑稽短剧)
github(http://github.com/Python)
readthedocs(https://readthedocs.org/)
1Monty Python 是英国六人喜剧团体。——译者注
你可以在 activestate(http://code.activestate.com/recipes/langs/python/)找到很多小代码示例。
本书的绝大部分代码使用的是安装在你电脑中的标准 Python 程序,包括所有内置函数和标准库。外部的包在以下地方提及:第 1 章中谈到的 requests
,具体细节在 9.1.3 节;附录 D 里面提及第三方 Python 软件的安装和详细的开发细节。
5.7 练习
(1) 创建文件 zoo.py。在该文件中定义函数 hours()
,输出字符串 'Open 9-5 daily'
。然后使用交互式解释器导入模块 zoo
并调用函数 hours()
。
(2) 在交互式解释器中,把模块 zoo
作为 menagerie
导入,然后调用函数 hours()
。
(3) 依旧在解释器中,直接从模块 zoo
导入函数 hours()
并调用。
(4) 把函数 hours()
作为 info
导入,然后调用它。
(5) 创建字典 plain
,包含键值对 'a':1
、'b':2
和 'c':3
,然后输出它。
(6) 创建有序字典 fancy
:键值对和练习 (5) 相同,然后输出它。输出顺序和 plain
相同吗?
(7) 创建默认字典 dict_of_lists
,传入参数 list
。给 dict_of_lists['a']
赋值 'something for a'
,输出 dict_of_lists['a']
的值。