引言
对于初学 python,或多或少在 import 一个 module 时遇到过 ImportError: attempted relative import with no known parent package
这样的错误信息。对于初学 python,遇到这样的问题是因为在执行 python xxx.py
程序时,xxx.py
程序中 import 了其他 package 下的 module,导致了 xxx.py
程序中的 import 不能正确查找到所需要 module 路径,这背后的原理是什么呢?本文通过资料查询和实际测试,对 python 中的 import 的机制进行梳理总结。
基本概念
当使用 python 进行一些工程化的工作时,对代码的组织就非常重要了。在组织 python 的程序文件时,最重要的两个概念就是 package 和 module 了。下面对这两个概念进行解释说明。
module
An object that serves as an organizational unit of Python code. Modules have a namespace containing arbitrary Python objects. Modules are loaded into Python by the process of importing.
上述摘自 python 官文文档中对 module 的解释。可以从两个方面来理解,一方面,module 是组织 python 程序文件的最小单位,通常一个 .py 文件就是一个 module;当然不是只有 .py 文件能作为 module,其他程序文件提供给 python 程序 import 也能作为 module。另一方面,一个 module 是一个命名空间,在该命名空间下可以包含许多任意的 python 对象。
package
A Python module which can contain submodules or recursively, subpackages. Technically, a package is a Python module with a
__path__
attribute.
上述摘自 python 官文文档中对 package 的解释,我们也可以从两个方面来理解。一方面,package 也是一个 module,可以用来被 import,此外 package 具有 __path__
熟悉,而 module 没有,下文会稍作解释。另一方面,package 是管理组织 .py 程序文件比 module 更大一级的单位,一个 package 中可以有多个 module 和多个子 package。
import 在背后做了哪些工作
我们通常在编写 python 程序时,会在文件的头部使用 import 来导入我们在编写程序过程中所需要的 builtin module 或者是安装的第三方 module,这样就可以在程序文件中使用导入的 module 提供的功能了。看一下官文文档中对 import 的描述。
The process by which Python code in one module is made available to Python code in another module.
那在 import 的背后,程序做了哪些工作?在了解了这些背后的细节后,就自然揭开了 ImportError: attempted relative import with no known parent package
错误的面纱。
首先解密当执行到 import 语句时,干的“第一件”事是什么?
import 的作用是导入 module,因此当执行到 import 语句时,“第一件事”就是查找 module,看看能够查找到指定的 module 名。import 在 sys.path 中查找 module,sys.path 是一个由字符串组成的列表,用于指定模块的搜索路径,默认的搜索路径如下所示。按照sys.path列表中元素顺序进行搜索,搜索到第一个满足条件的就不再往下搜索。
而对于 python xxx.py
执行程序,会在 sys.path 的首位置添加 xxx.py 所在的目录的绝对路径。如下所示:
在完成 import 的第一步工作后,就开始执行 “真正的” import 动作了。又因 import 的对象是 module,而 package 也是一种特殊的 module,而对于 import package 和 import module 在细节上是不同的,下面先来了解 import module 背后的工作。
import module 背后发生了什么? (注意,这里不考虑 import package) ^import-module
import 一个 module 的背后,其实就是执行了该 module,然后将执行结果保存到一个变量中(另一个视角为,该变量表示一个命名空间)。又因为除了作为 main module(下文会解释什么是 main module),其他 module 中主要是定义变量、类和函数,即主要用来定义 python 对象,因此执行该 module 就是获取该 module 下定义的 python 对象。因此换一个角度进行理解,import module 背后其实就是把该 module 中的所有 python 对象保存到一个命名空间下,然后在 import 了该 module 的程序文件中,就可以使用 xxx.yy 来使用该 module 中定义的python对象了,其中 xxx 是该命名空间,直率的理解就是将该命名空间下的所有python对象保存在名为 xxx 的变量中,然后使用 xxx.
的方式访问python对象。如下所示:
import xxx # 导入 mmodule xxx,并命名为 xxx
import xxx as x # 导入 module xxx,并命名为 x
来看一个简单的 import module 的例子,在同一个目录下有 main.py
和 mymodule.py
.
├── main.py
└── mymodule.py
# mymodule.py
NUM = 10
class A:
pass
print("mymodule")
# main.py
import mymodule
print(mymodule)
print(mymodule.NUM)
print(mymodule.A)
python main.py
运行程序,结果如下:
mymodule
<module 'mymodule' from '/workspace/pythonCode/test_dir/mymodule.py'>
10
<class 'mymodule.A'>
从运行结果来看,先执行了 mymodule.py module,因为在 print("mymodule")
语句在 mymodule.py 模块中。
其次,mymodule 作为一个 python 的 module
对象被打印输出。
import package 背后发生了什么?
首先,package 和文件中的文件夹具有同等性质,即一个 package 可以拥有多个子 package 和 多个 module;其次,在python 中,package 被当作一种特殊的 module看待。以下面这个程序目录为例,蓝色为 package,其他为 module。main.py
作为我们程序运行的 main module(下问会介绍什么是 main module)。
main.py
中内容如下,python main.py
运行该程序。
import mypackage
print(mypackage)
# 运行结果:
# <module 'mypackage' (<_frozen_importlib_external._NamespaceLoader object at 0x7f2c47556200>)>
从运行结果可知,package 被当作是一种 module。因此 import package 应该和 import module 的行为类似,会执行 module 表示的 .py 程序,而 package 又是一个 package,本身不是 .py 程序。(哈哈,有点绕绕的)因此,对于上述的示例,import mypackage
语句就什么也没执行,只是单独地将 mypackge 这个 package 表明为一个 module 并复制给变量(或者称为命名空间)mypackage
,所以 mypackage
中就不包含任何 python 对象,因此当尝试 xxx. 的方式调用该 package 下的 module 时,会报错,如下所示:
关于 import 某个 package 下的 module 的写法,想必只要学了几天python就没有不会的,这里就不再唠叨其中的细节,这里只介绍其中两种写法和其表达的含义。仍然以上面的示例为例。
第一种,import mypackage.module1
。这个 import 语句蕴含了两层含义,第一层,将 mypackage 下的 module1 导入(import),并将其保存到名为 mypackage.module1
的变量中。第二层,将 mypackage 这个 module 保存到名为 mypackage 的变量中。
第二种,from mypackage import module1 as m
。将 mypackage 下的 module1 导入,并保存到名为 m 的变量中,注意,这里 mypackage 这个 module 是没有被 import,这里需要和 import mypackage.module1 as m
语句进行区分。
import mypackage.module1 as m
语句和上述第一种几乎一样,只不过是将 mypackage 下的 module1 导入(import)保存到名为 m 的变量中。
理解了上述两种 import 中表达的含义之后,其他形式的 import 自然也就清楚了。
小结一下。无论是 import module 还是 import package,都是需要在 sys.path 中先查找到正确的路径,然后执行该 module。这里在补充两点,第一点,对于多次相同的 import,只会执行一次,执行成功后会将其结果缓存到 sys.modules 中。第二点,上述 import 的方式是 absolute import,没有谈到 relative import,两种 import 的方式存在细微的差别,将在 relative import 中需要避免的问题 解释。
此外,这里有必要再补充一下 package 下的 __init__.py
文件的用途。在上文中介绍到,import module 会将 module 表示的 .py 程序执行一次,而 import package 则什么都不会执行。这里需要补充的是,当 package 下存在 __init__.py
文件 时,import package 会执行 __init__.py
文件,将其结果保存到 package 对应的命名空间中。
在大多项目中,package 下都会存在一个 __init__.py
文件,用以在 import package 时进行一些预处理相关的操作,这可以作为另一个话题来写作了。
ImportError: attempted relative import with no known parent package ^relative-import
先说明 relative import 的规则,然后再解释 ImportError 的原因。
对于 relative import,需要先找到它的绝对路径,即将相对路径转换为绝对路径,然后再 import。转换的方法是,通过该 module 的 __package__
变量去计算绝对路径。下面看一个例子。
把程序之间 module 的 import 关系以图的方式呈现,如下所示:
由上图可以看到,在 /packageTwo 下的 moduleTwo.py 中使用了 relative import,我们以这个例子来解释 relative import 是如何查找 module 的。上述 relative import 的语句为 from .subPackage import submodule
,首先将该 relative import 语句转化为 absolute import。因为该 import 语句在 moduleTwo.py 中执行,而 moduleTwo 的 __package__
属性值为 packageTwo(可以在 moduleTwo.py 中 print(__package__)
查看),relative import 转换为 absolute import 的方式为在:获取执行该 relative import 语句的 module 的 __package__
属性值,然后将该属性值添加到 relative import 语句前,因此 from .subPackage import submodule
语句被转换为 from packageTwo.subPackage import submodule
,而 packageTwo 在 test_dir 目录下,test_dir 目录被添加到了 sys.path 中(回顾上文),因此最终该 relative import 就能被正确查找到。
上面解释了 relative import 的规则后,我们来看下 ImportError: attempted relative import with no known parent package
错误的原因。
仍然是上面的示例,这里运行 packageTwo 下的 moduleTwo.py,复现 ImportError 错误,如下图所示:
因为在上文中已经详细介绍了 relative import 的规则,这里就不再啰嗦的又分析一遍,直接给出造成该 ImportError 的原因:当 python xxx.py
执行python程序时,xxx.py
会被作为 main module,而 main module 是不属于任何 package 的,即 main module 的 __package__
属性变量为 None(print(__package__) 查看
),因此在上述情况下,不能将 relative import 转换为正确的 absolute import。
总结
本文总结了 python 的 import 机制,全文算是 关于import你需要知道的一切!一个视频足够了 的学习总结。关于 import 还有更多进阶内容,例如动态 import,这些就留到后面继续学习了。
参考资料
关于import你需要知道的一切!一个视频足够了