1

如果我执行一个模块,并删除所有(我的)对该模块的引用,它的功能将继续按预期工作。这很正常。

但是,如果该 execfile'd 模块导入其他模块,并且我删除了对这些模块的所有引用,则这些模块中定义的函数开始将它们的所有全局值视为无。当然,这会导致事情以一种非常令人惊讶的方式失败(例如,字符串常量上的 TypeError NoneType)。

我很惊讶口译员在这里做了一个特殊情况;execfile似乎没有足够的特殊性来导致函数在模块引用中表现不同。

我的问题:对于由 execfile'd 模块导入的模块,是否有任何干净的方法可以使 execfile 函数行为递归(或在有限上下文中全局)?


给好奇的人:

该应用程序是在 buildbot 下可靠的配置重新加载。buildbot 配置是可执行的 python,无论好坏。如果可执行配置是单个文件,则一切正常。__import__如果将该配置拆分为模块,则由于and的语义,来自顶级文件的任何导入都会卡在原始版本中sys.modules。我的策略是在配置前后保持 sys.modules 的内容不变,这样每次重新配置看起来就像一个初始配置。除了上述函数全局参考问题外,这几乎可以工作。


这是该问题的可重复演示:

import gc
import sys
from textwrap import dedent


class DisableModuleCache(object):
    """Defines a context in which the contents of sys.modules is held constant.
    i.e. Any new entries in the module cache (sys.modules) are cleared when exiting this context.
    """
    modules_before = None
    def __enter__(self):
        self.modules_before = sys.modules.keys()
    def __exit__(self, *args):
        for module in sys.modules.keys():
            if module not in self.modules_before:
                del sys.modules[module]
        gc.collect()  # force collection after removing refs, for demo purposes.


def reload_config(filename):
    """Reload configuration from a file"""
    with DisableModuleCache():
        namespace = {}
        exec open(filename) in namespace
        config = namespace['config']
        del namespace

    config()


def main():
    open('config_module.py', 'w').write(dedent('''
    GLOBAL = 'GLOBAL'
    def config():
        print 'config! (old implementation)'
        print GLOBAL
    '''))

    # if I exec that file itself, its functions maintain a reference to its modules,
    # keeping GLOBAL's refcount above zero
    reload_config('config_module.py')
    ## output:
    #config! (old implementation)
    #GLOBAL

    # If that file is once-removed from the exec, the functions no longer maintain a reference to their module.
    # The GLOBAL's refcount goes to zero, and we get a None value (feels like weakref behavior?).
    open('main.py', 'w').write(dedent('''
    from config_module import *
    '''))

    reload_config('main.py')
    ## output:
    #config! (old implementation)
    #None

    ## *desired* output:
    #config! (old implementation)
    #GLOBAL

    acceptance_test()


def acceptance_test():
    # Have to wait at least one second between edits (on ext3),
    # or else we import the old version from the .pyc file.
    from time import sleep
    sleep(1)

    open('config_module.py', 'w').write(dedent('''
    GLOBAL2 = 'GLOBAL2'
    def config():
        print 'config2! (new implementation)'
        print GLOBAL2

        ## There should be no such thing as GLOBAL. Naive reload() gets this wrong.
        try:
            print GLOBAL
        except NameError:
            print 'got the expected NameError :)'
        else:
            raise AssertionError('expected a NameError!')
    '''))

    reload_config('main.py')
    ## output:
    #config2! (new implementation)
    #None
    #got the expected NameError :)

    ## *desired* output:
    #config2! (new implementation)
    #GLOBAL2
    #got the expected NameError :)



if __name__ == '__main__':
    main()
4

2 回答 2

1

我认为你不需要这里的'acceptance_test'部分。问题实际上不是弱引用,而是模块在破坏时的行为。他们清除他们__dict__的删除。我依稀记得这样做是为了打破参考循环。我怀疑函数闭包中的全局引用做了一些花哨的事情,以避免在每次调用时进行哈希查找,这就是为什么你得到None而不是NameError.

这是一个更短的 sscce:

import gc
import sys
import contextlib
from textwrap import dedent


@contextlib.contextmanager
def held_modules():
    modules_before = sys.modules.keys()
    yield
    for module in sys.modules.keys():
        if module not in modules_before:
            del sys.modules[module]
    gc.collect()  # force collection after removing refs, for demo purposes.

def main():
    open('config_module.py', 'w').write(dedent('''
    GLOBAL = 'GLOBAL'
    def config():
        print 'config! (old implementation)'
        print GLOBAL
    '''))
    open('main.py', 'w').write(dedent('''
    from config_module import *
    '''))

    with held_modules():
        namespace = {}
        exec open('main.py') in namespace
        config = namespace['config']
    config()

if __name__ == '__main__':
    main()

或者,换句话说,不要删除模块并期望它们的内容继续运行。

于 2014-04-08T18:47:50.447 回答
1

您应该考虑import配置而不是配置exec它。

import用于类似的目的,效果很好。(具体来说,importlib.import_module(mod))。不过,我的配置主要由原语组成,而不是真正的功能。

sys.modules和你一样,我也有一个“守卫”上下文来恢复导入后的原始内容。另外,我使用sys.dont_write_bytecode = True(当然,您可以将其添加到您的DisableModuleCache-- 设置为 True in__enter__和 False in __exit__)。这将确保配置在您每次导入时实际“运行”。

这两种方法之间的主要区别(除了您不必依赖解释器在execing 之后保持的状态(我认为是半不干净的)这一事实之外)是配置文件由它们的模块标识-名称/路径(用于导入)而不是文件名


编辑:此方法实施的链接,作为Figura包的一部分。

于 2014-04-08T18:22:54.467 回答