第 5 章 Python 盒子:模块、包和程序

在自底向上的学习过程中,我们已经学完内置的数据类型,以及构建较大的数据和代码结构。本章落到实质问题,学习如何写出实用的大型 Python 程序。

5.1 独立的程序

到目前为止,我们已经学会在 Python 的交互式解释器中编写和运行类似下面的代码:

  1. >>> print("This interactive snippet works.")
  2. This interactive snippet works.

现在编写你的第一个独立程序。在你的计算机中,创建一个文件 test1.py,包含下面的单行 Python 代码:

  1. print("This standalone program works!")

注意,代码中没有 >>> 提示符,只有一行 Python 代码,而且要保证在 print 之前没有缩进。

如果要在文本终端或者终端窗口运行 Python,需要键入 Python 程序名,后面跟上程序的文件名:

  1. $ python test1.py
  2. This standalone program works!

第 5 章 Python 盒子:模块、包和程序 - 图1 你可以把在本书中已经看到的终端可交互的代码片段保存到文件中,然后直接运行。如果剪切或者粘贴,不要忘记删除 >>> 提示符和 (包括最后的空格)。

5.2 命令行参数

在你的计算机中,创建文件 test2.py,包含下面两行:

  1. import sys
  2. print('Program arguments:',sys.argv)

现在,使用 Python 运行这段程序。下面是在 Linux 或者 Mac OS X 系统的标准 shell 程序下的运行结果:

  1. $ python test2.py
  2. Program arguments: ['test2.py']
  3. $ python test2.py tra la la
  4. Program arguments: ['test2.py', 'tra', 'la', 'la']

5.3 模块和import语句

继续进入下一个阶段:在多个文件之间创建和使用 Python 代码。一个模块仅仅是 Python 代码的一个文件。

本书的内容按照这样的层次组织:单词、句子、段落以及章。否则,超过一两页后就没有很好的可读性了。代码也有类似的自底向上的组织层次:数据类型类似于单词,语句类似于句子,函数类似于段落,模块类似于章。以此类推,当我说某个内容会在第 8 章中说明时,就像是在其他模块中引用代码。

引用其他模块的代码时使用 import 语句,被引用模块中的代码和变量对该程序可见。

5.3.1 导入模块

import 语句最简单的用法是 import 模块,模块是不带 .py 扩展的另外一个 Python 文件的文件名。现在来模拟一个气象站,并输出天气预报。其中一个主程序输出报告,一个单独的具有单个函数的模块返回天气的描述。

下面是主程序(命名为 weatherman.py):

  1. import report
  2. description = report.get_description()
  3. print("Today's weather:", description)

以下是天气模块的代码(report.py):

  1. def get_description(): #看到下面的文档字符串了吗?
  2. """Return random weather, just like the pros"""
  3. from random import choice
  4. possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
  5. return choice(possibilities)

如果上述两个文件在同一个目录下,通过 Python 运行主程序 weatherman.py,会引用 report 模块,执行函数 get_description()。函数 get_description() 从字符串列表中返回一个随机结果。下面就是主程序可能返回和输出的结果:

  1. $ python weatherman.py
  2. Today's weather: who knows
  3. $ python weatherman.py
  4. Today's weather: sun
  5. $ python weatherman.py
  6. 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()。我们也可以编写类似于下面的函数,返回随机结果:

  1. def get_description():
  2. import random
  3. possibilities = ['rain', 'snow', 'sleet', fog', 'sun', 'who knows']
  4. return random.choice(possibilities)

同编程的其他方面一样,选择你所能理解的最清晰的风格。符合模块规范的命名(random.choice)更安全,但输入量略大。

这些 get_description() 的例子介绍了各种各样的导入内容,但没有涉及在什么地方进行导入——它们都在函数内部调用 import。我们也可以在函数外部导入 random

  1. >>> import random
  2. >>> def get_description():
  3. ... possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
  4. ... return random.choice(possibilities)
  5. ...
  6. >>> get_description()
  7. 'who knows'
  8. >>> get_description()
  9. 'rain'

如果被导入的代码被多次使用,就应该考虑在函数外部导入;如果被导入的代码使用有限,就在函数内部导入。一些人更喜欢把所有的 import 都放在文件的开头,从而使代码之间的依赖关系清晰。两种方法都是可行的。

5.3.2 使用别名导入模块

在主程序 weatherman.py 中,我们调用了 import report。但是,如果存在同名的另一个模块或者你想使用更短更好记的名字,该如何做呢?在这种情况下,可以使用别名 wr 进行导入:

  1. import report as wr
  2. description = wr.get_description()
  3. print("Today's weather:", description)

5.3.3 导入模块的一部分

在 Python 中,可以导入一个模块的若干部分。每一部分都有自己的原始名字或者你起的别名。首先,从 report 模块中用原始名字导入函数 get_description()

  1. from report import get_description
  2. description = get_description()
  3. print("Today's weather:", description)

用它的别名 do_it 导入:

  1. from report import get_description as do_it
  2. description = do_it()
  3. print("Today's weather:", description)

5.3.4 模块搜索路径

Python 会在什么地方寻找文件来导入模块?它使用存储在标准 sys 模块下的目录名和ZIP压缩文件列表作为变量 path。你可以读取和修改这个列表。下面是在我的 Mac 上 Python 3.3 的 sys.path 的内容:

  1. >>> import sys
  2. >>> for place in sys.path:
  3. ... print(place)
  4. ...
  5. LibraryFrameworks/Python.framework/Versions/3.3/lib/python33.zip
  6. LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3
  7. LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3/plat-darwin
  8. LibraryFrameworks/Python.framework/Versions/3.3/lib/python3.3/lib-dynload
  9. 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:

  1. from sources import daily, weekly
  2. print("Daily forecast:", daily.forecast())
  3. print("Weekly forecast:")
  4. for number, outlook in enumerate(weekly.forecast(), 1):
  5. print(number, outlook)

模块 1 是 boxes/sources/daily.py:

  1. def forecast():
  2. 'fake daily forecast'
  3. return 'like yesterday'

模块 2 是 boxes/sources/weekly.py:

  1. def forecast():
  2. """Fake weekly forecast"""
  3. return ['snow', 'more snow', 'sleet',
  4. 'freezing rain', 'rain', 'fog', 'hail']

还需要在 sources 目录下添加一个文件:init.py。这个文件可以是空的,但是 Python 需要它,以便把该目录作为一个包。

运行主程序 weather.py:

  1. $ python weather.py
  2. Daily forecast: like yesterday
  3. Weekly forecast:
  4. 1 snow
  5. 2 more snow
  6. 3 sleet
  7. 4 freezing rain
  8. 5 rain
  9. 6 fog
  10. 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(),但当键不存在时它会在字典中添加一项:

  1. >>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
  2. >>> print(periodic_table)
  3. {'Helium': 2, 'Hydrogen': 1}

如果键不在字典中,新的默认值会被添加进去:

  1. >>> carbon = periodic_table.setdefault('Carbon', 12)
  2. >>> carbon
  3. 12
  4. >>> periodic_table
  5. {'Helium': 2, 'Carbon': 12, 'Hydrogen': 1}

如果试图把一个不同的默认值赋给已经存在的键,不会改变原来的值,仍将返回初始值:

  1. >>> helium = periodic_table.setdefault('Helium', 947)
  2. >>> helium
  3. 2
  4. >>> periodic_table
  5. {'Helium': 2, 'Carbon': 12, 'Hydrogen': 1}

defaultdict() 也有同样的用法,但是在创建字典时,对每个新的键都会指定默认值。它的参数是一个函数。在本例中,把函数 int 作为参数传入,会按照 int() 调用,返回整数 0

  1. >>> from collections import defaultdict
  2. >>> periodic_table = defaultdict(int)

现在,任何缺失的值将被赋为整数 0

  1. >>> periodic_table['Hydrogen'] = 1
  2. >>> periodic_table['Lead']
  3. 0
  4. >>> periodic_table
  5. defaultdict(<class 'int'>, {'Lead': 0, 'Hydrogen': 1})

函数 defaultdict() 的参数是一个函数,它返回赋给缺失键的值。在下面的例子中,no_idea() 在需要时会被执行,返回一个值:

  1. >>> from collections import defaultdict
  2. >>>
  3. >>> def no_idea():
  4. ... return 'Huh?'
  5. ...
  6. >>> bestiary = defaultdict(no_idea)
  7. >>> bestiary['A'] = 'Abominable Snowman'
  8. >>> bestiary['B'] = 'Basilisk'
  9. >>> bestiary['A']
  10. 'Abominable Snowman'
  11. >>> bestiary['B']
  12. 'Basilisk'
  13. >>> bestiary['C']
  14. 'Huh?'

同样,可以使用函数 int()list() 或者 dict() 返回默认空的值:int() 返回 0list() 返回空列表([]),dict() 返回空字典({})。如果你删掉该函数参数,新键的初始值会被设置为 None

顺便提一下,也可以使用 lambda 来定义你的默认值函数:

  1. >>> bestiary = defaultdict(lambda: 'Huh?')
  2. >>> bestiary['E']
  3. 'Huh?'

使用 int 是一种定义计数器的方式:

  1. >>> from collections import defaultdict
  2. >>> food_counter = defaultdict(int)
  3. >>> for food in ['spam', 'spam', 'eggs', 'spam']:
  4. ... food_counter[food] += 1
  5. ...
  6. >>> for food, count in food_counter.items():
  7. ... print(food, count)
  8. ...
  9. eggs 1
  10. spam 3

上面的例子中,如果 food_counter 已经是一个普通的字典而不是 defaultdict 默认字典,那每次试图自增字典元素 food_counter[food] 值时,Python 会抛出一个异常,因为我们没有对它进行初始化。在普通字典中,需要做额外的工作,如下所示:

  1. >>> dict_counter = {}
  2. >>> for food in ['spam', 'spam', 'eggs', 'spam']:
  3. ... if not food in dict_counter:
  4. ... dict_counter[food] = 0
  5. ... dict_counter[food] += 1
  6. ...
  7. >>> for food, count in dict_counter.items():
  8. ... print(food, count)
  9. ...
  10. spam 3
  11. eggs 1

5.5.2 使用Counter()计数

说起计数器,标准库有一个计数器,它可以胜任之前或者更多示例所做的工作:

  1. >>> from collections import Counter
  2. >>> breakfast = ['spam', 'spam', 'eggs', 'spam']
  3. >>> breakfast_counter = Counter(breakfast)
  4. >>> breakfast_counter
  5. Counter({'spam': 3, 'eggs': 1})

函数 most_common() 以降序返回所有元素,或者如果给定一个数字,会返回该数字前的元素:

  1. >>> breakfast_counter.most_common()
  2. [('spam', 3), ('eggs', 1)]
  3. >>> breakfast_counter.most_common(1)
  4. [('spam', 3)]

也可以组合计数器。首先来看一下 breakfast_counter

  1. >>> breakfast_counter
  2. >>> Counter({'spam': 3, 'eggs': 1})

这一次,新建一个列表 lunch 和一个计数器 lunch_counter

  1. >>> lunch = ['eggs', 'eggs', 'bacon']
  2. >>> lunch_counter = Counter(lunch)
  3. >>> lunch_counter
  4. Counter({'eggs': 2, 'bacon': 1})

第一种组合计数器的方式是使用 +

  1. >>> breakfast_counter + lunch_counter
  2. Counter({'spam': 3, 'eggs': 3, 'bacon': 1})

你也可能想到,从一个计数器去掉另一个,可以使用 -。什么是早餐有的而午餐没有的呢?

  1. >>> breakfast_counter - lunch_counter
  2. Counter({'spam': 3})

那么什么又是午餐有的而早餐没有的呢 ?

  1. >>> lunch_counter - breakfast_counter
  2. Counter({'bacon': 1, 'eggs': 1})

和第 4 章中的集合类似,可以使用交集运算符 & 得到二者共有的项:

  1. >>> breakfast_counter & lunch_counter
  2. Counter({'eggs': 1})

两者的交集通过取两者中的较小计数,得到共同元素 'eggs'。这合情合理:早餐仅提供一个鸡蛋,因此也是共有的计数。

最后,使用并集运算符 | 得到所有元素:

  1. >>> breakfast_counter | lunch_counter
  2. Counter({'spam': 3, 'eggs': 2, 'bacon': 1})

'eggs' 又是两者共有的项。不同于合并,并集没有把计数加起来,而是取其中较大的值。

5.5.3 使用有序字典OrderedDict()按键排序

在前面几章的代码示例中可以看出,一个字典中键的顺序是不可预知的:你可以按照顺序添加键 abc,但函数 keys() 可能返回 cab。下面是第 1 章用过的一个例子:

  1. >>> quotes = {
  2. ... 'Moe': 'A wise guy, huh?',
  3. ... 'Larry': 'Ow!',
  4. ... 'Curly': 'Nyuk nyuk!',
  5. ... }
  6. >>> for stooge in quotes:
  7. ... print(stooge)
  8. ...
  9. Larry
  10. Curly
  11. Moe

有序字典 OrderedDict() 记忆字典键添加的顺序,然后从一个迭代器按照相同的顺序返回。试着用元组()创建一个有序字典:

  1. >>> from collections import OrderedDict
  2. >>> quotes = OrderedDict([
  3. ... ('Moe', 'A wise guy, huh?'),
  4. ... ('Larry', 'Ow!'),
  5. ... ('Curly', 'Nyuk nyuk!'),
  6. ... ])
  7. >>>
  8. >>> for stooge in quotes:
  9. ... print(stooge)
  10. ...
  11. Moe
  12. Larry
  13. Curly

5.5.4 双端队列:栈+队列

deque 是一种双端队列,同时具有栈和队列的特征。它可以从序列的任何一端添加和删除项。现在,我们从一个词的两端扫向中间,判断是否为回文。函数 popleft() 去掉最左边的项并返回该项,pop() 去掉最右边的项并返回该项。从两边一直向中间扫描,只要两端的字符匹配,一直弹出直到到达中间:

  1. >>> def palindrome(word):
  2. ... from collections import deque
  3. ... dq = deque(word)
  4. ... while len(dq) > 1:
  5. ... if dq.popleft() != dq.pop():
  6. ... return False
  7. ... return True
  8. ...
  9. ...
  10. >>> palindrome('a')
  11. True
  12. >>> palindrome('racecar')
  13. True
  14. >>> palindrome('')
  15. True
  16. >>> palindrome('radar')
  17. True
  18. >>> palindrome('halibut')
  19. False

这里把判断回文作为双端队列的一个简单说明。如果想要写一个快速的判断回文的程序,只需要把字符串反转和原字符串进行比较。Python 没有对字符串进行反转的函数 reverse(),但还是可以利用反向切片的方式进行反转,如下所示:

  1. >>> def another_palindrome(word):
  2. ... return word == word[::-1]
  3. ...
  4. >>> another_palindrome('radar')
  5. True
  6. >>> another_palindrome('halibut')
  7. False

5.5.5 使用itertools迭代代码结构

itertoolshttps://docs.python.org/3/library/itertools.html)包含特殊用途的迭代器函数。在for … in 循环中调用迭代函数,每次会返回一项,并记住当前调用的状态。

即使 chain() 的参数只是单个迭代对象,它也会使用参数进行迭代:

  1. >>> import itertools
  2. >>> for item in itertools.chain([1, 2], ['a', 'b']):
  3. ... print(item)
  4. ...
  5. 1
  6. 2
  7. a
  8. b

cycle() 是一个在它的参数之间循环的无限迭代器:

  1. >>> import itertools
  2. >>> for item in itertools.cycle([1, 2]):
  3. ... print(item)
  4. ...
  5. 1
  6. 2
  7. 1
  8. 2
  9. .
  10. .
  11. .

accumulate() 计算累积的值。默认的话,它计算的是累加和:

  1. >>> import itertools
  2. >>> for item in itertools.accumulate([1, 2, 3, 4]):
  3. ... print(item)
  4. ...
  5. 1
  6. 3
  7. 6
  8. 10

你可以把一个函数作为 accumulate() 的第二个参数,代替默认的加法函数。这个参数函数应该接受两个参数,返回单个结果。下面的例子计算的是乘积:

  1. >>> import itertools
  2. >>> def multiply(a, b):
  3. ... return a * b
  4. ...
  5. >>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
  6. ... print(item)
  7. ...
  8. 1
  9. 2
  10. 6
  11. 24

itertools 模块有很多其他的函数,有一些可以用在需要节省时间的组合和排列问题上。

5.5.6 使用pprint()友好输出

我们见到的所有示例都用 print()(或者在交互式解释器中用变量名)打印输出。有时输出结果的可读性较差。我们需要一个友好输出函数,比如 pprint()

  1. >>> from pprint import pprint
  2. >>> quotes = OrderedDict([
  3. ... ('Moe', 'A wise guy, huh?'),
  4. ... ('Larry', 'Ow!'),
  5. ... ('Curly', 'Nyuk nyuk!'),
  6. ... ])
  7. >>>

普通的 print() 直接列出所有结果:

  1. >>> print(quotes)
  2. OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'), ('Curly', 'Nyuk nyuk!')])

但是,pprint() 尽量排列输出元素从而增加可读性:

  1. >>> pprint(quotes)
  2. {'Moe': 'A wise guy, huh?',
  3. 'Larry': 'Ow!',
  4. 'Curly': 'Nyuk nyuk!'}

5.6 获取更多Python代码

有时标准库没有你需要的代码,或者不能很好地满足需求。有很多开源第三方 Python 软件供参考,如下所示:

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'] 的值。