【问题标题】:__init__ vs __enter__ in context managers上下文管理器中的 __init__ 与 __enter__
【发布时间】:2017-01-29 09:46:00
【问题描述】:

据我了解,上下文管理器的__init__()__enter__() 方法每个只被调用一次,一个接一个,没有机会在其间执行任何其他代码。将它们分成两种方法的目的是什么,我应该在每种方法中放入什么?

编辑:抱歉,没有关注文档。

编辑2:实际上,我感到困惑的原因是因为我在考虑@contextmanager 装饰器。使用@contextmananger创建的上下文管理器只能使用一次(第一次使用后生成器会耗尽),因此通常使用with语句中的构造函数调用编写;如果这是使用with 声明的唯一方法,那么我的问题将是有道理的。当然,实际上,上下文管理器比@contextmanager 可以创建的更通用;特别是上下文管理器通常可以被重用。希望这次我做对了?

【问题讨论】:

  • 您将创建上下文管理器与输入上下文混淆了。两者是不同的,您可以多次使用同一个上下文管理器。

标签: python python-3.x contextmanager


【解决方案1】:

据我了解,上下文管理器的__init__()__enter__() 方法每个只被调用一次,一个接一个,没有机会在其间执行任何其他代码。

而且你的理解是不正确的。 __init__ 在创建对象时调用,__enter__ 在使用 with 语句输入时调用,这是两个截然不同的东西。通常是在with初始化中直接调用构造函数,没有中间代码,但不一定是这样。

考虑这个例子:

class Foo:
    def __init__(self):
        print('__init__ called')
    def __enter__(self):
        print('__enter__ called')
        return self
    def __exit__(self, *a):
        print('__exit__ called')

myobj = Foo()

print('\nabout to enter with 1')
with myobj:
    print('in with 1')

print('\nabout to enter with 2')
with myobj:
    print('in with 2')

myobj可以单独初始化,并在多个with块中输入:

输出:

__init__ called

about to enter with 1
__enter__ called
in with 1
__exit__ called

about to enter with 2
__enter__ called
in with 2
__exit__ called

此外,如果__init____enter__ 不分开,甚至无法使用以下内容:

def open_etc_file(name):
    return open(os.path.join('/etc', name))

with open_etc_file('passwd'):
    ...

因为初始化(在open 内)明显与with 条目分开。


contextlib.manager 创建的管理器是单入的,但它们同样可以在with 块之外构建。举个例子:

from contextlib import contextmanager

@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)

您可以将其用作:

def heading(level=1):
    return tag('h{}'.format(level))

my_heading = heading()
print('Below be my heading')
with my_heading:
     print('Here be dragons')

输出:

Below be my heading
<h1>
Here be dragons
</h1>

但是,如果您尝试重用 my_heading(因此,tag),您将得到

RuntimeError: generator didn't yield

【讨论】:

  • 天哪,我记得的所有示例都有构造函数调用发生在 with 语句中(例如,with Foo()...)。现在一切都说得通了。谢谢
  • 哦,等等,@contextmanager 呢?既然它依赖于一个生成器,那么它不是在第一次使用时就耗尽了它,从而使对象的重复使用变得不可能吗?
  • 所以说@contextmanager装饰器只能生成有用的上下文管理器的子集是正确的;其余的需要显式编写一个符合上下文管理器 API 的类?
  • 请注意措辞。 __init__ 不是真正的构造函数,而是 Python 中的发起者。 __new__ 是构造函数。您可以在__init__ 的方法签名中看到,如果“self”作为第一个参数传递,则构造函数已经被调用,否则您将没有“self”的实例。
【解决方案2】:

Antti Haapalas 的回答非常好。我只是想详细说明一下参数的用法(例如myClass(* args)),因为这对我来说有点不清楚(回想起来我问自己为什么......)

with 语句中使用参数来初始化你的类与以通常的方式使用类没有什么不同。 调用将按以下顺序进行:

  1. __init__(班级分配)
  2. __enter__(输入上下文)
  3. __exit__(离开上下文)

简单示例:

class Foo:
    def __init__(self, i):
        print('__init__ called: {}'.format(i))
        self.i = i
    def __enter__(self):
        print('__enter__ called')
        return self
    def do_something(self):
        print('do something with {}'.format(self.i))
    def __exit__(self, *a):
        print('__exit__ called')

with Foo(42) as bar:
    bar.do_something()

输出:

__init__ called: 42
__enter__ called
    do something with 42
__exit__ called

如果您想确保您的调用(几乎)只能在上下文中使用(例如,强制调用__exit__),请参阅stackoverflow 帖子here。在 cmets 中,您还将找到如何使用参数的问题的答案。

【讨论】:

    猜你喜欢
    • 2018-05-22
    • 1970-01-01
    • 2023-03-26
    • 2018-04-21
    • 1970-01-01
    • 2017-03-25
    • 1970-01-01
    • 2022-10-31
    相关资源
    最近更新 更多