python:函数*有时*维护对其模块的引用

python:函数*有时*维护对其模块的引用,python,python-import,python-module,execfile,refcounting,Python,Python Import,Python Module,Execfile,Refcounting,如果我执行一个模块的文件,并删除对该模块的所有引用,它的函数将继续按预期工作。这很正常 但是,如果该execfile的模块导入其他模块,并且我删除了对这些模块的所有引用,那么在这些模块中定义的函数开始将其所有全局值视为无。当然,这会导致异常的失败,并且以一种非常令人支持的方式(例如,字符串常量上的TypeError NoneType) 我很惊讶口译员在这里做了一个特例执行文件似乎不够特殊,无法导致函数在模块引用时表现出不同的行为 我的问题是:对于由execfile的模块导入的模块,是否有任何干净

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

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

我很惊讶口译员在这里做了一个特例<代码>执行文件似乎不够特殊,无法导致函数在模块引用时表现出不同的行为

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


好奇的人:

该应用程序是在buildbot下重新加载的可靠配置。无论好坏,buildbot配置都是可执行的python。如果可执行配置是一个文件,那么一切工作都相当顺利。如果将该配置拆分为模块,则由于
\uuuuu import\uuuu
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()

您应该考虑<代码>导入>代码>配置,而不是<代码> Exc> <代码> 我使用

import
实现类似的目的,效果非常好。(特别是,
importlib.import\u模块(mod)
)。不过,我的配置主要由原语组成,而不是真正的函数

与您一样,我也有一个“保护”上下文,用于在导入后恢复
sys.modules
的原始内容。另外,我使用的是
sys.dont\u write\u bytecode=True
(当然,您可以将其添加到
禁用模块缓存中--
中设置为True,在
中设置为True,在
中设置为False。这将确保每次导入配置时都能“运行”

两种方法之间的主要区别,(除了您不必依赖于解释程序在代码< > Exc/<代码> ING(我认为半不干净)之后的状态),即配置文件由它们的模块名/路径(用于导入)标识而不是文件名。



编辑:这种方法的一部分,作为软件包的一部分。

我认为您不需要这里的“验收测试”部分。问题其实不是weakrefs,而是模块在销毁时的行为。他们在删除时清除了自己的
\uuuuuuu dict\uuuuuu
。我隐约记得这样做是为了打破ref循环。我怀疑函数闭包中的全局引用可以避免每次调用时进行哈希查找,这就是为什么会得到
None
,而不是
namererror

下面是一个短得多的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()

或者,换句话说,不要删除模块并期望其内容继续工作。

您必须提供一个最小的工作示例。如果您不提供显式全局或局部变量,
execfile
以当前全局和局部变量执行文件。它不做任何类似于模块导入或创建新名称空间的事情;它几乎相当于exec open(filename)。read()
@Marcin,user2357112:我添加了一个可复制的演示。这一切都是有道理的,但您忽略了一个事实,即直接执行模块时,行为是不同的(并且更可取)。我认为这是因为直接执行的模块在其功能出现之前就已经超出了范围,以至于有人关心它是否能够正常工作。你知道我在cpython的什么地方会发现这两种情况之间的差异吗?我把“验收测试”放在那里,以防有人认为他们有更好的重新加载配置实现我发现,在删除模块之前简单地运行
gc.get\u referers(module)
,会产生一个副作用,即添加必要的引用以使一切正常工作。奇怪的这使得所有python版本2.5+都能完美地工作。我再次发现exec的工作方式与import完全不同<代码>\uuuu导入\uuuuu
在2.5-2.7中继续具有相同的不良行为。事实上,导入模块具有我想要的行为;gc的模块没有将其属性全部设置为“无”。在我看来,这是模块对象的行为,而不是其他任何行为,而且我在这两种情况下都使用相同的模块对象定义,因此我无法完全看到相关的区别。此外,我还被python2.6困住了:(更新:我看到python2.7有一个importlib的后端口。它具有与OP中相同的不好的None'ing行为。此外,我注意到,
\uuu import\uuu
在两个版本中都具有与importlib.import\u模块相同的行为;这种全局丢弃行为似乎是python3中修复的无数事情之一。最后:exec()OP中的策略在python3中运行良好=/