1 模块简介
作为一个Python初学者,你首先要学会的知识就是如何引入其它模块或者包。但是,我发现有些开发者虽然使用Python很多年,依然不了解Python引入机制的灵活性。这篇文章,我们就会研究以下的主题:
- 常规的引入
- 使用from
- 相对引入
- 选择性引入
- 局部引入
- 引入的陷阱
2 模块使用
2.1 常规的引入
常规的引入,最常见的形式如下,
import sys
你所需要做的就是使用关键字"import",然后指定你实际想要引入的模块或者包。import最友好的方式就是它支持多个包同时引入,如下,
import os,sys,time
尽管这种方式节省空间,但是它与Python编码规范中将每个引入作为单独一行这一建议相冲突。
你在引入一个模块时,你想重命名,Python支持如下方式,
import sys as system
print(sys.platform)
这段代码简单地将我们的引入重命名为"system"。我们可以用这个新模块名字,按照之前调用sys的方式调用这个模块的所有方法。当然,通过使用点号,也可以引入特定的的子模块,例如,
import urllib.error
你可能不经常看到这种引入方式,但是这种方式很容易理解。
2.2 使用"from module import something"
可能有很多次,你仅仅想引入模块或者库的一部分,让我们看看Python是如何完成这个任务的,
from functools import lru_cache
上述代码允许你直接调用lru_cache。如果你已经按照正常方式引入functools,那么你也可以按照如下方式调用lru_cache,
functools.lru_cache(* args)
取决于你想要做什么,上述调用方式或许更好。在复杂的代码基线中,了解方法是从哪里引入,是非常重要。当然,如果你的代码维护的很好,并且模块化程度很高,那么从一个模块引入部分方法,就很方便。
当然,你也可以使用from方法,引入所有的方法,例如,
from os import *
这种方式在一些场景中使用,是非常方便。但是它经常让你的命名空间变得混乱。你可能定义了一个函数或者最高级别的变量,却和你所引入的项拥有相同的名字,如果你想使用os模块的项,实际上使用的是你所定义的项。最终你的程序会以令人困惑的逻辑错误结束。Tkinter模块是我唯一推荐可以全部引入的标准库。
如果你碰巧编写过你自己的模块或者包,有些人推荐你在__init__.py文件中引入所有的项,以便你的模块或者包更容易使用。我个人更推荐显式而非隐式的调用。
你可能也遇到同一个包引入多个项,例如
from os import path,walk,unlink
from os import uname,remove
上述代码,我们从os模块中引入5个函数。你也会注意到,我们可以多次引入相同的模块。如果你愿意,你可以使用括号来引入多个项,
from os import (path,walk,unlik,uname,
remove,rename)
这是一个有用的技巧,你也可以使用另一种方式,
from os import path,walk,unlik,uname, \
remove,rename
上述代码中反斜杠就是Python的中行连接符,这意味着这一行代码与下一行代码相连。
2.3 相对引入
PEP 328介绍了相对引入是如何产生的,以及选择什么样的特定语法。背后的思想主要就是使用句点来决定如何相对引入其他的模块或者包。主要原因是为了避免意外覆盖掉标准库。我们使用PEP 328建议的目录结构,看看我们是否能够让它正常工作。首先,在你的磁盘上创建文件和目录如下,
zhb@zhb-H81-M1:~/Desktop/work/my_package$ tree
.
├── __init__.py
├── __init__.pyc
├── module_a.py
├── subpackage1
│ ├── __init__.py
│ ├── module_x.py
│ ├── module_y.py
└── subpackage2
├── __init__.py
└── module_z.py
在最顶层的__init__.py文件中,输入如下代码,
from . import subpackage1
from . import subpackage2
在subpackage1子目录中的__init__.py中,输入如下代码,
from . import module_x
from . import module_y
编辑module_x.py文件,输入如下代码,
from .module_y import spam as ham
def main():
ham()
编辑module_y.py文件,输入如下代码,
def spam():
print("spam " * 3)
打开终端,进入到包含my_package的目录下,不要直接进入到my_poackage目录。在这个目录中运行Python解释器,我使用的是ipython,因为它的自动补全很好用,
In [1]: import my_package
In [2]: my_package.subpackage1.module_x
Out[2]: <module 'my_package.subpackage1.module_x' from 'my_package/subpackage1/module_x.pyc'>
In [3]: my_package.subpackage1.module_x.main()
spam spam spam
当你转入包时,相对引入这个机制很棒。如果你创建了很多相关的代码,这可能是你要选择方法。你将会发现相对引入在PyPI中的很多包中使用。当然,如果你需要引入的层数多于一层,那么你需要添加点号。但是,根据PEP 328,你的引入一般不会超过两层。
如果你在module_x.py中添加“if __name__ == "__main__"”,然后尝试着运行它,你就会以一个非常困惑的错误结束,代码如下,
from .module_y import spam as ham
def main():
ham()
if __name == "__main__":
# This won't work
main()
进入到subpackage1目录,运行如下命令,
python module_x.py
如果你使用的是Python2,你将会在终端上看到如下错误,
Traceback (most recent call last):
File "module_x.py", line 1, in <module>
from .module_y import spam as ham
ValueError: Attempted relative import in non-package
这意味着module_x.py是一个包中的模块,你尝试将它作为一个脚本运行,这个与相对引入不兼容。
如果你想在你的代码(其他路径下)中使用这个模块,你可以将它添加到Python的引入搜索路径中,如下所示,
import sys
sys.path.append("path/to/folder/containing/my_package")
import my_package
记住,你添加的路径是包含my_package的目录,而不是my_package本身的目录,原因就是my_package就是这个包,如果你把my_package本身的目录添加进去,你在使用这个包的时候,就会出现问题。
2.4 选择性引入
当你想要使用一个模块或者包,如果它不存在,你又想要回退,这时候,可以考虑选择性引入。你可能使用选择性引入是为了支持软件的多版本或者加速。下面的例子将会展示你如何使用选择性引入去支持不同的Python版本,
try:
# For Python 3
from http.client import responses
except ImportError: # For Python 2.5-2.7
try:
from httplib import responses
except ImportError: # For Python 2.4
from BaseHTTPServer import BaseHTTPRequestHandler as _BHRH
responses = dict([(k,v[0]) for k,v in _BHRH.responses.items()])
lxml包也使用了选择性引入,
try:
from urlparse import urljoin
from urllib2 import urlopen
except ImportError:
# Python 3
from urllib.parse import urljoin
from urllib.request import urlopen
正如你所看到的,选择性引入有着很好的作用,可用于增加指令集合。
2.5 局部引入
局部引入是指你在局部作用域引入一个模块。当你在Python代码的最上层引入,也就是在全局域中引入这个模块,这意味着后面的任何函数或者方法都可以使用这个模块,让我们来看看局部引入是如何工作的,实例如下,
import sys
def square_root(a):
import math
return math.sqrt(a)
def my_pow(base_num,power):
return math.pow(base_num,power)
if __name__ == "__main__":
print(square_root(49))
print(my_pow(2,3))
控制台输出,
Traceback (most recent call last):
File "/home/zhb/workspace/PythonTest/test.py", line 14, in <module>
print(my_pow(2,3))
File "/home/zhb/workspace/PythonTest/test.py", line 10, in my_pow
return math.pow(base_num,power)
NameError: global name 'math' is not defined
7.0
这里,我们在全局域引入了sys模块,但实际上我们并没有使用它。在square_root函数中,我们将Python的math模块引入到这个函数的局部域中,这意味着,math模块只有在square_root函数中可以被使用。如果我们在my_pow函数中使用math模块,我们将会得到一个NameError。
使用局部引入的一个好处就是你可能会使用一个模块,但是这个模块下载时需要很长的时间,你可以将它放入一个调用不频繁的函数中,而不是放在模块的全局域中。这个主要取决于你想要做什么。坦白而言,我几乎没有将模块引入到局部域中,主要是因为如果引入分散在模块中,它很难清楚地告诉你将要发生什么。一般而言,所有的引入应该放在模块的顶部。
2.6 引入的陷阱
程序员会陷入一些常见的引入陷阱之中,主要是两类,
- 循环引入
- 覆盖引入
循环引入
当你创建两个相互引入的模块,就会发生循环引入。让我们通过一个实例来解释我刚才所提及的概念,在到a.py中输入如下代码,
# a.py
import b
def a_test():
print("in a_test")
b.b_test()
a_test()
在同一目录下创建另一个文件b.py,在b.py中输入如下代码,
# b.py
import a
def b_test():
print("in b_test")
a.a_test()
b_test()
分别在控制台中执行,
python a.py
python b.py
控制台输出,
zhb@zhb-H81-M1:~/Desktop/work/my_package$ python a.py
in a_test
Traceback (most recent call last):
File "a.py", line 2, in <module>
import b
File "/home/zhb/Desktop/work/my_package/b.py", line 2, in <module>
import a
File "/home/zhb/Desktop/work/my_package/a.py", line 8, in <module>
a_test()
File "/home/zhb/Desktop/work/my_package/a.py", line 6, in a_test
b.b_test()
AttributeError: 'module' object has no attribute 'b_test'
zhb@zhb-H81-M1:~/Desktop/work/my_package$ python b.py
in b_test
Traceback (most recent call last):
File "b.py", line 2, in <module>
import a
File "/home/zhb/Desktop/work/my_package/a.py", line 2, in <module>
import b
File "/home/zhb/Desktop/work/my_package/b.py", line 8, in <module>
b_test()
File "/home/zhb/Desktop/work/my_package/b.py", line 6, in b_test
a.a_test()
AttributeError: 'module' object has no attribute 'a_test'
主要是因为每个模块互相引入对方。模块a想引入模块b,由于模块b又在尝试引入模块a,而模块a已经被执行。所以你需要重构你的代码,以避免这类错误的发生。
覆盖引入
当程序员创建一个和Python模块相同名字的时候,就会发生覆盖引入。我们来构建一个人为的例子,在这个案例中,创建一个名为math.py的文件,输入如下代码,
import math
def square_root(number):
return math.sqrt(number)
square_root(72)
尝试着运行代码,你会得到如下错误信息,
zhb@zhb-H81-M1:~/Desktop/work/my_package$ python math.py
Traceback (most recent call last):
File "math.py", line 1, in <module>
import math
File "math.py", line 6, in <module>
print(square_root(72))
File "math.py", line 4, in square_root
return math.sqrt(number)
AttributeError: 'module' object has no attribute 'sqrt'
这里发生了什么?当你运行这段代码的时候,Python首先要找的模块是"math"是在当前脚本所在的目录。这种情况下,它找到的模块就是我们正在运行和试着去使用的 math.py。但是我们的模块并没有一个函数或者属性叫做sqrt,所以就会报出一个AttributeError错误。
2.7 总结
在这篇文章中,我们已经介绍了很多关于引入的知识,但是依然还有很多关于Python的引入系统的知识。PEP 302介绍了引入钩子(import hooks),允许你做一些更酷的事情,例如,直接从github上引入。Python还有一个importlib的标准库,值得查看学习。当然,你也可以多看看别人的源码,不断挖掘和学习更多有用的妙招。