跳到主要內容

Web Day 18 非同步端點與 httpx

Web Day 18 非同步端點與 httpx

執行需求:CPU 可跑。今天是「Web 系統實戰:用 FastAPI 打造能上線的後端」系列的第十八篇。我們已經能用 pytest 與 TestClient 把同步端點測得很完整;今天往前走一步,把端點改成 async def,並把測試客戶端換成 httpx 0.28 的 AsyncClient。在 FastAPI 裡,async 與 sync 可以混用,但測試端的 asyncio 流程需要一些觀念,今天會把這件事一次說清楚。

引言

很多人對 async 的印象是「比較快」。這個說法對了一半:async 端點在「等待 I/O」的場景(例如打另一個 API、查資料庫、讀檔案)能讓單一 worker 在等待時去處理別的請求;但純 CPU 的工作(例如數學運算、字串處理)並不會因為加了 async 就變快。今天我們會看的是「async 怎麼寫、怎麼測、怎麼避免踩坑」,重點放在測試端,因為這是品質篇章的核心。

今天的範例同樣接續 Day 16-17 的專案結構。我們會做四件事:第一,說明 async def 端點與同步端點的差異;第二,用 httpx.AsyncClient + pytest-asyncio 寫測試;第三,用 asyncio.gather 並發送多個請求;第四,整理常見踩雷,包括「同步函式裡呼叫 await」、「在 async 函式裡呼叫阻塞式函式」等幾乎一定會遇到的雷區。

async 端點與 sync 端點:什麼時候該用哪個

FastAPI 允許你在同一個應用裡混用 sync 與 async 端點。它會在內部用 anyio 安排它們的執行:

  • async def 端點在「主事件迴圈」或 anyio worker thread 上執行;當它內部 await 一個真正的非同步 I/O(例如 httpx 的 async client)時,事件迴圈會切換到別的任務。
  • def 端點會被丟到一個 thread pool(anyio 提供,預設 40 條 thread)執行,所以它能放心呼叫阻塞式的 I/O(例如 requests、SQLAlchemy 的 sync engine)。

簡單的判斷原則:

  • 端點會呼叫非同步函式(httpx async、asyncpg、motor 等):用 async def。
  • 端點會呼叫阻塞式函式(requests、psycopg、檔案讀寫):用普通 def,讓 FastAPI 用 thread pool 處理。
  • 端點只有純計算:兩者都行;用 def 比較省事,避免錯誤使用 await。

今天的範例刻意做一個「打另一個 HTTP API」的端點,這是 async 的典型舞台:

# app/services/external.py
# 模擬外部 HTTP API 的呼叫:async 版本
import httpx

DEFAULT_TIMEOUT = 5.0


async def fetch_user_name(user_id: int) -> str:
    # 真實情境會打另一個服務;這裡用一個會被攔截的 URL
    async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
        response = await client.get(f"https://api.example.com/users/{user_id}")
        response.raise_for_status()
        return response.json()["name"]

這段程式是「打外部 API」的非同步版本。async with httpx.AsyncClient(...) 會在結束時自動關閉連線;await client.get(...) 把控制權交還給事件迴圈。寫在 sync 端點裡會壞掉,因為 httpx.AsyncClient 只能在 async 環境裡用,這是後面踩雷段落會再提醒的事。

接著看端點怎麼用這個 service:

# app/main.py
# 一個會打外部 API 的 async 端點
import asyncio
from fastapi import FastAPI, HTTPException

from app.services.external import fetch_user_name

app = FastAPI(title="Web Day 18 範例", version="0.18.0")


@app.get("/users/{user_id}/name")
async def get_user_name(user_id: int):
    try:
        name = await fetch_user_name(user_id)
    except httpx.HTTPStatusError as exc:  # type: ignore[name-defined]
        raise HTTPException(status_code=502, detail=f"上游回應 {exc.response.status_code}")
    except httpx.RequestError:  # type: ignore[name-defined]
        raise HTTPException(status_code=504, detail="上游連線失敗")
    return {"user_id": user_id, "name": name}


@app.get("/users/batch")
async def get_user_names_batch(ids: list[int]):
    # 並發打多個請求
    results = await asyncio.gather(
        *(fetch_user_name(user_id) for user_id in ids),
        return_exceptions=True,
    )
    out = []
    for user_id, result in zip(ids, results):
        if isinstance(result, Exception):
            out.append({"user_id": user_id, "error": str(result)})
        else:
            out.append({"user_id": user_id, "name": result})
    return out

get_user_name 用 async def 是因為內部 await 了 fetch_user_name;get_user_names_batch 用 asyncio.gather 並發送多個請求,這是 async 最大的威力:總延遲取決於「最慢的那一個」,而不是「所有相加」。return_exceptions=True 是重要的細節:當其中一個請求拋錯,gather 不會立刻中斷整個批次,而是把例外放進結果陣列,讓我們有選擇性地處理。

測試 async 端點:AsyncClient + pytest-asyncio

TestClient 預設就能處理 async 端點(內部用 portal 把 sync 與 async 橋接起來),所以昨天的測試在 async 端點上也跑得動。但當你想「真的在事件迴圈裡跑測試」時,就要用 httpx.AsyncClient 搭配 pytest-asyncio。這在兩種情境特別有用:要並發、要對 async 中間件(例如 lifespan)做事。

先安裝套件(昨天的 pyproject.toml 已經寫好了,這裡只是確認):

# 命令列:確認 pytest-asyncio 已安裝
uv sync --extra dev
uv run python -c "import pytest_asyncio; print(pytest_asyncio.__version__)"
# 輸出(範例):0.24.x

接著在 pyproject.toml 設定 asyncio mode:

# pyproject.toml(節錄)
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"
asyncio_mode = "auto"

asyncio_mode = "auto" 讓 pytest-asyncio 自動把所有 async def test_xxx 視為非同步測試,不需要每個測試加 @pytest.mark.asyncio。另一個模式是 "strict",每個非同步測試都要加標記;對小專案來說 "auto" 比較省事。

然後改寫 conftest.py,加上 async client fixture:

# tests/conftest.py
# async client:對 async 端點做測試
import pytest
import httpx

from app.main import app


@pytest.fixture
async def async_client():
    # 與 ASGITransport 配合,讓 AsyncClient 直接呼叫 ASGI 應用
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        yield client

ASGITransport 是 httpx 0.28 的新介面(從 0.27 開始引進),它讓 AsyncClient 直接驅動 ASGI 應用、不需要佔用真的 port,也不需要 uvicorn 啟動。比之前的 httpx.AsyncClient(app=app, ...) 寫法更明確,因為 httpx 把「驅動 ASGI」這件事獨立成 transport 物件,乾淨很多。

現在可以寫第一個 async 測試:

# tests/test_async_users.py
# async 端點的基本測試
import pytest


@pytest.mark.asyncio
async def test_get_user_name(async_client, monkeypatch):
    # 把 httpx.AsyncClient 換成假實作,避免真的打到外部
    async def fake_fetch(user_id: int) -> str:
        return f"使用者 {user_id}"

    monkeypatch.setattr("app.main.fetch_user_name", fake_fetch)

    response = await async_client.get("/users/42/name")
    assert response.status_code == 200
    assert response.json() == {"user_id": 42, "name": "使用者 42"}

這個測試用 monkeypatch.setattr 把 app.main.fetch_user_name 換成回傳假資料的版本,這樣測試不會真的打外部 API;同時它真的在事件迴圈裡跑(await 整段),所以 async 中間件、非同步背景任務(Day 19)的行為都會被正確觸發。

完整實作:並發測試與假 server

接著用 asyncio.gather 同時送多個請求、驗證並發行為:

# tests/test_async_users.py(接續)
@pytest.mark.asyncio
async def test_batch_returns_all_users(async_client, monkeypatch):
    async def fake_fetch(user_id: int) -> str:
        return f"使用者 {user_id}"

    monkeypatch.setattr("app.main.fetch_user_name", fake_fetch)

    response = await async_client.get("/users/batch", params={"ids": "1,2,3"})
    assert response.status_code == 200
    body = response.json()
    assert len(body) == 3
    names = [item["name"] for item in body]
    assert names == ["使用者 1", "使用者 2", "使用者 3"]


@pytest.mark.asyncio
async def test_batch_continues_when_one_fails(async_client, monkeypatch):
    async def fake_fetch(user_id: int) -> str:
        if user_id == 2:
            raise RuntimeError("boom")
        return f"使用者 {user_id}"

    monkeypatch.setattr("app.main.fetch_user_name", fake_fetch)

    response = await async_client.get("/users/batch", params={"ids": "1,2,3"})
    assert response.status_code == 200
    body = response.json()
    assert body[0] == {"user_id": 1, "name": "使用者 1"}
    assert body[1]["error"]  # 有 error 欄位
    assert body[2] == {"user_id": 3, "name": "使用者 3"}

第二個測試特別重要:當其中一個子任務失敗,gather(..., return_exceptions=True) 讓其他任務繼續完成,端點回傳 200 並把每筆結果(含錯誤)放在同一個 JSON 陣列裡。這種「一個失敗不會拖垮整批」的設計在批次 API 很常見,也是 return_exceptions=True 的標準使用情境。

另一種測試手法是「架一個假的 HTTP server 給 async client 打」。當我們想驗證「端點真的會呼叫外部 API 而且用的方法正確」,可以先用 aiohttp 或 FastAPI 自己架一個接收端,再用真的 AsyncClient 打過去:

# tests/test_async_users.py(用真的假 server)
import pytest
from fastapi import FastAPI
import httpx


@pytest.fixture
async def fake_upstream():
    server = FastAPI()
    received = []

    @server.get("/users/{user_id}")
    async def fake_handler(user_id: int):
        received.append(user_id)
        return {"name": f"假使用者 {user_id}"}

    transport = httpx.ASGITransport(app=server)
    async with httpx.AsyncClient(transport=transport, base_url="http://upstream") as client:
        # 把 httpx.get 的 base URL 換成假 server
        yield client, received


@pytest.mark.asyncio
async def test_calls_real_upstream(fake_upstream, monkeypatch):
    upstream_client, received = fake_upstream

    # 把 app/services/external.py 裡的 AsyncClient 換成這個假 client
    monkeypatch.setattr("app.services.external.httpx.AsyncClient", lambda **kw: upstream_client)

    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=__import__("app.main", fromlist=["app"]).app)) as client:
        response = await client.get("/users/7/name")

    assert response.status_code == 200
    assert response.json() == {"user_id": 7, "name": "假使用者 7"}
    assert 7 in received

這個寫法比較繞,但能驗證「真的會呼叫 upstream,而且呼叫的路徑、URL 是對的」。日常開發用 monkeypatch.setattr 換成函式就夠,只有在懷疑「真的有呼叫嗎」時才需要這個手法。

常見錯誤與踩雷

第一個雷是「sync 函式裡呼叫 await」。當你寫了一個普通 def get_data(),內部卻 await fetch_xxx(),Python 直接拋 SyntaxError: 'await' outside async function。這個錯誤很明確,看到訊息就會改;但變形版是「async 函式裡呼叫 sync 函式,sync 函式又呼叫 httpx 的 sync client」:這會卡住事件迴圈,雖然不報錯,但整個 worker thread 被鎖死,吞吐量暴跌。解法是「async 端點裡只 await async 函式;sync 端點裡只呼叫 sync 函式」。

第二個是「async 端點裡呼叫 requests」。requests 是阻塞式的,呼叫時整個事件迴圈會被卡住,直到回應回來。如果你的端點會打 5 個外部 API、每個花 200 毫秒,總時間會是 1 秒(同步相加);但如果你先 await 第一個、await 第二個,總時間也是 1 秒。真正的差異只在「還能空出時間給別的 worker」,對單一使用者來說沒差。結論:單純為了「變快」而改 async 沒有意義,真正有意義的是「async 端點能讓 FastAPI 在等待時處理別人的請求」。

第三個是「忘記 await」。async 函式回傳的是一個 coroutine 物件,需要 await 才會執行。如果你寫成 result = async_func()(沒有 await),result 會是 coroutine 物件而不是預期的值。這個錯誤在 httpx.AsyncClient 上特別常見,因為 async client 的方法「看起來」跟 sync client 很像,只是多了 await。解法是養成「看到 async 函式就 await」的肌肉記憶。

第四個是「pytest-asyncio 沒裝或沒啟用 auto mode」。如果忘了在 pyproject.toml 設定 asyncio_mode = "auto",async 測試預設不會被 pytest-asyncio 收集,而是被當成「回傳 coroutine 的普通測試」處理,於是測試函式根本不會執行,uv run pytest 顯示 no tests ran 或 collected 0 items。遇到這個狀況先檢查 pyproject.toml 跟 uv sync 是否都做好了。

效能與實務提醒

第一個提醒是「async 不是免費的」。每個 async 端點都要付出事件迴圈的額外成本:coroutine 的建立與切換、anyio 對 bridge thread 的管理。如果你的端點只是純計算或簡單 CRUD(純資料庫查詢),用 def 反而更簡單、效能也差不多。FastAPI 的官方建議是「先寫 sync、看到 I/O 等待時再升級 async」。

第二個是「httpx 的 async 連線池」。當你在 async 端點裡用 async with httpx.AsyncClient(...) 重新產生一個 client,每次都會重新開 TCP 連線。如果你的服務要並發打同一個 upstream,正確做法是用 lifespan 或 app state 共用一個 client:

# app/main.py(用 lifespan 共用 AsyncClient)
from contextlib import asynccontextmanager
import httpx
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 啟動時建立 client,關閉時釋放
    async with httpx.AsyncClient(timeout=5.0) as client:
        app.state.http = client
        yield


app = FastAPI(title="Web Day 18 範例", lifespan=lifespan)


@app.get("/users/{user_id}/name")
async def get_user_name(user_id: int):
    response = await app.state.http.get(f"https://api.example.com/users/{user_id}")
    return {"user_id": user_id, "name": response.json()["name"]}

lifespan 是 FastAPI 0.93 開始的推薦寫法(取代舊的 on_event("startup"))。這裡把 AsyncClient 放進 app.state,所有端點共用同一條 client 與連線池,吞吐量會明顯提升;測試時也只要在 lifespan 裡做事就能驗證啟動流程。

第三個是「測試不要真的打外部 API」。CI 環境可能沒有對外網路,打到一半 timeout 會讓測試不穩定。今天的 monkeypatch.setattr 手法幾乎是最簡單、最穩定的替代方案;正式專案若想做得更系統,可以考慮 VCR.py(把 HTTP 互動錄下來、重播)但這是更進階的議題。

async 端點與 SQLAlchemy 的搭配

Web Day 6 介紹的 SQLModel 是 SQLAlchemy 的薄包裝,而 SQLAlchemy 2.0 同時提供同步與非同步兩個 engine。當 async 端點需要查資料庫時,必須使用非同步 engine(create_async_engine)與非同步 session(AsyncSession),不能把同步 engine 拿來在 async 端點裡呼叫。

把昨天的同步設定改成非同步版本,差異其實不大:

# app/db.py(非同步版本)
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.models import Item  # 假設有這個模型

DATABASE_URL = "sqlite+aiosqlite:///./app.db"

engine = create_async_engine(DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, expire_on_commit=False)


async def get_db():
    async with async_session() as session:
        yield session

這段程式用了 sqlite+aiosqlite 這個特殊的 URL scheme,告訴 SQLAlchemy 我們要走 aiosqlite(非同步 SQLite 驅動)。PostgreSQL 對應的是 postgresql+asyncpg,MySQL 對應的是 mysql+aiomysql。選錯驅動會在啟動時就報錯,這是好事,因為錯誤訊息會很明確。

端點的寫法相對單純:用 async with 取代 yield、呼叫 await session.execute(...) 取代 session.exec(...):

# app/main.py(async 版本)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from fastapi import Depends, FastAPI

from app.db import get_db
from app.models import Item, ItemIn, ItemOut

app = FastAPI(title="Web Day 18 async 範例", version="0.18.0")


@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(payload: ItemIn, db: AsyncSession = Depends(get_db)):
    item = Item(name=payload.name, price=payload.price)
    db.add(item)
    await db.commit()
    await db.refresh(item)
    return item


@app.get("/items", response_model=list[ItemOut])
async def list_items(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Item))
    return result.scalars().all()

注意 db.commit() 與 db.refresh() 在非同步版本都要 await,這是新手最常漏掉的一個 await。少了 await,這個敘述會回傳一個 coroutine 物件而不會真的提交交易,於是後續的查詢讀不到剛剛新增的資料,看起來就像「明明寫了但沒生效」。這個錯誤的除錯方式是把 await db.commit() 改成 await db.commit(); assert db.in_transaction() is False(同步版本要 db.commit(); assert not db.in_transaction()),斷言失敗就會告訴你 commit 沒真的執行。

測試 async + async session:換掉 fixture

昨天的 fixture 用的是同步 SQLAlchemy 2.0 + :memory: SQLite,要改成非同步版本時,只需要把 engine 換成 create_async_engine、session 換成 AsyncSession:

# tests/conftest.py(async 版本)
import pytest
import httpx
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlmodel import SQLModel


@pytest.fixture
async def async_engine():
    eng = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        echo=False,
    )
    async with eng.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    try:
        yield eng
    finally:
        async with eng.begin() as conn:
            await conn.run_sync(SQLModel.metadata.drop_all)
        await eng.dispose()


@pytest.fixture
async def async_session(async_engine):
    async_session_factory = async_sessionmaker(async_engine, expire_on_commit=False)
    async with async_session_factory() as session:
        yield session


@pytest.fixture
async def async_client(async_session):
    from app.db import get_db
    from app.main import app

    async def override_get_db():
        yield async_session

    app.dependency_overrides[get_db] = override_get_db
    try:
        transport = httpx.ASGITransport(app=app)
        async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
            yield client
    finally:
        app.dependency_overrides.clear()

這個 fixture 比同步版本複雜一些,但核心觀念一樣:每個測試函式拿到一份專屬的 async_session,結束時 session 與連線會被正確關閉。測試本體的寫法則跟昨天幾乎一樣,只是要加 await。實務上你通常會同時維護「同步版」與「非同步版」兩個 fixture 集,等專案完全轉到非同步再把同步版刪掉;混用期間可以靠 pytest 的 marker(@pytest.mark.unit、@pytest.mark.integration)分類。

# tests/test_async_items.py
import pytest
from sqlalchemy import select

from app.models import Item


@pytest.mark.asyncio
async def test_create_and_list_items(async_client, async_session):
    response = await async_client.post("/items", json={"name": "pen", "price": 10})
    assert response.status_code == 201

    response = await async_client.get("/items")
    assert response.status_code == 200
    body = response.json()
    assert len(body) == 1
    assert body[0]["name"] == "pen"

    rows = (await async_session.execute(select(Item))).scalars().all()
    assert len(rows) == 1

兩個 fixture 共用同一條 async_engine,因此 async_session 寫進去的資料,async_client 透過端點寫進去的資料,能在同一個 in-memory SQLite 裡看到。這是「用 dependency_overrides 共用 session」的核心價值:測試函式、端點、teardown 看到的都是同一份記憶體狀態。

順便提醒,aiosqlite 目前在 Linux/macOS 用原生 thread、在 Windows 走 ProactorEventLoop;如果你在 Windows 上開發,遇到奇怪的事件迴圈錯誤,可以把測試改成用同步 SQLAlchemy,這是過渡期最務實的作法。等正式部署到 Linux 容器再切回非同步,這也是品質篇一直強調的「環境一致」原則。

async 端點的生命週期管理:背景任務的入口

今天的最後一段,預告明天的主題。async 端點常常會需要「先回應、把工作丟到背景」。FastAPI 提供 BackgroundTasks,用法是這樣的:

# app/main.py(先給個預告)
from fastapi import BackgroundTasks, FastAPI

app = FastAPI(title="Web Day 18 預告", version="0.18.0")


def write_audit_log(user_id: int):
    # 假裝寫到檔案或送審計系統
    with open("audit.log", "a", encoding="utf-8") as f:
        f.write(f"user {user_id} 做了某件事\n")


@app.post("/actions")
async def perform_action(user_id: int, background: BackgroundTasks):
    # 立刻回應,真正的寫檔丟到背景
    background.add_task(write_audit_log, user_id)
    return {"status": "accepted"}

background.add_task 把函式排進 FastAPI 的 background queue,回應送出後才執行。這是明天 Web Day 19 的主題:什麼樣的工作適合用 BackgroundTasks、什麼樣的工作必須上 Celery 之類的外部 broker。我們今天先記得這個 API,明天會完整展開。

小結

今天把 async 端點與 httpx 0.28 的 AsyncClient 接上線了。FastAPI 對 sync 與 async 混用友善,但測試端要看情境選 TestClient(簡單)或 AsyncClient(要並發、要 lifespan)。asyncio.gather(return_exceptions=True) 是批次 API 的好朋友,monkeypatch 是取代「真的打外部 API」的最直接手段。ASGITransport 是 httpx 0.28 的新介面,讓我們不用啟動 uvicorn 就能在事件迴圈裡測 ASGI 應用。明天我們會把 async 端點推進到「背景任務」:在回應送出去之後才做的事,怎麼測、怎麼不被阻塞。

今天另外把 SQLAlchemy 2.0 的非同步設定也帶進來:用 create_async_engine 與 AsyncSession 取代同步版本,並把 commit、refresh、execute 全部加上 await。fixture 集也對應改造,讓 async_session 與 async_client 共用同一條 in-memory engine。雖然這篇的核心是 httpx 與 asyncio,但「async 端點最終都會需要 async 資料庫」,早點熟悉對後續章節很有幫助。

async 的學習路徑:怎麼判斷我已經準備好了

很多新手會問「我什麼時候該開始寫 async 端點」。幾個簡單的判斷:第一,你的端點會呼叫另一個 HTTP API、資料庫連線、訊息佇列嗎?是的話,async 是合理的選擇。第二,你預期會在高峰期同時有數百個慢請求嗎?是的話,async 能讓 FastAPI 更有效率地用 worker。第三,你已經熟悉 sync 端點的測試嗎?還沒的話,先把 sync 與同步測試寫穩,再升級 async。

最後一個判斷是「這個工作真的會等 I/O 嗎」。如果你的「外部 API」其實只是 in-memory 字典查表(你寫了一個假服務),那同步版就跑得比 async 版快了。把這種場景強行升級成 async,會讓程式碼變得複雜、測試變得難寫,卻換不到任何效能收益。FastAPI 官方說明有句話很實在:「如果你不知道該不該用 async,先用 sync 就對了」。

另外兩個有助於決策的線索是「你已經在 asyncio 生態裡了」與「團隊裡有人能 cover on-call」。前者代表你的其他套件(資料庫驅動、快取 client、訊息佇列)都已經是非同步;後者代表出問題時有人能馬上看懂 coroutine、TaskGroup、asyncio.gather 的狀態。如果兩者都不確定,先用 sync、之後再升級,是風險最低的路徑。

結語

今天的 async 測試都假設「工作是當下完成的」。但很多真實情境是「使用者按了一個按鈕、伺服器收到請求、立刻回 200、實際工作丟到背景慢慢做」。明天,Web Day 19「背景任務:FastAPI BackgroundTasks 與 Celery 入門」會把這個模式拆開來看:什麼時候用 BackgroundTasks 就好、什麼時候必須上 Celery、Windows 與小型服務的替代方案是什麼。我們也會用今天的 AsyncClient 來測背景任務的執行狀況。

如果今晚就想動手:把昨天的同步測試改寫成 async 版本,是最直接的練習。把 def test_xxx 改成 async def test_xxx、把 client.get 改成 await async_client.get、確認 pytest-asyncio 的設定就緒,跑一次 uv run pytest。這個練習大約 15 分鐘就做完,但會讓你對今天的工具鏈留下更深的印象。

延伸資源

  • FastAPI 官方 Async 測試教學(0.116,2025-07):https://fastapi.tiangolo.com/advanced/async-tests/
  • httpx Async 與 ASGITransport(0.28,2025):https://www.python-httpx.org/async/
  • pytest-asyncio 官方文件(0.24,2025):https://pytest-asyncio.readthedocs.io/
  • anyio 官方說明(2025):https://anyio.readthedocs.io/
  • Python asyncio 官方教學(3.13):https://docs.python.org/3/library/asyncio.html

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...