对自定义包的引用

需求驱动学习。

前言

这篇文章是包和单元测试的姊妹篇,内容是如何在自己的工程中导入自定义包,而不出现导入错误。

在文章:包和单元测试中,已经叙述了如何单元测试的导入问题,本质上讲,只要导入的模块在搜索路径中,python就可以发现该模块。也验证了python命令会将运行文件所在目录加到sys.path中,而python -m unittest命令,将运行命令所在目录加入到sys.path中。

实验

为了写笔记,该系列实验仍然在Windows上进行,使用Python 2.7。

1. 同级目录引用自定义包

目录结构

如下:

1
2
3
4
5
6
7
8
9
my_project
./foo
./__init__.py
./bar.py
./tests
./__init__.py
./test_foo.py
./test_bar.py
./reference_foo_bar.py
  • my_project是项目目录
  • foo是包目录
  • tests是对包的单元测试
  • reference_foo_bar.py是与包目录同级的工程文件,即同在my_project下。

各文件内容

  • 两个__init__.py文件、test_foo.py都为空

  • bar.py内容:

1
2
def dumb_true():
return True
  • test_bar.py内容如下,但今天的实验中不会用到单元测试。
1
2
3
4
5
6
7
8
9
10
11
import unittest

from foo import bar

class TestBar(unittest.TestCase):
def test_bar_true(self):
self.assertTrue(bar.dumb_true())


if __name__ == "__main__":
unittest.main()
  • reference_foo_bar.py内容如下:
1
2
3
4
5
6
from foo import bar

if bar.dumb_true():
print "Hi, we can import foo and use it."
else :
print "Hi, we also imported foo but something wrong."

运行测试

测试命令如下:

1
2
cd new_project
python reference_foo_bar.py

测试结果如下:

1
Hi, we can import foo and use it.

太棒了,这是一个好的征兆,我们成功引用了模块foo.bar下的dumb_true函数,如果不明白原理,请看姊妹篇文章:包和单元测试

2. 不同目录引用自定义包

我们使用的标准库和第三方库,都是这种情况,因为这些包都不在我们工程的目录下。本质上讲,他们也都是自定义的,只不过在安装他们的时候,将他们所在的目录,放到了Python的搜索路径中,即sys.path

我们本实验中自定义的包,指我们自己写的工具包,这样我们可以在自己项目中的各处都可以使用。

目录结构

本实验目录结构如下,

1
2
3
4
5
6
7
8
9
10
my_project
./foo
./__init__.py
./bar.py
./tests
./__init__.py
./test_foo.py
./test_bar.py
./sub_project
./reference_foo_bar.py

建立新目录sub_project,并将reference_foo_bar.py移至此目录。

运行测试

1
2
cd my_project
python subproject\reference_foo_bar.py #linux 下用: python subproject/reference_foo_bar.py

得错误信息:找不到模块foo

1
2
3
4
Traceback (most recent call last):
File "sub_project\reference_foo_bar.py", line 1, in <module>
from foo import bar
ImportError: No module named foo

问题来了

当前的搜索路径中包含...\sub_project,在本目录下是找不到foo的。

怎样才能让Python搜索到,我们自定义的包foo呢,

方案1:安装我们自定义的包

模仿我们安装的标准库,与第三方的包,我们可以为foo写一个setup.py,然后安装它,这样Python永远都能找到它,任何工程也都能导入它,但是我们的包不完善,需要经常修改,并且我们这个包,也仅仅适用于我们当前的工程,所以这并不是一个理想的选择。

打包的教程在此:有兴趣者,请戳

方案2:在每个文件中,修改sys.path

在每个文件中,都将foo所在的目录的绝对路径添加到sys.path

1
2
import sys
sys.path.append(absolute_path)

但这样也存在明显的缺陷,丑陋而繁琐。

方案3:使用相对导入

这是一个馊主意。

相对导入只在包下才能工作,所以把my_project变成包,然后使用相对导入。

py_project下加入__init__.py

修改reference_foo_bar .py的内容为:

1
2
3
4
5
6
from ..foo import bar

if bar.dumb_true():
print "Hi, we can import foo and use it."
else :
print "Hi, we also imported foo but something wrong."

运行相对导入要掌握正确的姿势,不然,蛋碎。

在new_project的父目录运行:

1
python -m new_project.sub_project.reference_foo_bar

运行成功。。。但这是一个馊主意,我们总不能把我们所有的项目都搞成包吧。包可以是项目,但项目不是包。

所以,放弃该方法。

参考资料

  1. 导入原理:
    http://docs.python-guide.org/en/latest/writing/structure/

  2. -m 原理