前言

在软件工程中,我们从大的宏观方向,要看业务目标、工程架构,到具体实施时就要选择适合工程实现的编程语言和配套组件。在选择编程语言时根据项目的不同,我们可能会有很多需要考虑的因素,从编程语言本身的角度来看,他们是“大同小异”的,但如果从差异角度看,每种编程语言除了语法体现不同外,执行方式、性能、内存管理、模块组织、组件、第三方库等又会有很大差异,但这些却又是我们要需要考虑的关键点。

下面我们聊一下python语言的模块引入方式,当我们知道了其引入方式,那么组织模块就是自己的事了~~

当然,本文中不一定会列出所有引入方式,我们只关注常用的、个人认为较好的方式。

在开始聊这个话题前,我们首先要区分的概念是 package、module、class,我们粗略总结:

1. package是一个层级的概念,它通常是一个目录,在package中可能会包含多个module;

2. module是一个文件(通常是package中的文件),在文件中我们可以定义类或函数等;

3. class 类,它被包含在module文件中;

 

然后,对于引用格式,我们做一个“感性”的总结就是:package要用“from”,module和class要用import,如:

from package import module

import package.module    # 注意,不能这样引用:import package.module.class

基本的模块引入方式

为方便举例说明,现假设有如下目录结构及文件:

$ tree .
├── package1
│   ├── module1.py
│   └── module2.py
└── test.py

$ cat package1/module1.py 
class class1:
    def hello(self):
        print('I am class1 in module1')

$ cat package1/module2.py 
class class2:
    def hello(self):
        print('I am class2 in module2')

$ cat test.py 
from package1 import module1  # 方式1
from package1.module2 import class2  # 方式2  注意,不能这样引入:import package1.module2.class2

module1.class1().hello()
class2().hello()

可以看到,test.py中展示了两种基本的引入模块和类的方式,并调用了hello方法。我们执行test.py,结果如下:

$ python3 test.py 
I am class1 in module1
I am class2 in module2

当然我们还可以用其他方式引入模块,例如我们修改test.py文件为 :

import package1.module1
import package1.module2
from package1 import *  # 注意,前面一定要有两个import语句,否则是不正确的

module1.class1().hello()
module2.class2().hello()

这种方式没有语法错误,但个人不建议用这种方式,因为不太直观。

进一步理解模块的概念

细心的你可以已经注意到,上面的目录结构中不能直接引入package1中的所有模块(from package1 import * ),这是为什么呢?

其实原因很简单:因为现在的package1还是一个普通的目录,还算不上是一个真正意义上的package,需要在package下放一个__init__.py文件来标识当前目录为一个package!

这里要说一下,我们在设计python程序的时候,只要是我们认为应该是一个package的目录都应该包含一个__init__.py文件(即使该文件为空)。

下面我们在package1目录下添加空的__init__.py文件,并修改test.py文件内容:

$ tree .
├── package1
│   ├── __init__.py
│   ├── module1.py
│   └── module2.py
└── test.py

$ cat test.py 
from package1 import *
module1.class1().hello()
module2.class2().hello()

如果此时执行test.py会发现依然报错!

这是因为,虽然此时package1是一个“package”了,但__init__.py文件内容为空(我们可以把__init__.py文件看做是一个package的“对外接口”,可以起到屏蔽“包”内细节的做用)。使用__init__.py文件一般需要注意以下两点:

1. __init__.py文件内一般使用 __all__ 关键字来限定package内对外的模块。

2. __init__.py文件尽量“轻”,即最好不要在里面定太多代码(如定义类等),虽然这样做不会报错。

下面通过修改__init__.py文件和test.py文件来分别说明__init__.py文件的使用方式:

$ cat package1/__init__.py 
__all__ = ['module1', 'module2']

$ cat test.py 
from package1 import *

module1.class1().hello()
module2.class2().hello()

# 运行脚本
$ python3 test.py 
I am class1 in module1
I am class2 in module2

当然,如果__init__.py文件内包含某个类,package外面也可以直接访问到,如:

$ cat package1/__init__.py 
__all__ = ['module1', 'module2']

class class3:
    def hello(self):
        print('I am class3 in __init__.py')


$ cat test.py 
from package1 import module1, module2, class3

module1.class1().hello()
module2.class2().hello()
class3().hello()


$ python3 test.py 
I am class1 in module1
I am class2 in module2
I am class3 in __init__.py

注意,test.py 文件中指明了需要引入的内容 (from package1 import module1, module2, class3),如果将这个引入方式改为全部引入(from package1 import * )则会报错,因为 import * 引入的package内容需要__init__.py文件中__all__关键字指定!

当然,如果要引入具体module的所有内容,可以通过 import * 的方式,如:from package1.module1 import *

更复杂的引用

我们可以想到,引用还有很多其他情况,如不同package间、不同目录下或不同层级目录下的相互引用等。

其实,所有的引用,在了解了以上原理后都是很好了解的,不过这里还要先提一下”环境变量“,关于环境变量概念和作用这里略过。python添加环境变量的方法:

sys.path.append('user_path')  # 其中 user_path 即我们要添加的路径

添加到环境变量中的路径,在本工程中各文件中都可以直接访问到。下面我们利用和之前相似的例子进行说明。

假设我们现在有一个和package1同级的包,package2,且package2中有一个moudle3.py文件,此时package1中的module2文件需要访问package2的module3.py,文件目录及内容如下:

$ tree .
├── package1
│   ├── __init__.py
│   ├── module1.py
│   └── module2.py
├── package2
│   ├── __init__.py
│   └── module3.py
└── test.py

$ cat package1/__init__.py 
__all__ = ['module1', 'module2']

$ cat package1/module1.py 
class class1:
    def hello(self):
        print('I am class1 in module1')

$ cat package1/module2.py 
from package2.module3 import class3  # 这里我们引用了package2的class3
class class2:
    def hello(self):
        print('I am class2 in module2')

$ cat package2/__init__.py 
__all__ = ['module3']

$ cat package2/module3.py 
class class3:
    def hello(self):
        print('I am class3 in module3 which of package2')

$ cat test.py 
import sys, os
curr_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(curr_dir)  # 这里我们添加了环境变量

from package1 import module1, module2

module1.class1().hello()
module2.class2().hello()
module2.class3().hello()  # 通过package1的module2引用到了package2中module3下的class3

请注意其中的注释。test.py 脚本运行结果如下:

$ python3 test.py 
I am class1 in module1
I am class2 in module2
I am class3 in module3 which of package2

 

根据我们工程的实际情况可能会有很多更复杂的情况,其原理是不变的。