# 生命週期事件 { #lifespan-events }

你可以定義在應用程式**啟動**之前要執行的邏輯（程式碼）。也就是說，這段程式碼會在應用開始接收請求**之前**、**只執行一次**。

同樣地，你也可以定義在應用程式**關閉**時要執行的邏輯（程式碼）。在這種情況下，這段程式碼會在處理了**許多請求**之後、**只執行一次**。

因為這些程式碼分別在應用開始接收請求**之前**與**完成**處理請求之後執行，所以涵蓋了整個應用的**生命週期**（「lifespan」這個詞稍後會很重要 😉）。

這對於為整個應用設定需要**共用**於多個請求的**資源**，以及在之後進行**清理**，非常有用。比如資料庫連線池、或載入一個共用的機器學習模型。

## 使用情境 { #use-case }

先從一個**使用情境**開始，然後看看如何用這個機制解決。

想像你有一些要用來處理請求的**機器學習模型**。🤖

同一組模型會在多個請求間共用，所以不是每個請求或每個使用者各有一個模型。

再想像一下，載入模型**需要一段時間**，因為它必須從**磁碟**讀取大量資料。所以你不想在每個請求都做一次。

你可以在模組／檔案的最上層載入，但這也表示即使只是要跑一個簡單的自動化測試，也會去**載入模型**，導致測試**變慢**，因為它得等模型載入完才能執行與模型無關的程式碼部分。

我們要解決的正是這件事：在開始處理請求之前再載入模型，但只在應用程式即將開始接收請求時載入，而不是在匯入程式碼時就載入。

## 生命週期（Lifespan） { #lifespan }

你可以使用 `FastAPI` 應用的 `lifespan` 參數，搭配「context manager」（稍後會示範），來定義這些 *startup* 與 *shutdown* 邏輯。

先看一個例子，接著再深入說明。

我們建立一個帶有 `yield` 的非同步函式 `lifespan()`，如下：

{* ../../docs_src/events/tutorial003_py310.py hl[16,19] *}

這裡我們透過在 `yield` 之前把（假的）模型函式放進機器學習模型的字典中，來模擬昂貴的 *startup* 載入模型操作。這段程式會在應用**開始接收請求之前**執行，也就是 *startup* 階段。

接著，在 `yield` 之後，我們卸載模型。這段程式會在應用**完成處理請求之後**、也就是 *shutdown* 前執行。這可以用來釋放資源，例如記憶體或 GPU。

/// tip

`shutdown` 會在你**停止**應用程式時發生。

也許你要啟動新版本，或只是不想再跑它了。🤷

///

### Lifespan 函式 { #lifespan-function }

首先要注意的是，我們定義了一個帶有 `yield` 的 async 函式。這和帶有 `yield` 的依賴（Dependencies）非常相似。

{* ../../docs_src/events/tutorial003_py310.py hl[14:19] *}

函式在 `yield` 之前的部分，會在應用啟動前先執行。

`yield` 之後的部分，會在應用結束後再執行。

### 非同步內容管理器（Async Context Manager） { #async-context-manager }

你會看到這個函式被 `@asynccontextmanager` 裝飾。

它會把函式轉換成所謂的「**非同步內容管理器（async context manager）**」。

{* ../../docs_src/events/tutorial003_py310.py hl[1,13] *}

Python 中的**內容管理器（context manager）**可以用在 `with` 陳述式中，例如 `open()` 可以作為內容管理器使用：

```Python
with open("file.txt") as file:
    file.read()
```

在較新的 Python 版本中，也有**非同步內容管理器**。你可以用 `async with` 來使用它：

```Python
async with lifespan(app):
    await do_stuff()
```

當你像上面那樣建立一個內容管理器或非同步內容管理器時，在進入 `with` 區塊之前，會先執行 `yield` 之前的程式碼；離開 `with` 區塊之後，會執行 `yield` 之後的程式碼。

在我們的範例中，並不是直接用它，而是把它傳給 FastAPI 來使用。

`FastAPI` 應用的 `lifespan` 參數需要一個**非同步內容管理器**，所以我們可以把剛寫好的 `lifespan` 非同步內容管理器傳給它。

{* ../../docs_src/events/tutorial003_py310.py hl[22] *}

## 替代事件（已棄用） { #alternative-events-deprecated }

/// warning

目前建議使用上面所述，透過 `FastAPI` 應用的 `lifespan` 參數來處理 *startup* 與 *shutdown*。如果你提供了 `lifespan` 參數，`startup` 與 `shutdown` 事件處理器將不會被呼叫。要嘛全用 `lifespan`，要嘛全用事件，不能同時混用。

你大概可以直接跳過這一節。

///

也有另一種方式可以定義在 *startup* 與 *shutdown* 期間要執行的邏輯。

你可以定義事件處理器（函式）來在應用啟動前或關閉時執行。

這些函式可以用 `async def` 或一般的 `def` 宣告。

### `startup` 事件 { #startup-event }

要加入一個在應用啟動前執行的函式，使用事件 `"startup"` 來宣告：

{* ../../docs_src/events/tutorial001_py310.py hl[8] *}

在這個例子中，`startup` 事件處理器函式會用一些值來初始化 items 的「資料庫」（其實就是個 `dict`）。

你可以註冊多個事件處理函式。

而且在所有 `startup` 事件處理器都完成之前，你的應用不會開始接收請求。

### `shutdown` 事件 { #shutdown-event }

要加入一個在應用關閉時執行的函式，使用事件 `"shutdown"` 來宣告：

{* ../../docs_src/events/tutorial002_py310.py hl[6] *}

在這裡，`shutdown` 事件處理器函式會把一行文字 `"Application shutdown"` 寫入檔案 `log.txt`。

/// info

在 `open()` 函式中，`mode="a"` 表示「append（附加）」；也就是說，這行文字會加在檔案現有內容之後，而不會覆寫先前的內容。

///

/// tip

注意這裡我們使用的是標準 Python 的 `open()` 函式來操作檔案。

這涉及 I/O（輸入／輸出），也就是需要「等待」資料寫入磁碟。

但 `open()` 並不使用 `async` 與 `await`。

所以我們用一般的 `def` 來宣告事件處理器，而不是 `async def`。

///

### 同時使用 `startup` 與 `shutdown` { #startup-and-shutdown-together }

你的 *startup* 與 *shutdown* 邏輯很可能是相關聯的：你可能會先啟動某個東西再把它結束、先取得資源再釋放它，等等。

如果把它們拆成兩個彼此不共享邏輯或變數的獨立函式，會比較麻煩，你得把值存在全域變數或用其他技巧。

因此，現在建議改用上面介紹的 `lifespan`。

## 技術細節 { #technical-details }

給有興趣鑽研的同好一點技術細節。🤓

在底層的 ASGI 技術規範中，這屬於 [Lifespan Protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html) 的一部分，並定義了 `startup` 與 `shutdown` 兩種事件。

/// info

你可以在 [Starlette 的 Lifespan 文件](https://www.starlette.dev/lifespan/) 讀到更多關於 Starlette `lifespan` 處理器的資訊。

也包含如何處理可在程式其他區域使用的 lifespan 狀態。

///

## 子應用程式 { #sub-applications }

🚨 請記住，這些生命週期事件（startup 與 shutdown）只會在主應用程式上執行，不會在[子應用程式 - 掛載](sub-applications.md)上執行。
