使用__future__样式导入Python中的模块特定功能

时间:2015-04-27 20:24:58

标签: python

Python未来语句from __future__ import feature提供了一种简化向新语言功能过渡的好方法。是否可以为Python库实现类似的功能:from myproject.__future__ import feature

在import语句中设置模块宽常量很简单。对我来说不明显的是如何确保这些常量不会传播到导入模块中执行的代码 - 它们还应该要求将来导入以启用新功能。

最近在NumPy的discussion of possible indexing changes中出现了这个问题。我不认为它会在NumPy中实际使用,但我可以看到它对其他项目有用。

作为一个具体的例子,假设我们确实希望改变索引在NumPy的未来版本中的工作方式。这将是一个向后不兼容的变化,因此我们决定使用未来的声明来简化过渡。使用此新功能的脚本如下所示:

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

5 个答案:

答案 0 :(得分:4)

Python中的__future__既是一个模块,也不是。 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导入位于文件的开头 - 此时它的全局变量不应包含除此伪造返回的值之外的任何其他内容模块;否则访问该值仅供检查。

然而真正的问题是:你如何使用它 - 找出调用函数的模块是非常昂贵的,并且会限制此功能的有用性。

因此,可能更有成效的方法是使用import hooksfrom mypackage.__future__ import more_magic的模块上对抽象语法树进行源代码翻译,可能将所有object[index]更改为__newgetitem__(operand, index)

答案 1 :(得分:3)

Python本身的做法非常简单:

importer中,当您尝试从.py文件导入时,代码首先会扫描模块future statements

请注意,在将来的语句之前允许的唯一内容是字符串,注释,空行和其他将来的语句,这意味着它不需要完全解析代码来执行此操作。这很重要,因为未来的语句可以改变代码的解析方式(事实上,这就是拥有它们的重点......); lexer步骤可以处理字符串,注释和空行,并且可以使用非常简单的专用解析器解析将来的语句。

然后,如果找到任何将来的语句,Python会设置相应的标志位,然后重新搜索到文件的顶部并使用这些标志调用compile。例如,对于from __future__ import unicode_literals,它flags |= __future__.unicode_literals.compiler_flagflags0更改为0x20000

在这个“真正的编译”步骤中,将来的语句被视为普通导入,并且您将在模块的全局变量中的变量__future__._Feature中得到unicode_literals值。

现在,你不能完全做同样的事情,因为你不会重新实现或包装编译器。但是你可以做的是使用你未来的语句来表示AST转换步骤。像这样:

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表示空白行,注释或字符串,以及可识别的未来导入的标志(如果需要,可以动态查找)或{ {1}}其他任何事情。而且你必须为每个标志写AST transformers。但它们可以非常简单 - 例如,您可以将None个节点转换为不同的Subscript节点,甚至转换为基于索引形式调用不同函数的Subscript个节点。

要将其挂钩到导入系统,请参阅PEP 302。请注意,这在Python 3.3中变得更简单,并且在Python 3.4中再次变得更简单,因此如果您需要其中一个版本,请阅读import system文档以获取最低版本。

有关在现实生活中使用的导入钩子和AST变换器的一个很好的例子,请参阅MacroPy。 (请注意,它使用旧的2.3样式导入钩子机制;再次,如果你可以使用3.3+或3.4+,你自己的代码可以更简单。当然你的代码不会动态生成变换,这是最复杂的MacroPy的一部分......)

答案 2 :(得分:2)

不,你不能。真正的__future__导入是特殊的,因为它的效果是发生它的单个文件的本地。但是普通导入是全局的:一旦一个模块执行import blahblah就会被执行并且在全球范围内可用;稍后执行import blah的其他模块只检索已导入的模块。这意味着如果from numpy.__future__更改了numpy中的内容,import numpy所做的一切都会看到更改。

顺便说一句,我不认为这是邮件列表消息的建议。我把它看作是暗示 全局的效果,相当于设置像numpy.useNewIndexing = True这样的标志。这意味着如果您知道应用程序的所有部分都可以使用,那么您应该只在应用程序的顶层设置该标志。

答案 3 :(得分:2)

不,没有合理的方法可以做到这一点。让我们完成要求。

首先,您需要确定哪些模块启用了自定义future语句。标准进口不是这样的,但您可以要求它们,例如调用一些启用函数并将__name__作为参数传递。这有点难看:

from numpy.future import new_indexing
new_indexing(__name__)

面对importlib.reload()而言,这已经分崩离析了,但是meh。

接下来,您需要确定您的调用方是否在其中一个模块中运行。你首先要通过inspect.stack()(在所有Python实现中都没有工作,错过C扩展模块等)拉出堆栈,然后用inspect.getmodule()来解决这个问题。等。

坦率地说,这只是一个坏主意。

答案 4 :(得分:2)

如果"功能"您想控制的可以归结为更改名称,这很容易做到,比如

from module.new_way import something

VS

from module.old_way import something

你建议的功能当然不是,但我认为这是在不同范围内有不同行为的唯一Pythonic方式(我认为你的意思是范围,而不是模块,例如,如果有人做了什么,在函数定义中导入),因为作用域名称由解释器本身控制并得到很好的支持。