from __future__ import absolute_import 实际上做了什么?

What does from __future__ import absolute_import actually do?

我有answered a question regarding absolute imports in Python, which I thought I understood based on reading the Python 2.5 changelog and accompanying PEP。然而,在安装 Python 2.5 并尝试制作一个正确使用 from __future__ import absolute_import 的示例后,我意识到事情并不那么清楚。

直接从上面链接的变更日志中,这个声明准确地总结了我对绝对导入变更的理解:

Let's say you have a package directory like this:

pkg/
pkg/__init__.py
pkg/main.py
pkg/string.py

This defines a package named pkg containing the pkg.main and pkg.string submodules.

Consider the code in the main.py module. What happens if it executes the statement import string? In Python 2.4 and earlier, it will first look in the package's directory to perform a relative import, finds pkg/string.py, imports the contents of that file as the pkg.string module, and that module is bound to the name "string" in the pkg.main module's namespace.

所以我创建了这个确切的目录结构:

$ ls -R
.:
pkg/

./pkg:
__init__.py  main.py  string.py

__init__.pystring.py 为空。 main.py 包含以下代码:

import string
print string.ascii_uppercase

正如预期的那样,运行使用 Python 2.5 失败并出现 AttributeError:

$ python2.5 pkg/main.py
Traceback (most recent call last):
  File "pkg/main.py", line 2, in <module>
    print string.ascii_uppercase
AttributeError: 'module' object has no attribute 'ascii_uppercase'

但是,在 2.5 更新日志中,我们发现了这一点(强调已添加):

In Python 2.5, you can switch import's behaviour to absolute imports using a from __future__ import absolute_import directive. This absolute-import behaviour will become the default in a future version (probably Python 2.7). Once absolute imports are the default, import string will always find the standard library's version.

我因此创建了 pkg/main2.py,与 main.py 相同,但带有额外的未来导入指令。现在看起来像这样:

from __future__ import absolute_import
import string
print string.ascii_uppercase

运行 这与 Python 2.5,但是...失败 AttributeError:

$ python2.5 pkg/main2.py
Traceback (most recent call last):
  File "pkg/main2.py", line 3, in <module>
    print string.ascii_uppercase
AttributeError: 'module' object has no attribute 'ascii_uppercase'

这与 import string 总是 找到启用绝对导入的 std-lib 版本的说法完全矛盾。更重要的是,尽管绝对导入计划成为 "new default" 行为的警告,我使用 Python 2.7,有或没有 __future__ 指令都遇到了同样的问题:

$ python2.7 pkg/main.py
Traceback (most recent call last):
  File "pkg/main.py", line 2, in <module>
    print string.ascii_uppercase
AttributeError: 'module' object has no attribute 'ascii_uppercase'

$ python2.7 pkg/main2.py
Traceback (most recent call last):
  File "pkg/main2.py", line 3, in <module>
    print string.ascii_uppercase
AttributeError: 'module' object has no attribute 'ascii_uppercase'

以及 Python 3.5,有或没有(假设 print 语句在两个文件中都已更改):

$ python3.5 pkg/main.py
Traceback (most recent call last):
  File "pkg/main.py", line 2, in <module>
    print(string.ascii_uppercase)
AttributeError: module 'string' has no attribute 'ascii_uppercase'

$ python3.5 pkg/main2.py
Traceback (most recent call last):
  File "pkg/main2.py", line 3, in <module>
    print(string.ascii_uppercase)
AttributeError: module 'string' has no attribute 'ascii_uppercase'

我已经测试了它的其他变体。我创建了一个空模块,而不是 string.py——一个名为 string 的目录,只包含一个空的 __init__.py——而不是从 main.py 发出导入,我有 cd' 到 pkg 和 运行 直接从 REPL 导入。这些变体(或它们的组合)都没有改变上面的结果。我无法将此与我所阅读的有关 __future__ 指令和绝对导入的内容相协调。

在我看来,the following 很容易解释这一点(这来自 Python 2 文档,但此声明在 Python 3 的相同文档中保持不变):

sys.path

(...)

As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter. If the script directory is not available (e.g. if the interpreter is invoked interactively or if the script is read from standard input), path[0] is the empty string, which directs Python to search modules in the current directory first.

那我错过了什么?为什么 __future__ 声明似乎没有按照它所说的去做,这两部分文档之间以及描述的行为和实际行为之间的矛盾的解决方法是什么?

仅当您从包中导入模块并且该模块从该包中导入另一个子模块时,绝对导入和相对导入之间的区别才会发挥作用。查看区别:

$ mkdir pkg
$ touch pkg/__init__.py
$ touch pkg/string.py
$ echo 'import string;print(string.ascii_uppercase)' > pkg/main1.py
$ python2
Python 2.7.9 (default, Dec 13 2014, 18:02:08) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pkg.main1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pkg/main1.py", line 1, in <module>
    import string;print(string.ascii_uppercase)
AttributeError: 'module' object has no attribute 'ascii_uppercase'
>>> 
$ echo 'from __future__ import absolute_import;import string;print(string.ascii_uppercase)' > pkg/main2.py
$ python2
Python 2.7.9 (default, Dec 13 2014, 18:02:08) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pkg.main2
ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> 

特别是:

$ python2 pkg/main2.py
Traceback (most recent call last):
  File "pkg/main2.py", line 1, in <module>
    from __future__ import absolute_import;import string;print(string.ascii_uppercase)
AttributeError: 'module' object has no attribute 'ascii_uppercase'
$ python2
Python 2.7.9 (default, Dec 13 2014, 18:02:08) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pkg.main2
ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> 
$ python2 -m pkg.main2
ABCDEFGHIJKLMNOPQRSTUVWXYZ

请注意 python2 pkg/main2.py 与启动 python2 然后导入 pkg.main2 有不同的行为(相当于使用 -m 开关)。

如果你想 运行 一个包的子模块总是使用 -m 开关来防止解释器链接 sys.path 列表并正确处理子模块的语义.

此外,我更喜欢对包子模块使用显式相对导入,因为它们提供更多语义和更好的错误消息以防失败。

变更日志措辞草率。 from __future__ import absolute_import 不关心某些东西是否是标准库的一部分,并且 import string 不会总是给你绝对导入的标准库模块。

from __future__ import absolute_import 意味着如果您 import string,Python 将始终寻找顶级 string 模块,而不是 current_package.string。但是,它不会影响 Python 用来决定哪个文件是 string 模块的逻辑。当你这样做时

python pkg/script.py

pkg/script.py 看起来不像 Python 包的一部分。按照正常程序,将 pkg 目录添加到路径中,并且 pkg 目录中的所有 .py 文件看起来都像顶级模块。 import string 发现 pkg/string.py 不是因为它正在进行相对导入,而是因为 pkg/string.py 似乎是顶级模块 string。这不是标准库 string 模块的事实没有出现。

要运行文件作为pkg包的一部分,你可以

python -m pkg.script

在这种情况下,pkg 目录将不会添加到路径中。但是,当前目录将添加到路径中。

您还可以向 pkg/script.py 添加一些样板,使 Python 将其视为 pkg 包的一部分,即使 运行 作为文件也是如此:

if __name__ == '__main__' and __package__ is None:
    __package__ = 'pkg'

但是,这不会影响 sys.path。您需要一些额外的处理来从路径中删除 pkg 目录,如果 pkg 的父目录不在路径中,您也需要将其粘贴在路径中。