Python 的 import 机制

本文转载于https://loggerhead.me/posts/python-de-import-ji-zhi.html#_1

一直对 Python 的 import 机制不甚了解,这次在写 Easy-Karabiner 的时候就踩坑了,顺便了解了一下,发现这玩意儿还不是那么「符合直觉」,遂写篇博客讲讲。

  • 模块与包
  • 两种形式的 import
  • import 的搜索路径
  • 相对 import 与 绝对 import
    • 相对 import
    • 绝对 import
    • 两者的差异
  • import 的大致过程
  • Tips
  • 参考

模块与包

在了解 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__ 中列出的东西

参考


  1. 官方说法是「Python 安装时设定的默认路径」(The installation-dependent default path),而这玩意儿实际上是通过 site 模块来设置的。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python中,import语句用于引入其他模块或包中的功能。通过import,我们可以将其他模块中的函数、类和变量等引入到当前的代码中以供使用。 根据引用和引用的信息,当项目结构复杂时,import可能会出现错误。其中最常见的错误是"ImportError: attempted relative import with no known parent package"。这个错误表示在使用相对导入时,没有找到已知的父包。 为了理解问题的本质,我们需要了解import的工作机制。在引用中提供了一个示例项目目录,我们将以此为例来说明。 在示例项目目录中,有一个名为pakg1的包,包中包含有__init__.py文件和module1.py、module2.py两个模块,还有一个名为pakg1_1的子包,子包中也包含有__init__.py文件。另外,还有一个名为pakg2的包,同样包含有__init__.py文件。 首先,我们来讨论绝对导入。绝对导入是指通过完整的包路径来引入其他模块或包中的功能。例如,如果我们想在entrance1.py中引入pakg1包中的module1模块,可以使用以下语句: ```python from pakg1 import module1 ``` 这里的from关键字表示从某个包或模块中引入功能,pakg1表示包的路径,module1表示要引入的模块名。这样,我们就可以在entrance1.py中使用pakg1.module1中的功能了。注意,为了使包被识别为一个包,需要在包目录下添加一个名为__init__.py的空文件。 绝对导入可以确保我们引入的功能来自于我们指定的包或模块,避免了可能的命名冲突。同时,绝对导入也更易于理解和维护。 在这个问题中,并没有具体提到相对导入的情况。但相对导入是指通过"."来引入当前包或模块中的其他功能。例如,如果我们想在pakg1.module1中引入pakg1_1子包中的某个模块,可以使用以下语句: ```python from .pakg1_1 import module3 ``` 这里的"."表示当前包或模块的路径,pakg1_1表示要引入的子包名,module3表示要引入的模块名。相对导入可以简化代码,不需要指定完整的包路径,但需要注意相对导入只能在包内部使用。 综上所述,Pythonimport语句可以用于引入其他模块或包中的功能。绝对导入通过完整的包路径引入功能,相对导入通过"."来引入当前包或模块中的功能。使用import语句时需要注意包的结构和导入方式,以避免出现报错。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [python import详解](https://blog.csdn.net/qq_34062683/article/details/124672435)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值