---
# System prepended metadata

title: 程式碼開發注意事項

---

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