1. 程式人生 > >編寫高質量Python程式(三)基礎語法

編寫高質量Python程式(三)基礎語法

本系列文章為《編寫高質量程式碼——改善Python程式的91個建議》的精華彙總。

關於匯入模組

Python的3種引入外部模組的方式:import語句、from ... import ...__import__函式。其中前兩種比較常見。

在使用 import 時,應注意:

  • 優先使用 import Aimport 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中常用的異常處理語法是tryexceptelsefinally,它們可以有多種組合。語法形式如下:

# 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
  • 任何形式的數值型別零,如 00L0.00j
  • 空的序列,如 ''()[]
  • 空的字典,如 {}
  • 當用戶定義的類中定義了 __nonzero__()__len__() 方法,並且該方法返回整數 0False 的時候。
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與演算法之路】