在'with'语句中调用构造函数

51

我有以下代码:

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

为什么我的第一个示例中的代码没有被调用?

5个回答

60

__enter__方法应该返回上下文对象。 with ... as ...使用__enter__的返回值来确定要给你什么对象。由于您的__enter__未返回任何内容,因此它隐式地返回None,所以testNone

with Test('first') as test:
    print(f'in {test.name}')

test = Test('second')
with test:
    print(f'in {test.name}')

所以test是空值(none)。然后调用test.name就产生了一个错误。该错误被引发,于是调用了Test('first').__exit____exit__返回True,表示错误已被处理(实际上,你的__exit__except块一样起作用),因此代码在第一个with块之后继续执行,因为你告诉Python一切都好了。

考虑

def __enter__(self):
    print(f'entering {self.name}')
    return self

除非您确实打算无条件地抑制块中的所有错误(并充分理解抑制其他程序员的错误以及 KeyboardInterrupt StopIteration 和各种系统信号的后果),否则请勿从 __exit__ 返回 True


2
有趣的是,最初的 PEP-310 提案中 with 的行为与 OP 预期的一致。这在 PEP-343 中被更改了。我想知道为什么他们没有将对象作为默认上下文,当 __enter__ 返回 None 时,因为这是一个非常常见的情况。 - Barmar
5
有时候__enter__会返回None,因为as目标确实应该设置为None。如果将返回值为None特殊处理,那么会破坏一些真正需要使用None的情况。节省几行return self的代码不值得这样做。 - user2357112
(不,我指的不是具有普通返回None语句的__enter__方法。我说的是如果__enter__返回某些状态变量的值,该值允许为None,或者如果它返回某些计算的结果,其输出可能为None的情况。) - user2357112
@user2357112支持Monica,这样做会导致后面尝试调用None.__exit__()时出错吗? - Barmar
4
不。with 调用上下文管理器的 __exit__ 方法,而不是 __enter__ 返回值的 __exit__ 方法。 - user2357112

10
你的问题在于__enter__方法返回None,因此test被赋值为None。接着你尝试访问(None).name,这会引发一个错误。由于你的__exit__方法总是返回True,它将抑制任何错误。根据文档的说法:

从该方法返回一个真值会导致with语句抑制异常并继续执行紧随with语句之后的语句。


4

我认为这种行为是因为__enter__必须返回一个被操作的东西,这种情况下将使用名称test访问它。通过将__enter__更改如下

def __enter__(self):
    print(f"entering {self.name}")
    return self

我们得到了预期的行为。


4
__enter__ 方法不一定需要返回值,但是 Test 类的使用假定它会返回一个值。 - chepner

3
原因是第一种情况和第二种情况并不相同。
在第一种情况中:
1. 对象被创建,调用 `__init__` 方法; 2. 然后 `with` 语句调用 `__enter__` 方法; 3. 接着 `as` 将 `__enter__` 方法的返回值存储到变量 `test` 中; 4. 由于 `__enter__` 没有返回值,所以 `test` 是 `None`。
在第二种情况中:
1. 对象被创建,调用 `__init__` 方法; 2. 然后赋值给变量 `test`; 3. 接着 `with` 语句调用 `__enter__` 方法; 4. 但是没有对 `__enter__` 方法的返回值进行处理; 5. 因此,`test` 仍然指向最初创建的对象。
在两种情况下,`with` 处理的对象都会调用 `__exit__` 方法,因此你会看到正确的标签被打印出来;只是在第一种情况中,`test` 标识符没有绑定到同一个对象。
注意:`__enter__` 方法不必返回 `self`。它完全可以返回其他内容,例如您可以打开文件并使 `__enter__` 返回流,而 `__exit__` 可以关闭该文件。如果假定 `__enter__` 应返回 `self`,那就是多余的,可以忽略。

0

解释:

__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__ 的结果后调用返回的 selfname。目前 __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。我还展示了一个例子。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接