Python的包及其导入

包是一种通过用 .模块名 来构造 Python 模块命名空间的方法。 例如,模块名 A.B 表示 A 包中名为 B 的子模块。正如模块的使用使得不同模块的作者不必担心彼此的全局变量名称一样,包 A 和包 B 下有同名模块也不会冲突,如 A.aB.a 来自两个命名空间。

注意事项

1、包是目录级的,目录是用来组成 py 文件的。包的本质就是一个包含 __init__.py 文件的目录

2、无论是 import 形式还是 from...import 形式,凡是在导入语句中(而不是在使用时),都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则是错误的。可以带有一连串的点,如 item.subitem.subsubitem,但都必须遵循点的左边都必须是一个包个原则。对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类等,它们都可以用点的方式调用自己的属性。

3、使用 import 导入文件时,产生的命名空间中的名字来源于文件;使用 import 导入包时,产生的命名空间的名字同样来源于文件,并且来源与对应的包下的 __init__.py 文件。

4、导入包本质就是在导入包下的 __init__.py 文件。在 Python3 中,即使包下没有 __init__.py 文件,使用 import 导入包仍然不会报错,而在 Python2 中,包下一定要有该文件,否则 import 导入包会报错。

5、创建包的目的不是为了直接去运行,而是用来被其他程序导入使用。包只是模块的一种组织形式而已,包即模块。

6、对比 import itemfrom item import name 的应用场景,如果我们想直接使用 name 那必须使用后者。

创建一个简单的包环境

文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|-- glance/                        # Top-level package
| |-- __init__.py # Initialize the glance package
| |
| |-- api # Subpackage for api
| | |-- __init__.py
| | |-- policy.py
| | `-- versions.py
| |
| |-- cmd # Subpackage for cmd
| | |-- __init__.py
| | `-- manage.py
| |
| `-- db # Subpackage for db
| |-- __init__.py
| `-- models.py
|
`-- test.py

我用的是 Linux 环境,因此写了 shell 脚本 mkWorkDir.sh 来自动生成环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

mkdir -pv glance/{api,cmd,db}

touch glance/__init__.py
touch glance/{api,cmd,db}/__init__.py

cat >| glance/api/policy.py << 'EOF'
# policy.py
def get():
print('from policy.py')


EOF

cat >| glance/api/versions.py << 'EOF'
# versions.py
def create_resource(conf):
print('from versions.py: ', conf)


EOF

cat >| glance/cmd/manage.py << 'EOF'
# manage.py
def main():
print('from manage.py')


EOF

cat >| glance/db/models.py << 'EOF'
# models.py
def register_models(engine):
print('from models.py: ', engine)


EOF

如果是 Windows 则使用如下代码创建目录,最后根据上述脚本中的文件内容粘贴进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os

os.makedirs('glance/api')
os.makedirs('glance/cmd')
os.makedirs('glance/db')

l = []
l.append(open('glance/__init__.py', 'w'))
l.append(open('glance/api/__init__.py', 'w'))
l.append(open('glance/api/policy.py', 'w'))
l.append(open('glance/api/versions.py', 'w'))
l.append(open('glance/cmd/__init__.py', 'w'))
l.append(open('glance/cmd/manage.py', 'w'))
l.append(open('glance/db/__init__.py', 'w'))
l.append(open('glance/db/models.py', 'w'))
map(lambda f: f.close(), l)

包的导入

我们在与包 glance 同级别的文件 test.py 中测试。包的用户可以从包中导入单个模块,例如:

1
import glance.db.models

这会加载子模块 glance.db.models 。但引用它时必须使用它的全名。

1
glance.db.models.register_models('mysql')

导入子模块的另一种方法是:

1
from glance.db import models

这也会加载子模块 models ,并使其在没有包前缀的情况下可用,因此可以按如下方式使用:

1
models.register_models('oracle')

另一种形式是直接导入所需的函数或变量:

1
from glance.db.models import register_models

同样,这也会加载子模块 models,但这会使其函数 register_models() 直接可用:

1
register_models('oracle')

当使用 from package import item 时,item 可以是包的子模块(或子包),也可以是包中定义的其他名称,如函数,类或变量。。 import 语句首先测试是否在包中定义了 item;如果没有,import 语句假定它是一个模块并尝试加载它。如果找不到它,则引发 ImportError 异常。要注意 from a import b.c 的形式是错误的语法

相反,当使用 import item.subitem.subsubitem 这样的语法时,除了最后一项之外的每一项都必须是一个包;最后一项可以是模块或包,但不能是前一项中定义的类或函数或变量。比如 import urllib.request.urlopen 中最后一项 urlopen 就它前一项 request 中定义的函数,这种导入就是错误的。

对于每个目录下的 __init__.py 文件,不管是哪种导入方式,只要是第一次导入包或者是包的任何其他部分,都会依次执行包下的 __init__.py 文件。这个文件可以为空,但是也可以存放一些初始化包的代码、作者、包版本等信息。

单独导入包

单独导入包名称时不会导入包中所有包含的所有子模块,如:

1
2
3
4
5
6
7
8
# 在与 glance 同级的 test.py 中
import glance
glance.cmd.manage.main()

'''
执行结果:
AttributeError: module 'glance' has no attribute 'cmd'
'''

解决方法是在每一个包的 __init__.py 中导入子包或子模块:

在顶级包 glance 中的 __init__.py 中导入子包 cmd

1
2
# glance/__init__.py
from . import cmd

在子包 cmd 中的 __init__.py 中导入模块 manage

1
2
# glance/cmd/__init__.py
from . import manage

外部程序的执行结果:

1
2
3
4
5
6
7
8
# 在与 glance 同级的 test.py 中
import glance
glance.cmd.manage.main()

'''
执行结果:
from manage.py
'''

再比如我们平常所说的内置模块 urllibjson,找到实际的存放路径后发现,实际上这两个模块就是包,但是在导入上有差异。json 的导入:

1
2
3
4
5
6
7
8
9
10
11
12
import json

data = {'name': 'Jerry', 'sex': 'male'}
print(json.dumps(data, indent=4))

'''
执行结果:
{
"name": "Jerry",
"sex": "male"
}
'''

urllib 的导入:

1
2
3
4
5
6
7
8
9
10
11
12
import urllib

url = 'http://www.baidu.com'
res = urllib.request.urlopen(url)
print(res)
'''
执行结果:
Traceback (most recent call last):
File "/Python3/test.py", line 20, in <module>
res = urllib.request.urlopen(url)
AttributeError: module 'urllib' has no attribute 'request'
'''

这是因为在 json 目录下的 __init__.py 中定义了 dump()dumps()load()loads() 等方法,导入 json 包就会自动导入 __init__.py ,所以可以直接使用 json.dumps() 方法。

而在 urllib 目录下的 __init__.py 文件内容为空,什么都没有定义。虽然可以正常导入包,但是这样的包直接导入后没有任何属性。所以这种情况的包,不应该直接导入,而是导入包中的模块来使用:

1
2
3
4
5
6
import urllib.request

url = 'http://www.baidu.com'

res = urllib.request.urlopen(url)
print(res)

或导入包中模块里定义的函数:

1
2
3
4
5
6
from urllib.request import urlopen

url = 'http://www.baidu.com'

res = urlopen(url)
print(res)

从包中导入 *

当用户写 from glance.api import * 会发生什么?理想情况下,人们希望这会以某种方式传递给文件系统,找到包中存在哪些子模块,并将它们全部导入。这可能需要很长时间,导入子模块可能会产生不必要的副作用,这种副作用只有在显式导入子模块时才会发生。

唯一的解决方案是让包作者提供一个包的显式索引。import 语句使用下面的规范:

如果一个包的 __init__.py 代码定义了一个名为 __all__ 的列表,它会被视为在遇到 from package import * 时应该导入的模块名列表。在发布该包的新版本时,包作者可以决定是否让此列表保持更新。包作者如果认为从他们的包中导入 * 的操作没有必要被使用,也可以决定不支持此列表。

1
2
3
4
5
glance/
`-- api
|-- __init__.py
|-- policy.py
`-- versions.py

例如文件 glance/api/__init__.py 可以包含以下代码:

1
2
3
4
5
6
7
8
9
# 在 __init__.py 中定义
x = 10


def func():
print('from api.__init__.py')


__all__ = ['x', 'func', 'policy']

此时我们在与 glance 同级的文件中执行 from glance.api import * 就导入 __all__ 中的内容, 而 versions 不在 __all__ 中,因此不能被导入。

如果没有定义 __all__,那么 from glance.api import * 语句 会从包 glance.api 中导入所有子模块到当前命名空间;它只确保导入了包 glance.api (可能运行任何在 __init__.py 中的初始化代码),然后导入包中定义的任何名称。

虽然某些模块被设计为在使用 from ... import * 时只导出遵循某些模式的名称,但在生产代码中它仍然被认为是不好的做法。请记住,使用 from package import specific_submodule 没有任何问题! 实际上,除非导入的模块需要使用来自不同包的同名子模块,否则这是推荐的写法。

绝对导入与相对导入

我们的最顶级包 glance 是写给别人用的,然后在 glance 包内部也会有彼此之间互相导入的需求,这时候就有绝对导入和相对导入两种方式:

  • 绝对导入:以 glance 作为起始。

  • 相对导入:用 . 或者 .. 的方式最为起始(只能在一个包中使用,不能用于不同目录内)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|-- glance/                        # Top-level package
| |-- __init__.py # Initialize the glance package
| |
| |-- api # Subpackage for api
| | |-- __init__.py
| | |-- policy.py
| | `-- versions.py
| |
| |-- cmd # Subpackage for cmd
| | |-- __init__.py
| | `-- manage.py
| |
| `-- db # Subpackage for db
| |-- __init__.py
| `-- models.py
|
`-- test.py

例如:我们在 glance/api/version.py 中想要导入 glance/cmd/manage.py

绝对导入

1
2
from glance.cmd import manage
manage.main()

相对导入

1
2
from ..cmd import manage
manage.main()

然后在 test.py 中测试这两种导入:

1
2
3
from glance.api import versions

versions.manage.main()

注意:可以用 import 导入内置或者第三方模块(已经在 sys.path 中),但是要绝对避免使用 import ... 的方式来导入自定义包的子模块(没有在 sys.path 中),而应该使用 from ... import ... 的绝对或者相对导入,且包的相对导入只能用 from 的形式。

比如我们想在 glance/api/versions.py 中导入 glance/api/policy.py,有可能你看到这两个模块是在同一个目录下,就直接这么来处理:

1
2
3
4
5
# 在 version.py 中

import policy

policy.get()

没错,我们单独运行 version.py 是一点问题没有的,运行 version.py 的路径搜索就是从当前路径开始的,于是在导入 policy 时能在当前目录下找到。

但是,子包中的模块 version.py 极有可能是被一个 glance 包同一级别的其他程序导入,比如在和 glance 同级下的一个 test.py 文件中导入 version.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在与 glance 同级的 test.py 中

from glance.api import versions


'''
执行结果:
Traceback (most recent call last):
File "/Python3/test.py", line 12, in <module>
from glance.api import versions
File "/Python3/glance/api/versions.py", line 8, in <module>
import policy
ModuleNotFoundError: No module named 'policy'
'''

这是因为,此时我们在 test.py 中导入 versions, 在 versions.py 中执行 import policy 需要找从 sys.path 也就是从当前目录(test.py 所在的目录)下找 policy.py,明显是无法找到的。

所以,在 glance/api/versions.py 中应该将 import policy 改为 from glance.api import policyfrom . import policy

两种导入方式的特点:

使用绝对路径导入,不管在包内部还是外部导入了就能使用,好处是非常直观。缺点是但是一旦包的位置或目录结构发生了变化,就无法按照原有的路径正常导入,必须跟着路径一并发生修改,维护起来非常麻烦。

使用相对路径导入,可以随意移动包,只要能找到包的位置,就可以使用包里的模块,并且包里的子包也可以正常使用其它兄弟包的子模块的内容。缺点是使用了相对路径后就不能在包内直接执行子模块了。因为相对导入是基于当前模块的名称 __name__ 进行导入的:直接执行 .py 文件时 __name__ == __main__,而在外部程序导入子包(或模块)的时候, __name__ 是包(或模块)所在的目录。

例如直接执行子包里的 versions.py__name__ == __main__,此时的 import 就无法从 __main_ 中正常导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# versions.py
def create_resource(conf):
print('from versions.py: ', conf)


# 在 version.py 中

print('__name__ = ', __name__)

from . import policy

policy.get()
'''
__name__ = __main__
Traceback (most recent call last):
File "/Python3/glance/api/versions.py", line 10, in <module>
from . import policy
ImportError: cannot import name 'policy' from '__main__' (/Python3/glance/api/versions.py)
'''

执行和 glance 同级下的一个 test.py 文件时,子模块中的 . 指的就是从该模块的 __name__ 所在的目录导入,glance.api.versions 所在的目录就是 glance.api,因此导入 policy 正常:

1
2
3
4
5
6
7
8
9
10
11
12
# 在与 glance 同级的 test.py 中

from glance.api import versions

versions.policy.get()

'''
执行结果:
__name__ = glance.api.versions
from policy.py
from policy.py
'''
有钱任性,请我吃包辣条
0%