Python的模块、包和标准库

Published on 2017 - 01 - 22

独立的程序

到目前为止,我们已经学会在 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!

命令行参数

在你的计算机中,创建文件 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']

模块和import语句

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

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

导入模块

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 都放在文件的开头,从而使代码之间的依赖关系清晰。两种方法都是可行的。

使用别名导入模块

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

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

导入模块的一部分

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

模块搜索路径

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

>>> import sys
>>> for place in sys.path:
...        print(place)
...
/Library/Frameworks/Python.framework/Versions/3.3/lib/python33.zip
/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3
/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/plat-darwin
/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/lib-dynload
/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/site-packages

最开始的空白输出行是空字符串 '',代表当前目录。如果空字符串是在 sys.path 的开始位置,Python 会先搜索当前目录:import report 会寻找文件 report.py。

第一个匹配到的模块会先被使用,这也就意味着如果你在标准库之前的搜索路径上定义一个模块 random,就不会导入标准库中的 random 模块。

我们已使用过单行代码、多行函数、独立程序以及同一目录下的多个模块。为了使 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

Python标准库

Python 的一个显著特点是具有庞大的模块标准库,这些模块可以执行很多有用的任务,并且和核心 Python 语言分开以避免臃肿。当我们开始写代码时,首先要检查是否存在想要的标准模块。在标准库中你会经常碰到一些“珍宝”!

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

使用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})

可以使用交集运算符 & 得到二者共有的项:

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

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

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

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

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

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

一个字典中键的顺序是不可预知的:你可以按照顺序添加键 a、b 和 c,但函数 keys() 可能返回 c、a 和 b。下面是一个例子:

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

双端队列:栈+队列

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

使用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 模块有很多其他的函数,有一些可以用在需要节省时间的组合和排列问题上。

使用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!'}

参考文档