"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()" 方法，
      用來將變數還原到之前的值。

   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。

class contextvars.Token

   *Token* 物件由 "ContextVar.set()" 方法回傳，可以傳遞給
   "ContextVar.reset()" 方法，用以將變數的值還原為相對應的 *set* 之前
   的值。

   The token supports context manager protocol to restore the
   corresponding context variable value at the exit from "with" block:

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

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

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

   在 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():
       # 即使不把目前處理中的用戶端（client）傳入此函式
       # 仍可取得其位址

       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')  # 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())

   # 你可以使用 telnet 或 curl 測試:
   #     telnet 127.0.0.1 8081
   #     curl 127.0.0.1:8081
