Python 导包技巧

Python 小记 2019-04-25 3772 字 1297 浏览 点赞

起因

起初学 Go 语言的时候,对它的导包规则感到痛苦,——“那么麻烦干嘛呢!”。但最近接到一些新功能开发任务,于是 “啪嗒啪嗒” 写代码,结果更痛苦了。公司项目目录规划打一开始就不合理,以此为基础的 “繁荣发展” 导致更多混乱与麻烦。于是马上翻开《Python cookbook》解惑,有些心得,这里记之。

场景1

场景1:如何模块化代码,同时保证导入方式统一?

公司项目是老项目,因此代码祖传,常常一个 .py 文件里有上千行代码。恰巧现在重构(虽然并不是严格意义上的重构),同事们有意识地将代码拆分成多个文件。然后就有了这样的现象:

from onedir.twodir.file1 import func1
from onedir.twodir.three.file2 imort func2
from onedir.file3 import func3

目录结构如下:

Guan@Orchard:~/python/08/demo$ tree onedir/
onedir/
├── file3.py
├── __init__.py
└── twodir
    ├── file1.py
    ├── __init__.py
    └── three
        ├── file2.py
        └── __init__.py

2 directories, 6 files

很显然,要我记住哪个函数在哪个模块中这是很困难、又没意义的事。况且 onedir 里边的代码只有一个模块在使用,所以为什么不让调用方式统一一下呢?这里需要用到 Python 包中的 \_\_init\_\_.py 文件。

现在,修改 onedir 目录下的 init 文件,修改后变成下面这样:

# onedir/__init__.py
from .file3 import *
from .twodir.file1 import *
from .twodir.three.file2 import *

利用 init 文件加载子模块,使得子模块中的代码在导入父级包时就可以直接使用。示例如下:

"""原来的导入方式"""
# from onedir.twodir.file1 import func1
# from onedir.twodir.three.file2 import func2
# from onedir.file3 import func3

""""现在的导入方式"""
from onedir import func1, func2, func3

func1()
func2()
func3()

# 输出:
func1
func2
func3

这里额外补充一下相对路径导入模块的姿势。

我们都知道,在执行 import xxx 语句时,解释器会先在当前目录下找有没有这个模块,如果没有,就去 sys.path 中的路径里去找。当两个 .py 文件在同级目录时:

Guan@Orchard:~/python/08$ tree demo/
demo/
├── view1.py
└── view2.py

0 directories, 3 files

在 view2.py 文件中可以直接 import view1,但是不可以 import .view1因为相对路径只允许 from ... import ... 这种导入方式。所以是不是 from . import view1 就可以了呢?也不行!

为控制篇幅,我主要是在这里抛结论,想获悉原理可以耐心去看看这篇文章:Python相对导入机制详解

结论:

  1. 在执行脚本所在的目录下,不能使用相对路径的导入方式。也就是说,如果我想在 demo 目录下运行 view2.py 文件,里边还有相对导入,那是绝对不可以的。当这个目录不是执行脚本所在目录时,就可以用相对导入了。
  2. 如果坚持直接运行 view2.py ,又想使用相对导入,请以 python -m demo.view2 这种方式运行脚本(这种方式下,Python2 要求 demo 目录中存在 init 文件,但 Python3 没有此要求)。

场景2

场景2:如何控制符号(函数,变量等)的导入?

我们现在已知的是,当一个函数名(变量、类同理)以下划线开头时,不允许 from xxx import * 的方式导入,但可以 from xxx import _xxx 这样导入。但不知道你们有没有觉得,下划线开头的符号看上去有点丑,我很不爱用,那么有没有其他控制方式呢?那当然是有的:__all__

在一个模块中定义变量 __all__,并指明允许导出的符号名,from ... import * 就不能导入没有列出的那些符号了。用法如下:

def func1():
    pass

def func2():
    pass

__all__ = ["func1"]  # 在其他模块中使用时,func1 可以被导入,func2 不行

当然,from xxx import func2 仍可以把 func2() 导进来。

场景3

场景3:如何让不同目录下的代码在统一的命名空间中导入?

目录结构如下:

Guan@Orchard:~/python/08/demo$ tree demo/
demo/
├── demo1
│   └── spam
│       └── a.py
└── demo2
    └── spam
        └── b.py

4 directories, 2 files

现在,需要使用 a.py 和 b.py 中的代码,寻常方式是:

import demo1.spam.a
import demo2.spam.b

尽管它们都在 spam 目录下,但因为顶级目录(demo1 和 demo2)不同,所以需要不同的入口。但其实,是可以统一 a.py 和 b.py 的入口的。解决方式如下:

import sys
sys.path.extend(["demo1", "demo2"])

import spam.a
import spam.b

事实上,此时的 spam 是一个命名空间包。现在打印 spam ,得到的结果会是:。同时,命名空间包也不会有 \_\_file_\_ 这个属性。

...
In [5]: spam
Out[5]:<module 'spam' (namespace)>

In [6]: spam.__file__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-b25cc88cc23e> in <module>()
----> 1 spam.__file__

AttributeError: module 'spam' has no attribute '__file__'

创建命名空间包的关键在于,统一命名空间的顶层目录中不能有 \_\_init_\_.py 文件。拿上面的例子来说,就是不能在 spam 目录下有这个文件。

你可以尝试导入一个目录,在两种情况下,一个是该目录下存在 init 文件,一个是不存在这个文件。然后打印这个目录,看看结果会有什么不同。

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论