contextvars --- コンテキスト変数


このモジュールは、コンテキストローカルな状態を管理し、保持し、アクセスするための API を提供します。 ContextVar クラスは コンテキスト変数 を宣言し、取り扱うために使われます。 非同期フレームワークで現時点のコンテキストを管理するのには、 copy_context() 関数と Context クラスを使うべきです。

状態を持つコンテキストマネージャは、並行コードで使用した際に、その状態が予期せずほかのコードに漏れるのを防ぐため、threading.local() ではなくコンテキスト変数を使うべきです。

より詳しくは PEP 567 を参照をしてください。

Added in version 3.7.

コンテキスト変数

class contextvars.ContextVar(name[, *, default])

このクラスは新しいコンテキスト変数を宣言するのに使われます。例えば、次の通りです:

var: ContextVar[int] = ContextVar('var', default=42)

必須のパラメータの name は内観やデバッグの目的で使われます。

オプションのキーワード専用引数 default は、現在のコンテキストにその変数の値が見付からなかったときに ContextVar.get() から返されます。

重要: コンテキスト変数は、モジュールのトップレベルで生成する必要があり、クロージャの中で作成すべきではありません。Context オブジェクトはコンテキスト変数への強参照を持っており、コンテキスト変数がガーベジコレクトされるのを防ぎます。

ContextVar は、格納する値の型について ジェネリック です。

name

変数の名前。読み出し専用のプロパティです。

Added in version 3.7.1.

get([default])

現在のコンテキストのコンテキスト変数の値を返します。

現在のコンテキストのコンテキスト変数に値がなければ、メソッドは:

  • メソッドの default 引数に値が指定されていればその値を返します。さもなければ

  • コンテキスト変数が生成された時にデフォルト値が渡されていれば、その値を返します。さもなければ

  • LookupError を送出します。

set(value)

現在のコンテキストのコンテキスト変数に新しい値を設定する際に呼び出します。

value は必須の引数で、コンテキスト変数の新しい値を指定します。

Token オブジェクトを返します。このオブジェクトを ContextVar.reset() メソッドに渡すことで、以前の値に戻すことができます。

便利な使い方として、トークンオブジェクトをコンテキストマネージャとして使えば、ContextVar.reset() を手動で呼び出す必要がなくなります。

var = ContextVar('var', default='default value')

with var.set('new value'):
    assert var.get() == 'new value'

assert var.get() == 'default value'

これは次のコードの短縮形です。

var = ContextVar('var', default='default value')

token = var.set('new value')
try:
    assert var.get() == 'new value'
finally:
    var.reset(token)

assert var.get() == 'default value'

Added in version 3.14: トークンをコンテキストマネージャとして使用できるようになりました。

reset(token)

コンテキスト変数の値を、 token を生成した ContextVar.set() が呼び出される前の値にリセットします。

例えば:

var = ContextVar('var')

token = var.set('new value')
# 'var' を使うコード。
# var.get() は 'new value' を返します。
var.reset(token)

# reset の呼び出し後は var の値が再びなくなるため、
# var.get() を呼ぶと LookupError が送出されます。

同じ token を 2 回使うことはできません。

class contextvars.Token

Token オブジェクトは ContextVar.set() メソッドから返されます。 これを ContextVar.reset() メソッドに渡すと、対応する set の呼び出し前の値に変数を戻せます。 1 つのトークンでコンテキスト変数をリセットできるのは 1 回だけです。

トークンは コンテキストマネージャプロトコル をサポートし、コンテキスト変数を自動的にリセットできます。 ContextVar.set() を参照してください。

トークンは、それを生成した ContextVar と同じ型について ジェネリック です。

Added in version 3.14: コンテキストマネージャとして使用できるようになりました。

var

読み出し専用のプロパティです。トークンを生成した ContextVar オブジェクトを指します。

old_value

読み出し専用のプロパティです。このトークンを作成した ContextVar.set() メソッドの呼び出しの前に持っていた値に設定されています。もし呼び出しの前に値が設定されていなければ Token.MISSING を指します。

MISSING

Token.old_value で利用されるマーカーオブジェクトです。

マニュアルでのコンテキスト管理

contextvars.copy_context()

現在の Context オブジェクトのコピーを返します。

次のスニペットは、現在のコンテキストのコピーを取得し、コンテキストに設定されているすべての変数とその値を表示します:

ctx: Context = copy_context()
print(list(ctx.items()))

この関数の複雑性は O(1) です。つまり、少数のコンテキスト変数を持つコンテキストと多くの変数を持つコンテキストで同程度の速度で動作します。

class contextvars.Context

ContextVar とその値を対応付けるマッピング。

Context() は、値を持たない空のコンテキストを生成します。現在のコンテキストのコピーを得るには、copy_context() 関数を利用します。

各スレッドには、実質的にそれぞれ独立した Context オブジェクトのスタックがあります。 current context は、現在のスレッドのスタックの最上部にある Context オブジェクトです。 スタック内のすべての Context オブジェクトは、入っている 状態と見なされます。

コンテキストに 入る には、その run() メソッドを呼び出します。 これにより、そのコンテキストが現在のスレッドのコンテキストスタックの最上部に積まれ、現在のコンテキストになります。

現在のコンテキストから 出る には、run() メソッドに渡したコールバックから戻ります。 これにより、そのコンテキストがコンテキストスタックの最上部から取り除かれ、現在のコンテキストが、そのコンテキストに入る前のものに戻ります。

各スレッドが独自のコンテキストスタックを持つため、異なるスレッドで値を代入した場合、ContextVar オブジェクトは threading.local() と同様に振る舞います。

ほかのスレッドで入っているコンテキストも含め、すでに入っているコンテキストに入ろうとすると、RuntimeError が送出されます。

コンテキストから出た後は、どのスレッドからでも再び入ることができます。

ContextVar.set() メソッドによる ContextVar の値の変更は、現在のコンテキストに記録されます。 ContextVar.get() メソッドは、現在のコンテキストに対応する値を返します。 コンテキストから出ると、そのコンテキストに入っていた間に行ったコンテキスト変数の変更は、実質的に元に戻ります (必要であれば、そのコンテキストに再び入ることで変更後の値を復元できます)。

Context は、 collections.abc.Mapping インターフェースを実装します。

run(callable, *args, **kwargs)

このコンテキストに入り、callable(*args, **kwargs) を実行してから、このコンテキストを出ます。 callable の戻り値を返すか、例外が発生した場合はそれを伝播させます。

例:

import contextvars

var = contextvars.ContextVar('var')
var.set('spam')
print(var.get())  # 'spam'

ctx = contextvars.copy_context()

def main():
    # 'copy_context()' と 'ctx.run(main)' を呼ぶ前に
    # 'var' を 'spam' に設定したため、次のようになります:
    print(var.get())  # 'spam'
    print(ctx[var])  # 'spam'

    var.set('ham')

    # 'var' を 'ham' に設定した後は:
    print(var.get())  # 'ham'
    print(ctx[var])  # 'ham'

# 'main' 関数が 'var' に行う変更はすべて
# 'ctx' 内にとどまります。
ctx.run(main)

# 'main()' 関数は 'ctx' コンテキスト内で実行されたので、
# 'var' の変更はその中にとどまります:
print(ctx[var])  # 'ham'

# 一方、'ctx' の外では 'var' は依然として 'spam' に設定されています:
print(var.get())  # 'spam'
copy()

コンテキストオブジェクトの浅いコピーを返します。

var in context

context に var の値が設定されていた場合 True を返します; そうでない場合は False を返します。

context[var]

ContextVar var の値を返します。コンテキストオブジェクト内で変数が設定されていない場合は、KeyError を送出します。

get(var[, default])

var がコンテキストオブジェクトの中に値を持てば、その値を返します。さもなければ、default を返します。default を指定していなければ、None を返します。

iter(context)

コンテキストオブジェクトに格納されている変数群のイテレータを返します。

len(proxy)

コンテキストオブジェクトに格納されている変数の数を返します。

keys()

コンテキストオブジェクト中のすべての変数のリストを返します。

values()

コンテキストオブジェクト中のすべての変数の値のリストを返します。

items()

コンテキストオブジェクト中のすべての変数について、変数とその値からなる2要素のタプルのリストを返します。

asyncio サポート

コンテキスト変数は asyncio に標準でサポートされており、追加の設定なしに使用できます。 例えば、以下はコンテキスト変数を使った単純な echo サーバーです。 リモートクライアントのアドレスを、そのクライアントを扱う Task 内で利用できるようにしています。

import asyncio
import contextvars

client_addr_var = contextvars.ContextVar('client_addr')

def render_goodbye():
    # 現在扱っているクライアントのアドレスを、この関数に
    # 明示的に渡さなくても参照できます。

    client_addr = client_addr_var.get()
    return f'Good bye, client @ {client_addr}\r\n'.encode()

async def handle_request(reader, writer):
    addr = writer.transport.get_extra_info('socket').getpeername()
    client_addr_var.set(addr)

    # ここから呼び出すどのコードでも、'client_addr_var.get()' を
    # 呼んでクライアントのアドレスを取得できます。

    while True:
        line = await reader.readline()
        print(line)
        if not line.strip():
            break

    writer.write(b'HTTP/1.1 200 OK\r\n')  # ステータス行
    writer.write(b'\r\n')  # ヘッダー
    writer.write(render_goodbye())  # 本文
    writer.close()

async def main():
    srv = await asyncio.start_server(
        handle_request, '127.0.0.1', 8081)

    async with srv:
        await srv.serve_forever()

asyncio.run(main())

# テストには telnet や curl を使えます:
#     telnet 127.0.0.1 8081
#     curl 127.0.0.1:8081