在 python 解释器中挂钩全局名称查找

Hook the global name lookup in a python interpreter

事情是这样的,我有一个代理保存对远程模块的引用,我将其中一些代理放到 sys.modules 这样我就可以像使用本地模块一样使用它。但是其他一些对象放在远程环境的 __builtin__ 模块中(例如为了方便调试或引用而设置的魔术变量)。我不想像 conn.__builtin__.var 那样引用这些变量,我必须替换本地 __builtin__(这似乎不适用于替换 sys.modules['__builtin__'] 或挂钩全局名称查找规则。 如何?对于模块,您只需重载 getattr 即可。但是在像 IPython 这样的交互式解释器中,谁是主要模块或者如何做到这一点? 更新: 正如@Nizam Mohamed 所指出的,是的,我可以获得 __main__ 模块,但我仍然无法修改它的名称查找角色。

我想把本地环境完全变成远程环境(用于调试控制台)

更新

现在我只是迭代所有 __builtin__.__dict__,如果有一个名称不在本地 __builtin__。我将名称添加到本地的 __builtin__。但与名称查找规则相比,它不是那么动态,如果我在本地找不到名称 __builtin__ 试试远程的。

here是类似的讨论。

this问题通过用sys.modules中的对象替换模块来模拟模块。但这不适用于 __builtin__ 名称查找,我还尝试将 __builtin__.__getattribute__ 替换为自定义查找,该自定义查找将首先使用原始查找,然后在失败时使用自定义查找。但是 __builtin__ 的全局名称查找从未调用到 __builtin__.__getattribute__ 甚至 __builtin__.__getattribute__('name') returns 所需的值,__builtin__.namename 从来没有 returns 一.

有一种方法可以获取模块将使用的所有名称的列表。虽然没有修改查找机制,但相信可以解决你的问题。

这里有一些代码(希望足够理解)你可以放在一个模块中,我们称之为 magic:

import sys

def magic():
    # Get the caller frame
    frame = sys._getframe().f_back

    # Unwind all internal Python import-related stuff
    while frame.f_code.co_filename.startswith('<'):
        frame = frame.f_back

    importer = frame

    # Iterate through names the module has/will use
    for name in importer.f_code.co_names:

        # If the module has not yet defined/imported this name
        if name not in importer.f_globals and \
                name not in importer.f_locals and \
                name not in __builtins__:

            # Replace the name in the importer's namespace
            # You'll have to replace the right-hand side by your specific code
            importer.f_globals[name] = 'hello world'

然后您可以导入它来施展魔法:

import magic
magic.magic()

print(hi)

然而,有两个缺点。首先,动态查找会失败:

import magic
magic.magic()

print(globals()['hi']) # KeyError: 'hi'

虽然这种特殊情况可以通过查看 importer.f_code.co_consts 中的字符串来解决,但它不适用于 print(globals()['h'+'i'])

等更高级的动态查找

第二个缺点是它在函数中不起作用:

import magic
magic.magic()

def f():
    print(hi) # NameError: name 'hi' is not defined

f()

这是因为在这种情况下,名称 hi 位于 f.__code__.co_names 而不是模块的 co_names

一个可能的解决方案是修改 magic 以尝试找到模块的所有函数并更改它们的代码,但它不适用于函数内部定义的函数等。

另一个解决方案是在函数中调用magic

import magic

def f():
    magic.magic()
    print(hi)

f()

使用 IPython shell

的 AST 转换

正如@asmeurer 所说,您可以编写一个简单的 AST 转换器来“挂钩”变量名查找。基础 class ast.NodeTransformer 提供了一个可以操作的 visit_Name 方法。你只需要重载这个方法来重新定义那些存在于远程模块但不存在于本地的变量。

以下模块可以用作 IPython extension:

testAST.py

import ast

modName = "undefined"
modAttr = []
user_ns = {}
class MyTransformer(ast.NodeTransformer):
    def visit_Name(self, node):
        if node.id in modAttr and not node.id in user_ns: 
          return self.getName(node)
        return node
    def getName(self, NameNode):
        return ast.Attribute(value=ast.Name(id=modName, ctx=ast.Load()), 
                             attr = NameNode.id, 
                             ctx  = NameNode.ctx)
def magic_import(self, line):
    global modName, modAttr, user_ns
    modName = str(line)
    if not self.shell.run_code( compile('import {0}'.format(line), '<string>', 'exec') ):
       user_ns = self.shell.user_ns
       modAttr = user_ns[line.strip()].__dict__
       self.shell.ast_transformers.append(MyTransformer())
       print modName, 'imported'
    
def load_ipython_extension(ip):
    ip.define_magic('magic_import', magic_import)

dummyModule.py

robot=" World"

用法:

In [1]: %load_ext testAST
In [2]: %magic_import dummyModule
In [3]: print "Hello" , robot
Hello World

In [4]: dummyModule.robot_II = "Human" 
In [5]: print "Hi", robot_II
Hi Human

这种方法的好处是对远程模块的任何修改都会立即生效,因为查找是在语言级别完成的,没有对象被复制和缓存。

此方法的一个缺点是无法处理动态查找。如果这对你很重要,也许 python_line_transforms 挂钩更合适。