[英]Is returning a value other than `self` in `__enter__` an anti-pattern?
Following this related question , while there are always examples of some library using a language feature in a unique way, I was wondering whether returning a value other than self
in an __enter__
method should be considered an anti-pattern.继此相关的问题,而总是有一种独特的方式使用语言功能的一些图书馆的例子,我想知道是否返回的值不是
self
在__enter__
方法应被视为一个反模式。
The main reason why this seems to me like a bad idea is that it makes wrapping context managers problematic.在我看来这不是个好主意的主要原因是它使包装上下文管理器有问题。 For example, in Java (also possible in C#), one can wrap an
AutoCloseable
class in another class which will take care of cleaning up after the inner class, like in the following code snippet:例如,在 Java 中(也可以在 C# 中),可以将
AutoCloseable
类包装在另一个类中,该类将在内部类之后进行清理,如下面的代码片段所示:
try (BufferedReader reader =
new BufferedReader(new FileReader("src/main/resources/input.txt"))) {
return readAllLines(reader);
}
Here, BufferedReader
wraps FileReader
, and calls FileReader
's close()
method inside its own close()
method.在这里,
BufferedReader
包装FileReader
,并调用FileReader
的close()
自身的内部方法close()
方法。 However, if this was Python, and FileReader
would've returned an object other than self in its __enter__
method, this would make such an arrangement significantly more complicated.但是,如果这是 Python,并且
FileReader
将在其__enter__
方法中返回 self 以外的对象,这将使这种安排变得更加复杂。 The following issues would have to be addressed by the writer of BufferedReader
: BufferedReader
的作者必须解决以下问题:
FileReader
for my own methods, do I use FileReader
directly or the object returned by its __enter__
method?FileReader
用于我自己的方法时,我是直接使用FileReader
还是其__enter__
方法返回的对象? What methods are even supported by the returned object?__exit__
method, do I need to close only the FileReader
object, or the object returned in the __enter__
method?__exit__
方法中,我是否只需要关闭FileReader
对象,或者__enter__
方法中返回的对象?__enter__
actually returns a different object on its call?__enter__
实际上在调用时返回了一个不同的对象,会发生什么? Do I now need to keep a collection of all of the different objects returned by it in case someone calls __enter__
several times on me?__enter__
? How do I know which one to use when I need to use on of these objects? And the list goes on.而这样的例子不胜枚举。 One semi-successful solution to all of these problems would be to simply avoid having one context manager class clean up after another context manager class.
所有这些问题的一个半成功的解决方案是简单地避免在一个上下文管理器类之后清理另一个上下文管理器类。 In my example, that would mean that we would need two nested
with
blocks - one for the FileReader
, and one for the BufferedReader
.在我的示例中,这意味着我们需要两个嵌套
with
块 - 一个用于FileReader
,另一个用于BufferedReader
。 However, this makes us write more boilerplate code, and seems significantly less elegant.然而,这让我们编写了更多的样板代码,而且看起来不那么优雅。
All in all, these issues lead me to believe that while Python does allow us to return something other than self
in the __enter__
method, this behavior should simply be avoided.总而言之,这些问题让我相信,虽然 Python 确实允许我们在
__enter__
方法中返回self
以外的东西,但这种行为应该简单地避免。 Is there some official or semi-official remarks about these issues?关于这些问题,有没有官方或半官方的说法? How should a responsible Python developer write code that addresses these issues?
负责任的 Python 开发人员应该如何编写解决这些问题的代码?
TLDR: Returning something other than self
from __enter__
is perfectly fine and not bad practice. TLDR:从
__enter__
返回self
以外的东西是非常好的,而且是不错的做法。
The introducing PEP 343 and Context Manager specification expressly list this as desired use cases.引入 PEP 343和上下文管理器规范明确将此列为所需用例。
An example of a context manager that returns a related object is the one returned by
decimal.localcontext()
.返回相关对象的上下文管理器的一个示例是
decimal.localcontext()
返回的decimal.localcontext()
。 These managers set the active decimal context to a copy of the original decimal context and then return the copy.这些管理器将活动的十进制上下文设置为原始十进制上下文的副本,然后返回该副本。 This allows changes to be made to the current decimal context in the body of the
with
statement without affecting code outside thewith
statement.这允许对
with
语句主体中的当前十进制上下文进行更改,而不会影响with
语句之外的代码。
The standard library has several examples of returning something other than self
from __enter__
.标准库有几个从
__enter__
返回self
以外的东西的例子。 Notably, much of contextlib
matches this pattern.值得注意的是,
contextlib
大部分内容都符合这种模式。
contextlib.contextmanager
produces context managers which cannot return self
, because there is no such thing. contextlib.contextmanager
产生不能返回self
上下文管理器,因为没有这样的东西。contextlib.closing
wraps a thing
and returns it on __enter__
. contextlib.closing
包装一个thing
并在__enter__
上返回它。contextlib.nullcontext
returns a pre-defined constant contextlib.nullcontext
返回一个预定义的常量threading.Lock
returns a boolean threading.Lock
返回一个布尔值decimal.localcontext
returns a copy of its argument decimal.localcontext
返回其参数的副本The context manager protocol makes it clear what is the context manager, and who is responsible for cleanup.上下文管理器协议明确了上下文管理器是什么,谁负责清理。 Most importantly, the return value of
__enter__
is inconsequential for the protocol .最重要的是,
__enter__
的返回值对于协议来说无关紧要。
A rough paraphrasing of the protocol is this: When something runs cm.__enter__
, it is responsible for running cm.__exit__
.该协议的粗略解释是:当某个东西运行
cm.__enter__
,它负责运行cm.__exit__
。 Notably, whatever code does that has access to cm
(the context manager itself);值得注意的是,无论代码做什么,都可以访问
cm
(上下文管理器本身); the result of cm.__enter__
is not needed to call cm.__exit__
. cm.__enter__
的结果不需要调用cm.__exit__
。
In other words, a code that takes (and runs) a ContextManager
must run it completely.换句话说,需要(并运行)一个
ContextManager
的代码必须完全运行它。 Any other code does not have to care whether its value comes from a ContextManager
or not.任何其他代码不必关心它的值是否来自
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
When context managers wrap others, the same rules apply: If the outer context manager calls the inner one's __enter__
, it must call its __exit__
as well.当上下文管理器包装其他上下文时,同样的规则适用:如果外部上下文管理器调用内部的
__enter__
,它也必须调用它的__exit__
。 If the outer context manager already has the entered inner context manager, it is not responsible for cleanup.如果外部上下文管理器已经有进入的内部上下文管理器,则不负责清理。
In some cases it is in fact bad practice to return self
from __enter__
.在某些情况下,从
__enter__
返回self
实际上是不好的做法。 Returning self
from __enter__
should only be done if self
is fully initialised beforehand;从
__enter__
返回self
应该只在self
事先完全初始化的情况下完成; if __enter__
runs any initialisation code, a separate object should be returned.如果
__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()
Notably, even though GoodContext
returns a different object, it is still responsible to clean up.值得注意的是,即使
GoodContext
返回一个不同的对象,它仍然负责清理。 Another context manager wrapping GoodContext
does not need to close the return value, it just has to call GoodContext.__exit__
.另一个包装
GoodContext
上下文管理器不需要关闭返回值,它只需要调用GoodContext.__exit__
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.