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

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

關於導入模塊

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 的語法層面上是合法的,但並非一般意義上的自增操做。this

使用 with 自動關閉資源

對文件操做完成後,應該當即關閉它們,由於打開的文件不只會佔用系統資源,並且可能影響其餘程序或者進程的操做,甚至會致使用戶指望與實際操做結果不一致。debug

Python 提供了 with 語句,語法爲:code

with 表達式 [as 目標]:
    代碼塊

with 語句支持嵌套,支持多個 with 子句,它們二者能夠相互轉換。with expr1 as e1, expr2 as e2 與下面的嵌套形式等價:orm

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與算法之路】

相關文章
相關標籤/搜索