在“with”语句中调用构造函数
Posted
技术标签:
【中文标题】在“with”语句中调用构造函数【英文标题】:Invoking a constructor in a 'with' statement 【发布时间】:2021-11-11 19:30:11 【问题描述】:我有以下代码:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering self.name')
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting self.name')
return True
with Test('first') as test:
print(f'in test.name')
test = Test('second')
with test:
print(f'in test.name')
运行它会产生以下输出:
entering first
exiting first
entering second
in second
exiting second
但我希望它会产生:
entering first
in first
exiting first
entering second
in second
exiting second
为什么不调用我的第一个示例中的代码?
【问题讨论】:
【参考方案1】:__enter__
方法应该返回上下文对象。 with ... as ...
使用__enter__
的返回值来确定给你什么对象。由于您的__enter__
什么都不返回,它隐式返回None
,所以test
是None
。
with Test('first') as test:
print(f'in test.name')
test = Test('second')
with test:
print(f'in test.name')
所以test
没有。那么test.name
是一个错误。该错误被引发,因此Test('first').__exit__
被调用。 __exit__
返回True
,表示错误已被处理(本质上,您的__exit__
就像except
块一样),因此代码在第一个with
块之后继续,因为您告诉Python一切都很好。
考虑
def __enter__(self):
print(f'entering self.name')
return self
你也可以考虑不从__exit__
返回True
,除非你真的打算无条件地抑制块中的所有错误(并且完全理解抑制其他程序员错误的后果,以及如KeyboardInterrupt
、StopIteration
,以及各种系统信号)
【讨论】:
有趣的是,with
的原始 PEP-310 提案的行为符合 OP 的预期。这在 PEP-343 中有所改变。我想知道为什么当__enter__
返回None
时,他们没有将对象设为默认上下文,因为这是很常见的情况。
@Barmar:有时__enter__
会返回None
,因为as
目标确实应该设置为None
。如果None
的返回值是特殊情况,那将打破真正需要None
的情况。节省几行 return self
是不值得的。
(不,我不是指带有普通return None
的__enter__
方法。我说的是如果__enter__
返回一些允许的状态变量的值的情况是None
,或者如果它返回一些计算的结果,其输出可能是None
。)
@user2357112supportsMonica 稍后尝试调用None.__exit__()
时不会导致错误吗?
@Barmar:没有。with
调用上下文管理器的__exit__
,而不是__enter__
返回值的__exit__
。【参考方案2】:
问题是您的__enter__
方法返回None
。因此,test
被分配了None
。
然后您尝试访问(None).name
,这会引发错误。由于您的 __exit__
方法总是返回 True
,因此它将抑制任何错误。根据the docs:
从此方法返回真值将导致 with 语句 抑制异常并继续执行该语句 紧跟 with 语句。
【讨论】:
【参考方案3】:我相信这种行为是因为__enter__
必须返回将被操作的东西,在这种情况下将使用名称test
访问。通过将__enter__
更改为以下内容
def __enter__(self):
print(f"entering self.name")
return self
我们得到了预期的行为。
【讨论】:
__enter__
没有有返回值,但Test
的使用方式假定它会。跨度>
【参考方案4】:
原因是第一种和第二种情况不一样。
在first
:
__init__
;
然后with
调用__enter__
;
然后as
将__enter__
的结果存储到test
因为__enter__
没有返回值,所以test
是None
。
在second
:
__init__
;
然后分配给test
;
然后with
调用__enter__
;
但是__enter__
的结果没有做任何事情;
所以test
一直引用最初创建的对象。
在这两种情况下,__exit__
都会为with
正在处理的对象调用,因此您会看到打印的正确标签;只是,在first
中,test
标识符没有绑定到同一个对象。
注意__enter__
不必返回self
。它可能会完全返回其他内容,例如您可以打开一个文件并让__enter__
返回流,而__exit__
可能会关闭它。如果给定__enter__
应该返回self
,那将是多余的,可能只是隐含的。
【讨论】:
【参考方案5】:解释:
__enter__
将None
作为输出,因为没有return
,因此它会直接触发__exit__
,因为None
没有属性name
,例如:
>>> None.name
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
None.__name__
AttributeError: 'NoneType' object has no attribute 'name'
>>>
如果您将其设置为调用__class__.__name__
(None
对象具有该属性,它会给出NoneType
),您可以轻松找到问题:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering self.name')
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting self.name')
return True
with Test('first') as test:
print(f'in test.__class__.__name__')
test = Test('second')
with test:
print(f'in test.__class__.__name__')
输出:
entering first
in NoneType
exiting first
entering second
in Test
exiting second
如您所见,上面写着in NoneType
,不返回任何值是造成这种情况的原因。在很多情况下,__enter__
不需要返回,但在这种情况下,Test
类需要它返回。
解决方案:
解决方案是保留Test
实例,以便在上下文管理器__enter__
结果之后调用返回的self
的name
。到目前为止__enter__
结果None
,因此None.name
属性不存在。所以如果你返回self
,test.name
属性就会存在。
解决方案是在 __enter__
魔术方法实现中返回 self
:
...
def __enter__(self):
print(f'entering self.name')
return self
...
完整代码:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering self.name')
return self
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting self.name')
return True
with Test('first') as test:
print(f'in test.name')
test = Test('second')
with test:
print(f'in test.name')
输出:
entering first
in first
exiting first
entering second
in second
exiting second
我给出的其他答案没有给出的额外信息是__enter__
方法实现给出None
的更具体的证明。我也举了一个例子。
【讨论】:
以上是关于在“with”语句中调用构造函数的主要内容,如果未能解决你的问题,请参考以下文章