相对导入的噩梦,PEP 366 是如何工作的?

33 投票
4 回答
7798 浏览
提问于 2025-04-15 23:22

我有一个“标准文件结构”,大概是这样的(我给它们起了容易理解的名字):

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 个回答

7

受到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 
46

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是包含顶层的目录。

7

加载代码看起来像是这个:

    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被加载,这样导入就可以是相对的。

撰写回答