在 `__enter__` 中返回除 `self` 以外的值是反模式吗?

Dea*_*itz 6 python

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

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

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

在这里,BufferedReader包装FileReader,并在其自己的方法中调用FileReader的方法。但是,如果这是 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 开发人员应该如何编写解决这些问题的代码?

Mis*_*agi 6

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

\n\n

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

\n\n
\n

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

\n
\n\n
\n\n

标准库有几个返回来自 以外的内容的self示例__enter__。值得注意的是,很多contextlib符合这种模式。

\n\n\n\n
\n\n

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

\n\n

该协议的粗略解释是:当某个东西运行时cm.__enter__,它负责运行cm.__exit__值得注意的是,无论代码执行什么操作,都可以访问cm(上下文管理器本身);的结果cm.__enter__不需要调用cm.__exit__

\n\n

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

\n\n
# entering a context manager requires closing it\xe2\x80\xa6\ndef managing(cm: ContextManager):\n    value = cm.__enter__()  # must clean up `cm` after this point\n    try:\n        yield from unmanaged(value)\n    except BaseException as exc:\n        if not cm.__exit__(type(exc), exc, exc.__traceback__):\n           raise\n    else:\n        cm.__exit__(None, None, None)\n\n# \xe2\x80\xa6other code does not need to know where its values come from\ndef unmanaged(smth: Any):\n    yield smth\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
\n\n

self在某些情况下,从 .com返回实际上是不好的做法__enter__。仅当事先完全初始化时才self应返回;如果运行任何初始化代码,则应返回一个单独的对象。__enter__self__enter__

\n\n
class BadContextManager:\n      """\n      Anti Pattern: Context manager is in inconsistent state before ``__enter__``\n      """\n      def __init__(self, path):\n          self.path = path\n          self._file = None  # BAD: initialisation not complete\n\n      def read(self, n: int):\n          return self._file.read(n)  # fails before the context is entered!\n\n      def __enter__(self) -> \'BadContextManager\':\n          self._file = open(self.path)\n          return self  # BAD: self was not valid before\n\n      def __exit__(self, exc_type, exc_val, tb):\n          self._file.close()\n\nclass GoodContext:\n      def __init__(self, path):\n          self.path = path\n          self._file = None  # GOOD: Inconsistent state not visible/used\n\n      def __enter__(self) -> TextIO:\n          if self._file is not None:\n             raise RuntimeError(f\'{self.__class__.__name__} is not re-entrant\')\n          self._file = open(self.path)\n          return self._file  # GOOD: value was not accessible before\n\n      def __exit__(self, exc_type, exc_val, tb):\n          self._file.close()\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n