提高程式碼可讀性和顏值的幾點建議(初學者必讀)

2020-07-16 10:05:01
學習過程中,我們經常會閱讀他人寫的程式碼,如果注意觀察就會發現,好的程式碼本身就是一份文件,解決同樣的問題,不同的人編寫的程式碼,其可讀性千差萬別。

有些人的設計風格和程式碼風格猶如熱刀切黃油,從頂層到底層的程式碼看下來酣暢淋漓,注釋詳盡又精簡;深入到細節程式碼,無需註釋也能理解清清楚楚。而有些人,程式碼勉勉強強能跑起來,遇到稍微複雜的情況就會出崩潰,且程式碼中處處都是堆積在一起的變數、函數和類,很難理清程式碼的實現思路。

Python 創始人 Guido van Rossum(吉多·范羅蘇姆)說過,程式碼的閱讀頻率遠高於編寫程式碼的頻率。畢竟是在編寫程式碼的時候,我們自己也需要對程式碼進行反複閱讀和偵錯,來確認程式碼能夠按照期望執行。

本節,在讀者學會如何使用 Puython 函數的基礎上,教大家怎麼才能合理分解程式碼,提高程式碼的可讀性。

首先,大家在程式設計過程中,一定要圍繞一個中心思想:不寫重複性的程式碼。因為,重複程式碼往往是可以通過使用條件、迴圈、建構函式和類(後續章節會做詳細介紹)來解決的。

例如,仔細觀察下面的程式碼:
if i_am_rich:
    money = 100
    send(money)
else:
    money = 10
    send(money)
這段程式碼中,同樣的 send 語句出現了兩次,其實它完全可以進行合併,把程式碼改造成下面這樣:
if i_am_rich:
    money = 100
else:
    money = 10
send(money)

與此同時,還要學會刻意地減少程式碼的疊代層數,盡可能讓 Python 程式碼扁平化。例如:
def send(money):
    if is_server_dead:
        LOG('server dead')
        return
    else:
        if is_server_timed_out:
            LOG('server timed out')
            return
        else:
            result = get_result_from_server()
            if result == MONEY_IS_NOT_ENOUGH:
                LOG('you do not have enough money')
                return
            else:
                if result == TRANSACTION_SUCCEED:
                    LOG('OK')
                    return
                else:
                    LOG('something wrong')
                    return
上面這段程式碼層層縮排,如果我們沒有比較強的邏輯分析能力,理清這段程式碼是比較困難。其實,這段程式碼完全可以改成如下這樣:
def send(money):
    if is_server_dead:
        LOG('server dead')
        return

    if is_server_timed_out:
        LOG('server timed out')
        return

    result = get_result_from_server()

    if result == MONET_IS_NOT_ENOUGH:
        LOG('you do not have enough money')
        return

    if result == TRANSACTION_SUCCEED:
        LOG('OK')
        return

    LOG('something wrong')
可以看到,所有的判斷語句都位於同一層級,同之前的程式碼格式相比,程式碼層次清晰了很多。

另外,在使用函數時,函數的粒度應該盡可能細,不要讓一個函數做太多的事情。往往一個複雜的函數,我們要盡可能地把它拆分成幾個功能簡單的函數,然後合併起來。

如何拆分函數呢?這裡,舉一個二分搜尋的例子。給定一個非遞減整數陣列,和一個 target 值,要求你找到陣列中最小的一個數 x,滿足 x*x > target,如果不存在,則返回 -1。

大家不妨先獨立完成,寫完後再對照著來看下面的程式碼,找出自己的問題:
def solve(arr, target):
    l, r = 0, len(arr) - 1
    ret = -1
    while l <= r:
        m = (l + r) // 2
        if arr[m] * arr[m] > target:
            ret = m
            r = m - 1
        else:
            l = m + 1
    if ret == -1:
        return -1
    else:
        return arr[ret]

print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))

對於上面這樣的寫法,應付演算法比賽和面試已經綽綽有餘。但如果從工程的角度考慮,還需要進行深度優化:
def comp(x, target):
    return x * x > target

def binary_search(arr, target):
    l, r = 0, len(arr) - 1
    ret = -1
    while l <= r:
        m = (l + r) // 2
        if comp(arr[m], target):
            ret = m
            r = m - 1
        else:
            l = m + 1
    return ret

def solve(arr, target):
    id = binary_search(arr, target)
    if id != -1:
        return arr[id]
    return -1

print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))
在這段程式碼中,我們把不同功能的程式碼單獨提取出來作為獨立的函數。其中,comp() 函數作為核心判斷,提取出來之後,可以讓整個程式更清晰;同時,還把二分搜尋的主程式提取了出來,只負責二分搜尋;最後的 solve() 函數拿到結果,決定返回不存在,還是返回值。這樣一來,每個函數各司其職,閱讀性也能得到一定提高。