Что делает из __future__ import absolute_import на самом деле?

У меня ответил вопрос об абсолютном импорте в Python, который, как я думал, я понял, основываясь на чтении журнал изменений Python 2.5 и сопровождающий PEP. Однако при установке Python 2.5 и попытке создать пример правильного использования from __future__ import absolute_import, я понимаю, что все не так ясно.

Прямо из приведенного выше списка изменений, это утверждение точно суммировало мое понимание абсолютного изменения импорта:

Скажем, у вас есть каталог пакетов следующим образом:

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

Определяет пакет с именем pkg, содержащий подмодули pkg.main и pkg.string.

Рассмотрим код в модуле main.py. Что произойдет, если он выполнит оператор import string? В Python 2.4 и ранее он сначала будет искать в каталоге пакетов для выполнения относительного импорта, находит pkg/string.py, импортирует содержимое этого файла в качестве модуля pkg.string, и этот модуль привязан к имени "string" в пространстве имен модулей pkg.main.

Итак, я создал эту точную структуру каталогов:

$ ls -R
.:
pkg/

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

__init__.py и string.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 changelog, мы находим это (выделено мной):

В Python 2.5 вы можете переключить поведение import на абсолютный импорт с помощью директивы from __future__ import absolute_import. Это абсолютное поведение импорта станет дефолтом в будущей версии (возможно, Python 2.7). Когда абсолютный импорт по умолчанию, import string всегда будет искать стандартную версию библиотеки.

Таким образом, я создал 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 с включенным абсолютным импортом. Что еще, несмотря на предупреждение о том, что абсолютный импорт станет "новым по умолчанию", я столкнулся с этой проблемой, используя как 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 'd to pkg и запускать импорт непосредственно из REPL. Ни один из этих вариантов (а также их комбинация) не изменили результаты выше. Я не могу смириться с тем, что я прочитал о директиве __future__ и абсолютном импорте.

Мне кажется, что это легко объясняется следующим (это из документов Python 2, но это утверждение остается неизменным в том же docs для Python 3):

sys.path

(...)

Как инициализировано при запуске программы, первым элементом этого списка, path[0], является каталог, содержащий script, который использовался для вызова интерпретатора Python. Если каталог script недоступен (например, если интерпретатор вызывается интерактивно или если script считывается со стандартного ввода), path[0] - это пустая строка , которая направляет Python на поиск модулей в текущей сначала.

Так что мне не хватает? Почему оператор __future__, по-видимому, не делает то, что он говорит, и какова резолюция этого противоречия между этими двумя разделами документации, а также между описанным и фактическим поведением?

Ответ 1

Строка изменений неверно сформулирована. 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 добавляется в путь, а все .py файлы в каталоге pkg выглядят как модули верхнего уровня. 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 родительский каталог не находится на пути, вам также нужно будет придерживаться этого пути.

Ответ 2

Разница между абсолютным и относительным импортом вступает в силу только тогда, когда вы импортируете модуль из пакета, и этот модуль импортирует другой подмодуль из этого пакета. Увидеть разницу:

$ 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 и правильно обрабатывает семантику подмодуля.

Кроме того, я предпочитаю использовать явный относительный импорт для подмодулей пакетов, так как они предоставляют больше семантики и улучшают сообщения об ошибках в случае сбоя.