这个工作适合用上下文管理器吗?

5 投票
3 回答
689 浏览
提问于 2025-04-17 06:55

下面的代码做了以下几件事:

  • 创建了一个导入钩子
  • 创建了一个上下文管理器,用来设置 meta_path,并在退出时进行清理。
  • 将输入程序中所有的导入记录到 imports.log 文件里。

我在想,在这种情况下使用上下文管理器是否是个好主意,因为我其实没有标准的 try/finally 流程,只是简单的设置和清理。

还有一件事——在这一行:

with CollectorContext(cl, sys.argv, 'imports.log') as cc:

cc 会变成 None 吗?难道它不应该是一个 CollectorContext 对象吗?

from __future__ import with_statement
import os
import sys

class CollectImports(object):
    """
    Import hook, adds each import request to the loaded set and dumps
    them to file
    """

    def __init__(self):
        self.loaded = set()

    def __str__(self):
        return str(self.loaded)

    def dump_to_file(self, fname):
        """Dump the loaded set to file
        """
        dumped_str = '\n'.join(x for x in self.loaded)
        open(fname, 'w').write(dumped_str)

    def find_module(self, module_name, package=None):
        self.loaded.add(module_name)


class CollectorContext(object):
    """Sets the meta_path hook with the passed import hook when
    entering and clean up when exiting
    """

    def __init__(self, collector, argv, output_file):
        self.collector = collector
        self.argv = argv
        self.output_file = output_file

    def __enter__(self):
        self.argv = self.argv[1:]
        sys.meta_path.append(self.collector)

    def __exit__(self, type, value, traceback):
        # TODO: should assert that the variables are None, otherwise
        # we are quitting with some exceptions
        self.collector.dump_to_file(self.output_file)
        sys.meta_path.remove(self.collector)


def main_context():
    cl = CollectImports()

    with CollectorContext(cl, sys.argv, 'imports.log') as cc:
        progname = sys.argv[0]
        code = compile(open(progname).read(), progname, 'exec')
        exec(code)


if __name__ == '__main__':
    sys.argv = sys.argv[1:]
    main_context()

3 个回答

2

如果你总是希望在使用完某些资源后能自动清理,那你应该使用上下文管理器。我不太确定如果你用低级的特殊方法来实现上下文管理器,在哪里会用到 try..finally。不过如果你使用 @contextmanager 装饰器,那么你就可以用一种“自然”的方式来编写上下文管理器,这样你就可以在里面使用 try..finally,而不是把异常作为参数传入。

另外,cc 是你从 __enter__() 方法返回的值。在你的例子中,这个值是 None。我理解的上下文管理器的设计是,返回值就是“上下文”。上下文管理器的作用是设置和清理一些事情发生的环境。例如,数据库连接会创建事务,而数据库操作则在这些事务的范围内进行。

不过,上面的内容只是为了提供最大的灵活性。直接创建一个自我管理的上下文并返回 self 也是没问题的,甚至如果你不需要在 with 里面使用上下文的值,也可以不返回任何东西。因为你在任何地方都没有使用 cc,所以你可以直接这样做,而不必担心返回值:

with CollectorContext(cl, sys.argv, 'imports.log'):
        progname = sys.argv[0]
        code = compile(open(progname).read(), progname, 'exec')
        exec(code)
3

我觉得这个概念挺好的。而且,我也看不出有什么理由反对在 finally: 这个部分放入清理的代码,所以使用上下文管理器是非常合适的。

你的 cc 现在是 None,因为你就是这样设置的。

如果你不想这样,那就需要修改你的 __enter__ 方法,让它返回其他的东西

这个方法返回的值会绑定到使用这个上下文管理器的 with 语句中的 as 部分的标识符上。

def __enter__(self):
    self.argv = self.argv[1:]
    sys.meta_path.append(self.collector)
    return self
    # or
    return self.collector
    # or
    return "I don't know what to return here"

然后

with CollectorContext(cl, sys.argv, 'imports.log') as cc:
    print cc, repr(cc) # there you see what happens.
    progname = sys.argv[0]
    code = compile(open(progname).read(), progname, 'exec')
    exec(code)
1

谢谢大家,现在它运行得很顺利。其实我想让它返回一些东西,因为我想把“运行”这个过程放在上下文管理器里面,这样我就能得到下面这样的结果。

而且,现在我把旧的 sys.argv 存储起来,并在退出时恢复它。虽然这可能不是特别重要,但我觉得这样做还是挺不错的。

class CollectorContext(object):
    """Sets the meta_path hook with the passed import hook when
    entering and clean up when exiting
    """

    def __init__(self, collector, argv, output_file):
        self.collector = collector
        self.old_argv = argv[:]
        self.output_file = output_file
        self.progname = self.old_argv[1]

    def __enter__(self):
        sys.argv = self.old_argv[1:]
        sys.meta_path.append(self.collector)
        return self

    def __exit__(self, type, value, traceback):
        # TODO: should assert that the variables are None, otherwise
        # we are quitting with some exceptions
        self.collector.dump_to_file(self.output_file)
        sys.meta_path.remove(self.collector)
        sys.argv = self.old_argv[:]

    def run(self):
        code = compile(open(self.progname).read(), self.progname, 'exec')
        exec(code)


def main_context():
    cl = CollectImports()

    with CollectorContext(cl, sys.argv, 'imports.log') as cc:
        cc.run()

撰写回答