Python模块包导入


纸上得来终觉浅,绝知此事要躬行。

Python模块包导入


在之前的博客文章中,已经提及到了模块的导入方法,在这里我们回顾一下。从下面的目录结构可以看出,mypackage是一个包,其中包含了ab两个子模块且其中包含各一个文件,而且每个模块都包含了__init__.py这个文件。

$ tree
mypackage
|____ __init__.py
|____ a
|     |____ __init__.py
|     |____ bar.py
|____ b
      |____ __init__.py
      |____ foo.py
$ cat mypackage/a/bar.py
BAR = 1

$ cat mypackage/b/foo.py
FOO =  2

如下所示,就是最基础的导入方法,也叫做隐式的相对导入。隐式的相对导入就是没有告诉解释器我这个import相对于谁导入的,但是它默认的是相对的是当前的模块。

In [1]: import mypackage

In [2]: import mypackage.a

In [3]: import mypackage.a.bar
In [4]: from mypackage import a

In [5]: from mypackage.b import foo

In [6]: from mypackage.a.bar import BAR
  • 显式导入和隐式导入

Python包导入的这个概念里面,并没有一个绝对的导入,因为它没有Linux系统的根文件路径这个概念。在Python中,都是通过相对导入的,只不过分为显式导入和隐式导入。

现在,我们修改bar.py中的内容如下所示,之后我们引用bar.py的时候就能够使用foo.py中的FOO变量了。其中,.代表了当前模块,而..代表了上层的模块,...就代表了再上层模块,以此类推。

$ cat mypackage/a/bar.py
BAR = 1
from ..b.foo import FOO
In [1]: from mypackage/a/bar.py import FOO

In [2]: FOO
Out[2]: 2

当然,我们还可以使用如下两种的相对导入的写法。第一种使用了.就是相对于当前模块导入的foo这个模块,而第二种使用了.foo就是相对于当前模块的foo模块导入了FOO这个变量。其中,导入的方式不同,调用的方式也就不同了。

我们可以看到,在__init__.py中导入包下的某些模块或者模块里面的内容,这种方式是比较流行的。它相当于给这个包,提供了一个入口,我不再需要在这个模块里面的子模块去找了。

$ cat mypackage/b/__init__.py
from . import foo
from .foo import FOO
In [1]: from mypackage/b import foo, FOO

In [2]: foo.FOO, FOO
Out[2]: (2, 2)
  • 相对导入适合场景

第一个就是,在大型项目中,代码目录非常复杂,模块层级很深。使用相对导入可维护性更强,导入语句更简洁。第二个就是,在项目早期,由于需求变动很大,会偶尔改变某一个顶层包的名字或者移动位置。

一般情况下,如果模块的层级超过三级就需要使用这种显式的相对导入方法,否则就会使用默认的那种隐式的相对导入。

可以看下Flask里面的入口文件__init__.py里面的内容,分为三个部分。第一部分是非Flask包里面的内容,第二部分是Flask包内模块中的类和函数,第三部分直接导入json这个子包,而且因为非常常用而暴露了jsonify这个json函数。

$ cat flask/__init__.py
from werkzeug.exceptions import abort
from werkzeug.utils import redirect
from jinja2 import Markup, escape

from .app import Flask, Request, Response
from .config import Configfrom .blueprints import Blueprint
from .templating import render_template, render_template_string
...

from . import json
jsonify = json.jsonify
  • 常见模块导入错误

当我们在Python2中使用IPython或者Python解释器的时候,相对导入只能放在代码里面,而不能直接运行的。而在Python3中相对导入会有一些区别,相对于当前目录是可以导入,其他的都会报错。那什么方法是对的呢?就是之前的哪个Flask入口文件的示例,将导入写在代码里面就可以了。

# Python2
In [1]: from . import mypackage
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-b2a156b38b60> in <module>()
----> 1 from . import mypackage

ValueError: Attempted relative import in non-package

In [2]: from .mypackage import b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-0086e0c5eb87> in <module>()
----> 1 from .mypackage import b

ValueError: Attempted relative import in non-package
# Python3
In [1]: from . import mypackage

In [2]: mypackage
Out[2]: <module 'mypackage' from '/Users/escape/mypackage/__init__.py'>

In [3]: from .mypackage import b
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-18-0086e0c5eb87> in <module>()
----> 1 from .mypackage import b

ModuleNotFoundError: No module named '__main__.mypackage'; '__main__' is not a package

In [4]: from .mypackage.b import FOO
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-19-9efb2f0c110b> in <module>()
----> 1 from .mypackage.b import FOO

ModuleNotFoundError: No module named '__main__.mypackage'; '__main__' is not a package
  • 禁用隐式的相对导入

这个小节的语句from __future__ import absolute_import,可能之前我们都见过,它会禁用隐式的相对导入(implicit relative import)而不会禁用显式的相对导入(explicit relative import)。

# 隐式相对导入
In [1]: import mypackage

# 显式相对导入
In [2]: from . import mypackage

像上面的示例中,是可以导入是可以运行的,以为mypackage这个名称一般都不会有冲突的。但是,如果和标准库有冲突的话,那就有问题了。如果我们想引用标准库中的os模块,却引入了mypackage模块下的os文件中的内容,那不就尴尬了。

$ cat mypackage/os.py
a = 1

$ cat mypackage/__init__.py
from os import a
# Python2
In [1]: from mypackage import a

In [2]: a
Out: 1

而当我们禁用隐式的相对引入,再次执行的时候,会报错提示说os模块中并没有a这个模块。因为它引用的是标准库中的os模块,并不是我们定义的。

而在Python3中,是禁用隐式的相对导入这种方式的。不是不允许我们这样使用,而是导入os模块的话会优先引入标准库模块。

$ cat mypackage/__init__.py
from __future__ import absolute_import
from os import a
In [1]: from mypackage import a
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-1-aba2f1bd100d> in <module>()
----> 1 from mypackage import a/Users/escape/mypackage/__init__.py in <module>()
      1 from __future__ import absolute_import
----> 2 from os import a

ImportError: cannot import name a
  • 优先属性和模块的导入

我们知道,使用from xxx import *这样一次性导入xxx模块下全部内容的方式的写法是一个不好的形式。所以,可以在包的__init__.py文件里面,加入一个__all__这样一个属性。不在__all__之后的列表中,是不能通过import *的这种方式导入的。

这样,通常是为了减少模块导入的时间,也减少了不必导入的内容。但是这样使用方式,只适用于import *的这种方式。所以,使用from mymodule import b也是可以导入b的。

$ cat mymodule.py
__all__ = ['a']
a = 1
b = 2
In [1]: from mymodule import *

In [2]: a
Out[2]: 1

In [3]: b
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-89e6c98d9288> in <module>()
----> 1b

NameError: name 'b' is not defined

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !