在“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,所以testNone

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,除非你真的打算无条件地抑制块中的所有错误(并且完全理解抑制其他程序员错误的后果,以及如KeyboardInterruptStopIteration,以及各种系统信号)

【讨论】:

有趣的是,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__ 没有返回值,所以testNone

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__ 结果之后调用返回的selfname。到目前为止__enter__ 结果None,因此None.name 属性不存在。所以如果你返回selftest.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”语句中调用构造函数的主要内容,如果未能解决你的问题,请参考以下文章

重载构造函数调用其他构造函数,但不是作为第一条语句

“构造函数调用必须是构造函数中的第一个语句”Java中的问题[重复]

C++:调用无参数的构造函数为啥不加括号

Scala-构造函数

在java语言里如何在子类中调用父类的有参构造函数

this关键字