上下文管理器中的__init__和__enter__有什么区别?

56
据我了解,上下文管理器的__init__()__enter__()方法各被调用一次,一个接一个地调用,中间没有机会执行其他代码。将它们分成两个方法的目的是什么,我应该在每个方法中放置什么内容?
编辑:抱歉,没有注意到文档。
编辑2:实际上,我感到困惑的原因是因为我在思考@contextmanager装饰器。使用@contextmananger创建的上下文管理器只能使用一次(生成器在第一次使用后就会耗尽),因此通常会在with语句中写入构造函数调用;如果那是使用with语句的唯一方法,我的问题就有意义了。当然,在现实中,上下文管理器比@contextmanager可以创建的更普遍;特别是上下文管理器通常可以被重复使用。希望这次我理解得没错?

4
你混淆了创建上下文管理器和进入上下文的概念。两者是不同的,而且你可以多次使用同一个上下文管理器。 - Martijn Pieters
2个回答

94
据我了解,上下文管理器的__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

6
把构造函数调用放在with语句中的例子,我都记住了。现在一切都清楚了,谢谢。 - max
哦,等等,那 @contextmanager 呢?由于它依赖于生成器,第一次使用时是否会耗尽它,从而使对象的重复使用变得不可能? - max
所以说,只有一部分有用的上下文管理器可以使用@contextmanager装饰器生成;其余的需要明确编写符合上下文管理器API的类,这种说法是正确的吗? - max
请注意措辞。在Python中,__init__不是真正的构造函数,而是初始化器。__new__才是构造函数。你可以看到__init__方法签名中,如果将"self"作为第一个参数传递,那么构造函数已经被调用了,否则你就不会有一个"self"实例。 - CheradenineZK

18

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帖子此处。在评论中,您还将找到如何即使在那时使用参数的答案。


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