web-dev-qa-db-ja.com

コンテキストマネージャーでの__init__と__enter__

私が理解している限り、コンテキストマネージャの__init__()および__enter__()メソッドは、次々に正確に1回ずつ呼び出され、その間に他のコードが実行される可能性はありません。それらを2つの方法に分ける目的は何ですか?また、それぞれに何を入れるべきですか?

編集:申し訳ありませんが、ドキュメントに注意を払っていませんでした。

編集2:実際、混乱した理由は、@contextmanagerデコレータを考えていたからです。 @contextmanangerを使用して作成されたコンテキストマネージャーは1回しか使用できません(最初の使用後にジェネレーターが使い果たされるため)、多くの場合、withステートメント内のコンストラクター呼び出しで記述されます。それがwithステートメントを使用する唯一の方法であれば、私の質問は理にかなっているでしょう。もちろん、実際には、コンテキストマネージャーは@contextmanagerが作成できるものよりも一般的です。特に、コンテキストマネージャは一般に再利用できます。今回は正解だったらいいのに?

23
max

私が理解している限り、コンテキストマネージャの__init__()および__enter__()メソッドは次々に正確に1回ずつ呼び出され、その間に他のコードが実行される可能性はありません。

そして、あなたの理解は間違っています。 __init__はオブジェクトの作成時に呼び出され、__enter__withステートメントで入力されたときに呼び出されます。これらは2つのまったく異なるものです。多くの場合、コンストラクターは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
40
Antti Haapala

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 post here を参照してください。コメントには、その場合でも引数をどのように使用するかという質問に対する答えもあります。

6
SeparateReality