目標情境
你有一組按出現順序的區塊(例如 headings),
每個有一個開始位置 start,但還沒有 end。
想要自動補出每個 heading 的 end:
目前節點的結束 = 下一個節點的開始
最後一個節點的結束 = 全部元素總長度(total_elements)
原始程式片段
starts = [h['start'] for h in headings]
ends = starts[1:] + [total_elements]
# len(ends) == len(headings)
for h, e in zip(headings, ends):
h['end'] = e
為什麼這樣寫?
starts:收集每個區塊起點
starts[1:]:往右平移(對齊為「下一個開始」)
- [total_elements]:補最後一個區塊的結束邊界
zip(headings, ends):一一對應(第 i 個 heading 對應第 i 個 end)
這種方法避免用索引 for i in range(len(headings)):,更直觀且不容易 off-by-one。
簡化示例
假設:
headings = [
{'title': 'A', 'start': 0},
{'title': 'B', 'start': 10},
{'title': 'C', 'start': 25},
]
total_elements = 40 #文件最結尾元素的index
步驟:
starts = [0, 10, 25]
starts[1:] = [10, 25]
ends = [10, 25] + [40] → [10, 25, 40]
zip 對應:
A -> 10
B -> 25
C -> 40
輸出:
視覺化
時間軸:
A: [0 10)
B: [10 25)
C: [25 40)
每個區塊半開區間:start 包含、end 不含(常見設計,方便相鄰無重疊)。
等價(索引版本)對照(不建議)
for i, h in enumerate(headings):
if i < len(headings) - 1:
# len(headings) - 1 等效於
# headings.index(headings[-1])
h['end'] = headings[i+1]['start']
else:
h['end'] = total_elements
雖然功能相同,但 zip 寫法更扁平、少條件分支。
常見延伸
- 適用於:章節區間、Token 區間、影片片段、Log 段落、程式語法節點範圍
- 若需要「長度」也可以:
for h, e in zip(headings, ends):
h['end'] = e
h['length'] = e - h['start']
常見錯誤提醒
如果包含被刪除節點?
在建立 starts 之前要先過濾掉已移除的 heading,否則 end 會對錯物件。
推薦hahow線上學習python: https://igrape.net/30afN
使用next 與 迭代器 處理:
headings = [
{'title': 'A', 'start': 0},
{'title': 'B', 'start': 10},
{'title': 'C', 'start': 25},
]
total_elements = 40 #文件最結尾元素的index
it = iter(headings) # 取得一個「會記住位置」的迭代器
prev = next(it, None) # 取出第一個元素;若 headings 為空,回傳 None
if prev is not None: # 空列表時直接略過
for curr in it: # 這裡的 it 已「從第二個元素」開始
# 目的:把「上一個」的 end 補成「當前的 start」
prev['end'] = curr['start']
# 移動滑動視窗:現在這個 curr 會在下一輪變成 prev
prev = curr
# 迴圈結束時,prev 指向最後一個元素,沒人幫它設定 end
prev['end'] = total_elements
prev = next(it, None) 拿到第一個元素(例:{‘title’: ‘A’, ‘start’: 0}),接下來的 for curr in it: 會從「第二個元素」開始迭代,因為迭代器已經前進過一次。整個寫法的目的,就是把「前一個元素的 end = 下一個元素的 start」這件事寫得乾淨、無索引、無 if 分支。
- 原始資料與目標
資料:
headings = [
{'title': 'A', 'start': 0},
{'title': 'B', 'start': 10},
{'title': 'C', 'start': 25},
]
total_elements = 40
目標:補出
A.end = 10
B.end = 25
C.end = 40
2. 為什麼需要「前一個 vs 當前」模式?
因為我們要設定「上一個 heading 的 end = 下一個 heading 的 start」。
這是一種「滑動視窗長度為 2」的需求。
如果用索引版本:headings[i]['end'] = headings[i+1]['start']
,最後一個要特判。
改用 iterator + prev/curr,可以讓核心邏輯在無分支的 for 迴圈中執行。
3. 迭代器關鍵行為示意
it = iter(headings)
prev = next(it, None)
此時:
it 指向「下一個要被取出的元素位置」
prev = 第一個元素(A)
迭代器內部「已經消耗掉」第一個,所以剩下的可迭代元素是:B, C
接下來:
for curr in it:
...
curr
依序會是:
- 第一輪:
curr = B
- 第二輪:
curr = C
- 結束:沒有更多元素
4. 逐步模擬(表格)
5. 加上詳細註解版
6. 如果用 debug print 看會更清楚
it = iter(headings)
prev = next(it, None)
print("INIT prev =", prev)
if prev is not None:
for curr in it:
print("LOOP prev=", prev, "curr=", curr)
prev['end'] = curr['start']
prev = curr
print("AFTER LOOP prev (last) =", prev)
prev['end'] = total_elements
輸出:
7. 這種寫法的優點
8. 常見初學者誤解點
- 與「索引版」對照(語意拆解)
索引版:
for i in range(len(headings)-1):
headings[i]['end'] = headings[i+1]['start']
headings[-1]['end'] = total_elements
iterator 版是將:
「i 與 i+1」→ 「prev 與 curr」
「最後一個單獨處理」→ 「迴圈後補」
10. 如果要同時算長度(延伸)
it = iter(headings)
prev = next(it, None)
if prev is not None:
for curr in it:
prev['end'] = curr['start']
prev['length'] = prev['end'] - prev['start']
prev = curr
prev['end'] = total_elements
prev['length'] = prev['end'] - prev['start']
11. 更抽象:把它想成「兩兩配對」
你正在做的,其實就是「(A,B), (B,C),最後 (C,None)」。
iterator 寫法本質上就是自己做 pairwise。
12. 極簡 mental model(一句話版)
先抓第一個作「前一個」,然後讓迴圈只關心「設定前一個的 end」,最後補尾巴。
推薦hahow線上學習python: https://igrape.net/30afN