Python相对、绝对导入浅析

这篇文章从另外一个不同的视角来分析一下Python的import机制,主要的目的是为了搞懂import中absolute、relative import遇到的几个报错。  这里不同的视角是指从Python import hooks这个方面来展开,当然本身关于Python import hooks有很多的文章,我这里不打算展开聊这个方面的内容,文章中主要会结合代码和PEP 302 – New Import Hooks这个PEP。  1. 几个跟import相关模块属性 首先我们需要了解几个跟import相关的模块属性,因为后面我们分析代码的时候会频繁接触到这些属性,关于这些属性详细的介绍参考:import-related-module-attributes。  __name__:模块的全名称,用来唯一的标识一个模块。  __package__:模块的__package__属性必须设置,而且必须是字符串。当模块是一个包(package)的时候__package__==__name__,如果模块是非package并且是一个top-level模块那么__package__设置为空字符串,对于子模块那么__package__就是上层父模块的__name__。关于__package__更详细的内容可以参考:PEP 366 – Main module explicit relative imports。  __path__:这个属性就是用来区分一个模块是package还是py文件,如果模块是package那么__path__属性就必须设置,但是这个属性有可能没有太多的其它意义。更详细的__path__内容参考:module.__path__。  2. Python import hooks的入门 虽然本文的重点不是关于Python import hooks,但是因为文章是从这个视角来阐述的,所以还是稍微介绍一点关于这个方面的一点入门知识点。  一般情况下我们在代码中使用import foo,那么调用的其实是__builtin__.__import__。有时候我们想要在代码中动态的加载某个模块那么可以用impimportlib这两个模块来实现,但是有时候我们想要更多的控制Python的import,比如要实现一个自动安装、加载模块依赖的功能,那么此时import hooks就能派上用场了。  Python提供了好两种方式来做import hook:Meta hooks and Path hooks ,利用hook我们基本可以做到随心所欲的import(当然有一些规则需要遵守的)。Python也提供了一个import hooks的模板,叫ihooks(/usr/lib/python2.7/ihooks.py),也即是我们后面要重点分析的一个模块。  如果想使用ihooks来代替默认的import功能,那么在执行任何import之前执行如下代码即可:

#before any imports
import ihooks
ihooks.install()

这样后面所有的import操作都会进入到ihooks.ModuleImporter.import_module()函数中。  3. 剖析ihooks,imports_module参数 执行完上面提到的ihooks.install()以后import的入口变成了如下的import_module()函数。

    def import_module(self, name, globals=None, locals=None, fromlist=None,
                      level=-1):
        parent = self.determine_parent(globals, level)
        q, tail = self.find_head_package(parent, str(name))
        m = self.load_tail(q, tail)
        if not fromlist:
            return q
        if hasattr(m, "__path__"):
            self.ensure_fromlist(m, fromlist)
        return m

这个函数各个参数的具体含义可以参考builtin.__import__,重点说一下level这个参数:  - 用来表示absolute还是relative导入;  - 如果为0则表示是absolute导入;  - 大于0表示relative导入,相对导入的父目录的级数;  - -1意味着可能是absolute或relative导入。  locals参数暂时没有用到。  4. 剖析ihooks,determine_parent()函数

    def determine_parent(self, globals, level=-1):
        if not globals or not level:  #code 1
            return None
        pkgname = globals.get('__package__')
        if pkgname is not None:
            if not pkgname and level > 0:   #code 2
                raise ValueError, 'Attempted relative import in non-package'
        else:   #code 3
            # __package__ not set, figure it out and set it
            modname = globals.get('__name__')
            if modname is None:
                return None
            if "__path__" in globals:
                # __path__ is set so modname is already the package name
                pkgname = modname
            else:
                # normal module, work out package name if any
                if '.' not in modname:
                    if level > 0:
                        raise ValueError, ('Attempted relative import in '
                                           'non-package')
                    globals['__package__'] = None
                    return None
                pkgname = modname.rpartition('.')[0]
            globals['__package__'] = pkgname
        if level > 0:   #code 4
            dot = len(pkgname)
            for x in range(level, 1, -1):
                try:
                    dot = pkgname.rindex('.', 0, dot)
                except ValueError:
                    raise ValueError('attempted relative import beyond '
                                     'top-level package')
            pkgname = pkgname[:dot]
        try:
            return sys.modules[pkgname]   #code 5
        except KeyError:
            if level < 1:
                warn("Parent module '%s' not found while handling "
                     "absolute import" % pkgname, RuntimeWarning, 1)
                return None
            else:
                raise SystemError, ("Parent module '%s' not loaded, cannot "
                                    "perform relative import" % pkgname)

determine_parent()函数主要用来负责填充模块的__packge__属性、返回模块的锚点对应的模块(relative导入才有)。  在代码中我对一些关键的代码分支做了code n这样的标记,方便后面引用。  code 1:首先我们遇到的是code 1这个分支,globals为空的情况我还没有遇到过,但是level为0的情况就是前面分析过的level参数所示的情况:这是一个absolute导入,比如你在导入之前使用了from __future__ import absolute_import,那么level就是为0。也就是说如果是absolute导入那么就无须再找出父模块,也不会再设置__package__模块属性,为什么在这种情况下则不需要设置__package__模块属性呢?  让我们好好的读一读这段话(来自:PEP 366 Proposed Change):

As with the current __name__ attribute, setting __package__ will be the responsibility of the PEP 302 loader used to import a module.

意思就是说设置__package__是hooks中的loader(ModuleImporter包含了finder、loader)的责任,这个责任由determine_parent()来完成。

When the import system encounters an explicit relative import in a module without __package__ set (or with it set to None ), it will calculate and store the correct value ( __name__.rpartition(‘.’)[0] for normal modules and __name__ for package initialisation modules).

这句话的意思是说如果遇到了明确的relative导入并且__package__未设置那么loader会计算、存储正确的的__package__值。  从上面这两条综合来看就是说loader有责任设置__package__,但是也是在某些条件的前提下才需要负责,对于我们code 1遇到的这种情况(不是明确的relative导入),loader可以不用负这个责任。  code 2:这里的ValueError: Attempted relative import in non-package错误应该是Pythoner几乎都遇到过的,但是别急,我们后面还会继续遇到。这里之所以会报错就是因为__package__为空字符串则表示这是一个顶层的常规Python源码模块(top-level module),那么此时如果再有relative导入那么就没法进行模块的定位了。  code 3:这部分就是设置__package__,整个的流程基本跟PEP 366 Proposed Change提到的一致,首先通过__path__属性来判断这是一个package还是一个普通的源码模块,如果是package则直接设置__package____name__,否则通过__name__.rpartition('.')[0]计算得到。在这里我们又一次遇到了前面的ValueError,这里报错的原因跟前面大同小异,不再过多的解释。  至此我们完成了determine_parent()的第一个重要功能:设置模块的__package__属性。  code 4:如果是relative导入,那么需要计算相对的锚点是哪个,例如在spam.foo.test模块中执行import ..sub那么最后计算得出需要导入的模块是spam.sub。  在这个部分我们遇到了另外一个常见的错误ValueError: attempted relative import beyond top-level package,这个错误的原因就是我们在计算锚点的时候超过了最高模块,例如在spam.foo.test模块中执行import ...sub。  code 5:完成了最后一个功能:返回锚点模块。  5. 剖析ihooks,find_head_package()函数

    def find_head_package(self, parent, name):
        if '.' in name:
            i = name.find('.')
            head = name[:i]
            tail = name[i+1:]
        else:
            head = name
            tail = ""
        if parent:
            qname = "%s.%s" % (parent.__name__, head)
        else:
            qname = head

        q = self.import_it(head, qname, parent)   #code 1
        if q: return q, tail
        if parent:
            qname = head
            parent = None
            q = self.import_it(head, qname, parent)   #code 2
            if q: return q, tail
        raise ImportError, "No module named '%s'" % qname

从函数名我们就能大概猜到这个函数的作用,就是导入完整模块路径名中的第一个模块,类似就是如果我们要导入spam.foo.test,那么这个函数是先导入spam模块。  这个函数的理论我们从PEP-0302 Specification part 1: The Importer Protocol的第三段话中可以看到,大致的意思就是我们先做relative导入,  例如我们在spam中执行import foo,那么会要先尝试导入spam.foo(我们上面代码中标注的code 1),如果失败了则再执行absolute导入foo(我们上面代码中标注的code 2)。  6. 剖析ihooks,load_tail()函数 前面我们把第一个模块已经导入了那么接下来就是把剩下的(尾部)的模块导入了,这就是这个函数的功能。代码就不贴了,比较简单,就是循环把完整模块名中的每一个子模块导入,函数的理论可以参考PEP-0302 Specification part 1: The Importer Protocol的第四段话。  7. 剖析ihooks,ensure_fromlist()函数 这个函数就是把类似from spam import foo.testfoo.test部分导入。  8. 剖析ihooks,import_it()函数

    def import_it(self, partname, fqname, parent, force_load=0):
        if not partname:
            # completely empty module name should only happen in
            # 'from . import' or __import__("")
            return parent
        if not force_load:
            try:
                return self.modules[fqname]   #code 1
            except KeyError:
                pass
        try:
            path = parent and parent.__path__
        except AttributeError:
            return None
        partname = str(partname)
        stuff = self.loader.find_module(partname, path)   #code 2
        if not stuff:
            return None
        fqname = str(fqname)
        m = self.loader.load_module(fqname, stuff)   #code 3
        if parent:
            setattr(parent, partname, m)
        return m

这个函数是执行导入的核心函数,前面我们介绍的各种函数都是最终通过import_it()来执行最后的导入。  函数代码其实也挺简单的,特别是你能结合PEP-0302 Specification part 1: The Importer Protocol来看代码。  code 1:如果cache中已经存在该模块,那么直接返回该模块。  code 2:查找对应的模块,返回一个三元组,间接调用的imp.find_module。关于这个函数更多的内容除了上面的”PEP-0302 Specification part 1: The Importer Protocol”以外还可以参考imp.find_module。  code 3:加载对应的模块,就是调用imp内的各种函数,不再赘述。  整个import_module()函数介绍完成了,在阅读ihooks.py或者Python/import.c源码之前建议各位先把几个PEP以及Python Language Reference的几篇文章先通读一遍,如果有些你暂时没弄清楚的那么就可以留到源码中去弄清楚。  The import system PEP 302 – New Import Hooks imp — Access the import internals PEP 366 – Main module explicit relative imports

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏horstxu的博客

一图弄懂ASCII、GB2312、GBK、GB18030编码

最近项目中涉及到了解析文件内容的需求,文件中全都是中文,由于这一过程中碰到的乱码问题实在过多,所以特地花时间研究了一下中文编码。本文中先介绍一下ASCII,GB...

300
来自专栏开源优测

python selenium2源码 - 核心package: remote分析

说明 本章主要对selenium2核心package remote 进行说明,remote主要包含了以下几个模块: __init__.py ...

3045
来自专栏林冠宏的技术文章

全面总结: Golang 调用 C/C++,例子式教程

Golang 调用 C/C++ 的教程网上很多,就我目前所看到的,个人见解就是比较乱,坑也很多。希望本文能在一定程度上,做到更通俗明了。

110
来自专栏偏前端工程师的驿站

Java魔法堂:注解用法详解——@SuppressWarnings

一、前言                                     编码时我们总会发现如下变量未被使用的警告提示: ?   上述代码编译通过且可以...

16610
来自专栏有趣的Python

4-C++远征之起航篇-学习笔记

链接: https://pan.baidu.com/s/1SgdThGYaLDyXDFKvaBSa5A 密码: 2333

814
来自专栏编程

身为程序猿,怎能不懂RegExp?

正则表达式是程序猿的好朋友。这体现在两个方面:一、在我们敲的代码里面,可以用正则表达式非常轻巧、灵便、快捷的完成字符串的操作,比如匹配、搜索、提取子串等。二、我...

1885
来自专栏大数据挖掘DT机器学习

Python]新手写爬虫全过程(已完成)

今天早上起来,第一件事情就是理一理今天该做的事情,瞬间get到任务,写一个只用python字符串内建函数的爬虫,定义为v1.0,开发中的版本号定义为v0.x。数...

3579
来自专栏IT派

Python实现文字转语音功能

这是一篇简单的Python文字(汉字)转语音教程,当然对于其他语言工具在实现的方法上也是一样的 。

882
来自专栏编程

Python之路-day4

#读取文件内容 withopen("readme.txt")asmyFile: content = myFile.readlines() print(conte...

1716
来自专栏自动化测试实战

RF第三讲--定义用户关键字

3425

扫描关注云+社区