攝影或3C

為什麼 Python 要用 `max` 配合 `key=lambda`?從找最長文字的 Span 談起 ; #spans:list[dict] ; max(spans, key=lambda s: len(s.get(“text”, “”)))

在處理資料(例如解析 PDF、爬蟲抓取的 JSON 等)時,
我們常常需要從一個包含許多
「字典」(Dictionary) 或「物件」(Object) 的陣列中,
挑出某個在特定條件下最大的元素。

在 PyMuPDF 的操作中,最經典的例子就是:
**從一行字被切碎的多個 `spans` 中,
選出文字最長的那一個作為該行的「主要字型」**

## 情境:被切碎的一行字

假設我們解析出來的一條 `line` 裡面有 4 個 `span`,
它們被放在一個 list 裡:

spans = [
    {"text": "He", "font": "Helvetica", "size": 11},           # 長度 2
    {"text": "ll", "font": "Helvetica", "size": 11},           # 長度 2
    {"text": "BOLD", "font": "Helvetica-Bold", "size": 11},    # 長度 4
    {"text": "o world!", "font": "Helvetica", "size": 11}      # 長度 8
]

我們的任務是:
**找出文字最長的那個 `span`(也就是第 4 個),
並把整顆 dictionary 拿回來,
好讓我們可以讀它的 font 或 size。**

## 寫法 1:直覺的 List 推導式(會弄丟物件!)

很多人第一直覺會用推導式把長度抓出來:

# 抓出最長的長度是多少
max_length = max([len(span.get("text", "")) for span in spans])
print(max_length)
# 結果8

**盲點在哪裡?**

你只拿到了「最大長度是 8」這個數字,
**你把原本那個 dictionary 弄丟了**

如果你想拿到它的 `font`,你得再寫一個迴圈回去找它:

# 因為弄丟了只好再找一次
for span in spans:
    if len(span.get("text", "")) == max_length:
        main_span = span
        break

這種寫法雖然直觀,但需要多寫 4~5 行程式碼,
而且其實迴圈跑了兩次(找最大值跑一次,回頭找人又跑一次)。

## 寫法 2:發號碼牌的做法(`enumerate`

為了解決「弄丟物件」的問題,
我們可以幫每個長度配一個「號碼牌(Index)」:

# %%
# 1. 產生一個 (長度, index) 的比賽名單
lengths_with_index = [(len(span.get("text", "")), i) for i, span in enumerate(spans)]
# 內容會變這樣:[(2, 0), (2, 1), (4, 2), (8, 3)]

# 2.  max() 找出裡面最大的一組 (預設會比對 第一個元素長度)
max_length, max_index = max(lengths_with_index)

# 3. 靠著號碼牌 (max_index) 把原本的老大領回來
main_span = spans[max_index]

**好處在哪裡?**

邏輯非常嚴謹。「憑票根領東西」,
這是非常傳統且安全的工程寫法。
如果這是在 C++ 或者比較舊的語言裡,這幾乎是標準答案。

## 寫法 3:最 Pythonic 的解答(`key=lambda`

Python 的設計者早就發現了這個痛點:
我們真的很常需要「用某個標準評分,然後直接拿回那個人」。
於是他們允許在 `max()` 裡面傳入 `key` 參數:

# spans : list[dict]
main_span = max(spans, key=lambda s: len(s.get("text", "")))

你看,只有一行,清爽乾淨!我們來拆解它是怎麼運作的:

### 把它想成「選秀節目」的評分:

1. **`max(spans, …)`**

   `spans` 是參賽者名單。我們告訴函式:
「請從這些選手(字典實體)裡面幫我挑一個冠軍出來。」

2. **`key=`**

   這是大會規定的「評分標準」。
你不設定的話,`max()` 不知道要比什麼(字典不能直接比大小)。

3. **`lambda s: len(s.get(“text”, “”))`**

   這是一個沒有名字的簡短函式(匿名函式)。
它告訴裁判:「每當一個選手 `$s$` 走上前來,
你就量一下他身上 `text` 這個欄位的字串長度,這就是他的分數。」

### 運作過程的奧妙

*   裁判看完分數後,知道 8 分最高。

*   但裁判**不會回傳 8 分**
他會把當初拿到 8 分的
**那個選手(物件實體)直接指給您**

這就是整句魔法的關鍵:`max()` 靠 `key` 計算出分數,
但最後吐給您的,是原本陣列裡活生生的那個物件!

## 小結:我該用哪一種?

*   如果您覺得 `lambda` 像外星文,
或者在維護一個給多人看、且大家都不熟 Python 的腳本,
使用 **寫法 2 (`enumerate`)** 是完全可以的,
邏輯堅固且不易看錯。

*   但如果您要在 Python 的世界裡打滾,
看懂並學會使用 **寫法 3 (`max(…, key=…)`)** 是必須的,
因為它是處理「在字典列表 (`list[dict]`) 中,
依據特定欄位尋找極值物件」最乾淨、最優雅的標準答案。

推薦hahow線上學習python: https://igrape.net/30afN

## 進階補充 1:這跟 `sort()``min()` 完全一樣!

在 Python 中,只要遇到需要「比大小」的情境,
這套 API 設計全都是相通的。
學會了 `max()` 的 `key`,你就等於同時學會了其他函式:

# 1. 找最大最長
main_span = max(spans, key=lambda s: len(s.get("text", "")))

# 2. 找最小最短
shortest_span = min(spans, key=lambda s: len(s.get("text", "")))

# 3. 排序依長度由短到長
sorted_spans = sorted(spans, key=lambda s: len(s.get("text", "")))

# 4. 排序依長度由長到短
sorted_spans_desc = sorted(spans, key=lambda s: len(s.get("text", "")), reverse=True)

## 進階補充 2:Lambda 可以加型別檢查 (Typing) 嗎?

因為 `lambda` 的設計初衷是「簡短、用完就丟」,
Python 官方語法**不允許**直接在匿名函式內部加上型別註記
(例如不能寫 `lambda s: dict -> int:`)。

如果你的專案對型別檢查(如 `mypy`)要求很嚴格,你可以怎麼做?

**方法 A:改用標準的 `def` 函式(官方 PEP 8 最推薦)**

這能完美支援 Typing,也提高可讀性:

from typing import Dict, Any

def get_span_length(s: Dict[str, Any]) -> int:
    return len(s.get("text", ""))

main_span = max(spans, key=get_span_length)

**方法 B:為裝載 lambda 的變數註記 `Callable`**

from typing import Callable, Dict, Any

# Callable[[傳入參數型別...], 回傳型別]
score_func: Callable[[Dict[str, Any]], int] = \
lambda s: len(s.get("text", ""))

main_span = max(spans, key=score_func)

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡