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__
。有时候我们想要在代码中动态的加载某个模块那么可以用imp
、importlib
这两个模块来实现,但是有时候我们想要更多的控制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.test
中foo.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
Python相对、绝对导入浅析的更多相关文章
- 使用Python 将shapefile导入mongodb
使用Python 将shapefile导入mongodb 随着big data时代的到来,各个行业都在考虑能不能把big data的思路.方法引入进来,GIS行业也不能免俗. 下面就介绍一下如何将sh ...
- 6.python模块(导入,内置,自定义,开源)
一.模块 1.模块简介 模块是一个包含所有你定义的函数和变量的文件,其后缀名是.py.模块可以被别的程序引入,以使用该模块中的函数等功能.这也是使用python标准库的方法. 类似于函数式编程和面向过 ...
- python importlib动态导入模块
一般而言,当我们需要某些功能的模块时(无论是内置模块或自定义功能的模块),可以通过import module 或者 from * import module的方式导入,这属于静态导入,很容易理解. 而 ...
- Python importlib(动态导入模块)
使用 Python importlib(动态导入模块) 可以将字符串型的模块名导入 示例: import importlib module = 'module name' # 字符串型模块名 test ...
- Python import语句导入模块语法[转]
Python import语句导入模块语法 社区推荐:掘金是国内最活跃的技术社区,我们每日有优质Python开发实例分享,海量python开源库推送.来掘金,和更多懂技术的小伙伴交流. pytho ...
- [python]关于在python中模块导入问题追加总结
[背景] 最近在写程序时,我使用的eclipse编辑器运行都没有问题,然后部署到自动化环境上却偏偏报找不到相应模块问题,现在对该问题在之前的贴子上追加总结 原帖子:[python]关于python中模 ...
- 在python中重新导入模块
重新加载模块 倘若,更改了已经在 Python shell 中导入的模块,然后重新导入该模块,Python 会认为“我已经导入了该模块,不需要再次读取该文件”,所以更改将无效. 要解决这个问题,有以下 ...
- python中模块导入问题(已解决)
想在python中导入request包: 无此模块,于是先安装requests包: 但是提示"Requirement already satisfied".在提示的相应目录里,找到 ...
- Python模块及其导入
一.模块 1.模块的定义: 为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少, 很多编程语言都采用这种组织代码的方式.在Python中,一个.py文件 ...
- python random模块导入及用法
random是程序随机数,很多地方用到,验证码,图片上传的图片名称等,下面说说python random模块导入及用法 1,模块导入 import random 2,random用法 random.r ...
随机推荐
- LeetCode Graph Valid Tree
原题链接在这里:https://leetcode.com/problems/graph-valid-tree/ 题目: Given n nodes labeled from 0 to n - 1 an ...
- Docker的私有仓库
server 192.168.1.107 registry ---push client 192.168.1.103 --pull [192.168.1.107 ...
- KingBlog记录
本片文章其他人可能看不懂,只是我做记录用,不要见怪 KingBlogCMS总结 开发工具: VS2012+SOLServer2008+IIS 开发语言: ASP.NET.Html.js.JQ 第三方: ...
- 自定义view中错误:No resource identifier found for attribute X in package X
- RDIFramework.NET 中多表关联查询分页实例
RDIFramework.NET 中多表关联查询分页实例 RDIFramework.NET,基于.NET的快速信息化系统开发.整合框架,给用户和开发者最佳的.Net框架部署方案.该框架以SOA范式作为 ...
- Struts2(六):ResultType
本章节将继续学习struts2的返回类型的使用方法. 学习文档下载struts2 full包解压后会在doc下包含离线html文档. 点击运行后页面: 点击Guides向导终将会有向导列表 再点开后, ...
- iOS逆传值的三种方式
1.代理 2.block 2.通知中心
- W3cshool之JavaScript基础
1. JavaScript 对大小写敏感 名为 "myfunction"的函数和名为 "myFunction" 的函数是两个不同的函数,同样,变量 & ...
- Android——初探Dagger2依赖注入
1,在做项目时,经常需要在一个对象里去创建另一个对象的示例,这种行为是产生耦合的常见形式,对于一个大型项目来说,过多的相互依赖会导致代码难以维护,很容易就会碰到修改一个小需求需要大面积的修改各种代码, ...
- ACCESS自动编号清零
ACCESS的数据库,当每次删除所有记录后,表里的一个ID字段(自动编号),无限递增,位数无限扩.当每次执行删除查询时,程序就把“自动编号”型ID字段清零,然后重新从“初始值”开始,解决方法如下: ...