程式碼的品質攸關軟體專案的成敗,因此開發者必須掌握最佳實務與技巧。從測試優先的開發策略開始,確保程式碼的正確性是第一步。接著,透過簡化程式碼邏輯、避免魔術數字和重複程式碼,可以有效提升程式碼的可讀性和可維護性。同時,瞭解運算子優先順序、全域變數的使用限制以及 SESE 原則等,有助於避免常見的程式設計錯誤。此外,在追求程式碼效能的同時,也需注意避免過早最佳化,並保持程式碼的簡潔明瞭,才能在效能和可讀性之間取得平衡。最後,選擇適當的程式碼風格和排版,並遵循 DRY 原則,都有助於開發高品質、易維護的軟體專案。

寫測試優先(有時)

撰寫測試優先是軟體開發中的一個重要實踐,尤其是在處理複雜演算法時,如二元搜尋。一個好的測試策略不僅能確保程式碼的正確性,還能幫助開發者更深入地理解問題本身。

二元搜尋的例子

二元搜尋是一種在已排序陣列中尋找特定目標值的演算法。它的基本思想是透過比較中間元素與目標值來縮小搜尋範圍,直到找到目標值或確定目標值不存在。

原始的二元搜尋實作

def binarySearch(array, target):
    left = 0
    right = len(array) - 1
    while left <= right:
        mid = (left + right) // 2
        if array[mid] < target:
            left = mid + 1
        elif array[mid] > target:
            right = mid - 1
        else:
            return mid
    return -1

解開糾纏的程式碼

def binarySearchUT(array, target):
    left = 0
    right = len(array) - 1
    while left <= right:
        mid = (left + right) // 2
        if array[mid] < target: left = mid + 1
        if array[mid] > target: right = mid - 1
        if array[mid] == target: return mid
    return -1

內容解密:

  1. 二元搜尋的基本邏輯:透過不斷計算中間索引 mid,並比較 array[mid]target 的大小,來決定下一步的搜尋範圍。
  2. 程式碼簡化:將原始的 elifelse 分支改為多個獨立的 if 陳述式,使程式碼更直觀易懂。
  3. 效能考量:雖然簡化後的程式碼在某些情況下可能稍慢,但對於大多數應用場景來說仍然足夠高效。

測試的重要性

測試是確保程式碼正確性的關鍵步驟。透過事先撰寫測試案例,可以在開發過程中及時發現並修復錯誤。

如何測試測試?

  1. 注入錯誤資料:故意傳入錯誤或邊界資料,驗證測試程式是否能正確捕捉到錯誤。
  2. 簡單程式碼的自我驗證:對於邏輯簡單的程式碼,測試程式與被測程式可以相互驗證。

專家建議

在軟體開發過程中,累積經驗和學習最佳實踐是非常重要的。以下是一些寶貴的建議:

  1. 快速失敗:在開發初期就應當盡快進行測試,以避免在錯誤的方向上浪費過多時間。
  2. 閱讀他人程式碼:學習優秀的開源程式碼,可以幫助我們提升自己的程式設計水平。

編寫高品質程式碼的專家建議

在軟體開發的過程中,編寫高品質的程式碼不僅能夠提升程式的穩定性和可維護性,還能減少除錯的時間和成本。以下是一些提升程式碼品質的實用建議。

垂直對齊的重要性

  1. 使用垂直對齊來強調變數之間的關係,使錯誤更容易被察覺,並且使查詢更加容易。這需要使用等寬字型,如 Courier
M = [
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, -1, 1, 0, 0, 0],
    [0, 0, 0, 1, -1, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
]

程式碼解析:

  • 上述範例展示瞭如何使用垂直對齊來呈現二維陣列 M。透過保持每個元素垂直對齊,可以更容易地觀察和理解資料的結構。
  • 這種對齊方式特別適用於需要檢查或比較矩陣元素的情況。

字型與排版

在程式設計中,字型(font)和字型(typeface)經常被混用,但它們具有不同的含義。字型與字型的屬性相關,例如 Calibri ItalicCalibri BoldCalibri Mono-spaced,都是 Calibri 字型的不同字型。

程式碼範例:使用不同字型呈現文字

for c in range(1, length-1):
    ch = chr(32)  # 空白字元
    if L[c] == 1: 
        ch = chr(9607)  # ■ 符號
    if maxx == max1:
        canvas.create_text(c*12 + 640-12*r, (r-1)*10,
                           text=ch,
                           fill='red', font=('Helvetica', 8, 'bold'))

程式碼解析:

  • 這段程式碼根據 L[c] 的值決定要顯示的字元,並將其呈現在畫布上。
  • 使用不同的字型大小和顏色來強調特定的文字或符號,使視覺呈現更具層次感。

編寫強壯的程式碼

  1. 編寫強壯的程式碼,避免程式碼過於脆弱。強壯的程式碼應該能夠自我修復、允許使用者協助還原,或在必要時優雅地當機。

全域變數的使用

  1. 避免使用全域變數,因為大範圍的變數難以追蹤,特別是在偵測意外的變更時。不過,全域常數是可以接受的,因為它們的值不會被改變。

例外情況:

  • 當全域變數不是程式碼的一部分時,例如用於計算遞迴函式的回溯次數。
  • 用於記錄程式啟動時間,以便在不同函式中列印增量時間步長。

SESE 原則

  1. SESE(單一進入點和單一離開點)原則曾經被廣泛討論。雖然單一進入點有其合理性,但單一離開點可能過於嚴格。在某些情況下,直接傳回或中斷迴圈會更方便。

傳回多種資料型別的函式

  1. 避免編寫傳回不同資料型別的函式,因為這會增加使用的複雜度。例如,一個函式可能根據情況傳回字串或數字,這會導致呼叫時需要額外的判斷邏輯。

範例:二次方程求解

def quad(a, b, c):
    from math import sqrt
    disc = b*b - 4*a*c
    if disc < 0:
        return 'There are no real roots.'
    x1 = (-b + sqrt(disc)) / (2*a)
    x2 = (-b - sqrt(disc)) / (2*a)
    if disc == 0:
        return x1
    return x1, x2

程式碼解析:

  • 該函式根據判別式 disc 的值傳回不同型別的結果,可能是一個字串、一個數字或一個元組。
  • 這種實作方式需要呼叫者在處理傳回值時進行額外的型別檢查。

改進版二次方程求解函式

def quad(a, b, c):
    # 調整係數以防止溢位
    m = max(abs(a), abs(b), abs(c))
    if m != 0:
        a1 = a/m
        b1 = b/m
        c1 = c/m
    
    # 特例處理
    if a == 0 and b == 0 and c == 0:
        return 'All real numbers are roots.'
    if a == 0 and b != 0:
        x1 = -c/b
        if x1 == int(x1): 
            x1 = int(x1)
        return x1
    
    # 使用合理的演算法計算根
    from math import sqrt
    disc = b*b - 4*a*c
    if disc < 0:
        return 'There are no real roots.'
    
    # 對某些情況進行特殊處理,例如當 b*b 比 4*a*c 大得多時
    # ...

程式碼解析:

  • 改進後的 quad 函式處理了更多邊界情況,例如當 abc 為零時的特例。
  • 對係數進行縮放以避免運算中的溢位問題,並在必要時對結果進行合理的處理。

程式碼最佳化與可讀性:專家建議

在軟體開發過程中,編寫高效且可讀的程式碼是至關重要的。本文將探討如何最佳化程式碼以提高其效能和可維護性,同時參考專家建議來改善我們的程式設計實踐。

1. 瞭解運算子優先順序

在編寫程式碼時,瞭解運算子的優先順序至關重要。錯誤的運算子優先順序可能導致意想不到的結果。例如:

a = 6
b = 4
print(a + b/4)  # 輸出:7.0
print(a + b >> 2)  # 輸出:3
print(a + (b >> 2))  # 輸出:7

內容解密:

  • 在第一個 print 陳述式中,b/4 的結果是 1.0,然後加到 a 上,得到 7.0
  • 在第二個 print 陳述式中,b >> 2 的結果是 1,但由於運算子優先順序,a + b 並未被正確評估,因此結果是 3
  • 在第三個 print 陳述式中,加上括號後,b >> 2 正確地被評估為 1,然後加到 a 上,得到正確結果 7

2. 通用程式碼與特定程式碼的權衡

編寫通用程式碼可以提高程式碼的復用性,但也可能增加複雜度和開發時間。以下是一個例子:

def dataInput():
    s = '輸入一個整數:'
    posLimit = float('inf')
    negLimit = -float('inf')
    while True:
        try:
            data = input(s)
            num = int(data)  
            if not (negLimit < num < posLimit): raise Error
        except:
            s = '"' + str(data) + '" 不是一個整數!請再試一次。\n輸入一個整數:'
        else:
            print('輸入 = ', num)
            return num

內容解密:

  • 這個函式使用 try/except 區塊來捕捉非整數輸入,並提示使用者重新輸入。
  • 它檢查輸入是否在指定的範圍內,如果超出範圍則引發錯誤。

3. 可擴充套件性與複雜度的平衡

在設計程式碼時,需要考慮其可擴充套件性和複雜度。以下是一個 Sudoku 解題器的例子:

# 建構 9x9 網格的區塊列表
block = [[],[],[], [],[],[], [],[],[]]
block[0] = [matrix[0][0].value, matrix[0][1].value, matrix[0][2].value,
           matrix[1][0].value, matrix[1][1].value, matrix[1][2].value,
           matrix[2][0].value, matrix[2][1].value, matrix[2][2].value]
# ...

內容解密:

  • 這段程式碼手動建構了一個 9x9 網格的區塊列表,用於 Sudoku 解題。
  • 將其改寫為 n×n 網格的通用版本可能會更簡潔,但也可能更難除錯。

程式設計最佳實踐:避免重複程式碼與魔術數字

在軟體開發過程中,遵循特定的程式設計最佳實踐至關重要。本文將探討兩個重要的原則:避免使用魔術數字(Magic Numbers)以及不重複程式碼(DRY: Don’t Repeat Yourself)。這些原則有助於提高程式碼的可維護性、可讀性和可擴充套件性。

避免魔術數字

魔術數字是指在程式碼中直接使用的數字常數,它們的含義不明確。例如,在一個程式中,如果多次使用數字10作為陣列的長度,當需要將這個長度改為100時,就需要在整個程式中查詢並替換所有相關的10。這不僅麻煩,而且容易出錯。

# 不好的做法
array_length = 10
my_array = [0] * array_length

# 好的做法
MAX = 10
my_array = [0] * MAX

內容解密:

  1. 定義常數:將魔術數字定義為常數,如MAX,這樣當需要更改時,只需在一個地方修改。
  2. 提高可讀性:使用有意義的常數名稱可以提高程式碼的可讀性,使其他開發者更容易理解程式碼的意圖。
  3. 例外情況:在某些情況下,如使用數學公式或眾所周知的常數(如π),直接使用數字或常見的數學表示可能是可以接受的。

不重複程式碼(DRY原則)

DRY原則強調避免在程式碼中重複相同的邏輯或模式。這有助於減少維護成本,因為當需要更改某個邏輯時,只需在一個地方進行修改。

示例:檢查井字遊戲(Tic-Tac-Toe)的勝負

# 第一版:重複程式碼
def result(board):
    score = 'XXX'
    B = board
    if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
       B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
       B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
       B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
        return 'win'
    score = 'OOO'
    if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
       B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
       B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
       B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
        return 'win'
    return 'unk'

# 第二版:遵循DRY原則
def result(board):
    B = board
    for score in ('XXX', 'OOO'):
        if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
           B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
           B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
           B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
            return 'win'
    return 'unk'

內容解密:

  1. 減少重複:第二版透過迴圈檢查兩種不同的分數(‘XXX’和’OOO’),避免了重複的條件檢查。
  2. 提高可維護性:如果需要更改勝利條件的檢查邏輯,只需在一個地方進行修改。
  3. 簡化邏輯:透過抽象出共同的邏輯,使程式碼更加簡潔和易於理解。

撰寫程式碼的智慧:避免常見的錯誤與陷阱

在軟體開發的過程中,程式設計師們常常面臨著各種挑戰與抉擇。從效能最佳化到程式碼的可讀性,每一個決策都可能對專案的成功產生深遠的影響。本篇文章將探討一些在程式設計中常見的錯誤與陷阱,並提供專家的建議來避免這些問題。

1. 避免過早最佳化

最佳化程式碼是許多開發者的直覺反應,但過早的最佳化卻可能導致程式碼變得複雜難懂。Ken Thompson,一位UNIX的創始人,曾經說過:“百分之九十九的時間裡,簡單和暴力的方法就足夠了。” Barbara Liskov,2008年圖靈獎得主,也指出:“我認為效能被過度高估了,因為你需要的效能是足夠好的效能,而不是最好的效能。”

程式碼範例:最佳化前的思考

import random
import math

def hill_climbing():
    theta = 0
    for _ in range(1000):
        if random.random() < 0.8:
            theta += 0.3
        else:
            theta -= 0.1
    return theta

print(hill_climbing())

內容解密:

  • 這段程式碼實作了一個簡單的爬山演算法,用於模擬某種隨機過程。
  • random.random() 生成一個0到1之間的隨機浮點數,用於決定 theta 的更新方向。
  • 程式碼簡單直接,易於理解和維護。

2. 簡潔明瞭的程式碼優於聰明的技巧

“聰明的”程式碼往往會變成錯誤的溫床。簡單直接的程式碼更容易被理解和除錯。專家們一致認為,應當避免使用那些看似聰明但實際上難以理解的程式碼技巧。

程式碼範例:簡單 vs. 聰明

# 簡單的程式碼
if x > y:
    z = z + 3
if x <= y:
    z = z - 5

# 聰明的技巧(應避免)
z = z + 3*(x > y) - 5*(x <= y)

內容解密:

  • 第一個例子使用簡單的條件判斷來更新 z 的值,易於理解。
  • 第二個例子使用布林值轉換為整數的技巧來簡化表示式,但降低了可讀性。

3. 模擬Switch陳述式的不同方法

在Python中沒有內建的switch陳述式,但可以透過多種方法來模擬它。下面展示了幾種常見的方法,包括使用字典和列表。

程式碼範例:模擬Switch陳述式

def fn0(): print(0)
def fn1(): print(1)
def fn2(): print(2)
def fn3(): print(3)

# 使用if-elif-else
x = 1
if x == 0: fn0()
elif x == 1: fn1()
elif x == 2: fn2()
else: fn3()

# 使用列表
doIt = [fn0, fn1, fn2]
doIt[1]()  # 輸出:1

# 使用字典
dict = {0: fn0, 1: fn1, 2: fn2, 3: fn3}
dict[1]()  # 輸出:1
dict.get(2, fn3)()  # 輸出:2,使用預設值

內容解密:

  • if-elif-else 結構是實作條件分支的最直接方法。
  • 使用列表可以根據索引呼叫不同的函式,但不支援預設值。
  • 使用字典可以實作類別似switch的功能,並支援透過get方法設定預設值。