Использование импорта стиля __future__ для специфических для модуля функций в Python

Будущий оператор Python from __future__ import feature обеспечивает отличный способ облегчить переход на новые языковые функции. Возможно ли реализовать аналогичную функцию для библиотек Python: from myproject.__future__ import feature?

Простое задание констант модуля в инструкции импорта. Что для меня не очевидно, так это то, как вы могли бы обеспечить, чтобы эти константы не распространялись на код, выполняемый в импортированных модулях, - они также должны требовать будущего импорта, чтобы включить новую функцию.

Это недавно появилось в обсуждении возможных изменений индексации в NumPy. Я не ожидаю, что он действительно будет использоваться в NumPy, но я вижу, что он полезен для других проектов.

В качестве конкретного примера предположим, что мы хотим изменить, как работает индексирование в некоторой будущей версии NumPy. Это было бы несовместимым с обратным изменением, поэтому мы решили использовать будущее выражение для облегчения перехода. A script с использованием этой новой функции выглядит примерно так:

import numpy as np
from numpy.__future__ import orthogonal_indexing

x = np.random.randn(5, 5)
print(x[[0, 1], [0, 1]])  # should use the "orthogonal indexing" feature
# prints a 2x2 array of random numbers

# we also want to use a legacy project that uses indexing, but
# hasn't been updated to the use the "orthogonal indexing" feature
from legacy_project import do_something

do_something(x)  # should *not* use "orthogonal indexing"

Если это невозможно, то чем ближе мы можем получить для включения локальных опций? Например, можно написать что-то вроде:

from numpy import future
future.enable_orthogonal_indexing()

Использование чего-то вроде диспетчера контекста будет хорошо, но проблема в том, что мы не хотим распространять параметры для вложенных областей:

with numpy.future.enable_orthogonal_indexing():
    print(x[[0, 1], [0, 1]])  # should use the "orthogonal indexing" feature
    do_something(x)  # should *not* use "orthogonal indexing" inside do_something

Ответ 1

__future__ в Python является одновременно модулем, а также нет. Python __future__ на самом деле не импортируется нигде - это конструкция, используемая компилятором байт-кода Python, специально выбранная так, чтобы новый синтаксис не создавался. В каталоге библиотеки также есть __future__.py; он может быть импортирован как таковой: import __future__; а затем вы можете, например, получить доступ к __future__.print_function, чтобы узнать, какая версия Python делает эту опцию доступной и в какой версии функция включена по умолчанию.


Можно создать модуль __future__, который знает, что импортируется. Ниже приведен пример myproject/__future__.py, который может перехватывать импорт объектов по модулю:

import sys
import inspect

class FutureMagic(object):
    inspect = inspect

    @property
    def more_magic(self):
        importing_frame = self.inspect.getouterframes(
                  self.inspect.currentframe())[1][0]
        module = importing_frame.f_globals['__name__']
        print("more magic imported in %s" % module)

sys.modules[__name__] = FutureMagic()

В момент загрузки модуль заменяется экземпляром FutureMagic(). Всякий раз, когда more_magic импортируется из myproject.FutureMagic, будет вызываться метод свойства more_magic, и он распечатает имя модуля, который импортировал эту функцию:

>>> from myproject.__future__ import more_magic
more magic imported in __main__

Теперь у вас может быть учет модулей, которые импортировали эту функцию. Выполнение import myproject.__future__; myproject.__future__.more_magic будет запускать один и тот же механизм, но вы также можете убедиться, что импорт more_magic будет в начале файла - его глобальные переменные в этой точке не должны содержать ничего, кроме значений, возвращаемых из этого поддельного модуля; в противном случае доступ к этому значению осуществляется только для проверки.

Однако реальный вопрос заключается в следующем: как вы могли бы использовать это - выяснить, из какого модуля вызывается функция, довольно дорого и ограничит полезность этой функции.


Таким образом, возможно, более плодотворный подход может заключаться в использовании импортных крючков для перевода исходного кода на абстрактных синтаксических деревьях на модули, которые делают from mypackage.__future__ import more_magic, возможно, изменив все object[index] на __newgetitem__(operand, index).

Ответ 2

Способ Python делает это довольно просто:

В импортер при попытке импортировать из файла .py код сначала сканирует модуль для будущие заявления.

Обратите внимание, что единственное, что разрешено перед будущим оператором, это строки, комментарии, пустые строки и другие будущие утверждения, а это означает, что для этого не нужно полностью анализировать код. Это важно, потому что будущие утверждения могут изменить способ анализа кода (фактически, что весь смысл их...); строки, комментарии и пустые строки могут обрабатываться с помощью lexer, а будущие операторы могут быть проанализированы с помощью очень простого специализированного синтаксического анализатора.

Затем, если какие-либо будущие утверждения найдены, Python устанавливает бит соответствующего флага, затем повторно просматривает верхнюю часть файла и вызывает compile с этими флагами. Например, для from __future__ import unicode_literals он выполняет flags |= __future__.unicode_literals.compiler_flag, который меняет flags от 0 до 0x20000.

На этом этапе "реального компиляции" будущие операторы рассматриваются как нормальный импорт, и вы получите значение __future__._Feature в переменной unicode_literals в глобальных глоссалах модуля.


Теперь вы не можете сделать то же самое, потому что вы не собираетесь переопределять или обертывать компилятор. Но то, что вы можете сделать, это использовать ваши будущие заявления, чтобы сигнализировать шаг преобразования АСТ. Что-то вроде этого:

flags = []
for line in f:
    flag = parse_future(line)
    if flag is None:
        break
    flags.append(flag)
f.seek(0)
contents = f.read()
tree = ast.parse(contents, f.name)
for flag in flags:
    tree = transformers[flag](tree)
code = compile(tree, f.name)

Конечно, вы должны написать, что функция parse_future возвращает 0 для пустой строки, комментария или строки, флаг для распознанного будущего импорта (который вы можете искать динамически, если хотите) или None для чего угодно. И вы должны написать трансформаторы AST для каждого флага. Но они могут быть довольно простыми - например, вы можете преобразовать узлы Subscript в разные узлы Subscript или даже в узлы Call, которые вызывают разные функции на основе формы индекса.

Чтобы подключить его к системе импорта, см. PEP 302. Обратите внимание, что это упрощается в Python 3.3 и проще в Python 3.4, поэтому, если вы можете потребовать одну из этих версий, вместо этого прочитайте систему импорта docs для вашей минимальной версии.


Для отличного примера импортных крюков и трансформаторов AST, используемых в реальной жизни, см. MacroPy. (Обратите внимание, что он использует старый механизм захвата импорта в стиле 2.3, и ваш собственный код может быть проще, если вы можете использовать 3,3 или 3,4+. И, конечно, ваш код не генерирует преобразования динамически, что является самым сложным часть MacroPy...)

Ответ 3

Нет, вы не можете. Реальный __future__ импорт особенный, поскольку его эффекты локальны для отдельного файла, где он встречается. Но обычный импорт является глобальным: когда один модуль выполняет import blah, blah выполняется и доступен глобально; другие модули, которые позже выполняют import blah, просто получают уже импортированный модуль. Это означает, что если from numpy.__future__ что-то изменяет в numpy, все, что делает import numpy, увидит изменение.

В стороне, я не думаю, что это то, что предлагает это сообщение списка рассылки. Я читал его как предложение глобального эффекта, эквивалентного установке флага типа numpy.useNewIndexing = True. Это означает, что вы должны устанавливать этот флаг только на верхнем уровне вашего приложения, если знаете, что все части вашего приложения будут работать с этим.

Ответ 4

Нет, нет разумного способа сделать это. Давайте рассмотрим требования.

Во-первых, вам нужно выяснить, какие модули включены в ваш пользовательский будущий оператор. Стандартный импорт не подходит для этого, но вы можете потребовать от них, например. вызовите некоторую функцию включения и передайте __name__ в качестве параметра. Это несколько уродливо:

from numpy.future import new_indexing
new_indexing(__name__)

Это разваливается перед лицом importlib.reload(), но meh.

Затем вам нужно выяснить, работает ли ваш вызывающий абонент в одном из этих модулей. Вы начнете, вытаскивая стек через inspect.stack() (который не будет работать под всеми реализациями Python, пропущенными модулями расширения C и т.д.), а затем goof вокруг с inspect.getmodule() и т.п.

Честно говоря, это всего лишь плохая идея.

Ответ 5

Если "функция", которую вы хотите контролировать, может быть сведена к изменению имени, тогда это легко сделать, например

from module.new_way import something

против

from module.old_way import something

Функция, которую вы предложили, конечно, не является, но я бы сказал, что это единственный питонический способ иметь различное поведение в разных областях (и я думаю, что вы имеете в виду область действия, а не модуль, например, что, если кто-то делает импорт внутри определения функции), поскольку имена областей управления контролируются и хорошо поддерживаются самим интерпретатором.