
整理筆記的時候我習慣把一個主題拆成獨立項目來歸檔。loopx 就是其中一個以“循環”為主線做技術梳理的項目名它既是一個學習索引也是一組工具函數集合專門用來總結循環相關的基礎語法、異步事件循環、循環依賴以及死循環排查等內容。循環在編程里看似簡單但真正放到并發場景、依賴管理、線上故障排查中就會發現“會寫 for”只是第一步理解循環背后的機制更重要。本文適合剛入門但不想只停留在語法的初學者也適合需要系統排查循環依賴、處理事件循環阻塞問題的后端開發者。整篇文章會圍繞“循環”這條主線展開從語法、事件循環、循環依賴到實戰工具函數和線上排查方法一次性把環的概念和坑點梳理清楚。1. 背景與核心概念1.1 為什么整理 loopx 這個循環專題日常開發中“循環”這個詞有多種含義第一次接觸時很容易混在一起。比如我們寫for i in range(10)是循環Node.js 里的 Event Loop事件循環也是循環Spring 啟動時出現的循環依賴錯誤還是“循環”。它們的共同點是“形成了一個環”但解決問題的思路完全不同。我整理 loopx 的初衷就是把這些“環”分開寫代碼時的循環是語法問題事件循環是運行時調度問題循環依賴是架構設計問題死循環是代碼質量與運維問題。把它們放到一個專題里對比學習理解會更高效。否則遇到一個“循環”報錯可能連搜索引擎都不知道該用哪個關鍵詞。因此本文不會只講一種語言的一種循環寫法而是把循環相關的知識點打通提供一個能直接套用的排查思路和工具函數集。1.2 循環技術涉及的范圍loopx 項目里覆蓋的內容主要有四類分類代表問題說明語法循環for、while、迭代器、生成器解決“怎么寫循環”的問題事件循環Node.js Event Loop、瀏覽器事件循環、asyncio解決“異步任務如何調度”的問題循環依賴Spring Bean 循環依賴、Maven 循環依賴、數據庫外鍵循環解決“模塊互相引用”的問題異常循環死循環、CPU 飆高、阻塞事件循環解決“循環導致系統不可用”的問題從這張表可以看到loopx 不是某個熱門開源框架而是一套圍繞循環場景整理的技術地圖。后續章節會按這個地圖展開最終還會給出一個可直接復用的 Python 工具函數模塊方便日常開發中直接使用。1.3 本文適合讀者與學習目標如果你滿足以下任一種情況本文會比較適合你剛學習編程想系統理解 for、while、迭代器、生成器的區別和使用場景。用 Node.js、Python 做過異步開發但沒想清楚事件循環和異步回調的關系。在 Spring 或 Maven 項目里遇到循環依賴報錯希望了解成因與解決方案。遇到線上 CPU 飆高、程序卡死的問題需要掌握死循環排查流程。需要在項目中做批量任務、重試機制、分塊處理想借鑒一組成熟的循環工具函數。讀完后你會得到三類收獲循環語法的多語言對比、循環依賴的解決方案、一組可復制到項目里的工具函數和排查清單。2. 環境準備與實驗約定2.1 運行環境本文示例以常見開發環境為例具體版本需要根據你的項目實際情況調整。操作系統Windows 10/11、macOS、Linux 均可本文示例不依賴特定系統命令。Python建議 3.8 及以上版本用于運行 loopx 工具函數示例。Node.js建議 16 及以上版本用于演示事件循環。Java建議 8 及以上版本用于理解 Spring 循環依賴示例。IDE推薦 VS Code 或 PyCharm隨手測試 Python 代碼比較方便。如果你本機版本較低例如 Python 3.6部分類型注解可能不兼容稍微修改即可運行不影響整體思路。2.2 代碼示例的結構為了讓示例有真實感本文會按以下目錄結構組織代碼loopx/ ├── loopx.py # 循環工具函數集合 ├── test_loopx.py # 簡單測試腳本 ├── event_loop_demo.js # Node.js 事件循環示例 └── README.md # 說明文檔可選代碼示例會盡量保持獨立可運行。涉及第三方庫時會在對應位置說明安裝命令沒有依賴的示例則直接使用標準庫完成。你不需要一開始就搭建完整工程可以先新建一個loopx.py文件邊看邊把函數復制進去測試。3. 常用編程語言中的循環語法拆解循環是編程語言里最基礎也最容易寫出“反直覺代碼”的部分。下面分別看 Python、Java、JavaScript 三種語言的常見寫法重點不是羅列語法而是對比它們的邊界條件。3.1 Python 循環遍歷風格與生成器Python 中最常用的是for...in循環。它與 C 語言的 for 不太一樣本質是“迭代器遍歷”。可以直接遍歷列表、元組、字典、集合、字符串等可迭代對象。# 遍歷列表 names [Alice, Bob, Charlie] for name in names: print(name) # 遍歷字典 user {name: Tom, age: 18} for key, value in user.items(): print(key, value) # 使用 range 生成數字序列 for i in range(1, 5): print(i)這里需要注意range的行為range(1, 5)會輸出 1 到 4不包含 5。如果寫成range(5)輸出 0 到 4。新手經常在邊界這里踩坑比如想輸出 1 到 100寫成了range(1, 100)漏掉了 100。Python 還有一個特性是for...else。當循環正常結束沒有被 break 中斷時else塊會執行。很多教程會忽略它但它非常適合用來做“查找失敗后的兜底處理”。target 8 numbers [1, 3, 5, 7, 9] for n in numbers: if n target: print(找到了, n) break else: print(沒有找到, target)此外Python 的列表推導式在簡單循環場景下比 for 更簡潔squares [x * x for x in range(10)] print(squares)這里等價于squares [] for x in range(10): squares.append(x * x)生成器是循環場景中另一個重要概念。把列表推導式的方括號改成圓括號就變成了生成器表達式它可以避免一次性創建大量數據在內存里gen (x * x for x in range(1000000)) print(sum(gen))這個例子中如果使用列表推導式會一次性生成百萬個元素而生成器是邊遍歷邊計算內存占用低很多。實際處理大文件、大日志、接口分頁數據時生成器幾乎是最優選擇。3.2 Java 循環for、for-each 與 StreamJava 中傳統 for 循環仍然很常用尤其是需要下標訪問的場景。經典寫法如下int[] nums {1, 2, 3, 4, 5}; for (int i 0; i nums.length; i) { System.out.println(nums[i]); }增強 for 循環for-each適合只遍歷不改結構的情況ListString names Arrays.asList(Alice, Bob); for (String name : names) { System.out.println(name); }需要注意的是for-each 遍歷時不能直接調用list.remove()否則會拋出ConcurrentModificationException。如果需要在遍歷過程中刪除元素應使用迭代器ListString list new ArrayList(Arrays.asList(A, B, C)); IteratorString it list.iterator(); while (it.hasNext()) { String item it.next(); if (B.equals(item)) { it.remove(); } }Java 8 之后的 Stream 也提供了類似循環的能力適合過濾、轉換、聚合邏輯ListInteger numbers Arrays.asList(1, 2, 3, 4, 5); ListInteger evens numbers.stream() .filter(n - n % 2 0) .collect(Collectors.toList()); System.out.println(evens);這里 filter 替代了 for if 的寫法代碼更聲明式。但要注意Stream 不是所有循環場景的替代品比如狀態化循環、需要下標或者需要提前中斷的復雜邏輯傳統 for 更合適。3.3 JavaScript 循環for...of、forEach 與退出機制JavaScript 中有多種遍歷方式最容易混淆的是forEach與for...of。const arr [10, 20, 30]; // for...of for (const item of arr) { if (item 20) { break; } console.log(item); } // forEach arr.forEach((item) { console.log(item); });關鍵區別是for...of可以使用break、continue、return控制流程而forEach不能直接 break只能通過拋出異常或提前 return 跳過本次回調。換句話說如果需要中斷遍歷用for...of或傳統 for。JavaScript 中遍歷對象的屬性可以用for...in但它會包含原型鏈上的可枚舉屬性一般建議配合Object.prototype.hasOwnProperty判斷。否則容易遍歷出非自身屬性引發意外結果。const obj { name: loopx, type: note }; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { console.log(key, obj[key]); } }日常遍歷數組推薦for...of需要 key-value 時用Object.entries()const obj { name: loopx, type: note }; for (const [key, value] of Object.entries(obj)) { console.log(key, value); }掌握語法只是第一步。下面進入事件循環部分看看異步場景里“循環”是如何運轉的。4. 事件循環異步編程的底層機制4.1 為什么需要事件循環如果你只寫同步代碼事件循環對你來說可能沒什么存在感。但一旦涉及網絡請求、文件讀寫、定時器、數據庫操作異步機制就不可回避。以 Node.js 為例JavaScript 是單線程的不可能同時執行兩段代碼。為了實現高并發的 I/O 處理Node.js 引入了事件循環機制主線程先把耗時的 I/O 操作交給底層線程池或系統內核處理自己繼續執行后面的任務當 I/O 完成后系統通知 Node.js 把對應的回調放入隊列在合適的時機取出來執行。這個“取出來執行”的時機就是事件循環調度的過程。如果理解不到位經常會寫出“明明先寫了setTimeout卻是最后執行的”的疑惑代碼。4.2 Node.js 事件循環與微任務Node.js 的事件循環分為多個階段主要包括timers執行setTimeout、setInterval的回調。pending callbacks執行上一輪延遲到現在的 I/O 回調。idle/prepare內部使用階段。poll獲取新的 I/O 事件執行與 I/O 相關的回調。check執行setImmediate回調。close callbacks執行 socket 或 handle 的 close 事件回調。每個階段結束后會先執行微任務隊列。所謂微任務指的是Promise.then、process.nextTick、queueMicrotask等回調。微任務會在當前階段結束、進入下一階段之前把隊列清空。看一個示例console.log(1); setTimeout(() { console.log(2); }, 0); Promise.resolve().then(() { console.log(3); }); console.log(4);輸出順序是1 4 3 2原因是同步代碼先執行輸出 1、4Promise 的回調屬于微任務在同步代碼結束后、setTimeout 定時器回調之前執行所以輸出 3 排在前面最后才輪到 0 毫秒定時器回調輸出 2。這個現象在面試和日常開發中都很常見。如果你在使用定時器卻發現回調遲遲不執行先想想事件循環階段與微任務的執行順序是不是有長任務阻塞了循環。4.3 Python asyncio 事件循環Python 3.4 引入 asyncio用于編寫異步 I/O 代碼。asyncio 的核心也是一個事件循環它不斷檢查任務隊列找到可以執行的任務并調度。import asyncio async def say_hello(): print(hello) await asyncio.sleep(1) print(world) async def main(): await asyncio.gather( say_hello(), say_hello(), ) asyncio.run(main())上面代碼中兩個say_hello()協程并發執行總耗時約 1 秒而不是 2 秒。原因是遇到await asyncio.sleep(1)時協程讓出控制權事件循環會調度另一個協程繼續執行。使用 asyncio 最常見的誤區是在協程中寫了time.sleep()而不是await asyncio.sleep()導致事件循環被阻塞其他協程全部卡住。原因在于time.sleep()是同步阻塞調用會讓頂層循環停住。import asyncio import time async def bad_func(): time.sleep(1) # 錯誤示范阻塞事件循環 async def good_func(): await asyncio.sleep(1) # 正確讓出控制權所以在 asyncio 代碼中凡是可能阻塞的操作盡量換成對應的異步版本。如果確有必要調用同步阻塞的第三方庫通常需要把它放到線程池中執行避免拖死整個事件循環。5. 循環依賴工程中的“環”問題5.1 Spring Bean 循環依賴Spring 項目中當 Bean A 依賴 Bean B而 Bean B 又依賴 Bean A 時就會形成循環依賴。Spring 默認對單例 Bean 的 Setter 注入循環依賴是支持的但構造器注入的循環依賴默認無法解決啟動時會出現類似下面的報錯Requested bean is currently in creation: Is there an unresolvable circular reference?解決思路一般有三種構造器注入改為 Setter 注入或字段注入利用三級緩存提前暴露對象。使用Lazy注解在注入處生成一個代理對象延遲真正依賴的解析。重新梳理對象職責把互相依賴的代碼做拆分這是根本解法。Service public class AService { private final BService bService; public AService(Lazy BService bService) { this.bService bService; } }實際項目中不建議為了繞過循環依賴盲目使用Lazy。循環依賴往往是設計問題的信號優先調整分層讓依賴方向變得清晰。5.2 Maven 循環依賴Maven 多模塊項目里如果 module-a 依賴 module-b而 module-b 又依賴 module-a構建時會報Dependency cycle錯誤。Maven 不允許模塊之間存在循環依賴因為這會破壞構建順序的確定性。排查方法是用依賴分析插件mvn dependency:tree然后觀察模塊之間的依賴方向。解決思路通常是抽取公共模塊把 A 和 B 都依賴的公共類放到 module-common讓 A 和 B 都指向它。依賴方向變成“樹狀”而不是“環狀”。5.3 數據庫外鍵循環引用數據庫表設計同樣存在循環引用。例如用戶表 user、訂單表 orderuser 有默認收貨地址字段指向 order 表order 又有 user_id 外鍵指向 user 表兩個表互相外鍵引用。這種設計在插入數據時很容易因為約束校驗導致失敗。實踐建議是能不使用物理外鍵就不要使用用應用層邏輯維護關聯關系。如果必須保留外鍵刪除數據時要先刪除子表記錄再刪除父表記錄。需要修改時先禁用或延遲約束檢查再執行 DML執行完恢復。這種“循環引用”帶來的問題不是語法層面的而是架構和數據字典層面的。提前評估表關系能省下很多運維成本。6. 完整實戰案例loopx 循環工具函數集6.1 需求與函數設計了解了循環相關概念后下面實現一個輕量工具模塊 loopx.py。這個模塊不是重復造輪子而是把循環場景里常用的能力集中起來方便以后復制到項目中使用。我設計了四個核心函數safe_range帶邊界保護的數字序列生成函數避免不小心產生無限循環。retry帶重試次數的循環執行函數適合處理網絡抖動等臨時錯誤。chunked把大列表分成小批次適合分批寫庫或分批請求接口。run_with_deadline帶超時控制的循環執行函數避免任務卡死。整體的設計思路是循環不要裸寫要加限制、加超時、加批次這些是工程化使用循環的基本要求。6.2 完整代碼新建loopx.py內容如下# 文件路徑loopx/loopx.py loopx循環場景工具函數集合。 import time from typing import Any, Callable, Iterable, List, Optional def safe_range(start: int, end: Optional[int] None, step: int 1) - Iterable[int]: 安全地生成數字序列。 如果只傳一個參數則按 range(end) 處理如果 end 為 None則返回空迭代器。 主要作用是避免因參數錯誤導致無限循環。 if end is None: end start start 0 if step 0: raise ValueError(step 不能為 0) return range(start, end, step) def retry( func: Callable[[], Any], max_retries: int 3, delay: float 0.5, exceptions: tuple (Exception,), ) - Any: 循環執行 func并支持失敗重試。 :param func: 需要執行的函數 :param max_retries: 最多重試次數 :param delay: 每次重試之間等待的秒數 :param exceptions: 需要捕獲的異常類型 :return: func 的返回值 attempt 0 while attempt max_retries: try: return func() except exceptions as e: attempt 1 if attempt max_retries: raise print(f第 {attempt} 次執行失敗{e}{delay} 秒后重試...) time.sleep(delay) return None def chunked(data: List[Any], size: int) - Iterable[List[Any]]: 將列表按指定大小分塊。 :param data: 輸入列表 :param size: 每個分塊的最大元素數量 if size 0: raise ValueError(size 必須大于 0) for i in range(0, len(data), size): yield data[i:i size] def run_with_deadline( func: Callable[[], Any], timeout: float 5.0, poll_interval: float 0.1, ) - Any: 在指定時間內循環執行 func直到成功或超時。 適用于等待某個條件成立的場景。 :param func: 返回 True 表示成功False 表示繼續循環 :param timeout: 最長等待時間秒 :param poll_interval: 狀態檢查間隔秒 start_time time.time() while True: result func() if result: return result if time.time() - start_time timeout: raise TimeoutError(f等待超時超過 {timeout} 秒) time.sleep(poll_interval)為了讓模塊更完善再補充一個讀取大量日志并按批次統計需求的小函數def count_in_chunks(file_path: str, keyword: str, chunk_size: int 10000): 分塊讀取文件統計包含指定關鍵字的行數。 避免一次性把大文件全部讀入內存。 count 0 buffer [] with open(file_path, r, encodingutf-8) as f: for line in f: buffer.append(line) if len(buffer) chunk_size: count sum(1 for item in buffer if keyword in item) buffer.clear() if buffer: count sum(1 for item in buffer if keyword in item) return count6.3 測試與輸出新建test_loopx.py驗證函數是否能按預期工作# 文件路徑loopx/test_loopx.py from loopx import chunked, retry, run_with_deadline, safe_range def test_safe_range(): assert list(safe_range(5)) [0, 1, 2, 3, 4] assert list(safe_range(1, 5)) [1, 2, 3, 4] print(safe_range 測試通過) def test_chunked(): assert list(chunked([1, 2, 3, 4, 5], 2)) [[1, 2], [3, 4], [5]] print(chunked 測試通過) def test_retry(): call_count 0 def flaky_func(): nonlocal call_count call_count 1 if call_count 3: raise RuntimeError(臨時錯誤) return ok result retry(flaky_func, max_retries3, delay0.1) assert result ok print(retry 測試通過) def test_run_with_deadline(): state {count: 0} def wait_func(): state[count] 1 return state[count] 3 result run_with_deadline(wait_func, timeout2, poll_interval0.05) assert result is True print(run_with_deadline 測試通過) if __name__ __main__: test_safe_range() test_chunked() test_retry() test_run_with_deadline() print(全部測試通過)運行測試python test_loopx.py預期輸出safe_range 測試通過 chunked 測試通過 retry 測試通過 run_with_deadline 測試通過 全部測試通過6.4 運行思路與實際使用建議為什么把 retry 函數單獨抽出來因為在實際項目中網絡請求、數據庫連接、下游 RPC 都可能臨時失敗直接在業務代碼里寫多層 while try 既不美觀也無法統一控制重試次數和等待時間。抽成工具函數后調用方只需要寫一行data retry(lambda: call_remote_api(), max_retries5, delay1.0)chunked 函數則非常適合批量寫入場景。例如一次需要處理 10 萬條數據直接循環容易造成內存壓力分批處理更平穩for batch in chunked(all_ids, size500): batch_update_status(batch)run_with_deadline 適合輪詢等待某個資源就緒的場景比如等待接口返回狀態、等待文件生成完成。結合超時時間可以避免因為依賴的服務沒響應而讓進程掛死。7. 死循環與高 CPU 問題的排查思路7.1 常見死循環類型死循環是循環場景中最容易引發線上事故的問題典型類型包括忘記更新循環變量或循環條件永遠為真。在遞歸函數中缺少終止條件。for 循環遍歷集合時不斷往集合里追加元素導致永不結束。循環內 catch 住了所有異常并不斷重試沒有退出條件。多線程場景中因為鎖等待造成邏輯死鎖雖然不是嚴格意義上的死循環但表現同樣是“卡死”。我先看一個最典型的 Python 死循環i 0 while i 10: print(i) # 忘記寫 i 1導致 i 永遠為 0這段代碼會無限打印 0。解決方式是確保每次循環都改變循環變量的值或者在循環內設置明確的退出路徑i 0 while i 10: print(i) i 1還有一類隱藏坑遍歷列表時修改列表。nums [1, 2, 3, 4, 5] for n in nums: if n % 2 0: nums.remove(n)這種代碼在 Python 中雖然不會拋錯但結果不符合預期因為遍歷時列表長度在變化。更安全的方式是創建副本后再遍歷nums [1, 2, 3, 4, 5] for n in nums[:]: if n % 2 0: nums.remove(n)7.2 線上定位流程如果線上應用 CPU 飆高懷疑死循環導致可以按以下流程排查使用top命令查看 CPU 占用最高的進程 PID。使用top -Hp PID查看進程內線程的 CPU 占用。如果是 Java 應用使用jstack PID導出線程快照搜索RUNNABLE狀態的線程查看它的調用棧找到卡住的業務循環。如果是 Python 應用可以使用py-spy dump --pid PID查看當前執行棧。如果是 Node.js 應用可以在啟動時加--inspect用 Chrome DevTools 的 Profiler 抓取 CPU Profile定位熱點函數。定位到具體代碼后重點關注循環條件和退出條件尤其是while True、for內部有無 break、continue 是否誤用了標簽等。7.3 常見問題排查表問題現象常見原因解決思路CPU 100%進程卡住循環條件永遠為真檢查循環變量和條件增加退出日志Java 啟動時報 Circular referenceSpring 構造器注入循環依賴改用 Setter 注入、Lazy 或拆分模塊Node.js 定時器回調延遲嚴重主線程被同步循環阻塞優化同步邏輯或拆分成異步批次Python asyncio 協程不執行協程中調用了 time.sleep改用 await asyncio.sleep遍歷集合時異常循環中修改集合結構使用副本遍歷或迭代器 remove下游接口偶發超時沒有重試機制引入帶超時的 retry 工具函數大數據量批量處理內存溢出一次性加載全部數據使用 chunked 分批處理8. 最佳實踐與工程建議8.1 循環書寫的通用規范先總結幾條所有語言通用規范循環前先確認邊界條件尤其是start、end、step的值是否會無限循環。循環體盡量短小。如果循環體超過 20 行先考慮抽函數。不要裸用while True除非你在循環體內有明確的 break 條件。在循環開頭或結尾增加必要日志但避免在高頻循環里每次都打日志。優先使用語言提供的推導式、迭代器、Stream 等高級能力但不要過度封裝導致可讀性下降。遍歷集合時不要修改集合的長度和結構。一個比較推薦的模式是“循環 狀態 超時”deadline time.time() 30 while time.time() deadline: result try_do_something() if result.is_success(): break time.sleep(0.5)這樣能夠確保代碼不會因為異常情況無限等待。8.2 異步循環注意事項在異步環境寫循環時有幾個額外注意點不要在協程中使用同步阻塞調用否則會阻塞整個事件循環。異步循環中創建大量任務時要控制并發度不要一次性create_task幾千個。重試循環必須設置最大次數和超時時間避免異常導致無限重試打爆下游服務。定時器循環任務要考慮上一次執行是否完成。如果上一次沒執行完下一次又開始執行會造成任務堆積。在 Node.js 中如果要做定時批量任務可以用setInterval但要防止重入let running false; setInterval(async () { if (running) { console.log(上一次任務尚未完成跳過本次執行); return; } running true; try { await doBatch(); } finally { running false; } }, 1000);在 Python asyncio 中也有類似問題通常使用asyncio.Lock或一個簡單的布爾標志位。8.3 循環依賴規避策略循環依賴是架構層面最容易忽略的問題。建議從以下角度規避依賴方向保持單向。上層模塊可以依賴下層模塊下層模塊不要反向依賴上層。遇到 Spring 循環依賴優先考慮拆分 Service而不是直接加Lazy。Maven 多模塊項目遵循“common 模塊只被依賴不依賴業務模塊”原則。數據庫表之間的外鍵不要設計成雙向約束使用應用層事務保證數據一致性。代碼評審時把“新增依賴是否形成循環”作為必查項。使用jdeps或 Maven 依賴插件定期檢查模塊依賴關系也能幫助早期發現問題。8.4 可觀測性與監控涉及循環的應用最好增加可觀測性循環任務開始和結束時記錄任務名稱、耗時、成功狀態。重試次數、失敗原因要記錄到結構化日志或監控指標中。給死循環場景設計告警CPU 使用率超過閾值、任務執行時間超過預期、重試次數過多都要觸發告警。進程啟動時可以做一次自檢比如檢查配置文件里的循環次數是否在合理范圍。監控不是代碼寫完后的額外工作而應該從一開始就納入設計。尤其是定時任務、消息消費、批量處理這類循環密集型任務沒有監控意味著故障發生時很難快速定位。9. 后續學習路線與項目實踐建議從 loopx 這個專題延伸出去你可以繼續研究以下方向深入學習生成器和迭代器協議理解 Python 循環背后的迭代機制。閱讀 Node.js 事件循環官方文檔結合process.nextTick、setImmediate、Promise、queueMicrotask徹底搞懂執行順序。在 Spring 項目中嘗試用DependsOn、Lazy等注解解決不同的依賴順序問題。自己實現一個簡化的事件循環加深對異步調度的理解。把文中的 loopx.py 擴展成包含滑動窗口、令牌桶限流、異步重試等更多能力的工具包。實際項目中優先關注三類風險線上死循環導致的高 CPU、異步事件循環被阻塞導致的延遲升高、模塊循環依賴導致的構建與啟動失敗。每類風險都建議在測試環境通過混沌實驗和壓力測試提前發現。如果本文對你有幫助可以收藏備用。也建議你把 loopx.py 直接放進自己的工具庫遇到類似場景時改一改就能用。動手跑一遍示例比只看一遍收獲大得多。