相对导入的噩梦,PEP 366 是如何工作的?
我有一个“标准文件结构”,大概是这样的(我给它们起了容易理解的名字):
mainpack/
__main__.py
__init__.py
- helpers/
__init__.py
path.py
- network/
__init__.py
clientlib.py
server.py
- gui/
__init__.py
mainwindow.py
controllers.py
在这个结构中,比如每个包里的模块可能想通过相对导入来访问helpers
工具,像这样:
# network/clientlib.py
from ..helpers.path import create_dir
程序是通过运行__main__.py
文件来“作为脚本”执行的,方式是:
python mainpack/
为了遵循PEP 366,我在__main__.py
中写了这些代码:
___package___ = "mainpack"
from .network.clientlib import helloclient
但是当我运行时:
$ python mainpack
Traceback (most recent call last):
File "/usr/lib/python2.6/runpy.py", line 122, in _run_module_as_main
"__main__", fname, loader, pkg_name)
File "/usr/lib/python2.6/runpy.py", line 34, in _run_code
exec code in run_globals
File "path/mainpack/__main__.py", line 2, in <module>
from .network.clientlib import helloclient
SystemError: Parent module 'mainpack' not loaded, cannot perform relative import
出什么问题了?正确处理和有效使用相对导入的方式是什么?
我也试着把当前目录添加到PYTHONPATH里,但没有任何变化。
4 个回答
受到extraneon和taherh的启发,这里有一段代码,它会沿着文件树向上查找,直到找不到__init__.py
文件为止,从而构建出完整的包名。虽然这种方法有点“黑科技”,但似乎不管文件在目录树的深度如何,它都能正常工作。看起来绝对导入是非常被推荐的。
import os, sys
if __name__ == "__main__" and __package__ is None:
d,f = os.path.split(os.path.abspath(__file__))
f = os.path.splitext(f)[0]
__package__ = [f] #__package__ will be a reversed list of package name parts
while os.path.exists(os.path.join(d,'__init__.py')): #go up until we run out of __init__.py files
d,name = os.path.split(d) #pull of a lowest level directory name
__package__.append(name) #add it to the package parts list
__package__ = ".".join(reversed(__package__)) #create the full package name
mod = __import__(__package__) #this assumes the top level package is in your $PYTHONPATH
sys.modules[__package__] = mod #add to modules
在PEP 366中给出的“模板”似乎不太完整。虽然它设置了__package__
这个变量,但实际上并没有导入这个包,而导入包是让相对导入能够正常工作的必要条件。extraneon的解决方案是个不错的方向。
需要注意的是,仅仅把包含模块的目录放在sys.path
中是不够的,还需要明确导入对应的包。下面的代码看起来比PEP 366中提供的模板更好,可以确保一个Python模块无论通过什么方式调用(比如常规的import
,或者用python -m
,或者用python
,从任何位置)都能正常执行:
# boilerplate to allow running as script directly
if __name__ == "__main__" and __package__ is None:
import sys, os
# The following assumes the script is in the top level of the package
# directory. We use dirname() to help get the parent directory to add to
# sys.path, so that we can import the current package. This is necessary
# since when invoked directly, the 'current' package is not automatically
# imported.
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(1, parent_dir)
import mypackage
__package__ = str("mypackage")
del sys, os
# now you can use relative imports here that will work regardless of how this
# python file was accessed (either through 'import', through 'python -m', or
# directly.
如果脚本不在包目录的顶层,并且你需要导入顶层以下的模块,那么os.path.dirname
需要重复使用,直到parent_dir
是包含顶层的目录。
加载代码看起来像是这个:
try:
return sys.modules[pkgname]
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)
这让我觉得可能你的模块不在sys.path里。如果你正常启动Python,然后在提示符下输入“import mainpack”,会发生什么呢?它应该能够找到这个模块。
我自己也试过,结果遇到了同样的错误。看了一下资料,我找到了解决办法:
# foo/__main__.py
import sys
mod = __import__('foo')
sys.modules["foo"]=mod
__package__='foo'
from .bar import hello
hello()
这方法对我来说有点像是变通的做法,但确实有效。诀窍似乎是确保包foo
被加载,这样导入就可以是相对的。