Python模块导入与包构建最佳实践

[TOC]

最开始写程序的时候,都是一个文件里输入几行源码(python 的一个 web 框架bottle就特别强调自己是单文件框架)。随着程程式变大变复杂,一个文件很难承载如此多的功能,因此将代码拆分到不同的文件里,以模块(module)或者包(package)形式组织。既方便管理,也利于复用。python的模块和包非常简单,一个文件即模块,一个文件夹即一个包。

文件夹包必须在文件夹内声明一个init.py文件,包被导入的时候,默认会先执行这个文件的代码

导入方式

有了包机制,既可以把项目拆分封装。也可以实现独立的功能给别的项目使用。Python提供了 import 指令,鉴于历史原因,import 有相对导入(implicit/explicit relative import)和绝对导入(absolute import)两种方式。相对导入有隐式的导入(implicit)和显示导入(explicit)。

模块的导入都是相对于而言import 指令是加载包的模块,通过from import 语句不仅可以加载模块,也可以导入 python 对象(类,函数,变量等)。模块最小组织单位是文件,import 后面的如果是文件则被认为是模块,若是文件里的 python 对象,则加载的不是模块。

模块搜索

Python的模块搜索大致有三种步骤。

  • 首先搜索 sys.modules:这是一个列表,她存储了之前导入的所有模块,新导入的模块也会追加到这个列表里。若这里搜索不到,那么就会进行第二步。

  • 其次搜索built-in module:Python 的标准库和安装的第三方软件包。如果还搜索不到模块,就进行最后一步。

  • 最后搜索sys.path:被执行的python文件,其所在的目录会被追加到 sys.path 列表,也就是相对于被执行的文件的目录文件夹和系统在sys.path的也会被搜索。若任然找不到,最终会抛出一个ModuleNotFoundError错误。

Python文件加载方式

Python的文件加载方式有两种,直接运行和以模块方式加载。

  • top-level方式直接加载:python + 文件名。如 python filename.py,或者 python dir/filename.py ,这样的python文件是作为 top-level 脚本运行,脚本文件的__name__属性会被设置成 __main__,同时其__package__属性设置为None。因为此时的文件作为顶层模块,它不属于任何一个包。直接运行脚本,会把脚本所在的目录追加到sys.path 之中。

  • 模块方式加载,使用-m解释参数,然后跟着文件路径,其中/替换成.。如 python -m filename 或者 python dir.filename,filename.py 变成了一个模块,其模块名为所在python执行目录下的包.模块.文件名。例如他的模块名是 filename或者 dir.filename

    这种方式,不会把脚本所在位置加载 sys.path 之中,而是会把执行 python 命令所在的目录加载 sys.path 中。但是被执行的脚本肯定也在执行命令所在文件或者子文件中,因此效果类似自身也属于 sys.path 之中。

自2.6以后,python 的模块名可以用下面的方式打印:
module_name = '"{}.{}".format(__package__, __name__) if __package is not None else __name__

相对导入和绝对导入

上文介绍了 python 脚本加载和模块搜索的基本方式。基于此,python提供了以相对或绝对导入两种包、模块的import方式。因python2和3分裂的历史,2默认是相对导入,3则是绝对导入,并且日常开发也推荐使用绝对导入方式。

那么它们两种有什么区别呢?

导入都是针对而言

  • 绝对导入:文件的 import 或者 from import 导入语句,都是从包的根路径开始
  • 相对导入:导入的起始模块未必是从包路径开始,使用.或者.. 的方式是显示的相对导入,否则是隐式的

python3针对隐式相对导入会直接抛错。

Case Study

文字都过于抽象,下面针对code进行演示解析。文件目录结构如下,myproj 项目中,有一个pkg的包,包里有两个文件夹subpkg_asubpkg_b两个子包,子包分别有几个py模块文件。

➜  myproj tree
.
└── pkg
    ├── __init__.py
    ├── main.py
    ├── subpkg_a
    │   ├── __init__.py
    │   ├── hello.py
    │   └── world.py
    └── subpkg_b
        ├── __init__.py
        └── welcome.py

首先分析subpka_a包,即相对 subpkg_a 来分析 hello.py 和 world.py 直接的导入方式。暂时可以忽略其他文件或文件夹。

下面是几个文件的源码

➜  pkg cat subpkg_a/__init__.py subpkg_a/hello.py subpkg_a/world.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

if __package__:
    print('subpkg_a.__init__.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('subpkg_a.__init__.py: ', '{}'.format(__name__))
    
------------------------
#!/usr/bin/env python
# -*- coding:utf-8 -*-

if __package__:
    print('hello.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('hello.py: ', '{}'.format(__name__))


import world

------------------------
#!/usr/bin/env python
# -*- coding:utf-8 -*-


if __package__:
    print('world.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('world.py: ', '{}'.format(__name__))

def say_world():
    return 'world'

if __name__ == '__main__':
    print(say_world())

hello.py 文件直接导入了world模块,然后运行结果如下:

➜  pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
('world.py: ', 'world')

可以看见,hello.py 的模块名为 __main__ 。作为 top-level 执行的文件,其__package__None, __name____main__。 对于world.py 文件,因为它是被加载的模块,__name__就是文件名。

hello.py很好理解,作为top-level脚本执行,其本身不属于任何一个包。world.py是作为模块被加载的,但是hello.py 里也没指定从哪个包里加载。因此加载时候就按照模块搜索方式。因为hello.py执行的目录被加入了sys.path,world.py 与 hello.py 同级,因此自然能被搜索并成为模块。

隐式相对导入

由于上面的导入方式,模块都不属于任何一个包,自然就没有相对于绝对导入的说法。删掉subpka_a/__init__.py 文件也不会有影响。正如前面所介绍,python脚本若不是top-level,才有包概念的。修改执行方式如下:

➜  pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')

可以看到,subpak_a 包的__init__.py 文件也被加载执行了,这表示包 subpak_a 被导入了。-m 的语法告诉了解释器,把当前执行命令的目录加入到 sys.path。以模块的方式加载 hello.py 文件,并且指定了hello 模块的父级是 subpkg_a,同理,处于同级的 world.py 文件也被隐式的包含在 subkag_a 包里,它的模块名subpkg_a.world

上面的 import 语句中没有出现包 subpkg_a,所以是一种相对导入,也没有使用.或者.. 符号,所以是隐式的导入。隐式导入在python3下不支持,会抛错:

➜  pkg python3 -m subpkg_a.hello
subpkg_a.__init__.py:  subpkg_a.subpkg_a
hello.py:  subpkg_a.__main__
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 9, in <module>
    import world
ModuleNotFoundError: No module named 'world'

显示相对导入

subpak_a 包的层级很清楚,因此改为显示相对导入也很简单,即:

➜  pkg cat subpkg_a/hello.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

if __package__:
    print('hello.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('hello.py: ', '{}'.format(__name__))

from . import world

运行结果也正确:

➜  pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')

使用了python -m subpkg_a.hello 命令执行,hello.py 和 world.py 分别属于 subpkg_a 包,因此 hello.py 里的 . 表示在包 subpkg_a 内,相对于__main__模块的导入同级模块。即subpkg_a.world.subpkg_a.。因此不会报错。

需要注意,显示的相对导入只有以模块加载的方式才能使用,否则会抛 Attempted relative import in non-package的错误

➜  pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
Traceback (most recent call last):
  File "subpkg_a/hello.py", line 9, in <module>
    from . import world
ValueError: Attempted relative import in non-package

正如前文所述,以top-level 的运行hello.py文件,hello.py的模块名是__main__, world.py 的模块名是world,两者不属于任何一个包,自然也没有模块的层级。. 是指相对与包下面的模块的路径进行导入。正如这样没有包概念,因此抛错。

对于subpak_b包里的模块,需要使用..操作符,修改hello.py 如下:

...
from . import world
from ..subpkg_b import welcome

需要注意的是,python -m subpkg_a.hello的执行方式,最顶级的包是 subpkg_a,而subpkg_b 是搜索不到的,需要更上层的目录来执行:

➜  pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')
Traceback (most recent call last):
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 10, in <module>
    from ..subpkg_b import welcome
ValueError: Attempted relative import beyond toplevel package

➜  pkg cd ../
➜  myproj python -m pkg.subpkg_a.hello
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.__main__')
('world.py: ', 'pkg.subpkg_a.world')
('welcome.py: ', 'pkg.subpkg_b.welcome')

综上所述,相对导入,导入的路径中,都没有出现包名。

混用隐式和显示

通常对于subpak_a 和subpak_b,它们自身实现逻辑可以使用显示或者隐式导入。对于它的调用者,pkg下的main.py 可以直接引用这两个包。此时的参考包是pkg。

if __package__:
    print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('main.py: ', '{}'.format(__name__))
    
from subpkg_a import hello

在main中隐式相对导入了subpak_a 和 hello 模块

➜  myproj python pkg/main.py
('main.py: ', '__main__')
('welcome.py: ', 'subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.hello')
('world.py: ', 'subpkg_a.world')
Traceback (most recent call last):
  File "pkg/main.py", line 12, in <module>
    from subpkg_a import hello
  File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 11, in <module>
    from ..subpkg_b import welcome
ValueError: Attempted relative import beyond toplevel package

from subpkg_b import welcome语句正常执行了,from subpkg_a import hello也正常,这符合前面的说明。
此时main是top-level,它不属于任何一个包,但是subpkg_a,subpkg_b 也不属于任何一个包,但是它本身是一个包,所以导入它是没问题,并且它里面的hello和world导入也正常。

执行到 from ..subpkg_b import welcome 语句的时候报错了。正如前面的结果,subpkg_a 和 subpkg_b 是同级,可是在 subpkg_a 包并不知道 subpkg_b 包的存在,因此需要把他们共有的包 pkg 引入到包层级中,即

➜  myproj python -m pkg.main
('main.py: ', 'pkg.__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')

鉴于 welcome 已经被导入过,因此 hello.py 将不会再导入 welcome 模块。

绝对导入

隐式相对导入在py3被禁止了,显式相对导入也不是默认,那么最好还是使用绝对导入。即从包的起始位置书写import路径。

修改main.py,将import从跟包开始书写路径

➜  myproj cat pkg/main.py

if __package__:
    print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('main.py: ', '{}'.format(__name__))

from pkg.subpkg_b import welcome
from pkg.subpkg_a import hello

➜  myproj python pkg/main.py
('main.py: ', '__main__')
Traceback (most recent call last):
  File "pkg/main.py", line 11, in <module>
    from pkg.subpkg_b import welcome
ImportError: No module named pkg.subpkg_b

可以看到,与上次执行不一样,from pkg.subpkg_b import welcome 这一句就报错了。当前的 sys.path 是 main.py 所在的目录,并不包括 pkg 所在的目录,因此搜索包的时候,搜索不到 pkg。解决方案也有两种。

因为 sys.path 没有,那么加上即可。在main.py 中加上

➜  myproj cat pkg/main.py

if __package__:
    print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
    print('main.py: ', '{}'.format(__name__))

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

from pkg.subpkg_b import welcome
from pkg.subpkg_a import hello

➜  myproj python pkg/main.py
('main.py: ', '__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')

尽管针对 sys.path 进行 hack 可以实现绝对导入,可是这种方式始终一点也不 make sence。正如前面解决方式一样,可以使用 -m 以模块方式加载。毕竟 -m 可以把当前执行路径加入到sys.path中,去掉 sys.path.append的语句,再运行:

➜  myproj python -m pkg.main
('main.py: ', 'pkg.__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')

使用 -m 方式使用一个包,在python也是挺常见的,例如开启一个服务器和格式化json字符串

➜  myproj python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...


➜  myproj echo '[{"hello": "world"}, {"python": "life is short"}]' | python -m json.tool
[
    {
        "hello": "world"
    },
    {
        "python": "life is short"
    }
]

然而,实际生产中,写一个package或者lib,更多是被 install 后再导入运行。install 保证了它将会处在sys.path,被导入等同于以 -m 方式被加载执行。因此不太会有 sys.path 和ModuleNotFoundError问题。例如将hello改为

from subpkg_a import world

变成绝对导入之后,直接运行会报错:

➜  pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
Traceback (most recent call last):
  File "subpkg_a/hello.py", line 10, in <module>
    from subpkg_a import world
ImportError: No module named subpkg_a

main.py 改为

from subpkg_a import hello

模拟subpkg_a作为一个独立的lib,其本身使用绝对导入,然后pkg里的main使用这个包,直接运行,并没有报错

➜  myproj python pkg/main.py
('main.py: ', '__main__')
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.hello')
('world.py: ', 'subpkg_a.world')
➜  myproj

因此使用绝对导入开发一个 lib 是更好的实践。可是正如上面 subpkg_a 所面临的问题,开发过程中,直接运行,可能会报错,不得不使用 -m 的方式。为了更好的开发,可以使用下面介绍的包结构。

Python Lib 构建推荐

带有__init__.py 文件夹即成为一个包,包,模块相互组织起来即成为lib。先看一个相对导入,即构建包的时候。

➜  demo tree mylib
mylib
├── mylib
│   ├── __init__.py
│   ├── greet
│   │   ├── __init__.py
│   │   ├── hello.py
│   │   └── world.py
│   └── main.py
└── setup.py

2 directories, 6 files

其中 hello.py world.py 和 main.py 的内容如下:

➜  mylib cat greet/hello.py greet/world.py main.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from . import world

def say_hello():
    return 'hello ' + world.say_world()


if __name__ == '__main__':
    print(say_hello())
#!/usr/bin/env python
# -*- coding:utf-8 -*-

def say_world():
    return 'world'


if __name__ == '__main__':
    print(say_world())
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from .greet import hello


def do_greet():
    return hello.say_hello()


if __name__ == '__main__':
    print(do_greet())

运行 python main.py 也能正常运行

然后使用 setup打包进行安装。

➜  mylib cat setup.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-


from setuptools import setup, find_packages

setup(
    name='mylib',
    version='1.0.0',
    decription='simple lib demo',
    long_description='README.md',
    author='jiamin',
    author_email='maojiamin@daixm.com',
    licens='',
    packages=find_packages(exclude=('tests', 'docs')),
    test_suite='tests'
)

执行 python setup.py bdist_wheel,会在 lib 下的 dist 文件夹生成一个 mylib-1.0.0-py2-none-any.whl 包。使用 pip 可以直接安装

(venv) ➜  myproj pip list
Package    Version
---------- -------
pip        19.0.1
setuptools 40.6.3
wheel      0.32.3
(venv) ➜  myproj pip install ~/mylib/dist/mylib-1.0.0-py2-none-any.whl
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.
Processing /Users/master/mylib/dist/mylib-1.0.0-py2-none-any.whl
Installing collected packages: mylib
Successfully installed mylib-1.0.0
(venv) ➜  myproj pip list

Package    Version
---------- -------
mylib      1.0.0
pip        19.0.1
setuptools 40.6.3
wheel      0.32.3
(venv) ➜  myproj ipython

In [1]: import mylib

In [2]: from mylib.main import do_greet

In [3]: do_greet()
Out[3]: 'hello world'

In [4]:

使用相对导入也可以构建一个包。

更好的python包构建方式

使用显示相对导入包构建方式,一个好处就是,报名修改了,也会不用修改包内模块的导入语句。而绝对导入包含了包名。但是绝对导入对于本地包的处理,有更好的方式,因此也是python3的默认方式。

构建一个python lib。和包结构和相对导入类似,下面增加更多的应用场景。项目结构目录如下

➜  mylib tree
.
├── mylib
│   ├── __init__.py
│   ├── cron
│   │   ├── __init__.py
│   │   └── tasks.py
│   ├── greet
│   │   ├── __init__.py
│   │   ├── hello.py
│   │   └── world.py
│   └── main.py
├── setup.py
├── docs
├── README.md
└── tests
    ├── __init__.py
    ├── __init__.pyc
    └── test_greet.py

4 directories, 11 files

增加了tests目录和docs目录以及README.md。几个文件代码如下,所有导入都使用绝对导入,即从 mylib开始导入

➜  mylib cat mylib/greet/hello.py
from mylib.greet import world
...


➜  mylib cat mylib/cron/tasks.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from mylib.greet import hello

def do_task():
    return 'do task : ' + hello.say_hello()


if __name__ == '__main__':
    print(do_task())
    
    
    ➜  mylib cat tests/test_greet.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import unittest

from mylib.greet import world
from mylib.greet import hello


class TestGreet(unittest.TestCase):

    def test_say_world(self):
        self.assertEqual('world', world.say_world())

    def test_say_hello(self):
        self.assertEqual('hello world', hello.say_hello())


if __name__ == '__main__':
    unittest.main()

在mylib内,若想要执行 corn下面的task,必须以 -m 方式运行,否则会抛错

➜  mylib python mylib/cron/tasks.py
Traceback (most recent call last):
  File "mylib/cron/tasks.py", line 4, in <module>
    from mylib.greet import hello
ImportError: No module named mylib.greet

➜  mylib python -m mylib.cron.tasks
do task : hello world

同样的,执行 tests 也是需要制定 -m。

hydra 里的cron,都是使用 sys.path.append方式,将执行脚本追加到path。使用 -m 方式会比hack sys.path 更好

运行测试

➜  mylib python -m tests.test_greet
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

测试 测试

但是,其实setup工具,提供了测试方法,并且setup里还可以指定不同的测试方式,即声明 test_suite='tests'

➜  mylib python setup.py test
running test
running egg_info
creating mylib.egg-info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
writing manifest file 'mylib.egg-info/SOURCES.txt'
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
test_say_hello (tests.test_greet.TestGreet) ... ok
test_say_world (tests.test_greet.TestGreet) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

当运行了上面的测试之后,会发现当前目录多了一个文件夹mylib.egg-info

➜  mylib ls
mylib          mylib.egg-info setup.py       tests

安装项目

Lib开发完之后,自然是可以进行打包然后分发后install。然而测试开发的时候,如果执行脚本,都需要加上 -m,整个开发过程还是蛮繁琐,因此python的setup.py 提供了一个develop参数,进行了一次mock安装。

(venv) ➜  mylib pip list
Package    Version
---------- -------
pip        19.0.1
setuptools 40.6.3
wheel      0.32.3

(venv) ➜  mylib python setup.py develop
running develop
running egg_info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
Creating /Users/master/myproj/venv/lib/python2.7/site-packages/mylib.egg-link (link to .)
Adding mylib 1.0.0 to easy-install.pth file

Installed /Users/master/myproj/mylib
Processing dependencies for mylib==1.0.0
Finished processing dependencies for mylib==1.0.0

(venv) ➜  mylib pip list
Package    Version Location
---------- ------- --------------------------
mylib      1.0.0   /Users/master/myproj/mylib
pip        19.0.1
setuptools 40.6.3
wheel      0.32.3

(venv) ➜  mylib python setup.py develop
running develop
running egg_info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
Creating /Users/master/myproj/venv/lib/python2.7/site-packages/mylib.egg-link (link to .)
mylib 1.0.0 is already the active version in easy-install.pth

Installed /Users/master/myproj/mylib
Processing dependencies for mylib==1.0.0
Finished processing dependencies for mylib==1.0.0

(venv) ➜  mylib python mylib/cron/tasks.py
do task : hello world


由此可见,执行了 python setup.py develop, 会在环境的site-package创建一个 mylib.egg-link文件,这个文件的内容是 /Users/master/myproj/mylib,即指向当前开发环境包目录,因此等价于安装了包到环境中。自然可以通过pip list 查看。也就是可以直接使用脚本方式运行,不再需要 -m了,并且也能再开发的时候,进行针对安装以后的行为效果进行调试。

总结

程序规模变大变复杂,通常进行模块拆分和封包复用。python文件及模块的基本组织单位,文件夹则是基础包。包或者模块的引用可以使用 import 或者 from import 语法。

Import有相对导入和绝对导入,相对导入又有显式和隐式两种。显式则使用.或者..操作符。相对还是绝对,针对的是python文件被加载的方式。

直接运行python文件则是以top-level方式,当前文件模块名是__main__,它本身就是顶级模块,不存在包的概念。若使用-m参数,则以模块方式加载,模块方式加载都是相对包而言。. 表示在同一个包内,被相对被加载文件的路径进行加载导入的模块。

相对导入的文件里不会出现包名,绝对导入的文件里,import语句必须包含包名。同时所导入的包都必须从包名的根路径开始,写出完整的模块路径。

Python3不在支持隐式相对导入。官方也更推荐使用绝对导入。因此介绍了使用绝对导入构建一个lib,所使用的方式包括项目源码,文档,测试等,这也是facebook 的 tornado 的方式。其中使用 python setup.py test 进行单元测试。以及使用 pyton setup.py develop和mock安装,使得开发调试更方便。

参考:

  1. PEP 328: Absolute and Relative Imports
  2. Absolute vs Relative Imports in Python
  3. The import system
  4. Script vs. Module

推荐阅读更多精彩内容