python - 在 `__enter__` 中返回 `self` 以外的值是反模式吗?
问题描述
在这个相关的问题之后,虽然总是有一些库以独特的方式使用语言特性的例子,但我想知道返回一个方法以外的值是否self
应该__enter__
被视为反模式。
这在我看来是个坏主意的主要原因是它使包装上下文管理器成为问题。例如,在 Java 中(也可能在 C# 中),可以将一个AutoCloseable
类包装在另一个类中,该类将负责清理内部类,如下面的代码片段所示:
try (BufferedReader reader =
new BufferedReader(new FileReader("src/main/resources/input.txt"))) {
return readAllLines(reader);
}
在这里,BufferedReader
wraps并在其自己的方法中FileReader
调用FileReader
's方法。但是,如果这是 Python,并且会在其方法中返回 self 以外的对象,这将使这种安排变得更加复杂。以下问题必须由作者解决:close()
close()
FileReader
__enter__
BufferedReader
- 当我需要使用
FileReader
自己的方法时,是FileReader
直接使用还是使用其__enter__
方法返回的对象?返回的对象甚至支持哪些方法? - 在我的
__exit__
方法中,我只需要关闭FileReader
对象,还是__enter__
方法中返回的对象? - 如果
__enter__
在调用时实际上返回了一个不同的对象,会发生什么?我现在是否需要保留它返回的所有不同对象的集合,以防有人__enter__
多次调用我?当我需要使用这些对象时,我如何知道使用哪一个?
而这样的例子不胜枚举。所有这些问题的一个半成功的解决方案是简单地避免在另一个上下文管理器类之后清理一个上下文管理器类。在我的示例中,这意味着我们需要两个嵌套with
块——一个用于FileReader
,一个用于BufferedReader
. 然而,这使我们编写了更多样板代码,并且看起来明显不那么优雅。
总而言之,这些问题让我相信,虽然 Python 确实允许我们self
在__enter__
方法中返回其他内容,但应该避免这种行为。关于这些问题是否有一些官方或半官方的评论?一个负责任的 Python 开发人员应该如何编写代码来解决这些问题?
解决方案
self
TLDR:返回from以外的东西__enter__
完全没问题,而且也不错。
引入的PEP 343和上下文管理器规范明确将此列为所需用例。
返回相关对象的上下文管理器的一个示例是由
decimal.localcontext()
. 这些管理器将活动十进制上下文设置为原始十进制上下文的副本,然后返回该副本。这允许对语句主体中的当前十进制上下文进行更改,而with
不会影响语句外部的代码with
。
标准库有几个返回非self
from的示例__enter__
。值得注意的是,大部分都contextlib
符合这种模式。
contextlib.contextmanager
产生不能返回的上下文管理器self
,因为没有这样的东西。contextlib.closing
包装 athing
并将其返回到__enter__
.contextlib.nullcontext
返回一个预定义的常量threading.Lock
返回一个布尔值decimal.localcontext
返回其参数的副本
上下文管理器协议明确了什么是上下文管理器,以及谁负责清理。最重要的是,的返回值__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__
.
推荐阅读
- xamarin - 关于 Xamarin 表单 - Xaml 编译
- angular - 在编辑页面上预填充 formArray - Angular4
- dynamics-crm - 在 Dynamics 365 中更新交易货币实体时出现意外错误
- laravel - 将 Laravel 中的 CNAME 与通配符子域链接的问题
- python - 如何根据新职位列表重新排序列
- python - 在 python 中使用装饰器处理异常
- asp.net - Asp.Net WebForms 重定向而不刷新母版页
- url - 禁止路径末尾的斜线是什么意思?
- mysql - Delphi 10.2 Tokyo 的插入语句中的语法错误
- android - 使 RecyclerView 对触摸“透明”