編寫高質量Python程式(三)基礎語法
本系列文章為《編寫高質量程式碼——改善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與演算法之路】