在“远离” Python 很久之后,再回看之前的代码,感觉对 Python 的 import
语句还是存在困惑。总是对 Python 程序的结构不是很清楚,不知道什么时候直接用 import xxx
,什么时候又要用 from xxx import xxx
。正好借这篇文章,梳理一下。而且要写出实用的大型 Python 程序,也确实要理清楚这些细节。废话不多说,我们开始。
独立的程序
独立的 Python 程序存放在以 .py 为后缀的文件中,例如创建一个文件 test1.py,包含以下代码:
print("This standalone program works!")
如果要在文本终端或者终端窗口运行 Python,需要键入 Python 程序名,后面跟上程序的文件名:
$ python test1.py
This standalone program works!
命令行参数
通过 sys 模块,Python 程序可以很方便的获取到命令行传递给它的参数。例如创建文件 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 #从标准模块 random 中导入函数 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
:
- 主程序调用
import report
,然后运行report.get_description()
get_description()
函数调用from random import choice
,然后运行choice(possibilities)
第一种情况下,我们导入了整个 report 模块,但是需要把 report.
作为 get_description()
的前缀。在这个 import
语句之后,只要在名称前加 report.
,report.py 的所有内容(代码和变量)就会对主程序可见。通过模块名称限定模块的内容,可以避免命名冲突。因为其他模块可能也有函数 get_description()
,这样做不会被错误地调用。
第二种情况下,所有代码都在同一个函数下,并且没有其他名为 choice
的函数,所以我们直接从 random
模块导入函数 choice()
。
在函数 get_description()
中,我们直接在函数内部导入了 random 模块,我们也可以在函数外部导入:
from random import choice
def get_description():
possibilities = ['rain', 'snow', 'sleet', 'fog', 'sun', 'who knows']
return choice(possibilities)
如果被导入的代码被多次使用,就应该考虑在函数外部导入;如果被导入的代码使用有限,就在函数内部导入。一些人更喜欢把所有的 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 压缩文件。你可以读取和修改这个列表。下面是 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}
>>> 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)
>>> 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
是一个普通的字典,那每次试图自增字典元素 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() 计数
Python 标准库有一个计数器,它可以胜任很多工作:
>>> 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() 按键排序
有序字典 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 包含特殊用途的迭代器函数。在 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!'}
参考
Bill Lubanovic《Python语言及其应用》