# 程式碼開發注意事項
## 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 開發部工程師,謝謝大家。