Python导包说明

Posted by LANG on April 30, 2020

模块与包

模块 : 一个 .py 文件就是一个模块(module)

: init.py 文件所在目录就是包(package)

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

import

import 有两种形式:

  • import … 后面只能是模块或包

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

import 的搜索路径

假设现在有foo.py模块,import时的搜索路径如下:

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

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

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

-m参数

python -m [module]

-m参数主要是将模块当做脚本来运行,该模块的详细路径可以不用给定,使用-m参数之后python会像import一样去搜索该模块,然后将模块像脚本一样的直接运行。如果只给定包名,那么该包下需要有__main__.py模块

-m参数跟定的module需要去掉.py后缀

相对导入

from . import 从当前目录
from .. import 从上级目录
from ... import 从上上级目录

相对导入一般用于具有包结构的代码中,用来保证包中的模块在相互引用时不会受到其他包的影响

常见错误

当直接运行有包含有相对导入的模块时,就会出现以下错误:

ValueError: Attempted relative import in non-package

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

当含有相对导入的时候,路径需要有相对引入的最顶层的路径

Example

code/
|__ __init__.py
|__test/
|  |__ __init__.py
|  |__foo.py
|__temp/
|  |__ __init__.py
|  |__bar.py
|__top.py

foo.py中的代码:

from ..temp import bar

top.py中的代码:

import test.foo

首先直接运行foo.py,会抛出上面说到的异常,此时foo.py的路径没有包含code。 接着直接运行top.py,发现并不会报错,这是因为运行top.py时code路径已经包含了,所以foo.py在相对导入的时候就可以找到上级目录了。

绝对导入

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

当我们要运行包里的一个模块时,例如我们要运行上面code包中的test子包中的foo.py模块,此时相对导入不管有,除了用-m参数之外,还可以手动将code的绝对路径加到foo.py模块中的sys.path中来:

import sys
sys.path.append('/../code')

两种导入方式的差异

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

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

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

  • 对于相对 import,通过查看 name 变量,在「包层级」(package hierarchy)中搜索 例如上面code包中,每个模块对应的名为: top.py:top foo.py:test.foo bar.py:temp.bar 所以在执行top.py的时候,foo.py中通过__name__(temp.bar)找到了bar

  • 对于绝对 import,当不处于包层级中时,搜索 sys.path

import 的大致过程

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

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 会将模块执行一次,类似于直接运行。

模块删除与重载

使用del [module]可以将某个import的模块从当前模块中删除,那么就不能引用到该模块了

由于重复 import 只会执行第一次 import,可以使用reload [module]可以再次import某个模块,相当于再运行一次该模块,也可以在模块代码改变后,reload将重新导入修改过的模块。类似于热更新。

可以通过dir()查看当前import过的module。

Tips

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