# 程式碼開發注意事項 ## Python 寫作風格 以下介紹取自["Google Python 風格指南"](https://tw-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/),這本指南告訴我們 Google 團隊在開發開源的 Python 專案時所遵守的寫作風格。合格的寫作風格不光是能幫助他人能快速理解你撰寫的程式碼,更是為未來的你了解你當時撰寫程式碼的思路。因此多幫幫他人,也是幫助自己。 以下將分別介紹變數、函式和類別的寫作風格,以此讓大家了解合格的程式碼應該是如何被撰寫。 ### 命名 Python之父Guido推薦的規範: | Type | Public | Internal | | -------------------------- | ------------------ | ----------------------------------------------------------------- | | Modules | lower_with_under | _lower_with_under | | Packages | lower_with_under | | | Classes | CapWords | _CapWords | | Functions | lower_with_under() | _lower_with_under() | | Method Names | lower_with_under() | _lower_with_under() (protected) or __lower_with_under() (private) | | Function/Method Parameters | lower_with_under | | | Local Variables | lower_with_under | | ### 函式 下文所指的函式,包括函式, 方法, 以及生成器。 一個函式必須要有文檔字串, 除非它滿足以下條件: * 外部不可見 * 非常短小 * 簡單明瞭 文檔字串應該包含函式做什麼, 以及輸入和輸出的詳細描述。 通常, 不應該描述”怎麼做”, 除非是一些複雜的算法。 文檔字串應該提供足夠的信息, 當別人編寫程式碼調用該函式時, 他不需要看一行程式碼, 只要看文檔字串就可以了。 對於複雜的程式碼, 在程式碼旁邊加註解會比使用文檔字串更有意義。 以下為個別說明輸入輸出以及例外處理: **Args**: 列出每個參數的名字, 並在名字後使用一個冒號和一個空格, 分隔對該參數的描述。 如果描述太長超過了單行 80 字符,使用 2 或者 4 個空格的懸掛縮進(與文件其他部分保持一致)。 描述應該包括所需的類型和含義。 **Returns**: 描述返回值的類型和語義。 如果函式返回 None, 這一部分可以省略。 **Raises**: 列出與 API 有關的所有異常 以下為範例: ```python! def fetch_bigtable_rows(big_table, keys, other_silly_variable=None): """Fetches rows from a Bigtable. Retrieves rows pertaining to the given keys from the Table instance represented by big_table. Silly things may happen if other_silly_variable is not None. Args: big_table: An open Bigtable Table instance. keys: A sequence of strings representing the key of each table row to fetch. other_silly_variable: Another optional variable, that has a much longer name than the other args, and which does nothing. Returns: A dict mapping keys to the corresponding table row data fetched. Each row is represented as a tuple of strings. For example: {'Serak': ('Rigel VII', 'Preparer'), 'Zim': ('Irk', 'Invader'), 'Lrrr': ('Omicron Persei 8', 'Emperor')} If a key from the keys argument is missing from the dictionary, then that row was not found in the table. Raises: IOError: An error occurred accessing the bigtable.Table object. """ pass ``` ### 塊註解和行註解 最需要寫註解的是程式碼中那些技巧性的部分。 設想你在程式碼審查時要解釋該程式碼, 那麼你應該現在就給它寫註解。 對於複雜的操作, 應該在其操作開始前寫上若干行註解。 對於不是一目瞭然的, 應在其行尾添加註解。並且為了提高可讀性, 行註解應該至少離開程式碼 2 個空格。 以下為範例: ```python! # We use a weighted dictionary search to find out where i is in # the array. We extrapolate position based on the largest num # in the array and the array size and then do binary search to # get the exact number. if i & (i-1) == 0: # true iff i is a power of 2 ``` ### 類別 類應該在其定義下有一個用於描述該類的文檔字串。 如果你的類有公共屬性(Attributes), 那麼文檔中應該有一個屬性(Attributes)段. 並且應該遵守和函式參數相同的格式。 以下為範例: ```python! class SampleClass(object): """Summary of class here. Longer class information.... Longer class information.... Attributes: likes_spam: A boolean indicating if we like SPAM or not. eggs: An integer count of the eggs we have laid. """ def __init__(self, likes_spam=False): """Inits SampleClass with blah.""" self.likes_spam = likes_spam self.eggs = 0 def public_method(self): """Performs operation blah.""" ``` ### TODO TODO 註解應該在所有開頭處包含 ”TODO” 字串,緊跟著是用括號括起來的你的名字,email、地址或其它標識符。 然後是一個可選的冒號。 接著必須有一行註解, 解釋要做什麼. 主要目的是為了有一個統一的 TODO 格式, 這樣添加註解的人就可以搜索到(並可以按需提供更多細節)。 寫了 TODO 註解並不保證寫的人會親自解決問題。 當你寫了一個 TODO,請寫上你的名字。 以下為範例: ```python! # TODO(kl@gmail.com): Use a "*" here for string repetition. # TODO(Zeke) Change this to use relations. ``` ### 注意使用詞彙 在 ChatGPT 以及許多網路資源總是充斥著大陸用語,但很多大陸用語是無法做到精確表達,甚至與台灣用語習慣完全相反 (例如: 線性代數中行與列)。因此請多查看以下資料: * [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) * [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries) * [樂詞網](https://terms.naer.edu.tw/) ## Jira **Jira** 做為一套進度管理工具還是相當耐用的,系統裡有許多耐人尋味的功能可以使用,其提供的工作項目與進度管理模板更是針對不同管理方式而設計,如 Scrum 這種敏捷開發模式等。然而為了更好的追蹤工作項目,我們將產品生命週期中的開發任務分為三個階段,使其對應至三個不同的看板卡片,分別是**需求單**、**工程單**以及**測試單**,以下我們將一一介紹每張卡片代表的意義: * 需求單:由需求單位提出的需求說明,詳細內容會包含**需求來源、需求目的、需求說明及預估完成時間**等資訊,如 [範例](https://ai4dt.atlassian.net/browse/AI-35)。且通常一個需求單會被拆分為多個工作項目 **(工程單)**。 * 工程單:開發單位會將需求單拆解為多個工作項目,若需求功能較複雜,可能會需要跟其他開發人員 co-work,不同開發人員間會建立不同的工程單,主要填報內容會包含**需求單號與附加關聯連結、指派變更日期、變更處理人員、變更完成時間及變更內容及處理方法**等,其中**變更內容及處理方法**我們通常會以 **Check List** 搭配**描述具體開發任務**的方式來記錄之,如 [範例](https://ai4dt.atlassian.net/browse/AI-36)。 * 測試單:每當開發人員完成開發後,便會透過系統管理人員將寫好的程式源碼部署至測試環境中,讓測試人員可以依工程單填報之開發內容進行測試,測試人員需於測試過程填寫測試單,其中會紀錄測試人員、測試時間、測試的工程單號與附加關聯連結、測試方法與操作流程圖片或影片及測試結果等資訊。 最終這些內容會做為產品開發與內部測試是否完成之依據,是產品開發上市的重要指標之一。 ## GitLab 在開始說明 Gitlab 與 Commit message 之前,先推薦各位看下面兩篇文章,加深 Git 使用方法的印象及 Git commit message 代表的真正意涵: 1. [連猴子都能懂的 Git 入門指南](https://web.archive.org/web/20230518032601/https://backlog.com/git-tutorial/tw/) 2. [偉大的 Git commit message rules](https://hackmd.io/@howhow/git_commit) 看完了吧,那我們先簡述一下 Gitlab 是在 [幹嘛](https://www.devops.com.tw/post/gitlab-what-is-gitlab) 的,以及它一些好用的地方。 沒錯,又看了一篇,想不到吧😃 本節的重點其實是在討論 Git commit message 與 Jira 看板中工程單卡片的連結,為了可以從 Git 的節點中清楚的識別變更內容與實際任務中間的關聯性,我們在 Git commit message 上建立了一套規則,其格式如下: #### **[需求單號] + 需求單標題 + ({fix 變更次數 or issue 工作項次} + 變更內容)** 實際範例(以核算系統為例): #### [MARKQ1-1061][臺東縣] 新增一般日/特餐日道數、加工食品限制及其不符合請領原因(fix5 調整為 5/22 新需求) 我們來看一下對應的部分, #### [MARKQ1-1061] → [需求單號] #### [臺東縣] 新增一般日/特餐日道數、加工食品限制及其不符合請領原因 → 需求單標題 #### (fix5 調整為 5/22 新需求) → (fix 變更次數 + 變更內容) 如此一來,Code reviewer 不用花大把的時間通靈,僅透過 Commit message 提供的**工程單資訊**便可檢視這次的**變更是否與實際需求**有關,可大幅提升 Code review 的效率。 ## Python 套件中便利與效能間的取捨 我們拿出一個訊號處理中常見的問題: **給定一個時間戳以及一對訊號起始時間和訊號結束時間,如何找出訊號起始時間於時間戳的 index 以及訊號結束時間於時間戳的 index?** 以上問題我們可以使用 NumPy 輕易解決,程式碼如下: ```python! # 10 秒的時間戳,取樣頻率為 100 hz timestamp = [i / 100 for i in range(1000)] # 訊號起始和結束時間 signal_start = 0.88 signal_end = 2.34 # 引入 NumPy import numpy as np timestamp = np.array(timestamp) # 取得包含訊號起始和結束時間的時間戳 index signal_index = np.nonzero((timestamp >= signal_start) & (timestamp <= signal_end)) ``` 由以上程式碼我們看到 NumPy 提供簡易的語法供我們使用,但是在執行效率上可能不是跟它的簡單方便相符。 我們使用套件 time 和 psutil 分別觀察程式碼的執行時間以及記憶體使用量,結果如下: * 執行時間: 0.19836 秒。 其中 import numpy as np 需要 0.19816 秒,佔整體執行時間的 99.9%。 * 記憶體使用量: 50.918 MB。 其中 import numpy as np 需要 38.98 MB,佔整體記憶體使用量的 76.6%。 由以上結果我們發現 NumPy 雖然提供簡單方便的方法供我們解決問題。但若是需解決情境為須要及時處理或是邊緣式運算時,引用 NumPy 的開銷就不能被我們忽視。 接下來我們將會介紹在 Python 中如何呼叫已編譯的 C 語言函式庫以替代程式碼中 NumPy 的角色,提升整體程式碼的效率。 ## 在 Python 中如何呼叫已編譯的 C 語言函式庫 ### 二分搜尋法 關於找出找出訊號起始時間於時間戳的 index 以及訊號結束時間於時間戳的 index,我們可以使用二分搜尋法 (Binary Search) 找出 index。程式碼如下: (參考文章: [現代硬體架構上的演算法: 以 Binary Search 為例](https://hackmd.io/@RinHizakura/BJ-Zjjw43#%E7%8F%BE%E4%BB%A3%E7%A1%AC%E9%AB%94%E6%9E%B6%E6%A7%8B%E4%B8%8A%E7%9A%84%E6%BC%94%E7%AE%97%E6%B3%95-%E4%BB%A5-Binary-Search-%E7%82%BA%E4%BE%8B)) ```cpp! int find_index_timestamp(float *arr, int arr_length, float val) { if (arr[arr_length - 1] < val) { return -1; } int base = 0, len = arr_length; while (len > 1) { int half = len / 2; base += (arr[base + half - 1] < val) * half; len -= half; } return base; } ``` 當我們完成 C 程式碼的撰寫後,下一步便是如何在 Python 中呼叫該函式。 ### ctypes 在 Python 的外部函式庫中,有一套件名為 ctypes,該套件有 API 可以直接載入編譯好的共享函式庫,直接操作 C 函式。 關於編譯 C 程式碼,請注意編譯設定以及程式碼部屬環境。 當我在 Windows 上使用 mingw64 編譯程式碼後,將此編譯後的程式碼部屬至 Linux 時,ctypes 將會出現錯誤報告。原因便來自於編譯環境與程式碼部屬環境對不上。另外請在 commit message 或是程式碼註解中表示編譯環境,例如: * 編譯環境: ubuntu 24.02 * 編譯器: gcc 13.3.0 當確定以上設定後,便是執行編譯指令,指令如下: ```shell! gcc -shared -fPIC -o lib_find_index.so find_timestamp_index.c ``` 其中參數 shared 表示輸出為共享函式庫;fPIC 全名為 Position-Independent Code (位置無關程式碼),程式碼執行時會以相對地址載入記憶體,進而達成動態鏈接的目的;o 為輸出,輸出格式為 so 代表共享函式庫。 在 Python 中,我們可以開始使用共享函式庫,程式碼如下: ```python! import ctypes # 載入共享函式庫 lib = ctypes.CDLL('./lib_find_index.so') # 定義 C 函式型別 lib.find_index_timestamp.argtypes = [ctypes.POINTER(ctypes.c_float), ctypes.c_int, ctypes.c_float] lib.find_index_timestamp.restype = ctypes.c_int # 1. 設定輸入 # 10 秒,取樣頻率為 100 hz timestamp = [i / 100 for i in range(1000)] signal_start = 0.88 signal_end = 2.34 # 2. 將 timestamp 轉換成 c_float 型態的陣列 timestamp_array = (ctypes.c_float * len(timestamp))(*timestamp) # 3. 呼叫 C 函式找出 signal_start 和 signal_end 的 index start_index = lib.find_index_timestamp(timestamp_array, len(timestamp), signal_start) end_index = lib.find_index_timestamp(timestamp_array, len(timestamp), signal_end) ``` 最後我們比較使用 NumPy 與 ctypes 的執行結果。以下為使用 ctypes 的程式碼其執行時間與記憶體使用量: * 執行時間: 0.018 秒。 其中 import ctypes 所需花費時間為 0.0135 秒,佔整體時間的 75%。 * 記憶體使用量: 13.59 MB。 其中 import ctypes 所需花費的記憶體空間為 324 KB (0.316 MB);而載入共享函式庫所需花費的記憶體空間為 236 KB (0.23 MB)。 觀察以上結果,我們得出以下表格: | 項目 | NumPy | ctypes | 比例 | -------- | -------- | -------- | -------- | |執行時間 | 0.19836 sec | 0.018 sec | 9.07% | |記憶體使用量 | 50.918 MB | 13.59 MB | 26.69% | 我們可以得出結論,在處理 1000 筆資料時,使用 ctypes 載入共享函式庫處理訊號的開銷遠比使用 NumPy 處理訊號來的小。 但在這可以做個實驗,當時間戳數量來到 5000000 筆左右時,使用 ctypes 的程式碼執行時間將會超過使用 NumPy 的程式碼,其原因為使用 ctypes 的程式碼中步驟 2. 將 timestamp 轉換成 c_float 型態的陣列的時間開銷極大。 不過在一般訊號處理的情境中,要處理的訊號數量頂多上萬筆,因此不太需要擔心以上實驗情境。 ## 總結 請你對自己的程式碼負責。不論該程式碼是由 ChatGPT 或由你本人撰寫,一旦你的帳號上傳程式碼至 GitLab,你就必須對此程式碼負責。 因此請務必上傳程式碼前,請先在本地進行測試。如果程式碼在本地進行測試皆成功,但在正式機測試中出現錯誤,也請不要氣餒。 程式碼出錯了,再改就好。誰的程式碼不會出現錯誤? 最後希望大家能從以上內容學習到如何成為一個合格的 AI 開發部工程師,謝謝大家。