# `contextvars` --- 情境變數

本模組供 API 來管理、儲存及存取單一情境各自的狀態（context-local state）。 用 `ContextVar` 類別宣告和處理情境變數。 `copy_context()` 函式和 `Context` 類別應在非同步框架中管理目前的情境。

帶有狀態的 Context Manager 應該使用情境變數，而不是 `threading.local()`，才能防止它們的狀態在並行（concurrent）程式碼中使用時意外外溢並干擾到其他程式碼。

其他詳細資訊，請參閱 PEP 567。

在 3.7 版被加入.

## 情境變數

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

  此類別用在宣告新的情境變數，例如：

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

  必要參數 name 用於自我檢查（introspection）和除錯。

  當在目前的情境中找不到變數的值時，`ContextVar.get()` 會回傳可選的僅限關鍵字參數 default。

  重要：情境變數應該在最頂端的模組層級建立，絕對不要在閉包（closure）中建立。 `Context` 物件持有情境變數的強參照，這會阻止情境變數被正確地垃圾回收（garbage collected）。

  name

    這個變數的名稱。這是一個唯讀屬性。

    在 3.7.1 版被加入.

  get([default])

    回傳目前情境的情境變數值。

    如果在目前情境中沒有變數的值，此方法將：

    - 回傳方法的 default 引數值（如果有的話）；否則

    - 回傳情境變數的預設值（如果建立情境變數時有指定預設值的話）；否則

    - 會引發一個 `LookupError`。

  set(value)

    在目前的情境中，呼叫以設定情境變數的新值。

    value 屬必要引數，是情境變數的新值。

    回傳一個 `Token` 物件，該物件可透過 `ContextVar.reset()` 方法，用來將變數還原到之前的值。

    為了方便起見，token 物件可以用作情境管理器，以避免手動呼叫 `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'

    在 3.14 版被加入: 新增對將 token 用作情境管理器的支援。

  reset(token)

    將情境變數重設為使用 `ContextVar.set()` 建立 token 前的值。

    舉例來說：

        var = ContextVar('var')

        token = var.set('new value')
        # 使用 'var' 的程式碼；var.get() 回傳 'new value'。
        var.reset(token)

        # 在重設呼叫之後，var 又沒有值了，所以
        # var.get() 會引發 LookupError。

    同一個 token 不能被使用兩次。

class contextvars.Token

  Token 物件由 `ContextVar.set()` 方法回傳，可以傳遞給 `ContextVar.reset()` 方法，用以將變數的值還原為相對應的 set 之前的值。單一 token 不能重設情境變數超過一次。

  Token 支援情境管理器協定，可自動重設情境變數。請參閱 `ContextVar.set()`。

  在 3.14 版被加入: 新增對用作情境管理器的支援。

  var

    唯讀屬性。 指向建立 token 的 `ContextVar` 物件。

  old_value

    唯讀屬性。 值為變數在呼叫 `ContextVar.set()` 方法之前的值。如果變數在呼叫前沒有設定，則指向 `Token.MISSING`。

  MISSING

    由 `Token.old_value` 使用的標記物件。

## 手動情境管理

contextvars.copy_context()

  回傳目前 `Context` 物件的複本（copy）。

  以下程式碼片段會取得目前情境的複本，並顯示在其中設定的所有變數及其值：:

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

  這個函式具有 O(1) 的複雜度，也就是說，對於只有少許情境變數的情境和有大量情境變數的情境，速度都一樣快。

class contextvars.Context

  `ContextVars` 到其值的映射。

  `Context()` 會建立一個沒有值的空情境。要取得目前情境的複本，請使用 `copy_context()` 函式。

  每個執行緒都有自己的 `Context` 物件中目前主控中的堆疊（stack）。current context 是目前執行緒堆疊頂端的 `Context` 物件。 堆疊中的所有 `Context` 物件都被視為已進入。

  進入一個情境，可以藉由呼叫其 `run()` 方法來完成，此進入的動作會將一情境推到目前執行緒的情境堆疊的頂端，使該情境成為目前的情境。

  如果你傳遞給 `run()` 方法的回呼函式（callback functions），該函式回傳之後，就會自動退出目前的情境，這會將目前的情境還原到進入情境之前的狀態，方法是將情境從情境堆疊的頂端彈出。

  因為每個執行緒都有自己的情境堆疊，當值在不同的執行緒中被指定時， `ContextVar` 物件的行為與 `threading.local()` 相似。

  嘗試進入已進入的情境，包括在其他執行緒中進入的情境，會引發 `RuntimeError`。

  退出情境後，之後可以重新進入（從任何執行緒）。

  任何透過 `ContextVar.set()` 方法對 `ContextVar` 值的改變都會記錄在目前的情境中。 `ContextVar.get()` 方法回傳與目前情境相關的值。 退出情境實際造成的效果會像是將其在進入情境時對情境變數所做的任何變一一彈出並還原（如果需要，可以透過重新進入情境來還原值）。

  情境（Context）實作了 `collections.abc.Mapping` 介面。

  run(callable, *args, **kwargs)

    進入 Context，執行 `callable(*args, **kwargs)`，然後退出 Context。 回傳 callable 的回傳值，如果發生例外（exception），則傳播例外。

    例如：

        import contextvars

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

        ctx = contextvars.copy_context()

        def main():
            # 'var' 之前被設成 'spam'
            # 呼叫 'copy_context()' 和 'ctx.run(main)'，所以：
            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' 的變更會保存在 'ctx' 中：
        print(ctx[var])  # 'ham'

        # 但是，在 'ctx' 外, 'var' 的值仍然是 'spam':
        print(var.get())  # 'spam'

  copy()

    回傳情境物件的淺層複本（shallow copy）。

  var in context

    如果情境裡面有 var 的值，則回傳 `True`，否則回傳 `False`。

  context[var]

    回傳 var `ContextVar` 變數的值。如果該變數並沒有在情境物件中設定，則會引發 `KeyError` 錯誤。

  get(var[, default])

    如果 var 的值在情境物件中，則回傳 var 的值。否則回傳 default。如果沒有 default 值，則回傳 `None`。

  iter(context)

    回傳儲存於情境物件中變數的疊代器。

  len(proxy)

    回傳情境物件中的變數個數。

  keys()

    回傳情境物件中所有變數的串列。

  values()

    回傳情境物件中所有變數的值的串列。

  items()

    回傳情境物件中所有變數與其值的 2-元組（2-tuples）的串列。

## 對 asyncio 的支援

`asyncio` 原生支援情境變數，不需任何額外設定。 舉例來說，以下是一個簡單的 echo 伺服器，使用情境變數讓遠端用戶端的位址在處理該用戶端的任務中可用：

    import asyncio
    import contextvars

    client_addr_var = contextvars.ContextVar('client_addr')

    def render_goodbye():
        # The address of the currently handled client can be accessed
        # without passing it explicitly to this function.

        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)

        # In any code that we call, it is now possible to get the
        # client's address by calling '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')  # status line
        writer.write(b'\r\n')  # headers
        writer.write(render_goodbye())  # body
        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())

    # To test it you can use telnet or curl:
    #     telnet 127.0.0.1 8081
    #     curl 127.0.0.1:8081