Python笔记03:python中用import导入包的机制原理是什么?

简单地说,模块就是一个保存了Python代码的文件。模块能定义函数,类和变量,模块里也能包含可执行的代码。使用模块可以更加有逻辑地组织Python代码段,使代码更好用,更易懂。

为了组织好模块,会将多个模块分为。Python 处理包也是相当方便的,简单来说,包就是文件夹,但该文件夹下必须存在 __init__.py 文件。最简单的情况下,init.py 为空文件即可,当然它也可以执行包的一些初始化代码。

每个py文件被称之为模块,每个具有__init__.py文件的目录被称为包。只要模块或者包所在的目录在sys.path中,就可以使用import 模块或import 包来使用。

在使用一个模块中的函数或类之前,首先要导入该模块。模块的导入使用import语句,格式如下:

import module_name

调用模块的函数或类时,需要以模块名作为前缀,如下:

module_name.func()

如果不想在程序中使用模块名前缀符,可以使用from import语句从模块导入函数,如下:

from module_name import func
func()

上面的例子全部基于当前程序能够找到 module_name 这个模块的假设,下面先看几个简单的例子,对模块导入有一个大致的认识,然后深入探究模块导入的机制。

模块导入示例

同级目录下的调用,程序结构如下:

-- src
    |-- mod.py
    |-- test.py

若想在模块 test.py 中导入模块 mod, 则使用下面语句即可:

import mod
from mod import *

调用子目录下的模块,程序结构如下:

-- src
    |-- lib
    |    |-- mod.py
    |-- test.py

如果想在模块 test.py 中导入模块mod.py,可以在lib件夹中建立空文件__init__.py文件,然后像下面这样调用即可:

from lib.mod2 import *
import lib.mod2

调用上级目录下的文件,程序结构如下:

-- src
    |-- mod1.py
    |-- lib
    |    |-- mod2.py
    |-- sub
    |    |-- test.py

如果想在 test.py 中导入模块 mod1和mod2,则先将目录 src 加入到 sys.path,就可以导入mod1模块。然后在lib中建一个空文件__init__.py,就可以导入lib下的mod2模块,如下:

import sys
sys.path.append("..")
import mod1
import lib.mod2

项目中具体如何设置模块,如何设置导入,要考虑的问题比较多,具体可以参考极客学院的 模块与包 这篇文章。

导入机制探究

Python 提供了 import 语句来实现类库的引用,当我们执行一行 from package import module as mymodule 命令时,Python解释器会查找package 这个包的module模块,并将该模块作为 mymodule 引入到当前的工作空间。所以import语句主要是做了二件事:

  1. 查找相应的module
  2. 加载module到local namespace

在import的第一个阶段,主要是完成了查找要引入模块的功能。查找时首先检查 sys.modules (保存了之前import的类库的缓存),如果module没有被找到,则按照下面的搜索路径查找模块:

  1. .py 所在文件的目录
  2. PYTHONPATH 中的目录
  3. python安装目录,UNIX下,默认路径一般为/usr/local/lib/python/
  4. 3.x 中.pth 文件内容

其大致过程可以简化为:

def import(module_name):
    if module_name in sys.modules:
        return sys.modules[module_name]
    else:
        module_path = find(module_name)

        if module_path:
            module = load(module_path)
            sys.modules[module_name] = module
            return module
        else:
            raise ImportError

模块导入错误

在导入模块方面,可能会出现下面的情况:

  • 循环导入(circular imports)
  • 覆盖导入(Shadowed imports)

循环导入

如果你创建两个模块,二者相互导入对方,那么有可能会出现循环导入。例如创建 a.py如下:

import b

def a_test():
    print "in a_test"
    print b.x

a_test()

然后在同个文件夹中创建另一个模块,将其命名为b.py。

import a
x = 1

def b_test():
    print 'In test_b'
    a.a_test()

b_test()

当我们导入 a 模块时,会引发AttributeError,这是因为导入a时,在开始时导入 b 模块,而 b 模块调用 b_test 时需要a的 a_test,这时候 a 模块的 a_test 并没有成功加载。

注意不是所有的互相导入都会引发 AttributeError,官方文档这样说:

Circular imports are fine where both modules use the “import ” form of import. They fail when the 2nd module wants to grab a name out of the first (“from module import name”) and the import is at the top level. That’s because names in the 1st are not yet available, because the first module is busy importing the 2nd.

继续以上面的两个模块为例,如果将 a.py 和 b.py 改为下面代码,就不会出现循环导入的错误:

a.py

def a_test():
    import b
    print "in a_test"
    print b.x

a_test()

b.py

x = 1

def b_test():
    import a
    print 'In test_b'
    a.a_test()

b_test()

这样的话,先导入 a,再导入 b 的结果如下:

>>> import a
In test_b
in a_test
1
in a_test
1
>>> import b
>>> 

覆盖导入

当创建的模块与标准库中的模块同名时,如果导入这个模块,就会出现覆盖导入。举个例子,创建一个名叫math.py的文件,在其中写入如下代码:

import math

def square_root(number):
    return math.sqrt(number)

运行这个文件,你会得到以下信息(traceback):AttributeError: module 'math' has no attribute 'sqrt'。这是因为运行这个文件的时候,Python解释器首先在当前运行脚本所处的的文件夹中查找名叫math的模块。在这个例子中,解释器找到了我们正在执行的模块,试图导入它。但是我们的模块中并没有叫sqrt的函数或属性,所以就抛出了AttributeError。

==========================另一篇文章===================

 

模块与包

在了解 import 之前,有两个概念必须提一下:

  • 模块: 一个 .py 文件就是一个模块(module)
  • 包: __init__.py 文件所在目录就是包(package)

当然,这只是极简版的概念。实际上包是一种特殊的模块,而任何定义了 __path__ 属性的模块都被当做包。只不过,咱们日常使用中并不需要知道这些。

两种形式的 import

import 有两种形式:

  • import ...
  • from ... import ...

两者有着很细微的区别,先看几行代码。

1
2
3
from string import ascii_lowercase
import string
import string.ascii_lowercase

运行后发现最后一行代码报错:ImportError: No module named ascii_lowercase,意思是:“找不到叫 ascii_lowercase 的模块”。第 1 行和第 3 行的区别只在于有没有 from,翻翻语法定义发现有这样的规则:

  • import ... 后面只能是模块或包
  • from ... import ... 中,from 后面只能是模块或包,import 后面可以是任何变量

可以简单的记成:第一个空只能填模块或包,第二个空填啥都行。

import 的搜索路径

提问,下面这几行代码的输出结果是多少?

1
2
3
# foo.py
import string
print(string.ascii_lowercase)

是小写字母吗?那可不一定,如果目录树是这样的:

1
2
3
./
├── foo.py
└── string.py

foo.py 所在目录有叫 string.py 的文件,结果就不确定了。因为你不知道 import string 到底是 import 了 ./string.py 还是标准库的 string。为了回答这个问题,我们得了解一下 import 是怎么找到模块的,这个过程比较简单,只有两个步骤:

  1. 搜索「内置模块」(built-in module)
  2. 搜索 sys.path 中的路径

而 sys.path 在初始化时,又会按照顺序添加以下路径:

  1. foo.py 所在目录(如果是软链接,那么是真正的 foo.py 所在目录)或当前目录
  2. 环境变量 PYTHONPATH中列出的目录(类似环境变量 PATH,由用户定义,默认为空);
  3. site 模块被 import 时添加的路径1site 会在运行时被自动 import)。

import site 所添加的路径一般是 XXX/site-packages(Ubuntu 上是 XXX/dist-packages),比如在我的机器上是 /usr/local/lib/python2.7/site-packages。同时,通过 pip 安装的包也是保存在这个目录下的。如果懒得记 sys.path 的初始化过程,可以简单的认为 import 的查找顺序是:

  1. 内置模块
  2. .py 文件所在目录
  3. pip 或 easy_install 安装的包

回到前面的问题,因为 import string 是通过搜寻 foo.py 文件所在目录,找到 string.py 后 import 的,所以输出取决于 import string.py 时执行的代码。

相对 import 与 绝对 import

相对 import

当项目规模变大,代码复杂度上升的时候,我们通常会把一个一个的 .py 文件组织成一个包,让项目结构更加清晰。这时候 import 又会出现一些问题,比如:一个典型包的目录结构是这样的:

1
2
3
4
string/
├── __init__.py
├── find.py
└── foo.py

如果 string/foo.py 的代码如下:

1
2
3
# string/foo.py
from string import find
print(find)

那么 python string/foo.py 的运行结果会是下面的哪一个呢?

  • <module 'string.find' from 'string/find.py'>
  • <function find at 0x123456789>

按我们前面讲的各种规则来推导,因为 foo.py 所在目录 string/ 没有 string 模块(即 string.py),所以 import 的是标准库的 string,答案是后者。不过,如果你把 foo 当成 string 包中的模块运行,即 python -m string.foo,会发现运行结果是前者。同样的语句,却有着两种不同的语义,这无疑加重了咱们的心智负担,总不能每次咱们调试包里的模块时,都去检查一下执行的命令是 python string/foo.py 还是 python -m string.foo 吧?

相对 import 就是专为解决「包内导入」(intra-package import)而出现的。它的使用也很简单,from 的后面跟个 . 就行:

1
from .XXX import ...

比如:

1
2
3
4
# from string/ import find.py
from . import find
# from string/find.py import *
from .find import *

我们再看个复杂点的例子,有个包的目录结构长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
one/
├── __init__.py
├── foo.py
└── two/
    ├── __init__.py
    ├── bar.py
    └── three/
        ├── __init__.py
        ├── dull.py
        └── run.py

foo.pybar.pydull.py 中的代码分别是 print(1)print(2)print(3),并且 run.py 的代码如下:

1
2
3
4
from . import dull
from .. import bar
from ... import foo
print('Go, go, go!')

我们通过 python -m one.two.three.run 运行 run.py,可以看到 run.py 运行结果如下:

1
2
3
4
3
2
1
Go, go, go!

意思就是,from 后面出现几个 . 就表示往上找第几层的包。也可以将 run.py 改写成下面这样,运行结果是一样的:

1
2
3
4
from .dull import *
from ..bar import *
from ...foo import *
print('Go, go, go!')

好啦,相对 import 就介绍到这里,回到最初的问题。如果用相对 import,把 string/foo.py 改写成:

1
2
3
# string/foo.py
from . import find
print(find)

那么 python string/foo.py 和 python -m string.foo 的运行结果又是怎样呢?运行一下发现,两者的输出分别是:

1
2
3
4
Traceback (most recent call last):
  File "string/foo.py", line 1, in <module>
    from . import find
ValueError: Attempted relative import in non-package
1
<module 'string.find' from 'string/find.py'>

原因在于 python string/foo.py 把 foo.py 当成一个单独的脚本来运行,认为 foo.py 不属于任何包,所以此时相对 import 就会报错。也就是说,无论命令行是怎么样的,运行时 import 的语义都统一了,不会再出现运行结果不一致的情况。

绝对 import

绝对 import 和相对 import 很好区分,因为从行为上来看,绝对 import 会通过搜索 sys.path 来查找模块;另一方面,除了相对 import 就只剩绝对 import 了嘛 :) 也就是说:

  1. 所有的 import ... 都是绝对 import
  2. 所有的 from XXX import ... 都是绝对 import

不过,第 2 点只对 2.7 及其以上的版本(包括 3.x)成立喔!如果是 2.7 以下的版本,得使用

1
from __future__ import absolute_import

两者的差异

首先,绝对 import 是 Python 默认的 import 方式,其原因有两点:

  • 绝对 import 比相对 import 使用更频繁
  • 绝对 import 能实现相对 import 的所有功能

其次,两者搜索模块的方式不一样:

  • 对于相对 import,通过查看 __name__ 变量,在「包层级」(package hierarchy)中搜索
  • 对于绝对 import,当不处于包层级中时,搜索 sys.path

前面在介绍 sys.path 的初始化的时候,我在有个地方故意模棱两可,即:

foo.py 所在目录(如果是软链接,那么是真正的 foo.py 所在目录)或 当前目录

官方文档的原文是:

the directory containing the input script (or the current directory).

这是因为当模块处于包层级中的时候,绝对 import 的行为比较蛋疼,官方的说法是:

The submodules often need to refer to each other. For example, the surround module might use the echo module. In fact, such references are so common that the import statement first looks in the containing package before looking in the standard module search path. Thus, the surround module can simply use import echo or from echo import echofilter. If the imported module is not found in the current package (the package of which the current module is a submodule), the import statement looks for a top-level module with the given name.

但是在我的测试中发现,其行为可能是下面两者中的任意一种:

  • .py 文件所在目录
  • 当前目录

比如,对于目录结构如下的包:

1
2
3
4
5
6
7
8
father/
├── __init__.py
├── child/
│   ├── __init__.py
│   ├── foo.py
│   └── string.py
└── string/
    └── __init__.py

其中,foo.py 代码如下:

1
2
import string
print(string)

import string 真正导入的模块是:

versionpython -m child.foopython child/foo.py
2.7.11child/string.pychild/string.py
3.5.1string/__init__.pychild/string.py

如果将 foo.py 的代码改成(你可以 print(sys.path) 看看为什么改成这样):

1
2
3
4
import sys
sys.path[0] = ''
import string
print(string)

import 的模块就变成了:

versionpython -m child.foopython child/foo.py
2.7.11child/string.pystring/__init__.py
3.5.1string/__init__.pystring/__init__.py

为了避免踩到这种坑,咱们可以这样子:

  • 避免包或模块重名,避免使用 __main__.py
  • 包内引用尽量使用相对 import

import 的大致过程

import 的实际过程十分复杂,不过其大致过程可以简化为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def import(module_name):
    if module_name in sys.modules:
        return sys.modules[module_name]
    else:
        module_path = find(module_name)

        if module_path:
            module = load(module_path)
            sys.modules[module_name] = module
            return module
        else:
            raise ImportError

sys.modules 用于缓存,避免重复 import 带来的开销;load 会将模块执行一次,类似于直接运行。

Tips

  • import 会生成 .pyc 文件,.pyc 文件的执行速度不比 .py 快,但是加载速度更快
  • 重复 import 只会执行第一次 import
  • 如果在 ipython 中 import 的模块发生改动,需要通过 reload 函数重新加载
  • import * 会导入除了以 _ 开头的所有变量,但是如果定义了 __all__,那么会导入 __all__ 中列出的东西

参考资料:

https://loggerhead.me/posts/python-de-import-ji-zhi.html

https://github.com/xuelangZF/CS_Offer/blob/master/Python/Package.md

 

  • 26
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值