具有上下文管理器的生成器是一种反模式吗?

Are generators with context managers an anti-pattern?

我想知道这样的代码:

def all_lines(filename):
    with open(filename) as infile:
        yield from infile

上下文管理器的目的是明确控制某种形式的状态的生命周期,例如一个文件句柄。另一方面,生成器会保持其状态直到耗尽或删除。

我知道这两种情况在实践中都有效。但我担心这是否是个好主意。举个例子:

def all_first_lines(filenames):
    return [next(all_lines(filename), None) for filename in filenames]

我从不耗尽发电机。相反,当生成器对象被删除时,它们的状态被破坏。这在像 CPython 这样的引用计数实现中工作得很好,但是垃圾收集实现呢?我实际上依靠引用计数器来管理状态,这是上下文管理器明确设计要避免的事情!

即使在 CPython 中,如果生成器是引用循环的一部分并且需要销毁垃圾收集器,那么构建案例也不难。

总结一下:您是否认为避免在生成器中使用上下文管理器是明智的,例如将上述代码重构为这样的代码?

def all_lines(filename):
    with open(filename) as infile:
        return infile.readlines()

def first_line(filename):
    with open(filename) as infile:
        return next(infile, None)

def all_first_lines(filenames):
    return [first_line(filename) for filename in filenames]

您的问题有两个答案:

  • 绝对主义者:确实,上下文管理器将无法发挥作用,GC 将不得不清理不应该发生的烂摊子
  • 务实者:没错,但这真的是个问题吗?您的文件句柄将在几毫秒后释放,有什么麻烦?它对生产有可衡量的影响吗,还是只是自行车脱落?

我不是 Python alt 实现差异 (see this page for PyPy's example) 的专家,但我认为在 99% 的情况下不会出现此生命周期问题。如果您碰巧遇到产品,那么是的,您应该解决它(通过您的建议,或者生成器与上下文管理器的组合)否则,何必呢?我的意思是善意的:你的观点是严格有效的,但与大多数情况无关。

虽然它确实延长了对象的生命周期,直到生成器退出或被销毁,但它也可以使生成器更易于使用。

考虑在外部创建生成器并将文件作为参数传递而不是打开它。现在文件在退出上下文管理器后无法使用,即使生成器仍然可以被视为可用。

如果限制手柄保持的时间很重要,您可以在完成后使用 close method 明确关闭生成器。

这与作者 trio tries to solve with its nurseries for asynchronous tasks, where the nursery context manager waits for every task spawned from that nursery to exit before proceeding, the tutorial example illustrates this. This blog post 的问题类似,可以提供一些关于三重奏中完成的方式的推理,这可能是与该问题有些相关的有趣读物。