標籤: 編碼

  • 12 個我在工作中學到的生產級 Python 代碼風格

    在工作中,我們編寫的程式碼盡可能地易於人類閱讀。這意味著以下(非詳盡的)列表:

    • 變數名稱具有意義且較長(而非使用 a、b 和 c)
    • 函數名稱具有意義且較長
    • 大量的評論和文件解釋代碼
    • 類型提示無處不在
    • 字串似乎更長且更冗長
    • 等等等等

    這意味著代碼風格將不可避免地演變以適應上述情況。

    因此,以下是我在過去幾年的工作中學到的一些生產級 Python 代碼風格。

    1) 使用括號進行元組解包

    這是一些普通的元組解包:

    在生產級別的程式碼中,我們通常不會使用像 a 或 b 這樣的變數名稱——相反,我們的變數名稱會更長且更具描述性。

    因此,我們可能會使用括號來幫助元組解包,像這樣:

    注意到這樣一來,我們的元組解包可以容納更長(且更具描述性)的變數名稱

    一個更現實的例子:

    2) 多行列表推導式

    以下是普通列表推導式的樣子:

    在生產代碼中,我們通常不使用變量名 i——我們的變量通常更長且更具描述性,以至於我們無法將整個列表推導式塞進一行代碼中。

    我會如何重寫上述代碼:

    一個更現實的例子:

    3) 使用括號組合字串

    生產級的字串通常由於其冗長而無法放入一行中。因此,我們使用括號將它們組合起來。

    注意 — 在括號內,使用引號的字串字面值會自動合併,我們不需要使用 + 運算子來實現這一點

    4) 使用括號輔助的多行方法鏈接

      正常的方法鏈接:

    在生產級別的程式碼中,方法名稱大多數時候會更長,並且我們會將更多的方法鏈接在一起。

    再次,我們使用括號將所有這些內容放入多行中,而不是縮短任何方法名稱或變數名稱。

    請注意,如果我們在括號內進行方法鏈接,則不需要使用 \ 字符來進行明確的換行

    5) 索引嵌套字典

    正常索引嵌套字典的方式:

    這裡有一些問題:

    • 生產級代碼中的字典有更多層次的嵌套
    • 字典鍵的名稱變得更長了
    • 我們通常無法將整個嵌套索引代碼壓縮成一行。

    因此,我們將其拆分為多行,如下所示:

    如果這還不夠,我們將索引代碼拆分為更多行:

    或者如果我們仍然覺得這難以閱讀,我們可以這樣做:

    6) 撰寫易讀且資訊豐富的函數

    我們學生時代如何撰寫函式:

    包含此類代碼的 PR 很可能會被拒絕

    • 函數名稱不具有描述性
    • 參數變數名稱不佳
    • 沒有類型提示,所以我們第一眼無法知道每個參數應該是什麼數據類型
    • 沒有類型提示,所以我們也不知道我們的函數應該返回什麼
    • 沒有文件字串,所以我們必須推斷我們函數的功能

    以下是我們在生產級 Python 代碼中編寫函數的方式

    • 函數名稱應具有描述性
    • 參數名稱應該更具描述性,而不是例如 a, b, c
    • 每個參數都應該有類型提示
    • 函數的返回類型也應包括在內
    • 一個詳細描述函數功能、接收參數及其輸出的文檔字符串應作為三引號內的字符串包含。

    7) 盡可能減少縮排層級

    這是一個 for 迴圈。如果我們的條件被滿足,我們就做某事。

    一些同事和高級工程師可能會對這段代碼吹毛求疵 — 它可以通過減少縮進層次來寫得更好。

    讓我們重寫這個,同時減少 do_something() 的縮進層級

    注意到,僅僅通過使用 `if not condition` 而不是 `if condition`,`do_something()` 的縮進級別就減少了 1 級。

    在生產級別的程式碼中,可能會有更多的縮排層次,而如果層次太多,我們的程式碼就會變得令人煩躁且難以閱讀。因此,這個技巧讓我們能夠使程式碼稍微整潔一些,更易於人類閱讀,

    8) 帶有括號的布林條件

    這是一個使用 and 關鍵字連接三個條件的 if 語句。

    在生產級別的代碼中,條件變得更長,且可能會有更多的條件。因此,我們可以通過將這個龐大的條件重構為一個函數來解決這個問題。

    或者如果我們判斷沒有必要僅僅為此編寫一個新函數,我們可以使用括號來編寫我們的條件語句。

    這樣一來,我們就不必為了這一個條件語句而被迫編寫新的函數或變量,同時我們還能保持代碼的整潔和可讀性。

    有時候我可能真的更喜歡這樣寫,不過這只是基於個人偏好:

    9) 防止 None 值

    正常代碼,訪問對象的某些嵌套屬性。

    這段代碼存在一些可能導致我們的 PR 被拒絕的問題:

    • 如果 dog 是 None,我們會得到一個錯誤
    • 如果 dog.owner 是 None,我們也會得到一個錯誤
    • 基本上,這段程式碼並未防止 `dog` 或 `dog.owner` 可能為 `None` 的情況。

    在生產級代碼中,我們需要積極防範此類情況。以下是我如何重寫這段代碼的方式。

    Python 中的 and 和 or 運算符是短路運算符,這意味著一旦它們得到明確的答案,就會停止評估整個表達式。

    • 如果 dog 是 None,我們的表達式在 “if dog” 處終止
    • 如果 dog 不是 None,但 dog.owner 是 None,我們的表達式會在 “if dog and dog.owner” 處終止
    • 如果我們沒有任何 None 值,dog.owner.name 會成功被存取,並用於與字串 “bob” 進行比較

    這樣一來,我們就能額外防範狗或狗主人可能為 None 值的情況。

    10) 防止遍歷 None 值

    這是我們如何遍歷某些可迭代對象(例如列表、字典、元組等)的方式

    這個問題的一個缺點是它無法防止 mylist 為 None——如果 mylist 恰好是 None,我們會得到一個錯誤,因為無法遍歷 None。

    以下是我如何改進這段程式碼的方式:

    表達式“mylist or None”

    • 如果 mylist 為真值(例如非空可迭代對象),則返回 mylist
    • 如果 mylist 為假值(例如 None 或空的可迭代對象),則返回[]

    因此,如果 mylist 為 None,表達式“mylist or None”會返回[],這樣我們就不會遇到不想要的異常。

    11) 內部函數以 _ 開頭

    這是一個範例類別。在這裡,run 方法使用了其他方法 clean 和 transform

    在生產級代碼中,我們力求盡可能明確,因此嘗試區分內部方法和外部方法。

    • 外部方法——供其他類別和物件使用的方法
    • 內部方法 — 供類別本身使用的方法

    按照慣例,內部方法最好以下劃線 _ 開頭

    如果我們要重寫上述代碼,我們會得到:

    注意 — 在方法名稱前加上底線並不會隱藏它使其對其他類別和物件不可見。事實上,在功能上沒有任何區別。

    12) 用於常見功能的裝飾器

    這是一個包含 3 個函式的類別,每個函式執行不同的操作。然而,請注意,不同函式之間存在相似的步驟——try-except 區塊以及日誌功能。

    減少重複程式碼的一個好方法是撰寫一個包含共同功能的裝飾器函數。

    這樣一來,如果我們想要更新共用程式碼(try-except 和 logging 程式碼),我們不再需要在三個地方進行更新——我們只需要更新包含共用功能的裝飾器程式碼。

  • 10 件你可能不知道會破壞 Python 腳本的事情

    Python 以其簡單性和可靠性而聞名,但即使是最優秀的開發者也會遇到神秘的錯誤和意外的錯誤。有些陷阱是顯而易見的,但其他的則潛伏在陰影中,等待破壞你的腳本。如果你曾經對代碼中的某個問題感到困惑,那麼這些隱藏的 Python 殺手之一很可能是罪魁禍首。

    這裡有 10 件你可能不知道會破壞你的 Python 腳本的事情——以及如何避免它們。

    1. 可變的預設參數

    問題:在函數定義中使用可變對象(如列表或字典)作為默認參數可能會導致意外行為。

    # Bad practice
    def add_item(item, items=[]):
        items.append(item)
        return items
    
    
    print(add_item(1))  # [1]
    print(add_item(2))  # [1, 2] – Wait, what?!

    修正方法:始終使用不可變的默認值,如 None,並在函數內部初始化。

    def add_item(item, items=None):
        if items is None:
            items = []
        items.append(item)
        return items

    2. 混合環境中的不正確縮排

    問題: 混合使用制表符和空格可能會導致難以發現的錯誤,特別是在協作項目時。

    # Mixing spaces and tabs
    if True:
    	print("This will fail in some environments!")

    修正方法: 使用一致的縮排風格 — 最好是空格。配置您的編輯器自動將制表符轉換為空格。

    3. 在迭代時修改列表

    問題: 在迴圈中更改列表可能會導致跳過元素和不可預測的行為。

    numbers = [1, 2, 3, 4, 5]
    for num in numbers:
        if num % 2 == 0:
            numbers.remove(num)  # Oops! Skips elements

    修正方法:遍歷列表的副本或使用列表推導式。

    numbers = [num for num in numbers if num % 2 != 0]

    4. 使用 is 而不是 == 進行比較

    問題: is 運算符檢查對象的身份,而不是相等性。

    x = 256
    y = 256
    print(x is y)  # True (because of interning)
    
    
    x = 300
    y = 300
    print(x is y)  # False!

    修正方法: 使用 == 進行值比較,並且僅在必要時使用 is 進行身份檢查。

    5. 預設字典鍵的意外行為

    問題: 在常規字典中訪問缺失的鍵會引發 KeyError,而 defaultdict 會自動創建缺失的鍵,有時會導致微妙的錯誤。

    from collections import defaultdict
    
    d = defaultdict(list)
    d["missing"].append(42)  # Key is created automatically!

    修正方法: 當您需要安全查找時,使用 dict.get()

    value = my_dict.get("missing", [])

    6. 迴圈變數洩漏到全域範圍

    問題:在 Python 2 中,循環變量會洩漏到循環外,導致意外的修改。

    for i in range(5):
        pass
    print(i)  # i is still accessible

    修正方法: 使用函數範圍來封裝變量,或使用 Python 3,這已經修復。

    7. 浮點數精度錯誤

    問題: 浮點運算可能導致微小的不準確性。

    print(0.1 + 0.2)  # 0.30000000000000004

    修正方法: 當精確度至關重要時,使用 decimal 模組。

    from decimal import Decimal
    print(Decimal("0.1") + Decimal("0.2"))  # 0.3

    8. 多執行緒中的競爭條件

    問題:同時訪問共享資源的線程可能會導致不可預測的行為。

    import threading
    
    def increment(counter):
        for _ in range(1000):
            counter["count"] += 1
    
    counter = {"count": 0}
    threads = [threading.Thread(target=increment, args=(counter,)) for _ in range(10)]
    
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    
    print(counter["count"])  # Not 10000!

    修正方法: 使用線程安全的結構,如 threading.Lock

    9. 循環導入

    問題:兩個模組互相導入可能會導致ImportError

    # module_a.py
    import module_b
    
    # module_b.py
    import module_a  # Circular import error!

    解決方案: 通過使用本地導入或重構代碼來打破依賴關係。

    10. 默默忽略例外

    問題: 捕捉所有異常而不正確處理會使調試變得困難。

    try:
        risky_function()
    except Exception:
        pass  # Silent failure!

    修正方法: 記錄例外並在可能的情況下處理特定的例外。

    import logging
    
    try:
        risky_function()
    except ValueError as e:
        logging.error(f"Value error: {e}")

    結論

    Python 是一種強大的語言,但即使是經驗豐富的開發人員也可能會陷入這些陷阱。通過了解這些隱藏的陷阱,您可以編寫更可靠、可維護且無錯誤的 Python 代碼。