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

Invoking a constructor in a 'with' statement

我有以下代码:

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

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

__enter__ 方法应该 return 上下文对象。 with ... as ... 使用 __enter__ 的 return 值来确定给你什么对象。由于你的 __enter__ return 什么都没有,它隐含地 return 是 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__ returns True,表示错误已被处理(本质上,你的 __exit__ 就像一个 except 块),所以代码在第一个 with 块之后继续,因为你告诉 Python 一切都很好。

考虑

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

你也可以考虑不从 __exit__ returning True 除非你真的打算无条件地抑制块中的 all 错误(并充分理解抑制其他程序员错误的后果,以及KeyboardInterruptStopIteration和各种系统信号)

我认为这种行为是因为 __enter__ 必须 return 将要操作的东西,在这种情况下将使用名称 test 访问。通过将 __enter__ 更改为以下

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

我们得到了预期的行为。

问题是您的 __enter__ 方法 returns None。因此,test 被赋值为 None.

然后您尝试访问 (None).name,这会引发错误。由于您的 __exit__ 方法 returns True 总是,它将抑制任何错误。根据the docs:

Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement.

解释:

__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,而不是 returning 任何值是造成这种情况的原因。在很多情况下,__enter__ 不需要 return,但在这种情况下 Test class 需要 return.

解决方案:

解决方案是保留 Test 实例,以便它在上下文管理器 [=16= 之后调用 returned selfname ] 结果。到目前为止 __enter__ 结果 None,因此 None.name 属性不存在。因此,如果您 return selftest.name 属性将存在。

解决方法是 return self__enter__ 魔术方法实现中:

    ...
    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 的更具体证明。我也展示了一个例子。

原因是第一种情况和第二种情况不一样

first中:

  • 创建对象,调用__init__;
  • 然后 with 调用 __enter__;
  • 然后as__enter__的结果存入test
  • 因为 __enter__ 没有 return 值,testNone

second中:

  • 创建对象,调用__init__;
  • 然后分配给test;
  • 然后 with 调用 __enter__;
  • 但是 __enter__;
  • 的结果没有做任何事情
  • 所以test一直引用最初创建的对象。

在这两种情况下,都会为 with 正在处理的对象调用 __exit__,因此您会看到正在打印正确的标签;只是,在 first 中,test 标识符未绑定到同一对象。

注意 __enter__ 不必 return self。它可能 return 完全不同,例如您可以打开一个文件并使 __enter__ return 流,而 __exit__ 可能会关闭它。如果给定 __enter__ 应该 return self,那将是多余的,可以只是暗示。