本系列文章為《編寫高質量代碼——改善Python程式的91個建議》的精華彙總。 首發於公眾號【Python與演算法之路】 關於導入模塊 Python的3種引入外部模塊的方式: 語句、 和 函數。其中前兩種比較常見。 在使用 時,應註意: 優先使用 或 有節制的使用 儘量避免使用 對於 ,如果無節制的使 ...
本系列文章為《編寫高質量代碼——改善Python程式的91個建議》的精華彙總。
關於導入模塊
Python的3種引入外部模塊的方式:import
語句、from ... import ...
和 __import__
函數。其中前兩種比較常見。
在使用 import
時,應註意:
- 優先使用
import A
或import A as a
- 有節制的使用
from A import B
- 儘量避免使用
from A import *
對於 from a import ...
,如果無節制的使用,會帶來的問題:
- 命名空間的衝突
- 迴圈嵌套導入的問題(兩個文件相互導入對方的變數或函數或類)
i += 1
不等於 ++i
Python 解釋器會將 ++i
解釋為 +(+i)
,其中 +
表示正數符號。對於 --i
也是類似。
因此,要明白 ++i
在 Python 的語法層面上是合法的,但並不是通常意義上的自增操作。
使用 with
自動關閉資源
對文件操作完成後,應該立即關閉它們,因為打開的文件不僅會占用系統資源,而且可能影響其他程式或者進程的操作,甚至會導致用戶期望與實際操作結果不一致。
Python 提供了 with 語句,語法為:
with 表達式 [as 目標]:
代碼塊
with 語句支持嵌套,支持多個 with 子句,它們兩者可以相互轉換。with expr1 as e1, expr2 as e2
與下麵的嵌套形式等價:
with expr1 as e1:
with expr2 as e2:
使用 else
子句簡化迴圈(異常處理)
在迴圈中, else
子句提供了隱含的對迴圈是否由 break
語句引發迴圈結束的判斷。例子:
# 以下兩段代碼等價
# 藉助了一個標誌量 found 來判斷迴圈結束是不是由 break 語句引起的。
def print_prime(n):
for i in range(2, n):
found = True
for j in range(2, i):
if i % j == 0:
found = False
break
if found:
print("{} is a prime number".format(i))
def print_prime2(n):
for i in range(2, n):
for j in range(2, i):
if i % j == 0:
break
else:
print("{} is a prime number".format(i))
當迴圈“自然”終結(迴圈條件為假)時 else
從句會被執行一次,而當迴圈是由 break
語句中斷時,else
子句就不被執行。
與 for
語句相似,while
語句中的 else
子句的語意是一樣的: else
塊在迴圈正常結束和迴圈條件不成立時被執行。
遵循異常處理的幾點基本原則
Python中常用的異常處理語法是try
、except
、else
、finally
,它們可以有多種組合。語法形式如下:
# Run this main action first
try:
<statements>
# 當 try 中發生 name1 的異常時,進行處理
except <name1>:
<statements>
# 當 try 中發生 name2 或 name3 中的某一個異常時
except (name2, name3):
<statements>
# 當 try 中發生 name4 的異常時處理,並獲取對應實例
except <name4> as <data>:
<statements>
# 其他異常時,進行處理
except:
<statements>
# 沒有異常時,執行
else:
<statements>
# 無論有沒有異常,都執行
finally:
<statements>
異常處理,通常需要遵循以下幾點基本原則:
- 不推薦在
try
中放入過多的代碼。在 try 中放入過多的代碼帶來的問題是如果程式中拋出異常,將會較難定位,給 debug 和修複帶來不便,因此應儘量只在可能拋出異常的語句塊前面放入 try 語句。 - 謹慎使用單獨的
except
語句處理所有異常,最好能定位具體的異常。同樣也不推薦使用except Exception
或者except StandardError
來捕獲異常。如果必須使用,最好能夠使用raise
語句將異常拋出向上層傳遞。 - 註意異常捕獲的順序,在合適的層次處理異常。
- 用戶也可以繼承自內建異常構建自己的異常類,從而在內建類的繼承結構上進一步延伸。在這種情況下捕獲異常的順序顯得非常重要。為了更精確地定位錯誤發生的原因,推薦的方法是將繼承結構中子類異常在前面的
except
語句中拋出,而父類異常在後面的except
語句拋出。這樣做的原因是當try
塊中有異常發生的時候,解釋器根據except
聲明的順序進行匹配,在第一個匹配的地方便立即處理該異常。 - 異常捕獲的順序非常重要,同時異常應該在適當的位置被處理,一個原則就是如果異常能夠在被捕獲的位置被處理,那麼應該及時處理,不能處理也應該以合適的方式向上層拋出。向上層傳遞的時候需要警惕異常被丟失的情況,可以使用不帶參數的 raise 來傳遞。
- 用戶也可以繼承自內建異常構建自己的異常類,從而在內建類的繼承結構上進一步延伸。在這種情況下捕獲異常的順序顯得非常重要。為了更精確地定位錯誤發生的原因,推薦的方法是將繼承結構中子類異常在前面的
- 使用更為友好的異常信息,遵守異常參數的規範。通常來說有兩類異常閱讀者:使用軟體的人和開發軟體的人。
避免 finally 中可能發生的陷阱
無論 try
語句中是否有異常拋出,finally
語句總會被執行。由於這個特性,finally
語句經常被用來做一些清理工作。
但使用 finally
時,也要特別小心一些陷阱。
- 當
try
塊中發生異常的時候,如果在except
語句中找不到對應的異常處理,異常將會被臨時保存起來,當finally
執行完畢的時候,臨時保存的異常將會再次被拋出,但如果finally
語句中產生了新的異常或者執行了return
或者break
語句,那麼臨時保存的異常將會被丟失,從而導致異常屏蔽。 - 在實際應用程式開發過程中,並不推薦在
finally
中使用return
語句進行返回,這種處理方式不僅會帶來誤解而且可能會引起非常嚴重的錯誤。
深入理解 None,正確判斷對象是否為空
Python 中以下數據會當作空來處理:
- 常量
None
- 常量
False
- 任何形式的數值類型零,如
0
、0L
、0.0
、0j
- 空的序列,如
''
、()
、[]
- 空的字典,如
{}
- 當用戶定義的類中定義了
__nonzero__()
和__len__()
方法,並且該方法返回整數0
或False
的時候。
if list1 # value is not empty
Do something
else: # value is empty
Do some other thing
- 執行過程中會調用內部方法
__nonzero__()
來判斷變數list1
是否為空並返回其結果。
註:
__nonzero__()
方法 —— 該內部方法用於對自身對象進行空值測試,返回 0/1 或 True/False。
- 如果一個對象沒有定義該方法,Python 將獲取
__len__()
方法調用的結果來進行判斷。__len__()
返回值為 0 則表示為空。如果一個類中既沒有定義__len__()
方法也沒有定義__nonzero__()
方法,該類的實例用 if 判斷的結果都為 True。
格式化字元串時儘量使用 .format
方式而不是 %
推薦儘量使用 format
方式而不是 %
操作符來格式化字元串,理由:
-
format
方式在使用上較%
操作符更為靈活。使用format
方式時,參數的順序與格式化的順序不必完全相同 -
format
方式可以方便的作為參數傳遞weather = [("Monday", "rain"), ("Tuesday", "sunny"), ("Wednesday", "sunny"), ("Thursday", "rain"), ("Friday", "cloudy")] formatter = "Weather of '{0[0]}' is '{0[1]}'".format for item in map(formatter, weather): print(item)
-
%
最終會被 .format 方式所代替。根據 Python 的官方文檔,之所以仍然保留%
操作符是為了保持向後相容 -
%
方法在某些特殊情況下使用時需要特別小心,對於%
直接格式化字元的這種形式,如果字元本身為元組,則需要使用在%
使用(itemname,)
這種形式才能避免錯誤,註意逗號。
區別對待可變對象和不可變對象
Python 中一切皆對象,對象根據其值能否修改分為可變對象和不可變對象。
-
不可變對象
- 數字
- 字元串
- 元組
-
可變對象
- 字典
- 列表
- 位元組數組
在將可變對象作為函數預設參數的時候要特別緊惕,對可變對象的更改會直接影響原對象。
最好的方法是傳入 None
作為預設參數,在創建對象的時候動態生成可變對象。
-
對於一個可變對象,切片操作相當於淺拷貝。
-
對於不可變對象,當我們對其進行相關操作的時候,Python 實際上仍然保持原來的值而且重新創建一個新的對象,所以字元串對象不允許以索引的方式進行賦值,當有兩個對象同時指向一個字元串對象的時候,對其中一個對象的操作並不會影響另一個對象。
函數傳參既不是傳值也不是傳引用
對於Python中函數的傳參方法,既不是傳值,也不是傳引用。
正確的叫法應該是傳對象(call by object)或者說傳對象的引用(call-by-object-reference)。
函數參數在傳遞的過程中將整個對象傳入,
- 對於可變對象:它的修改在函數外部以及內部都可見,調用者和被調用者之間共用這個對象
- 對於不可變對象:由於並不能真正被修改,因此,修改往往是通過生成一個新對象然後賦值來實現的
慎用變長參數
慎用可變長度參數*args, **kwargs
,原因如下:
- 使用過於靈活。變長參數意味著這個函數的簽名不夠清晰,存在多種調用方式。另外變長參數可能會破壞程式的健壯性。
- 如果一個函數的參數列表很長,雖然可以通過使用
*args
和**kwargs
來簡化函數的定義,但通常這個函數可以有更好的實現方式,應該被重構。例如可以直接傳入元組和字典。
可變長參數適合在下列情況下使用:
- 為函數添加一個裝飾器
- 如果參數的數目不確定,可以考慮使用變長參數
- 用來實現函數的多態,或者在繼承情況下子類需要調用父類的某些方法的時候
深入理解 str()
和 repr()
的區別
函數 str()
和 repr()
都可以將 Python 中的對象轉換為字元串,兩者的使用以及輸出都非常相似。有以下幾點區別:
-
兩者的目標不同:
str()
主要面向用戶,其目的是可讀性,返回形式為用戶友好性和可讀性都較強的字元串類型- 而
repr()
面向開發人員,其目的是準確性,其返回值表示 Python 解釋器內部的含義,常用作 debug
-
在解釋器中直接輸入時預設調用
repr()
函數,而print
則調用str()
函數 -
repr()
的返回值一般可以用eval()
函數來還原對象。通常有如下等式:obj == eval(repr(obj))
-
一般,類中都應該定義
__repr__()
方法,而__str__()
方法則為可選,當可讀性比準確性更為重要的時候應該考慮定義__str__()
方法。如果類中沒有定義__str__()
方法,則預設會使用__repr__()
方法的結果來返回對象的字元串表示形式。用戶實現__repr__()
方法的時,最好保證其返回值可以用eval()
方法使對象重新還原。
分清靜態方法和類方法的適用場景
靜態方法:
class C(object):
@staticmethod
def f(arg1, arg2, ...):
類方法:
class C(object):
@classmethod
def f(cls, arg1, arg2, ...):
都可以通過類名.方法名
或者實例.方法名
的形式來訪問。
其中,靜態方法沒有常規方法的特殊行為,如綁定、非綁定、隱式參數等規則,而類方法的調用使用類本身作為其隱含參數,但調用本身並不需要顯示提供該參數。
類方法
- 在調用的時候沒有顯式聲明 cls,但實際上類本身是作為隱藏參數傳入的
- 類方法可以判斷出自己是通過基類被調用,還是通過某個子類被調用
- 類方法通過子類調用時,可以返回子類的屬性而非基類的屬性
- 類方法通過子類調用時,可以調用子類的其他類方法
靜態方法
- 既不跟特定的實例相關也不跟特定的類相關
- 靜態方法定義在類中的原因是,能夠更加有效地將代碼組織起來,從而使相關代碼的垂直距離更近,提高代碼的可維護性
文章首發於公眾號【Python與演算法之路】