首页 > 解决方案 > 在 `__enter__` 中返回 `self` 以外的值是反模式吗?

问题描述

这个相关的问题之后,虽然总是有一些库以独特的方式使用语言特性的例子,但我想知道返回一个方法以外的值是否self应该__enter__被视为反模式。

这在我看来是个坏主意的主要原因是它使包装上下文管理器成为问题。例如,在 Java 中(也可能在 C# 中),可以将一个AutoCloseable类包装在另一个类中,该类将负责清理内部类,如下面的代码片段所示:

try (BufferedReader reader = 
     new BufferedReader(new FileReader("src/main/resources/input.txt"))) {
  return readAllLines(reader);
}

在这里,BufferedReaderwraps并在其自己的方法中FileReader调用FileReader's方法。但是,如果这是 Python,并且会在其方法中返回 self 以外的对象,这将使这种安排变得更加复杂。以下问题必须由作者解决:close()close()FileReader__enter__BufferedReader

  1. 当我需要使用FileReader自己的方法时,是FileReader直接使用还是使用其__enter__方法返回的对象?返回的对象甚至支持哪些方法?
  2. 在我的__exit__方法中,我只需要关闭FileReader对象,还是__enter__方法中返回的对象?
  3. 如果__enter__在调用时实际上返回了一个不同的对象,会发生什么?我现在是否需要保留它返回的所有不同对象的集合,以防有人__enter__多次调用我?当我需要使用这些对象时,我如何知道使用哪一个?

而这样的例子不胜枚举。所有这些问题的一个半成功的解决方案是简单地避免在另一个上下文管理器类之后清理一个上下文管理器类。在我的示例中,这意味着我们需要两个嵌套with块——一个用于FileReader,一个用于BufferedReader. 然而,这使我们编写了更多样板代码,并且看起来明显不那么优雅。

总而言之,这些问题让我相信,虽然 Python 确实允许我们self__enter__方法中返回其他内容,但应该避免这种行为。关于这些问题是否有一些官方或半官方的评论?一个负责任的 Python 开发人员应该如何编写代码来解决这些问题?

标签: python

解决方案


selfTLDR:返回from以外的东西__enter__完全没问题,而且也不错。

引入的PEP 343上下文管理器规范明确将此列为所需用例。

返回相关对象的上下文管理器的一个示例是由decimal.localcontext(). 这些管理器将活动十进制上下文设置为原始十进制上下文的副本,然后返回该副本。这允许对语句主体中的当前十进制上下文进行更改,而with不会影响语句外部的代码with


标准库有几个返回非selffrom的示例__enter__。值得注意的是,大部分都contextlib符合这种模式。


上下文管理器协议明确了什么是上下文管理器,以及谁负责清理。最重要的是,的返回值__enter__对于协议来说是无关紧要的

协议的粗略解释是:当某事运行cm.__enter__时,它负责运行cm.__exit__值得注意的是,可以访问cm(上下文管理器本身)的任何代码;的结果cm.__enter__不需要调用cm.__exit__

换句话说,采用(并运行)a 的代码ContextManager必须完全运行它。任何其他代码都不必关心它的值是否来自 a ContextManager

# entering a context manager requires closing it…
def managing(cm: ContextManager):
    value = cm.__enter__()  # must clean up `cm` after this point
    try:
        yield from unmanaged(value)
    except BaseException as exc:
        if not cm.__exit__(type(exc), exc, exc.__traceback__):
           raise
    else:
        cm.__exit__(None, None, None)

# …other code does not need to know where its values come from
def unmanaged(smth: Any):
    yield smth

当上下文管理器包装其他人时,同样的规则适用:如果外部上下文管理器调用内部的 one's __enter__,它也必须调用它__exit__。如果外部上下文管理器已经有进入的内部上下文管理器,它不负责清理。


在某些情况下,self__enter__. 仅在事先完全初始化的情况下才self应从返回;如果运行任何初始化代码,则应返回一个单独的对象。__enter__self__enter__

class BadContextManager:
      """
      Anti Pattern: Context manager is in inconsistent state before ``__enter__``
      """
      def __init__(self, path):
          self.path = path
          self._file = None  # BAD: initialisation not complete

      def read(self, n: int):
          return self._file.read(n)  # fails before the context is entered!

      def __enter__(self) -> 'BadContextManager':
          self._file = open(self.path)
          return self  # BAD: self was not valid before

      def __exit__(self, exc_type, exc_val, tb):
          self._file.close()

class GoodContext:
      def __init__(self, path):
          self.path = path
          self._file = None  # GOOD: Inconsistent state not visible/used

      def __enter__(self) -> TextIO:
          if self._file is not None:
             raise RuntimeError(f'{self.__class__.__name__} is not re-entrant')
          self._file = open(self.path)
          return self._file  # GOOD: value was not accessible before

      def __exit__(self, exc_type, exc_val, tb):
          self._file.close()

值得注意的是,即使GoodContext返回不同的对象,它仍然负责清理。另一个上下文管理器包装GoodContext不需要关闭返回值,它只需要调用GoodContext.__exit__.


推荐阅读