每天开心一点

python import 中的相对引用还是绝对引用?

2017-11-29 16:00:00    june    1208    来源: http://python.jobbole.com/82604/

一些实践经验

相对引用还是绝对引用?

上面介绍了Python的两种引用方式,都可以解决引入歧义的问题。那我们应该使用哪一种呢?

先说明一下Python的默认引用方式,在Python2.4及之前,Python只有相对引用这一种方式,在Python2.5中实现了绝对引用,但默认没有打开,需要用户自己指定使用该引用方式。在之后的版本和Python3版本,绝对引用已经成为默认的引用方式。

其次,二种引用方式各有利弊。绝对引用代码更加清晰明了,可以清楚的看到引入的包名和层次,但是,当包名修改的时候,我们需要手动修改所有的引用代码。相对引用则比较精简,不会被包名修改所影响,但是可读性较差,不如完全引用清晰。

最后,对于两种引用的方式选择,还是有争论的。在PEP8中,Python官方推荐的是绝对引用,详细理由可以参考 这儿

However, explicit relative imports are an acceptable alternative to absolute imports, especially when dealing with complex package layouts where using absolute imports would be unnecessarily verbose:

Standard library code should avoid complex package layouts and always use absolute imports. Implicit relative imports should never be used and have been removed in Python 3.

规范打包发布

为了别人使用自己代码的方便,应该尽量使用规范的包分发机制。为自己的Python包编写正确的setup.py文件,添加相应的README.md文件。对于提供一些可执行命令的包,则可以使用 console_entrypoint 的机制来提供。因为打包和分发不是本文重点,不再详细叙述,大家可以查看官方文档。

使用virtualenv管理包依赖

在使用Python的时候,尽量使用virtualenv来管理项目,所有的项目从编写到运行都在特定的virtualenv中。并且为自己的项目生成正确的依赖描述文件。

关于virtualenv的用法,可以参考我之前的一篇文章 virtualenv教程

Python import实现

Python 提供了 import 语句来实现类库的引用,下面我们详细介绍当执行了 import 语句的时候,内部究竟做了些什么事情。

当我们执行一行  from package import module as mymodule 命令时,Python解释器会查找package这个包的module模块,并将该模块作为mymodule引入到当前的工作空间。所以import语句主要是做了二件事:

  1. 查找相应的module
  2. 加载module到local namespace

下面我们详细了解python是如何查找模块的。

查找module的过程

在import的第一个阶段,主要是完成了查找要引入模块的功能,这个查找的过程如下:

  1. 检查 sys.modules (保存了之前import的类库的缓存),如果module被找到,则⾛到第二步。
  2. 检查 sys.meta_path。meta_path 是一个 list,⾥面保存着一些 finder 对象,如果找到该module的话,就会返回一个finder对象。
  3. 检查⼀些隐式的finder对象,不同的python实现有不同的隐式finder,但是都会有 sys.path_hooks, sys.path_importer_cache 以及sys.path。
  4. 抛出 ImportError。

sys.modules

对于第一步中sys.modules,我们可以打开Python来实际的查看一下其内容:

可以看到sys.modules已经保存了一些包的信息,由这些信息,我们就可以直接知道要查找的包的位置等信息。

finder、loader和importer

在上文中,我们提到了sys.meta_path中保证了一些finder对象。在python中,不仅定义了finder的概念,还定义了loader和importor的概念。

  • finder的任务是决定自己是否根据名字找到相应的模块,在py2中,finder对象必须实现find_module()方法,在py3中必须要实现find_module()或者find_loader()方法。如果finder可以查找到模块,则会返回一个loader对象(在py3.4中,修改为返回一个module specs)。
  • loader则是负责加载模块,它必须实现一个load_module()的方法。
  • importer 则指一个对象,实现了finder和loader的方法。因为Python是duck type,只要实现了方法,就可以认为是该类。

sys.meta_path

在Python查找的时候,如果在sys.modules没有查找到,就会依次调用sys.meta_path中的finder对象。默认的情况下,sys.meta_path是一个空列表,并没有任何finder对象。

我们可以向sys.meta_path中添加一些定义的finder,来实现对Python加载模块的修改。比如下例,我们实现了一个会将每次加载包的信息打印出来的finder。

当我们执行的时候,就可以看到系统加载socket包时所发生的事情。

sys.path hook

Python import的hook分为二类,一类是上一章节已经描述的meta hook,另一类是 path hook。

当处理sys.path(或者package. path)时,就会调用对应的一部分的 Pack hook。Path Hook是通过向sys.path_hooks 中添加一个importer生成器来注册的。

sys.path_hooks 是由可被调用的对象组成,它会顺序的检查以决定他们是否可以处理给定的sys.path的一项。每个对象会使用sys.path项的路径来作为参数被调用。如果它不能处理该路径,就必须抛出ImportError,如果可以,则会返回一个importer对象。之后,不会再尝试其它的sys.path_hooks对象,即使前一个importer出错了。

详细可以参考 registering-hooks

python import hooks

在介绍完Python的引用机制与一些实现方法后,接下来我们介绍一些关于如何根据自己的需求来扩展Python的引用机制。

在开始详细介绍前,给大家展示一个实用性不高,但是很有意思的例子: 让Python在执行代码的时候自动安装缺失的类库。我们会实现一个autoinstall的模块,只要import了该模块,就可以打开该功能。如下所示,我们尝试引入tornado库的时候,iPython会提示我们没有安装。然后,我们引入了autoinstall,再尝试引入tornado,iPython就会自动的安装tornado库。

这个功能的实现其实很简单,利用了sys.meta_path。autoinstall的全部代码如下: