編寫高質量程式碼改善Python程式的很多建議
基礎語法
有節制地使用 from...import 語句
Python 提供三種方式來引入外部模組:import語句、from...import語句以及__import__函式,其中__import__函式顯式地將模組的名稱作為字串傳遞並賦值給名稱空間的變數。
使用import需要注意以下幾點:
-
優先使用import a的形式
-
有節制地使用from a import A
-
儘量避免使用from a import *
為什麼呢?我們來看看 Python 的 import 機制,Python 在初始化執行環境的時候會預先載入一批內建模組到記憶體中,同時將相關資訊存放在sys.modules中,我們可以通過sys.modules.items()檢視預載入的模組資訊,當載入一個模組時,直譯器實際上完成了如下動作:
-
在sys.modules中搜索該模組是否存在,如果存在就匯入到當前區域性名稱空間,如果不存在就為其建立一個字典物件,插入到sys.modules中
-
載入前確認是否需要對模組對應的檔案進行編譯,如果需要則先進行編譯
-
執行動態載入,在當前名稱空間中執行編譯後的位元組碼,並將其中所有的物件放入模組對應的字典中
從上可以看出,對於使用者自定義的模組,import 機制會建立一個新的 module 將其加入當前的區域性名稱空間中,同時在 sys.modules 也加入該模組的資訊,但本質上是在引用同一個物件,通過test.py所在的目錄會多一個位元組碼檔案。
優先使用 absolute import 來匯入模組
i+=1 不等於 ++i
首先++i或--i在 Python 語法上是合法,但並不是我們通常理解的自增或自減操作:
原來+或-只表示正負數符號。
使用 with 自動關閉資源
對於開啟的資源我們記得關閉它,如檔案、資料庫連線等,Python 提供了一種簡單優雅的解決方案:with。
先來看with實現的原理吧。
with的實現得益於一個稱為上下文管理器(context manager)的東西,它定義程式執行時需要建立的上下文,處理程式的進入和退出,實現了上下文管理協議,即物件中定義了__enter__()和__exit__(),任何實現了上下文協議的物件都可以稱為一個上下文管理器:
-
__enter__():返回執行時上下文相關的物件
-
__exit__(exception_type, exception_value, traceback):退出執行時的上下文,處理異常、清理現場等
with 表示式 [as 目標]:程式碼塊
包含with語句的程式碼塊執行過程如下:
-
計算表示式的值,返回一個上下文管理器物件
-
載入上下文管理器物件的__exit__()以備後用
-
呼叫上下文管理器物件的__enter__()
-
將__enter__()的返回值賦給目標物件
-
執行程式碼塊,正常結束呼叫__exit__(),其返回值直接忽略,如果發生異常,會呼叫__exit__()並將異常型別、值及 traceback 作為引數傳遞給__exit__(),__exit__()返回值為 false 異常將會重新丟擲,返回值為 true 異常將被掛起,程式繼續執行
於此,我們可以自定義一個上下文管理器:
Python 還提供contextlib模組,通過 Generator 實現,其中的 contextmanager 作為裝飾器來提供一種針對函式級別上的上下文管理器,可以直接作用於函式/物件而不必關心__enter__()和__exit__()的實現。
使用 else 子句簡化迴圈(異常處理)
Python 的 else 子句提供了隱含的對迴圈是否由 break 語句引發迴圈結束的判斷,有點繞哈,來看例子:
可以看出,else 子句在迴圈正常結束和迴圈條件不成立時被執行,由 break 語句中斷時不執行,同樣,我們可以利用這顆語法糖作用在 while 和 try...except 中。
遵循異常處理的幾點基本原則
異常處理的幾點原則:
-
注意異常的粒度,不推薦在 try 中放入過多的程式碼
-
謹慎使用單獨的 except 語句處理所有異常,最好能定位具體的異常
-
注意異常捕獲的順序,在適合的層次處理異常,Python 是按內建異常類的繼承結構處理異常的,所以推薦的做法是將繼承結構中子類異常在前丟擲,父類異常在後丟擲
-
使用更為友好的異常資訊,遵守異常引數的規範
避免 finally 中可能發生的陷阱
當 finally 執行完畢時,之前臨時儲存的異常將會再次被丟擲,但如果 finally 語句中產生了新的異常或執行了 return 或 break 語句,那麼臨時儲存的異常將會被丟失,從而異常被遮蔽。
在實際開發中不推薦 finally 中使用 return 語句進行返回。
深入理解 None,正確判斷物件是否為空
型別FalseTrue布林False (與0等價)True (與1等價)字串""( 空字串)非空字串,例如 " ", "blog"數值0, 0.0非0的數值,例如:1, 0.1, -1, 2容器[], (), {}, set()至少有一個元素的容器物件,例如:[0], (None,), ['']NoneNone非None物件
#3執行中會呼叫__nonzero__()來判斷自身物件是否為空並返回0/1或True/False,如果沒有定義該方法,Python 將呼叫__len__()進行判斷,返回 0 表示為空。如果一個類既沒有定義__len__()又沒有定義__nonzero__(),該類例項用 if 判斷為True。
連線字串優先使用 join 而不是 +
這一點之前我在博文裡總結過,+涉及到更多的記憶體操作。
格式化字串時儘量使用 .format 而不是 %
同上。
區別對待可變物件和不可變物件
Python 中一切皆物件,每個物件都有一個唯一的識別符號(id)、型別(type)和值。數字、字串、元組屬於不可變物件,字典、列表、位元組陣列屬於可變物件。
預設引數在初始化時僅僅被評估一次,以後直接使用第一次評估的結果,course 指向的是 list 的地址,每次操作的實際上是 list 所指向的具體列表,所以對於可變物件的更改會直接影響原物件。
最好的方法是傳入None作為預設引數,在建立物件的時候動態生成列表。
[]、() 和 {} 一致的容器初始化形式
其實就是列表生成式、元組生成式和字典生成式。
記住函式傳參既不是傳值也不是傳引用
正確的說法是傳物件(call by object)或傳物件的引用(call-by-object-reference),函式引數在傳遞過程中將整個物件傳入,對可變物件的修改在函式外部以及內部都可見,對不可變物件的”修改“往往是通過生成一個新物件然是賦值實現的。
警惕預設引數潛在的問題
其中就是預設引數如果是可變物件,在呼叫者和被呼叫者之間是共享的。
慎用變長引數
-
原因如下:
-
使用過於靈活,導致函式簽名不夠清晰,存在多種呼叫方式
-
使用*args和**kw簡化函式定義就意味著函式可以有更好的實現方法
-
使用場景:
-
為函式新增一個裝飾器
-
引數數目不確定
-
實現函式的多型或子類需要呼叫父類的某些方法時
深入理解 str() 和repr() 的區別
-
總結幾點:
-
str()面向使用者,返回使用者友好和可讀性強的字串型別;repr()面向 Python 直譯器或開發人員,返回 Python 直譯器內部的含義
-
直譯器中輸入a預設呼叫repr(),而print(a)預設呼叫str()
-
repr()返回值一般可以用eval()還原物件:obj == eval(repr(obj))
-
以上兩個方法分別呼叫內建的__str__()和__repr__(),一般來說類中都應該定義__repr__(),但當可讀性比準確性更為重要時應該考慮__str__(),使用者實現__repr__()方法的時候最好保證其返回值可以用eval()是物件還原
分清 staticmethod 和 classmethod 的適用場景
這兩種方法之前已經總結過了的,下面我們只討論它們的使用場景。
呼叫類方法裝飾器的修飾器的方法,會隱式地傳入該物件所對應的類,可以動態生成對應的類的類變數,同時如果我們期望根據不同的型別返回對應的類的例項,類方法才是正確的解決方案。
反觀靜態方法,當我們所定義的方法既不跟特定的例項相關也不跟特定的類相關,可以將其定義為靜態方法,這樣使我們的程式碼能夠有效地組織起來,提高可維護性。
當然,也可以考慮定義一個模組,將一組的方法放入其中,通過模組來訪問。