在Python 2和3中,导入结构既可以在包中也可以在包中运行?

时间:2014-05-30 10:10:03

标签: python python-3.x package python-2.x python-import

当我开发纯粹用于Python 2的软件包时,我可以使用普通的import b语法导入相对路径,而无需关心导入文件是否在包中。这样做的好处是我可以通过执行文件来运行任何文件的if __name__ == "__main__":块,并且所有导入都可以正常工作。

在添加Python 3支持后,我不得不转向新的相对导入语法,2.7也支持from . import b。但是,此语法 only 在包内部工作。直接直接执行文件不再有效:

Traceback (most recent call last):
  File "./a.py", line 2, in <module>
    from . import b
ValueError: Attempted relative import in non-package

解决方法是通过从上层目录导入文件来调用该文件:

python -m foo.a

但是,这会对工作目录提出一个要求,这会阻止您将输出传递给其他同样关心工作目录的程序。

有没有办法让你的蛋糕也吃掉它?即支持作为脚本运行和作为包的一部分导入,同时在Python 2和3中工作?


示例包结构:

foo/
foo/__init__.py
foo/a.py (imports b)
foo/b.py (imports c)
foo/c.py

我希望以下两个方法适用于(a,b,c)中的x:

import foo.x (in some file when foo/ is in path)

python[23] path/to/foo/x.py

下面的评论提到根据PEP 366设置__package__,但是“如果脚本移动到不同的包或子包,则需要手动更新样板。”

更新:我试图让PEP 366解决方案正常运行,但无法理解。它说:

  

需要操作sys.path的其他代码才能使直接执行工作,而顶层包不可导入。

从非导入包执行文件时就是这种情况。那些额外的代码会是什么样的?

3 个答案:

答案 0 :(得分:2)

  

有没有办法让你的蛋糕也吃掉它?即支持作为脚本运行和作为包的一部分导入,同时在Python 2和3中工作?

不.....也许......但是从我能收集到的你想要做的事情来看,你所面临的问题就更复杂了。我会创建一个像普通的包,你可以支持Python 2&amp; 3.然后使用setup script安装并将包导入脚本,而不必使用相对路径。这使您可以随时随地执行脚本,Python 2&amp; 3包的兼容性。


我仍然坚持我上面的原始声明,因为我相信你让它变得比它真正需要的更复杂。或者你没有向我们提供所有关于为什么要这样做的信息。尽管如此,如果你遵循PEP 366所说的那应该有效。在脚本所在的模块中(即包含if __name__ == "__main__":),然后将以下行添加到文件的开头(或主if __name__ == "__main__":之前):

if __name__ == "__main__" and __package__ == None:
        __package__ == "expected.package.name"
        sys.path.append(<path to root package 'expected'>)

这当然意味着如果您移动脚本,或者如果移动包,或者与该路径相关的任何内容被移动,您将需要手动更新这些(因此我仍然认为安装是通过这是更好的选择)。

答案 1 :(得分:0)

好吧,您可以在PYTHONPATH中添加ab个模块所在的目录(参考:https://docs.python.org/2/using/cmdline.html#envvar-PYTHONPATH)。

此外,如上面的链接所述,如果a是主模块,模块a所在的目录将自动添加到PYTHONPATH。例如,如果您在/test/a.py/test/b.py文件中有以下代码:

/test/a.py

if __name__ == '__main__':
    import sys
    print(sys.path)

    import b
    print('this is a')

/test/b.py

print('this is b')

您执行a as:

$ cd /test/
$ python3 a.py

你会得到一个输出:

['', '/usr/lib/python34.zip', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-linux', '/usr/lib/python3.4/lib-dynload', '/usr/lib/python3.4/site-packages']
this is b
this is a

另外,如果您执行:

$ python3 /test/a.py

你会得到输出:

['/test', '/usr/lib/python34.zip', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-linux', '/usr/lib/python3.4/lib-dynload', '/usr/lib/python3.4/site-packages']
this is b
this is a

答案 2 :(得分:0)

这是一个基于KronoS' answer的解决方案和注释,允许模块使用相同的样板,无论路径或包名称如何:

if __name__ == "__main__" and __package__ == None:
    import importlib
    import os.path
    import sys
    def _gen_path():
        head, tail = os.path.split(os.path.realpath(__file__))
        while head:
            if not os.path.isfile(os.path.join(head, '__init__.py')):
                yield head
                return
            head, tail = os.path.split(head)
            yield tail
    def _load_package():
        path = list(_gen_path())
        syspath = sys.path[:]
        sys.path[:0] = [path.pop()]
        package = '.'.join(reversed(path))
        importlib.import_module(package)
        sys.path = syspath
        return package
    __package__ = _load_package()

它将文件路径向上移动,只要有__init__.py个文件标记包,然后导入模块的父包,正确设置__package__。在此之后,像from ..bar import baz这样的相对导入就可以了。


将这些功能放在自己的模块中太糟糕了,让你回到原点。此外,似乎没有Python 2/3可移植方式来限制sys.path更改对该导入的影响,因此基目录中的任何内容都可能会影响任何使用父包导入的模块或包。