Effective Python學習筆記

molscar發表於2018-04-08

人生苦短我用 Python

注:最後附電子書地址

一、Pythonic Thinking

第1條: 確認自己所用的Python版本

  • 使用python -version檢視當前Python版本
  • Python的執行時版本:CPython,JyPython,IronPython和PyPy等
  • 優先考慮使用 Python 3

第2條: 遵循PEP 8 風格指南

PEP 8:http://www.python.org/dev/peps/pep-0008/

PEP 8:http://www.python.org/dev/peps/pep-0008/

空白:

  • 不要使用 tab 縮排,使用空格來縮排
  • 使用四個空格縮排,使用四個空格對長表示式換行縮排
  • 每行的字元數不應該超過 79
  • class和funciton之間用兩個空行,class的method之間用一個空行
  • list索引和函式呼叫,關鍵字引數賦值不要在兩旁加空格
  • 變數賦值前後都用一個空格

命名

  • 函式,變數以及屬性應該使用小寫,如果有多個單詞推薦使用下劃線進行連線,如lowercase_underscore
  • 被保護 的屬性應該使用 單個 前導下劃線來宣告。
  • 私有 的屬性應該使用 兩個 前導下劃線來進行宣告。
  • 類以及異常資訊 應該使用單詞 首字母大寫 形式,也就是我們經常使用的駝峰命名法,如CapitalizedWord。
  • 模組級 別的常量應該使用 全部大寫 的形式, 如ALL_CAPS。
  • 類內部的例項方法的應該將self作為其第一個引數。且self也是對當前類物件的引用。
  • 類方法應該使用cls來作為其第一個引數。且self引用自當前類。

表示式和語句( Python之禪: 每件事都應該有直白的做法,而且最好只有一種 )

  • 使用內聯否定(如 if a is not b) 而不是顯示的表示式(如if not a is b)。
  • 不要簡單地通過變數的長度(if len(somelist) == 0)來判斷空值。使用隱式的方式如來假設空值的情況(如if not somelistFalse來進行比較)。
  • 上面的第二條也適用於非空值(如[1],或者'hi')。對這些非空值而言 if somelist預設包含隱式的True
  • 避免將if , for, while, except等包含多個語塊的表示式寫在一行內,應該分割成多行。
  • 總是把import語句寫在Python檔案的頂部。
  • 當引用一個模組的時候使用絕對的模組名稱,而不是與當前模組路徑相關的名稱。例如要想引入bar包下面的foo模組,應該使用from bar import foo而不是import foo
  • 如果非要相對的引用,應該使用明確的語法from . import foo
  • 按照以下規則引入模組:標準庫,第三方庫,你自己的庫。每一個部分內部也應該按照字母順序來引入。

第3條: 瞭解 bytes、str與 unicode 的區別

備忘錄:

  • Python3 兩種字串型別:bytes和str,bytes表示8-bit的二進位制值,str表示unicode字元
  • Python2 兩種字串型別:str和unicode,str表示8-bit的二進位制值,unicode表示unicode字元
  • 從檔案中讀取或者寫入二進位制資料時,總應該使用 'rb' 或 'wb' 等二進位制模式來開啟檔案

Python3中的str例項和Python2中的unicode例項並沒有相關聯的二進位制編碼。所以要想將Unicode字元轉換成二進位制資料,就必須使用encode方法,反過來,要想把二進位制資料轉換成Unicode字元,就必須使用decode方法。

​ 當你開始寫Python程式的時候,在介面的最開始位置宣告對Unicode的編碼解碼的細節很重要。在你的程式碼中,最核心的部分應使用Unicode字元型別(Python3中使用str,Python2中使用unicode)並且不應該考慮關於字元編碼的任何其他方式。本文允許你使用自己喜歡的可替代性的文字編碼方式(如Latin-1,Shift JIS, Big5),但是應該對你的文字輸出編碼嚴格的限定一下(理想的方式是使用UTF-8編碼)。

由於字元型別的不同,導致了Python程式碼中出現了兩種常見的情形的發生。

  • 你想操作UTF-8(或者其他的編碼方式)編碼的8位元值 序列。

  • 你想操作沒有特定編碼的Unicode字元。 所以你通常會需要兩個工具函式來對這兩種情況的字元進行轉換,以此來確保輸入值符合程式碼所預期的字元型別。

  • 二進位制值和unicode字元需要經過encode和decode轉換,Python2的unicode和Python3的str沒有關聯二進位制編碼,通常使用UTF-8

  • Python2轉換函式:

    • to_unicode

      # Python 2
      def to_unicode(unicode_or_str):
          if isinstance(unicode_or_str, str):
              value = unicode_or_str.decode('utf-8')
          else:
              value = unicode_or_str
          return value # Instance of unicode
      複製程式碼
    • to_str

      # Python 2
      def to_str(unicode_or_str):
          if isinstance(unicode_or_str, unicode):
              value = unicode_or_str.encode('utf-8')
          else:
              value = unicode_or_str
          return value # Instance of str
      複製程式碼
  • Python2,如果str只包含7-bit的ascii字元,unicode和str是一樣的型別,所以:

    • 使用+連線:str + unicode
    • 可以對str和unicode進行比較
    • unicode可以使用格式字串,’%s’

    注:在Python2中,如果只處理7位ASCII的情形下,可以等價 str 和 unicode 上面的規則,在Python3中 bytes 和 str 例項絕不等價

  • 使用open返回的檔案操作,在Python3是預設進行UTF-8編碼,但在Pyhton2是二進位制編碼

    # python3
    with open(‘/tmp/random.bin’, ‘w’) as f:
        f.write(os.urandom(10))
    # >>>
    #TypeError: must be str, not bytes
    複製程式碼

    這時我們可以用二進位制方式進行寫入和讀取:

    # python3
    with open('/tmp/random.bin','wb) as f:
        f.write(os.urandom(10))
    複製程式碼

第4條:用輔助函式來取代複雜的表示式

  • 開發者很容易過度使用Python的語法特效,從而寫出那種特別複雜並且難以理解的單行表示式
  • 請把複雜的表示式移入輔助函式中,如果要反覆使用相同的邏輯,那就更應該這麼做
  • 使用 if/else 表示式,要比使用 or 或者 and 這樣的 Booolean 操作符更加清晰

第5條:瞭解切割序列的辦法

  • 分片機制自動處理越界問題,但是最好在表達邊界大小範圍是更加的清晰。(如a[:20] 或者a[-20:]

  • list,str,bytes和實現__getitem__和__setitem__ 這兩個特殊方法的類都支援slice操作

  • 基本形式是:somelist[start:end],不包括end,可以使用負數,-1 表示最後一個,預設正向選取,下標0可以省略,最後一個下標也可以省略

    a = ['a','b','c','d','e','f','g','h']
    print('Middle Two:',a[3:-3])
    >>>
    Middle Two: ['d','e'] 
    複製程式碼
  • slice list是shadow copy,somelist[0:]會複製原list,切割之後對新得到的列表進行修改不會影響原來的列表

    a = ['a','b','c','d','e','f','g','h']
    b = a[4:]
    print("Before:", b)
    b[1] = 99
    print("After:",b)
    print("Original:",a)
    >>>
    Before: ['e','f','g','h']
    After: ['e',99,'g','h']
    Original: ['a','b','c','d','e','f','g','h']
    複製程式碼
  • slice賦值會修改slice list,即使長度不一致(增刪改)

    print("Before:",a)
    a[2:7] = [99,22,14]
    print("After:",a)
    >>>
    Before: ['a','b','c','d','e','f','g','h']
    After: ['a','b',99,22,14,'h']
    複製程式碼
  • 引用-變化-追隨

    當為列表賦值的時候省去開頭和結尾下標的時候,將會用 這個引用 來替換整個列表的內容,而不是建立一個新的列表。同時,引用了這個列表的列表的相關內容,也會跟著發生變化。

    a = ['a','b','c','d','e','f','g','h']
    b = a
    print("Before:",b)
    a[:] = [101,102,103]
    print("After:",b)
    >>>
    Before: ['a','b','c','d','e','f','g','h']
    After: [101,102,103]
    
    # 解決方案:深拷貝
    import copy
    b = copy.copy(a)
    print("Before:",b)
    a[:] = [101,102,103]
    print("After:",b)
    >>>
    Before: ['a','b','c','d','e','f','g','h']
    After: ['a','b','c','d','e','f','g','h']
    複製程式碼

第6條: 避免在單次切片操作內同事指定 start、end和 stride(個人覺得還好)

備忘錄:

  • 在分片中指定startend,stride會讓人感到困惑,難於閱讀。
  • 儘可能的避免在分片中使用負數值。
  • 避免在分片中同時使用startendstride;如果非要使用,考慮兩次賦值(一個分片,一個調幅),或者使用內建模組itertoolsdeislice方法來進行處理。

步幅

Python 有針對步幅的特殊的語法,形如:somelist[start:end:stride]

a = ['red','orange','yellow','green','blue','purple']
odds = a[::2]
print(odds)
>>>
['red','yellow','blue']
複製程式碼

負數步幅

步幅為-1來實現字串的逆序,反向選取

# 當資料僅僅為ASCII碼內資料時工作正常
x = b'mongoose'
y = x[::-1]
print(y)
>>>
b'esoognom'

# 出現Unicode字元的時候就會報錯
w = '謝謝'
x = w.encode(utf-8')
y = a[::-1]
z = y.decode('utf-8')
>>>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9d in position 0: invalid start byte.
        
a = ['a','b','c','d','e','f','g','h']
a[2::2]     # ['c','e','g']
a[-2::-2]    # ['g','e','c','a']
a[-2:2:-2]   # ['g','e'] 尤其注意這裡,類似於座標軸,分片範圍是左閉右開,所以2的位置不可達
a[2:2:-2]    # []
複製程式碼

第7條: 用列表推導來代替 map 和 filter

備忘錄

  • 列表表示式比內建的map,filter更加清晰,因為map,filter需要額外的lambda表示式的支援。
  • 列表表示式允許你很容易的跳過某些輸入值,而一個map沒有filter幫助的話就不能完成這一個功能。
  • 字典和集合也都支援列表表示式。

第一個例子:

a = [1,2,3,4,5,6,7,8,9,10]
squares = [x*x for x in a]
print(squares)
>>>
[1,4,9,16,25,36,49,64,81,100]
複製程式碼

map和filter需要lambda函式,使得程式碼更不可讀

squares = map(lambda x: x **2 ,a)
複製程式碼

第二個例子:

even_squares = [x**2 for x in a if x%2==0]
print(even_squares)
>>>
[4,16,36,64,100]
複製程式碼

map:

alt = map(lambda x: x**2, filter(lambda x: x%2==0,a))
assert even_squares== list(alt)
複製程式碼

字典和集合 有他們自己的一套列表表示式。這使得書寫演算法的時候匯出資料結構更加的簡單。

chile_rank = {'ghost':1,'habanero':2,'cayenne':3}
rank_dict = {rank:name for name,rank in child_rank.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
print(rand_dict)
print(chile_len_set)
>>>
{1: 'ghost',2: 'habanero',3: 'cayenne'}
{8, 5, 7}
複製程式碼

第8條: 在列表表示式中避免使用超過兩個的表示式

備忘錄:

  • 列表表示式支援多層的迴圈和條件語句,以及每層迴圈內部的條件語句。
  • 當列表表示式內部多餘兩個表示式的時候就會變得難於閱讀,這種寫法應該避免使用。

第一個例子:

not:

squared = [[ x**2 for x in row] for row in matrix]
print(squared)
>>>
[[1, 4, 9],[16, 25, 36],[49, 64, 81]]
複製程式碼

prefer:

matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat)
>>>
[ 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

第二個例子:

not:

my_lists = [
    [[1, 2, 3],[4, 5, 6]],
    # ...
]
flat = [ x for sublist in my_lists
          for sublist2 in sublist
          for x in sublist2]

print(flat)
複製程式碼

prefer:

flat = []
for sublist in my_lists:
    for sublist2 in sublist:
        flat.append(sublist2)
複製程式碼

從這點來看,多行的列表表示式並不比原方案少多少程式碼。這裡,作者更加的建議使用正常的迴圈體語句。因為其比列表表示式更簡潔好看一點,也更加易讀,易懂。

第三個例子:

列表表示式同樣支援if條件語句。多個條件語句出現在相同的迴圈水平中也是一個隱式&的表達,即同時成立才成立。例如:你只想獲得列表中大於4且是偶數的值。那麼下面的兩個列表表示式是等價的。

a = [1,2,3,4,5,6,7,8,9,10]
b = [x for x in a if x> 4 if x%2 ==0]
c = [x for x in a if x > 4 and if x%2 ==0]
複製程式碼

條件語句可以被很明確的新增在每一層迴圈的for表示式的後面,起到過濾的作用。例如:你想過濾出每行總和大於10且能被3正處的元素。雖然用列表表示式表示出這段程式碼很短,但是其可讀性確實很糟糕。

matrix = [[ 1, 2, 3],[ 4, 5, 6],[ 7, 8, 9]]
filtered = [[x for x in row if x%3==0]
            for row in matrix if sum(row) >= 10 ]
print(filtered)
>>>
[[6],[9]]
複製程式碼

第9條: 資料量較大的地方考慮使用生成器表示式

備忘錄

  • 當遇到大輸入事件的時候,使用列表表示式可能導致一些問題。
  • 生成器表示式通過迭代的方式來處理每一個列表項,可以防止出現記憶體危機。
  • 當生成器表示式 處於鏈式狀態時,會執行的很迅速。

列表生成式的缺點

列表生成式會給輸入列表中的每一個只建立一個新的只包含一個元素的列表。這對於小的輸入序列可能是很好用的,但是大的輸入序列而言就很有可能導致你的程式崩潰。

生成器表示式的好處

Python提供了一個generator expression(生成器表示式),在程式執行的過程中,生成其表示式不實現整個輸出序列,相反,生成其表示式僅僅是對從表示式中產生一個專案的迭代器進行計算,說白了就是每次僅僅處理一個迭代項,而不是整個序列。

生成器表示式通過使用類似於列表表示式的語法(在()之間而不是[]之間,僅此區別)來建立。

舉例:

it = ( len(x) for x in open('/tmp/my_file.txt'))
print(it)
>>>
<generator object <genexpr> at 0x101b81480>

print(next(it))
print(next(it))
>>>
100
57
複製程式碼

鏈式操作:

roots = ((x,x**0.5) for x in it)
print(next(roots))
>>>
(15,3.872983346207417)
複製程式碼

第10條:enumerate 比range更好用

備忘錄:

  • enumerate提供了簡潔的語法,再迴圈迭代一個迭代器的同時既能獲取下標,也能獲取當前值。
  • 可以新增第二個引數來指定 索引開始的序號,預設為0

Prefer

for i, flavor in enumerate(flavor_list):
    print(‘%d: %s’ % (i + 1, flavor))
複製程式碼

not

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
        print(‘%d: %s’ % (i + 1, flavor))
        
# 也可以通過指定 索引開始的下標序號來簡化程式碼
for i, flavor in enumerate(flavor_list,1):
    print("%d: %s"%(i,flavor))
複製程式碼

第11條:用 zip 函式來同時遍歷兩個迭代器

備忘錄

  • 內建的zip函式可以並行的對多個迭代器進行處理。
  • Python3中,zip 採用懶模式生成器獲得的是元組;而在Python2中,zip返回的是一個包含了其處理好的所有元祖的一個集合。
  • 如果所處理的迭代器的長度不一致時,zip會預設截斷輸出,使得長度為最先到達尾部的那個長度。
  • 內建模組itertools中的zip_longest函式可以並行地處理多個迭代器,而可以無視長度不一致的問題。

Prefer:

# 求最長字串
names = [‘Cecilia’, ‘Lise’, ‘Marie’]
max_letters = 0
letters = [len(n) for n in names]
for name, count in zip(names, letters):
    if count > max_letters:
        longest_name = name
        max_letters = count
        
print(longest_name)
>>>
Cecilia
複製程式碼

not:

for i, name in enumerate(names):
	count = letters[i]
    if count > max_letters:
        longest_name = name
        max_letters = count
複製程式碼

第12條: 在for 和while 迴圈體後避免使用else語句塊

備忘錄

  • Python有用特殊的語法能夠讓else語塊在迴圈體結束的時候立刻得到執行。
  • 迴圈體後的else語塊只有在迴圈體沒有觸發break語句的時候才會執行。
  • 避免在迴圈體的後面使用else語塊,因為這樣的表達不直觀,而且容易誤導讀者。
for i in range(3):
    print('Loop %d' % i)
else:
    print('Else block')
>>>
Loop 0
Loop 1
Loop 2
Else block
複製程式碼

第13條: 合理利用 try/except/else/finally

備忘錄

  • try/finally組合語句可以使得你的程式碼變得很整潔而無視try塊中是否發生異常。
  • else塊可以最大限度的減少try塊中的程式碼的長度,並且可以視覺化地辨別try/except成功執行的部分。
  • else塊經常會被用於在try塊成功執行後新增額外的行為,但是要確保程式碼會在finally塊之前得到執行。\
  1. finally 塊

    總是會執行,可以用來關閉檔案控制程式碼之類的

  2. else 塊

    try 塊沒有發生異常則執行 else 塊,有了 else 塊,我們可以儘量減少 try 塊的程式碼量

示例:

UNDEFINED = object()
def divide_json(path):
    handle = open(path, 'r+') # May raise IOError
    try:
        data = handle.read() # May raise UnicodeDecodeError
        op = json.loads(data) # May raise ValueError
        value = (op['numerator'] / op['denominator']) # May raise ZeroDivisionError
    except ZeroDivisionError as e:
        return UNDEFINED
    else:
        op[‘result’] = value
        result = json.dumps(op)
        handle.seek(0)
        handle.write(result) # May raise IOError
        return value
    finally:
        handle.close() # Always runs
複製程式碼

二、函式

第14條: 返回 exceptions 而不是 None

備忘錄

  • 返回None的函式來作為特殊的含義是容易出錯的,因為None和其他的變數(例如 zero,空字串)在條件表示式的判斷情景下是等價的。
  • 通過觸發一個異常而不是直接的返回None是比較常用的一個方法。這樣呼叫方就能夠合理地按照函式中的說明文件來處理由此而引發的異常了。

示例:

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
複製程式碼

返回 None 容易造成誤用,下面的程式分不出 0 和 None

x, y = 0, 5
result = divide(x, y)
if not result:
    print('Invalid inputs')  # This is wrong!
else:
    assert False
複製程式碼

raise exception:

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') from e
複製程式碼

呼叫者看到該函式的文件中描述的異常之後,應該就會編寫相應的程式碼來處理它們了。

x, y = 5, 2
try:
    result = divide(x, y)
except ValueError:
    print("Invalid inputs")
else:
    print("Result is %.1f"% result)
 >>>
 Result is 2.5
複製程式碼

第15條: 瞭解閉包中是怎樣使用外圍作用域變數

備忘錄

  • 閉包函式可以從變數被定義的作用域內引用變數。
  • 預設地,閉包不能通過賦值來影響其檢索域。
  • Python3中,可以使用nonlocal關鍵字來突破閉包的限制,進而在其檢索域內改變其值。(global 關鍵字用於使用全域性變數,nonlocal 關鍵字用於使用區域性變數(函式內))
  • Python2中沒有nonlocal關鍵字,替代方案就是使用一個單元素(如列表,字典,集合等等)來實現與nonlocal一致的功能。
  • 除了簡單的函式,在其他任何地方都應該盡力的避免使用nonlocal關鍵字。

Python編譯器變數查詢域的順序:

  • 當前函式的作用域
  • 任何其他的封閉域(比如其他的包含著的函式)。
  • 包含該段程式碼的模組域(也稱之為全域性域)
  • 內建域(包含了像len,str等函式的域)

考慮如下示例:

# 優先排序
def sort_priority2(values, group):
    found = False    # 作用域:sort_priority2
    def helper(x):
        if x in group:
            found = True      # 作用域: helper
            return (0, x)
        return (1, x)   # found在helper的作用域就會由helper轉至sort_priority2函式
    
    values.sort(key=helper)
    return found

values = [1,5,3,9,7,4,2,8,6]
group = [7,9]
# begin to call
found = sort_priority2(values, group)
print("Found:",found)
print(values)
>>>
Found: False
[7, 9, 1, 2, 3, 4, 5, 6, 8]
複製程式碼

排序的結果是正確的,但是很明顯分組的那個標誌是不正確的了。group中的元素無疑可以在values裡面找到,但是函式卻返回了False,為什麼會發生這樣的狀況呢?(提示:Python 編譯器變數查詢域的順序)

把資料放到外邊

Python3中,對於閉包而言有一個把資料放到外邊的特殊的語法。nonlocal語句習慣於用來表示一個特定變數名稱的域的遍歷發生在賦值之前。 唯一的限制就是nonlocal不會向上遍歷到模組域級別(這也是為了防止汙染全域性變數空間)。這裡,我定義了一個使用了nonlocal關鍵字的函式。

def srt_priority3(numbers, group):
    found = False
    def helper(x):
        nonlocal found 
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found
複製程式碼

當資料在閉包外將被賦值到另一個域時,nonlocal 語句使得這個過程變得很清晰。它也是對global語句的一個補充,可以明確的表明變數的賦值應該被直接放置到模組域中。

然而,像這樣的反模式,對使用在那些簡單函式之外的其他的任何地方。nonlocal引起的副作用是難以追蹤的,而在那些包含著nonlocal語句和賦值語句交叉聯絡的大段程式碼的函式的內部則尤為明顯。

當你感覺自己的nonlocal語句開始變的複雜的時候,我非常建議你重構一下程式碼,寫成一個工具類。這裡,我定義了一個實現了與上面的那個函式功能相一致的工具類。雖然有點長,但是程式碼卻變得更加的清晰了(詳見第23項:對於簡單介面使用函式而不是類裡面的__call__方法)。

class Sorter(object):
    def __init__(self, group):
        self.group = group
        self.found = False

    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)

sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter is True

複製程式碼

Python2中的作用域

不幸的是,Python2是不支援nonlocal關鍵字的。為了實現相似的功能,你需要廣泛的藉助於Python的作用與域規則。雖然這個方法並不是完美的,但是這是Python中比較常用的一種做法。

# Python2
def sort_priority(numbers, group):
    found = [False]
    def helper(x):
        if x in group:
            found[0] = True
            return (0, x)
        return (1, x)
    numbers.sort(sort=helper)
    return found[0]

複製程式碼

就像上面解釋的那樣,Python 將會橫向查詢該變數所在的域來分析其當前值。技巧就是發現的值是一個易變的列表。這意味著一旦檢索,閉包就可以修改found的狀態值,並且把內部資料的改變傳送到外部,這也就打破了閉包引發的區域性變數作用域無法被改變的難題。其根本還是在於列表本身元素值可以被改變,這才是此函式可以正常工作的關鍵。

found為一個dictionary型別的時候,也是可以正常工作的,原理與上文所言一致。此外,found還可以是一個集合,一個你自定義的類等等。

第16條: 考慮使用生成器而不是返回列表

備忘錄

  • 相較於返回一個列表的情況,替代方案中使用生成器可以使得程式碼變得更加的清晰。
  • 生成器返回的迭代器,是在其生成器內部一個把值傳遞給了yield變數的集合。
  • 生成器可以處理很大的輸出序列就是因為它在處理的時候不會完全的包含所有的資料。

考慮以下兩種版本程式碼,一個用 **list **,另一個用 generator

def index_words(text):
    result = []
    if text:
        result.append(0)
    for index, letter in enumerate(text):
        if letter == ' ':
            result.append(index + 1)
    return result

address = 'Four score and seven years ago...'
result = index_words(address)
print(result[:3]) # [0, 5, 11]
複製程式碼

generator

def index_words_iter(text):
    if text:
        yield 0
    for index, letter in enumerate(text):
        if letter == ' ':
            yield index + 1

result = list(index_words_iter(address))
複製程式碼

使用 **generator ** 比較簡單,減少了 list 操作

另一個 **generator **的好處是更有效率地使用記憶值,generator不需要有存全部的資料

import itertools

def index_file(handle):
    offset = 0
    for line in handle:
        if line:
            yield offset
        for letter in line:
            offset += 1
            if letter == ' ':
                yield offset

with open('/tmp/address.txt', 'r') as f:
    it = index_file(f)
    results = itertools.islice(it, 0, 3)
    print(list(results))
    
>>>
[0, 5, 11]
複製程式碼

不管address.txt 多大都能處理

第17條: 遍歷引數的時候小心一點

備忘錄

  • 多次遍歷輸入引數的時候應該多加小心。如果引數是迭代器的話你可能看到奇怪的現象或者缺少值現象的發生。
  • Pythoniterator協議定義了容器和迭代器在iternext下對於迴圈和相關表示式的關係。
  • 只要實現了__iter__方法,你就可以很容易的定義一個可迭代的容器類。
  • 通過連續呼叫兩次iter方法,你就可以預先檢測一個值是不是迭代器而不是容器。兩次結果一致那就是迭代器,否則就是容器了。

generator不能重用:

def read_visits(data_path):
    with open(data_path,'r') as f:
        for line in f:
            yield int(line)

it = read_visits('tmp/my_numbers.txt')
print(list(it))
print(list(it)) # 這裡其實已經執行到頭了
>>>
[15, 35, 80]
[]
複製程式碼

造成上述結果的原因是 一個迭代器每次只處理它本身的資料。如果你遍歷一個迭代器或者生成器本身已經引發了一個StopIteration的異常,你就不可能獲得任何資料了。

解決方案:

每次呼叫都建立iterator避免上面list分配記憶體

def normalize_func(get_iter):  # get_iter 是函式
    total = sum(get_iter())    # New iterator
    result = []
    for value in get_iter():   # New iterator
       percent = 100 * value / total
       result.append(percent)
        
    return result

percentages = normalize_func(lambda: read_visits(path))
複製程式碼

for迴圈會呼叫內建iter函式,進而呼叫物件的__iter__方法,__iter__會返回iterator物件(實現__next__方法)

用iter函式檢測iterator:

def normalize_defensive(numbers):
    if iter(numbers) is iter(numbers): # 是個迭代器,這樣不好
        raise TypeError('Must supply a container')
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result

visits = [15, 35, 80]
normalize_defensive(visits)
visits = ReadVIsitors(path)
normalize_defensive(visits)

# 但是如果輸入值不是一個容器類的話,就會引發異常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
複製程式碼

第18條: 減少位置引數上的干擾

備忘錄

  • 通過使用*args定義語句,函式可以接收可變數量的位置引數。
  • 你可以通過*操作符來將序列中的元素作為位置變數。
  • 帶有*操作符的生成器變數可能會引起程式的記憶體溢位,或者機器當機。
  • 為可以接受*args的函式新增新的位置引數可以產生難於發現的問題,應該謹慎使用。

舉例:

def log(message, values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', [1, 2])
log('Hi there', [])
複製程式碼
def log(message, *values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', 1, 2)
log('Hi there')
複製程式碼

第二個就比第一個要更有彈性

不過傳入生成器的時候,因為變長引數在傳給函式的時候,總要先轉換為元組,所以如果生成器迭代的資料很大的話,可能會導致程式崩潰

第19條: 使用關鍵字引數來提供可選行為

備忘錄

  • 函式的引數值即可以通過位置被指定,也可以通過關鍵字來指定。
  • 相較於使用位置引數賦值,使用關鍵字來賦值會讓你的賦值語句邏輯變得更加的清晰。
  • 帶有預設引數的關鍵字引數函式可以很容易的新增新的行為,尤其適合向後相容。
  • 可選的關鍵字引數應該優於位置引數被考慮使用。

關鍵字引數的好處:

  1. 程式碼可讀性的提高
  2. 以在定義的時候初始化一個預設值
  3. 在前面的呼叫方式不變的情況下可以很好的擴充函式的引數,不用修改太多的程式碼

如果本來的函式如下

def flow_rate(weight_diff, time_diff, period=1):
    return (weight_diff / time_diff) * period
複製程式碼

如果後來函式修改了

def flow_rate(weight_diff, time_diff,
              period=1, units_per_kg=1):
    return ((weight_diff / units_per_kg) / time_diff) * period
複製程式碼

那麼可以如下使用

flow_per_second = flow_rate(weight_diff, time_diff)
flow_per_hour = flow_rate(weight_diff, time_diff, period=3600)
pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)
pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2) # 不推薦
複製程式碼

第20條: 使用None和文件說明動態的指定預設引數

備忘錄

  • 預設引數只會被賦值一次:在其所在模組被載入的過程中,這有可能導致一些奇怪的現象。
  • 使用None作為關鍵字引數的預設值會有一個動態值。要在該函式的說明文件中詳細的記錄一下。

第一個例子:

not:

def log(message, when=datetime.now()):
    print(‘%s: %s’ % (when, message))
    
log(‘Hi there!’)
sleep(0.1)
log(‘Hi again!’)
>>>
2014-11-15 21:10:10.371432: Hi there!
2014-11-15 21:10:10.371432: Hi again!
複製程式碼

prefer:

def log(message, when=None):
    """Log a message with a timestamp.

    Args:
        message: Message to print
        when: datetime of when the message occurred.
            Default to the present time.
    """
    when = datetime.now() if when is None else when
    print("%s: %s" %(when, message))

# 測試

log('Hi there!')
sleep(0.1)
log('Hi again!')
>>>
2014-11-15 21:10:10.472303: Hi there!
2014-11-15 21:10:10.473395: Hi again!
複製程式碼

上述方法造成 when 第一次被賦值之後便不會再重新賦值

第二個例子:

not:

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)
>>>
Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}
複製程式碼

prefer:

def decode(data, default=None):
    """Load JSON data from string.

    Args:
        data: JSON data to be decoded.
        default: Value to return if decoding fails.
            Defaults to an empty dictionary.
    """

    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

# 現在測試一下
foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)
>>>
Foo: {'stuff': 5}
Bar: {'meep': 1}
複製程式碼

第21條: 僅強調關鍵字引數

備忘錄

  • 關鍵字引數使得函式呼叫的意圖更加的清晰,明顯。
  • 使用keyword-only引數可以強迫函式呼叫者提供關鍵字來賦值,這樣對於容易使人疑惑的函式引數很有效,尤其適用於接收多個布林變數的情況。
  • Python3中有明確的keyword-only函式語法。
  • Python2中可以通過**kwargs模擬實現keyword-only函式語法,並且人工的觸發TypeError異常。
  • keyword-only在函式引數列表中的位置很重要,這點大家尤其應該明白!

下面的程式使用上不方便,因為容易忘記 ignore_overflow 和 ignore_zero_division 的順序

def safe_division(number, divisor, ignore_overflow,
                  ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

result = safe_division(1, 10**500, True, False)
result = safe_division(1, 0, False, True)
複製程式碼

用 keyword 引數可解決此問題,在 Python 3 可以宣告強制接收 keyword-only 引數。

下面定義的這個 safe_division_c 函式,帶有兩個只能以關鍵字形式來指定的引數。引數列表裡面的 * 號,標誌著位置引數就此終結,之後的那些引數,都只能以關鍵字的形式來指定

def safe_division_c(number, divisor, *,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

safe_division_c(1, 10**500, True, False)
>>> 
TypeError: safe_division_c() takes 2 positional arguments but 4 were given

safe_division(1, 0, ignore_zero_division=True)  # OK
...
複製程式碼

Python 2 雖然沒有這種語法,但可以用 ** 操作符模擬

注:* 操作符接收可變數量的位置引數,** 接受任意數量的關鍵字引數

# Python 2
def safe_division(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_division = kwargs.pop('ignore_zero_division', False)
    if kwargs:
        raise TypeError("Unexpected **kwargs: %r"%kwargs)
    # ···

# 測試
safe_division(1, 10)
safe_division(1, 0, ignore_zero_division=True)
safe_division(1, 10**500, ignore_overflow=True)
# 而想通過位置引數賦值,就不會正常的執行了
safe_division(1, 0, False, True)
>>>
TypeError:safe_division() takes 2 positional arguments but 4 were given.
複製程式碼

三、類和繼承

第22條: 儘量使用輔助類來維護程式的狀態,避免dict巢狀dict或大tuple

備忘錄

  • 避免字典中巢狀字典,或者長度較大的元組。
  • 在一個整類(類似於前面第一個複雜類那樣)之前考慮使用 namedtuple 製作輕量,不易發生變化的容器。
  • 當內部的字典關係變得複雜的時候將程式碼重構到多個工具類中。

dictionaries 以及 tuples 拿來存簡單的資料很方便,但是當資料越來越複雜時,例如多層 dictionaries 或是 n-tuples,程式的可讀性就下降了。例如下面的程式:

class SimpleGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = []

    def report_grade(self, name, score):
        self._grades[name].append(score)

    def average_grade(self, name):
        grades = self._grades[name]
        return sum(grades) / len(grades)

複製程式碼

正是由於字典很容易被使用,以至於對字典過度的擴充會導致程式碼越來越脆弱。例如:你想擴充一下SimpleGradebook類來根據科目儲存成績的學生的集合,而不再是整體性的儲存。你就可以通過修改_grade字典來匹配學生姓名,使用另一個字典來包含成績。而最裡面的這個字典將匹配科目(keys)和成績(values)。你還想根據班級內總體的成績來追蹤每個門類分數所佔的比重,所以期中,期末考試相比於平時的測驗而言更為重要。實現這個功能的一個方式是改變最內部的那個字典,而不是讓其關聯著科目(key)和成績(values)。我們可以使用元組(tuple)來作為成績(values)。

class WeightedGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = {}

    def report_grade(self, name, subject, score, weight):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        grade_list.append((score, weight))

    def average_grade(self, name):
        by_subject = self._grades[name]
        score_sum, score_count = 0, 0
        for subject, scores in by_subject.items():
            subject_avg, total_weight = 0, 0
            for score, weight in scores:
                subject_avg += score * weight
                total_weight += weight
            score_sum += subject_avg / total_weight
            score_count += 1
        return score_sum / score_count
複製程式碼

這個類使用起來貌似也變的超級複雜了,並且每個位置引數代表了什麼意思也不明不白的。

重構成多個類

你可以從依賴樹的底端開始,將其劃分成多個類:一個單獨的成績類好像對於如此一個簡單的資訊權重太大了。一個元組,使用元組似乎很合適,因為成績是不會改變的了,這剛好符合元組的特性。這裡,我使用一個元組(score, weight)來追蹤列表中的成績資訊。

import collections

Grade = collections.namedtuple('Grade', ('score', 'weight'))


class Subject(object):
    def __init__(self):
        self._grades = []

    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))

    def average_grade(self):
        total, total_weight = 0, 0
        for grade in self._grades:
            total += grade.score * grade.weight
            total_weight += grade.weight
        return total / total_weight


class Student(object):
    def __init__(self):
        self._subjects = {}

    def subject(self, name):
        if name not in self._subjects:
            self._subjects[name] = Subject()
        return self._subjects[name]

    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count


class Gradebook(object):
    def __init__(self):
        self._students = {}

    def student(self, name):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]
複製程式碼

第23條: 對於簡單介面使用函式而不是類的例項

備忘錄

  • Python中,不需要定義或實現什麼類,對於簡單介面元件而言,函式就足夠了。
  • Python中引用函式和方法的原因就在於它們是first-class,可以直接的被運用在表示式中。
  • 特殊方法__call__允許你像呼叫函式一樣呼叫一個物件例項。
  • 當你需要一個函式來維護狀態資訊的時候,考慮一個定義了__call__方法的狀態閉包類哦(詳見第15項:瞭解閉包是怎樣與變數作用域的聯絡)。

Python中的許多內建的API都允許你通過向函式傳遞引數來自定義行為。這些被API使用的hooks將會在它們執行的時候回撥給你的程式碼。例如:list型別的排序方法中有一個可選的key 引數來決定排序過程中每個下標的值。這裡,我使用一個lambda表示式作為這個鍵鉤子,根據名字中字元的長度來為這個集合排序。

names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
print(names)
>>>
['Plato', Socrates', 'Aristotle', 'Archimedes']

複製程式碼

在其他的程式語言中,你可能期望一個抽象類作為這個hooks。但是在Python中,許多的hooks都是些無狀態的有良好定義引數和返回值的函式。而對於hooks而言,使用函式是很理想的。因為更容易藐視,相對於類而言定義起來也更加的簡單。函式可以作為鉤子來工作是因為Pythonfirst-class函式:在程式設計的時候函式,方法可以像其他的變數值一樣被引用,或者被傳遞給其他的函式。

Python允許類來定義__call__這個特殊的方法。它允許一個物件像被函式一樣來被呼叫。這樣的一個例項也引起了callable這個內True的事實。

current = {'green': 12, 'blue': 3}
incremetns = [
    ('red', 5),
    ('blue', 17),
    ('orange', 9)
]

class BetterCountMissing(object):

    def __init__(self):
        self.added = 0

    def __call__(self):
        self.added += 1
        return 0

counter = BetterCountMissing()
counter()
assert callable(counter)
# 這裡我使用一個BetterCountMissing例項作為defaultdict函式的預設的hook值來追蹤預設值被新增的次數。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
    result[key] += amount
assert counter.added == 2
複製程式碼

第24條: 使用@classmethod多型性構造物件

備忘錄

  • Python的每個類只支援單個的構造方法,__init__
  • 使用@classmethod可以為你的類定義可替代構造方法的方法。
  • 類的多型為具體子類的組合提供了一種更加通用的方式。

使用 @classmethod起到多型的效果:一個對於分層良好的類樹中,不同類之間相同名稱的方法卻實現了不同的功能的體現。

下面的函式 generate_inputs() 不夠一般化,只能使用 PathInputData ,如果想使用其它 InputData 的子類,必須改變函式。

class InputData(object):
    def read(self):
        raise NotImplementedError

class PathInputData(InputData):
    def __init__(self, path):
        super().__init__()
        self.path = path

    def read(self):
        return open(self.path).read()

def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir, name))
複製程式碼

問題在於建立 InputData 子類的物件不夠一般化,如果你想要編寫另一個 InputData 的子類就必須重寫 read 方法幸好有 @classmethod,可以達到一樣的效果。

class GenericInputData(object):
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls, config):
        raise NotImplementedError

class PathInputData(GenericInputData):
    def __init__(self, path):
        super().__init__()
        self.path = path

    def read(self):
        return open(self.path).read()

    @classmethod
    def generate_inputs(cls, config):
        data_dir = config['data_dir']
        for name in os.listdir(data_dir):
            yield cls(os.path.join(data_dir, name))
複製程式碼

第25條: 使用super關鍵字初始化父類

備忘錄

  • Python的解決例項化次序問題的方法MRO解決了菱形繼承中超類多次被初始化的問題。
  • 總是應該使用super來初始化父類。

先看一個還行的例子:

class MyBaseClass(object):
    def __init__(self, value):
        self.value = value
        
class TimesTwo(object):
    def __init__(self):
        self.value *= 2


class PlusFive(object):
    def __init__(self):
        self.value += 5


# 多繼承例項,注意繼承的次序哦
class OneWay(MyBaseClass, TimesTwo, PlusFive):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)

foo = OneWay(5)
print("First ordering is ( 5 * 2 ) + 5 = ", foo.value)
>>>
First ordering is (5 * 2 ) + 2 = 15
複製程式碼

不使用 **super() **在多重繼承時可能會造成意想不到的問題,下面的程式造成所謂的 **diamond inheritance **。

class MyBaseClass(object):
    def __init__(self, value):
        self.value = value

class TimesFive(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value *= 5

class PlusTwo(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value += 2

class ThisWay(TimesFive, PlusTwo):
    def __init__(self, value):
        TimesFive.__init__(self, value)
        PlusTwo.__init__(self, value)

# 測試
foo = ThisWay(5)
print('Should be (5 * 5) + 2 = 27 but is', foo.value)
>>>
Should be (5 * 5) + 2 = 27 but is 7
複製程式碼

注:foo.value 的值是 7 ,而不是 27。因為 PlusTwo.__init__(self, value) 將值重設為 5 了。

使用 super()可以正確得到 27

# 現在,菱形繼承的超類,也就是最頂上的那個`MyBaseClass`只會被初始化一次,而其他的兩個父類會按照被宣告的順序來初始化了。
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):# Python 2
class MyBaseClass(object):
    def __init__(self, value):
        self.value = value

class TimesFiveCorrect(MyBaseClass):
    def __init__(self, value):
        super(TimesFiveCorrect, self).__init__(value)
        self.value *= 5

class PlusTwoCorrect(MyBaseClass):
    def __init__(self, value):
        super(PlusTwoCorrect, self).__init__(value)
        self.value += 2

class GoodWay(PlusTwoCorrect, TimesFiveCorrect):
    def __init__(self, value):
        super(GoodWay, self).__init__(value)

foo = GoodWay(5)
print("Should be 5 * (5 + 2) = 35 and is " , foo.value)
>>>
Should be 5 * (5 + 2) = 35 and is 35
複製程式碼

python中父類例項化的規則是按照MRO標準來進行的,MRO 的執行順序是 DFS

# Python 2
from pprint import pprint
pprint(GoodWay.mro())
>>>
[<class '__main__.GoodWay'>,
<class '__main__.TimesFiveCorrect'>,
<class '__main__.PlusTwoCorrect'>,
<class '__main__.MyBaseClass'>,
<class 'object'>]
複製程式碼

最開始初始化GoodWay的時候,程式並沒有真正的執行,而是走到這條繼承樹的樹根,從樹根往下才會進行初始化。於是我們會先初始化MyBaseClassvalue5,然後是PlusTwoCorrectvalue會變成7,接著TimesFiveCorrectvalue就自然的變成35了。

Python 3 簡化了 **super() **的使用方式

class Implicit(MyBaseClass):
    def __init__(self, value):
        super().__init__(value * 2)
複製程式碼

第26條: 只在用編寫Max-in元件的工具類的時候使用多繼承

備忘錄

  • 如果可以使用mix-in實現相同的結果輸出的話,就不要使用多繼承了。
  • mix-in類需要的時候,在例項級別上使用可插拔的行為可以為每一個自定義的類工作的更好。
  • 從簡單的行為出發,建立功能更為靈活的mix-in

如果你發現自己渴望隨繼承的便利和封裝,那麼考慮mix-in吧。它是一個只定義了幾個類必備功能方法的很小的類。Mix-in類不定義以自己的例項屬性,也不需要它們的初始化方法__init__被呼叫。Mix-in可以被分層和組織成最小化的程式碼塊,方便程式碼的重用。

mix-in 是可以替換的 class ,通常只定義 methods ,雖然本質上上還是通過繼承的方式,但因為 mix-in 沒有自己的 state ,也就是說沒有定義 attributes ,使用上更有彈性。

範例1:

注:hasattr 函式動態訪問屬性,isinstance 函式動態檢測物件型別

import json

class ToDictMixin(object):
    def to_dict(self):
        return self._traverse_dict(self.__dict__)

    def _traverse_dict(self, instance_dict):
        output = {}
        for key, value in instance_dict.items():
            output[key] = self._traverse(key, value)
        return output

    def _traverse(self, key, value):
        if isinstance(value, ToDictMixin):
            return value.to_dict()
        elif isinstance(value, dict):
            return self._traverse_dict(value)
        elif isinstance(value, list):
            return [self._traverse(key, i) for i in value]
        elif hasattr(value, '__dict__'):
            return self._traverse_dict(value.__dict__)
        else:
            return value
複製程式碼

使用示例:

class BinaryTree(ToDIctMixin):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


# 這下把大量的Python物件轉換到一個字典中變得容易多了。
tree = BinaryTree(10, left=BinaryTree(7, right=BinaryTree(9)),
    right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
>>>
{'left': {'left': None,
         'right': {'left': None, 'right': None, 'value': 9},
         'value': 7},
 'right': {'left': {'left': None, 'right': None, 'value': 11},
         'right': None,
         'value': 13},
  'value': 10
}
複製程式碼

範例2:

# 在這個例子中,唯一的必須條件就是類中必須有一個to_dict方法和接收關鍵字引數的__init__構造方法
class JsonMixin(object):
    @classmethod
    def from_json(cls, data):
        kwargs = json.loads(data)
        return cls(**kwargs)

    def to_json(self):
        return json.dumps(self.to_dict())
    
class DatacenterRack(ToDictMixin, JsonMixin):
    def __init__(self, switch=None, machines=None):
        self.switch = Switch(**switch)
        self.machines = [Machine(**kwargs) for kwargs in machines]

class Switch(ToDictMixin, JsonMixin):
    def __init__(self, ports=None, speed=None):
        self.ports = ports
        self.speed = speed

class Machine(ToDictMixin, JsonMixin):
    def __init__(self, cores=None, ram=None, disk=None):
        self.cores = cores
        self.ram = ram
        self.disk = disk

# 將這些類從JSON傳中序列化也是簡單的。這裡我校驗了一下,保證資料可以在序列化和反序列化正常的轉換。
serialized = """{
    "switch": {"ports": 5, "speed": 1e9},
    "machines": [
        {"cores": 8, "ram": 32e9, "disk": 5e12},
        {"cores": 4, "ram": 16e9, "disk": 1e12},
        {"cores": 2, "ram": 4e9, "disk": 500e9}
    ]
}"""

deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)
複製程式碼

第27條: 多使用公共屬性,而不是私有屬性

備忘錄

  • Python 編譯器無法嚴格保證 private 欄位的私密性
  • 不要盲目將屬性設定為 private,而是應該從一開始就做好規劃,並允子類更多地訪問超類的內部的API
  • 應該多用 protected 屬性,並且在文件中把這些欄位的合理用法告訴子類的開發者,而不要試圖用 private 屬性來限制子類的訪問
  • 只有當子類不受自己控制的收,才可以考慮使用 private 屬性來避免名稱衝突

Python 裡面沒有真正的 "private variable",想存取都可以存取得到。

下面的程式看起來我們沒辦法得到 __private_field

class MyObject(object):
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

    def get_private_field(self):
        return self.__private_field

foo = MyObject()
print(foo.__private_field) # AttributeError
複製程式碼

但其實只是名稱被改掉而已

print(foo.__dict__)
# {'_MyObject__private_field': 10, 'public_field': 5}

print(foo._MyObject__private_field)
複製程式碼

一般來說 Python 慣例是在變數前加一個底線代表 **protected variable **,作用在於提醒開發者使用上要注意。

class MyClass(object):
    def __init__(self, value):
        # This stores the user-supplied value for the object.
        # It should be coercible to a string. Once assigned for
        # the object it should be treated as immutable.
        self._value = value

    def get_value(self):
        return str(self._value)

class MyIntegerSubclass(MyClass):
    def get_value(self):
        return self._value

foo = MyIntegerSubclass(5)
assert foo.get_value() == 5

複製程式碼

雙底線的命名方式是為了避免父類和子類間的命名衝突,除此之外儘量避免使用這種命名。

第28條:自定義容器型別要從collections.abc來繼承

備忘錄

  • 如果要定製的子類比較簡單,那就可以直接從Python的容器型別(如list或dict)中繼承
  • 想正確實現自定義的容器型別,可能需要編寫大量的特殊方法
  • 編寫自制的容器型別時,可以從collection.abc 模組的抽象類基類中繼承,那些基類能確保我們的子類具備適當的介面及行為

collections.abc 裡面的 abstract classes 的作用是讓開發者方便地開發自己的 container ,例如 list。一般情況下繼承list 就ok了,但是當結構比較複雜的時候就需要自己自定義,例如 list 有許多 方法,要一一實現有點麻煩。

下面程式中 SequenceNode 是想要擁有 list 功能的 binary tree。

class BinaryNode(object):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

class IndexableNode(BinaryNode):
    def _search(self, count, index):
        found = None
        if self.left:
            found, count = self.left._search(count, index)
        if not found and count == index:
            found = self
        else:
            count += 1
        if not found and self.right:
            found, count = self.right._search(count, index)
        return found, count

    def __getitem__(self, index):
        found, _ = self._search(0, index)
        if not found:
            raise IndexError('Index out of range')
        return found.value

class SequenceNode(IndexableNode):
    def __len__(self):
        _, count = self._search(0, None)
        return count
複製程式碼

以下是 SequenceNode的一些 list 常用的操作

tree = SequenceNode(
	10,
    left=SequenceNode(
    	5,
        left=SequenceNode(2),
        right=SequenceNode(
        	6, 
        	right=SequenceNode(7))),
    right=SequenceNode(
		15, 
		left=SequenceNode(11)))

print('Index 0 =', tree[0]) 
print('11 in the tree?', 11 in tree)
print('Tree has %d nodes' % len(tree))
>>>
Index 0 = 2
11 in the tree? True
Tree has 7 nodes
複製程式碼

但是使用者可能想使用像 count()以及 index()等 list 的 方法 ,這時候可以使用 collections.abc的 **Sequence **。子類只要實現 __getitem__以及 __len__, **Sequence **以及提供count()以及 index()了,而且如果子類沒有實現類似 Sequence 的抽象基類所要求的每個方法,collections.abc 模組就會指出這個錯誤。

from collections.abc import Sequence

class BetterNode(SequenceNode, Sequence):
    pass

tree = BetterNode(
   # ...
)

print('Index of 7 is', tree.index(7))
print('Count of 10 is', tree.count(10))
>>>
Index of 7 is 3
Count of 10 is 1
複製程式碼

四、元類和屬性

第29條: 用純屬性取代 get 和 set 方法

備忘錄

  • 使用public屬性避免set和get方法,@property定義一些特別的行為
  • 如果訪問物件的某個屬性的時候,需要表現出特殊的行為,那就用@property來定義這種行為
  • @property 方法應該遵循最小驚訝原則,而不應該產生奇怪的副作用
  • 確保@property方法是快速的,如果是慢或者複雜的工作應該放在正常的方法裡面

示例1:

不要把 java 的那一套 getter 和 setter 帶進來

not:

class OldResistor(object):
    def __init__(self, ohms):
        self._ohms = ohms
    
    def get_ohms(self):
        return self._ohms
    
    def set_ohms(self, ohms):
        self._ohms = ohms
複製程式碼

prefer:

class Resistor(object):
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0
複製程式碼

示例2:

使用@property,來繫結一些特殊操作,但是不要產生奇怪的副作用,比如在getter裡面做一些賦值的操作

class VoltageResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
        self._voltage = 0
    
    # 相當於 getter
    @property
    def voltage(self):
        return self._voltage
	
    # 相當於 setter
    @voltage.setter
    def voltage(self, voltage):
        self._voltage = voltage
        self.current = self._voltage / self.ohms

r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
# 會執行 setter 方法
r2.voltage = 10
print('After:  %5r amps' % r2.current)
複製程式碼

第30條: 考慮@property來替代屬性重構

備忘錄

  • 使用@property給已有屬性擴充套件新需求
  • 可以用 @property 來逐步完善資料模型
  • 當@property太複雜了才考慮重構

@property可以把簡單的數值屬性遷移為實時計算,只定義 getter 不定義 setter 那麼就是一個只讀屬性

class Bucket(object):
    def __init__(self, period):
        self.period_delta = timedelta(seconds=period)
        self.reset_time = datetime.now()
        self.max_quota = 0
        self.quota_consumed = 0

    def __repr__(self):
        return ('Bucket(max_quota=%d, quota_consumed=%d)' %
                (self.max_quota, self.quota_consumed))


    @property
    def quota(self):
        return self.max_quota - self.quota_consumed
    
    @quota.setter
    def quota(self, amount):
        delta = self.max_quota - amount
        if amount == 0:
            # Quota being reset for a new period
            self.quota_consumed = 0
            self.max_quota = 0
        elif delta < 0:
            # Quota being filled for the new period
            assert self.quota_consumed = 0
            self.max_quota = amount
        else:
            # Quota being consumed during the period
            assert self.max_quota >= self,quota_consumed
            self.quota_consumed += delta
複製程式碼

這種寫法的好處就在於:從前使用的Bucket.quota 的那些舊程式碼,既不需要做出修改,也不需要擔心現在的Bucket類是如何實現的,可以輕鬆無痛擴充套件新功能。但是@property也不能濫用,而且@property的一個缺點就是無法被複用,同一套邏輯不能在不同的屬性之間重複使用如果不停的編寫@property方法,那就意味著當前這個類的程式碼寫的確實很糟糕,此時應該重構了。

TODO

第31條: 用描述符來改寫需要複用的 @property 方法

備忘錄

  • 如果想複用 @property 方法及其驗證機制,那麼可以自定義描述符類

  • WeakKeyDictionary 可以保證描述符類不會洩露記憶體

  • 通過描述符協議來實現屬性的獲取和設定操作時,不要糾結於__getatttttribute__ 的方法的具體運作細節

property最大的問題是可能造成 duplicated code 這種 code smell。

下面的程式 math_grade以及 math_grade就有這樣的問題。

class Exam(object):
    def __init__(self):
        self._writing_grade = 0
        self._math_grade = 0

    @staticmethod
    def _check_grade(value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')

    @property
    def writing_grade(self):
        return self._writing_grade

    @writing_grade.setter
    def writing_grade(self, value):
        self._check_grade(value)
        self._writing_grade = value

    @property
    def math_grade(self):
        return self._math_grade

    @math_grade.setter
    def math_grade(self, value):
        self._check_grade(value)
        self._math_grade = value
複製程式碼

可以使用 **descriptor **解決,下面的程式將重複的邏輯封裝在 Grade 裡面。但是這個程式根本 **不能用 **,因為存取到的是 class attributes,例如 exam.writing_grade = 40其實是Exam.__dict__['writing_grade'].__set__(exam, 40),這樣所有 Exam 的 instances 都是存取到一樣的東西 ( Grade())。

class Grade(object):
    def __init__(self):
        self._value = 0

    def __get__(self, instance, instance_type):
        return self._value

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._value = value

class Exam(object):
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

exam = Exam()
exam.writing_grade = 40
複製程式碼

解決方式是用個 dictionary 存起來,這裡使用 WeakKeyDictionary避免 memory leak。

from weakref import WeakKeyDictionary

class Grade(object):
    def __init__(self):
        self._values = WeakKeyDictionary()
    def __get__(self, instance, instance_type):
        if instance is None: return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value
複製程式碼

第32條: 用 __getattr__, __getattribute__, 和__setattr__ 實現按需生產的屬性

備忘錄

  • 通過__getttattr____setattr__,我們可以用惰性的方式來載入並儲存物件的屬性
  • 要理解 __getattr____getattribute__ 的區別:前者只會在待訪問的屬性缺失時觸發,而後者則會在每次訪問屬性的時候觸發
  • 如果要在__getattributte____setattr__ 方法中訪問例項屬性,那麼應該直接通過 super() 來做,以避免無限遞迴
  • obj.name,getattr和hasattr都會呼叫__getattribute__方法,如果name不在obj.__dict__裡面,還會呼叫__getattr__方法,如果沒有自定義__getattr__方法會AttributeError異常
  • 只要有賦值操作(=,setattr)都會呼叫__setattr__方法(包括a = A())

__getattr____getattribute__都可以動態地存取 attributes ,不同點在於如果 __dict__找不到才會呼叫 __getattr__,而 __getattribute__每次都會被呼叫到。

class LazyDB(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        value = 'Value for %s' % name
        setattr(self, name, value)
        return value

class LoggingLazyDB(LazyDB):
    def __getattr__(self, name):
        print('Called __getattr__(%s)' % name)
        return super().__getattr__(name)

data = LoggingLazyDB()
print('exists:', data.exists)
print('foo:   ', data.foo)
print('foo:   ', data.foo)
複製程式碼
class ValidatingDB(object):
    def __init__(self):
        self.exists = 5

    def __getattribute__(self, name):
        print('Called __getattribute__(%s)' % name)
        try:
            return super().__getattribute__(name)
        except AttributeError:
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value

data = ValidatingDB()
print('exists:', data.exists)
print('foo:   ', data.foo)
print('foo:   ', data.foo)
複製程式碼

可以控制什麼 attributes 不應該被使用到,記得要丟 **AttributeError **。

try:
    class MissingPropertyDB(object):
        def __getattr__(self, name):
            if name == 'bad_name':
                raise AttributeError('%s is missing' % name)
            value = 'Value for %s' % name
            setattr(self, name, value)
            return value

    data = MissingPropertyDB()
    data.foo  # Test this works
    data.bad_name
except:
    logging.exception('Expected')
else:
    assert False
複製程式碼

__setattr__每次都會被呼叫到。

class SavingDB(object):
    def __setattr__(self, name, value):
        # Save some data to the DB log
        pass
        super().__setattr__(name, value)

class LoggingSavingDB(SavingDB):
    def __setattr__(self, name, value):
        print('Called __setattr__(%s, %r)' % (name, value))
        super().__setattr__(name, value)
複製程式碼

很重要的一點是 __setattr__以及 __getattribute__一定要呼叫父類的 __getattribute__,避免無限迴圈下去。

這個會爆掉,因為存取 self._data又會呼叫 __getattribute__

class BrokenDictionaryDB(object):
    def __init__(self, data):
        self._data = {}

    def __getattribute__(self, name):
        print('Called __getattribute__(%s)' % name)
        return self._data[name]
複製程式碼

呼叫 super().__getattribute__('_data')

class DictionaryDB(object):
    def __init__(self, data):
        self._data = data

    def __getattribute__(self, name):
        data_dict = super().__getattribute__('_data')
        return data_dict[name]
複製程式碼

第33條: 用元類來驗證子類

備忘錄

  • 通過元類,我們可以在生成子類物件之前,先驗證子類的定義是否合乎規範
  • Python2 和 Python3 指定元類的語法略有不同
  • 使用元類對型別物件進行驗證
  • Python 系統把子類的整個 class 語句體處理完畢之後,就會呼叫其元類的__new__ 方法

第34條: 用元類來註冊子類

備忘錄

  • 在構建模組化的 Python 程式時候,類的註冊是一種很有用的模式
  • 開發者每次從基類中繼承子類的時,基類的元類都可以自動執行註冊程式碼
  • 通過元類來實現類的註冊,可以確保所有子類都不會洩露,從而避免後續的錯誤

首先,定義元類,我們要繼承 type, python 預設會把那些類的 class 語句體中所含的相關內容,傳送給元類的 new 方法。

class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print(meta, name, bases, class_dict)
        return type.__new__(meta, name, bases, class_dict)

# 這是 python2 寫法
class MyClassInPython2(object):
    __metaclass__ = Meta
    stuff = 123

    def foo(self):
        pass

# python 3
class MyClassInPython3(object, metaclass=Meta):
    stuff = 123

    def foo(self):
        pass


class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
        # Don't validate the abstract Polygon class
        if bases != (object,):
            if class_dict['sides'] < 3:
                raise ValueError('Polygons need 3+ sides')
        return type.__new__(meta, name, bases, class_dict)

class Polygon(object, metaclass=ValidatePolygon):
    sides = None  # Specified by subclasses

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

class Triangle(Polygon):
    sides = 3

print(Triangle.interior_angles())
複製程式碼

第35: 用元類來註解類的屬性

備忘錄

  • 藉助元類,我們可以在某個類完全定義好之前,率先修改該類的屬性
  • 描述符與元類能夠有效的組合起來,以便對某種行為做出修飾,或者在程式執行時探查相關資訊
  • 如果把元類與描述符相結合,那就可以在不使用 weakerf 模組的前提下避免記憶體洩露

五、並行與併發

第36條: 用 subprocess 模組來管理子程式

備忘錄

  • 使用 subprocess 模組執行子程式管理自己的輸入和輸出流
  • subprocess 可以並行執行最大化CPU的使用
  • communicate 的 timeout 引數避免死鎖和被掛起的子程式

最基本的

import subprocess

proc = subprocess.Popen(
    ['echo', 'Hello from the child!'],
    stdout=subprocess.PIPE)
out, err = proc.communicate()
print(out.decode('utf-8'))
複製程式碼

傳入資料

import os

def run_openssl(data):
    env = os.environ.copy()
    env['password'] = b'\xe24U\n\xd0Ql3S\x11'
    proc = subprocess.Popen(
        ['openssl', 'enc', '-des3', '-pass', 'env:password'],
        env=env,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE)
    proc.stdin.write(data)
    proc.stdin.flush()  # Ensure the child gets input
    return proc


def run_md5(input_stdin):
    proc = subprocess.Popen(
        ['md5'],
        stdin=input_stdin,
        stdout=subprocess.PIPE)
    return proc
複製程式碼

模擬 pipes

input_procs = []
hash_procs = []
for _ in range(3):
    data = os.urandom(10)
    proc = run_openssl(data)
    input_procs.append(proc)
    hash_proc = run_md5(proc.stdout)
    hash_procs.append(hash_proc)

for proc in input_procs:
    proc.communicate()
for proc in hash_procs:
    out, err = proc.communicate()
    print(out.strip())
複製程式碼

第37條: 可以用執行緒來執行阻塞時I/O,但不要用它做平行計算

備忘錄

  • 因為GIL,Python thread並不能並行執行多段程式碼
  • Python保留thread的兩個原因:1.可以模擬多執行緒,2.多執行緒可以處理I/O阻塞的情況
  • Python thread可以並行執行多個系統呼叫,這使得程式能夠在執行阻塞式I/O操作的同時,執行一些平行計算

第38條: 線上程中使用Lock來防止資料競爭

備忘錄

  • 雖然Python thread不能同時執行,但是Python直譯器還是會打斷運算元據的兩個位元組碼指令,所以還是需要鎖
  • thread模組的Lock類是Python的互斥鎖實現

比較有趣的是 **Barrier **這個 Python 3.2 才加進來的東西,以前要用 **Semaphore **來做。

from threading import Barrier
from threading import Thread
from threading import Lock

class LockingCounter(object):
    def __init__(self):
        self.lock = Lock()
        self.count = 0

    def increment(self, offset):
        with self.lock:
            self.count += offset

class LockingCounter(object):
    def __init__(self):
        self.lock = Lock()
        self.count = 0

    def increment(self, offset):
        with self.lock:
            self.count += offset

def worker(sensor_index, how_many, counter):
    # I have a barrier in here so the workers synchronize
    # when they start counting, otherwise it's hard to get a race
    # because the overhead of starting a thread is high.
    BARRIER.wait()
    for _ in range(how_many):
        # Read from the sensor
        counter.increment(1)

def run_threads(func, how_many, counter):
    threads = []
    for i in range(5):
        args = (i, how_many, counter)
        thread = Thread(target=func, args=args)
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()

BARRIER = Barrier(5)
counter = LockingCounter()
run_threads(worker, how_many, counter)
print('Counter should be %d, found %d' %
      (5 * how_many, counter.count))
複製程式碼

第39條: 用 Queue 來協調各執行緒之間的工作

備忘錄

  • 管線是一種優秀的任務處理方式,它可以把處理流程劃分為若干階段,並使用多條Python執行緒同時執行這些任務
  • 構建併發式的管線時,要注意許多問題,包括:如何防止某個階段陷入持續等待的狀態之中、如何停止工作執行緒,以及如何防止記憶體膨脹等
  • Queue類具備構建健壯併發管道的特性:阻塞操作,快取大小和連線(join)
from queue import Queue
from threading import Thread

class ClosableQueue(Queue):
    SENTINEL = object()

    def close(self):
        self.put(self.SENTINEL)

    def __iter__(self):
        while True:
            item = self.get()
            try:
                if item is self.SENTINEL:
                    return  # Cause the thread to exit
                yield item
            finally:
                self.task_done()


class StoppableWorker(Thread):
    def __init__(self, func, in_queue, out_queue):
        super().__init__()
        self.func = func
        self.in_queue = in_queue
        self.out_queue = out_queue

    def run(self):
        for item in self.in_queue:
            result = self.func(item)
            self.out_queue.put(result)
def download(item):
    return item

def resize(item):
    return item

def upload(item):
    return item

download_queue = ClosableQueue()
resize_queue = ClosableQueue()
upload_queue = ClosableQueue()
done_queue = ClosableQueue()
threads = [
    StoppableWorker(download, download_queue, resize_queue),
    StoppableWorker(resize, resize_queue, upload_queue),
    StoppableWorker(upload, upload_queue, done_queue),
]


for thread in threads:
    thread.start()
for _ in range(1000):
    download_queue.put(object())
download_queue.close()


download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print(done_queue.qsize(), 'items finished')
複製程式碼

第40條: 考慮用協程來併發地執行多個函式

備忘錄

  • 執行緒有三個大問題:

    • 需要特定工具去確定安全性
    • 單個執行緒需要8M的記憶體
    • 執行緒啟動消耗
  • coroutine只有1kb的記憶體消耗

  • generator可以通過send方法把值傳遞給yield

    def my_coroutine():
        while True:
            received = yield
            print("Received:", received)
    it = my_coroutine()
    next(it)
    it.send("First")
    ('Received:', 'First')
    
    複製程式碼
  • Python2不支援直接yield generator,可以使用for迴圈yield

第41條: 考慮用 concurrent.futures 來實現真正的平行計算

備忘錄

  • CPU瓶頸模組使用C擴充套件
  • concurrent.futures的multiprocessing可以並行處理一些任務,Python2沒有這個模組
  • multiprocessing 模組所提供的那些高階功能,都特別複雜,開發者儘量不要直接使用它們

使用 concurrent.futures 裡面的 **ProcessPoolExecutor **可以很簡單地平行處理 CPU-bound 的程式,省得用 multiprocessing 自定義。

from concurrent.futures import ProcessPoolExecutor

start = time()
pool = ProcessPoolExecutor(max_workers=2)  # The one change
results = list(pool.map(gcd, numbers))
end = time()
print('Took %.3f seconds' % (end - start))
複製程式碼

六、內建模組

第42條: 用 functools.wraps 定義函式修飾器

備忘錄

  • 裝飾器可以對函式進行封裝,但是會改變函式資訊

  • 使用 functools 的 warps 可以解決這個問題

    def trace(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # …
        return wrapper
    @trace
    def fibonacci(n):
        # …
    
    複製程式碼

第43條: 考慮用 contextlib 和with 語句來改寫可複用的 try/finally 程式碼

備忘錄

  • 使用with語句代替try/finally,增加程式碼可讀性
  • 使用 contextlib 提供的 contextmanager 裝飾函式就可以被 with 使用
  • with 和 yield 返回值使用

contextlib.contextmanager,方便我們在做 **context managers **。

from contextlib import contextmanager

@contextmanager
def log_level(level, name):
    logger = logging.getLogger(name)
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        yield logger
    finally:
        logger.setLevel(old_level)

with log_level(logging.DEBUG, 'my-log') as logger:
    logger.debug('This is my message!')
    logging.debug('This will not print')

logger = logging.getLogger('my-log')
logger.debug('Debug will not print')
logger.error('Error will print')
複製程式碼

第44條: 用 copyreg 實現可靠的 pickle 操作

備忘錄

  • pickle 模組只能序列化和反序列化確認沒有問題的物件
  • copyreg的 pickle 支援屬性丟失,版本和匯入類表資訊

使用 copyreg這個內建的 module ,搭配 pickle使用。

pickle使用上很簡單,假設我們有個 class:

class GameState(object):
    def __init__(self):
        self.level = 0
        self.lives = 4

state = GameState()
state.level += 1  # Player beat a level
state.lives -= 1  # Player had to try again
複製程式碼

可以用 pickle儲存 object

import pickle
state_path = '/tmp/game_state.bin'
with open(state_path, 'wb') as f:
    pickle.dump(state, f)

with open(state_path, 'rb') as f:
    state_after = pickle.load(f)
# {'lives': 3, 'level': 1}
print(state_after.__dict__)
複製程式碼

但是如果增加了新的 field, game_state.binload 回來的 object 當然不會有新的 field (points),可是它仍然是 GameState 的 instance,這會造成混亂。

class GameState(object):
    def __init__(self):
        self.level = 0
        self.lives = 4
        self.points = 0

with open(state_path, 'rb') as :
    state_after = pickle.load(f)
# {'lives': 3, 'level': 1}
print(state_after.__dict__)
assert isinstance(state_after, GameState)
複製程式碼

使用 copyreg可以解決這個問題,它可以註冊用來 serialize Python 物件的函式。

Default Attribute Values

pickle_game_state() 返回一個 tuple ,包含了拿來 unpickle 的函式以及傳入函式的引數。

import copyreg

class GameState(object):
    def __init__(self, level=0, lives=4, points=0):
        self.level = level
        self.lives = lives
        self.points = points

def pickle_game_state(game_state):
    kwargs = game_state.__dict__
    return unpickle_game_state, (kwargs,)

def unpickle_game_state(kwargs):
    return GameState(**kwargs)

copyreg.pickle(GameState, pickle_game_state)
複製程式碼
Versioning Classes

copyreg也可以拿來記錄版本,達到向後相容的目的。

如果原先的 class 如下

class GameState(object):
    def __init__(self, level=0, lives=4, points=0, magic=5):
        self.level = level
        self.lives = lives
        self.points = points
        self.magic = magic

state = GameState()
state.points += 1000
serialized = pickle.dumps(state)
複製程式碼

後來修改了,拿掉 lives ,這時原先使用預設引數的做法不能用了。

class GameState(object):
    def __init__(self, level=0, points=0, magic=5):
        self.level = level
        self.points = points
        self.magic = magic

# TypeError: __init__() got an unexpected keyword argument 'lives'
pickle.loads(serialized)
複製程式碼

在 serialize 時多加上版本號, deserialize 時加以判斷

def pickle_game_state(game_state):
    kwargs = game_state.__dict__
    kwargs['version'] = 2
    return unpickle_game_state, (kwargs,)

def unpickle_game_state(kwargs):
    version = kwargs.pop('version', 1)
    if version == 1:
        kwargs.pop('lives')
    return GameState(**kwargs)

copyreg.pickle(GameState, pickle_game_state)
複製程式碼
Stable Import Paths

重寫程式時,如果 class 改名了,想要 load 的 serialized 物件當然不能用,但還是可以使用 copyreg解決。

class BetterGameState(object):
    def __init__(self, level=0, points=0, magic=5):
        self.level = level
        self.points = points
        self.magic = magic

copyreg.pickle(BetterGameState, pickle_game_state)
複製程式碼

可以發現 unpickle_game_state()的 path 進入 dump 出來的資料中,當然這樣做的缺點就是 unpickle_game_state()所在的 module 不能改 path 了。

state = BetterGameState()
serialized = pickle.dumps(state)
print(serialized[:35])
>>>
b'\x80\x03c__main__\nunpickle_game_state\nq\x00}'
複製程式碼

第45條: 用 datetime 替代 time 來處理本地時間

備忘錄

  • 不要使用time模組在轉換不同時區的時間
  • 而用datetime配合 pytz 轉換
  • 總數保持UTC時間,最後面再輸出本地時間

第46條: 使用內建演算法與資料結構

備忘錄

  • 使用 Python 內建的模組來描述各種演算法和資料結構
  • 開發者不應該自己去重新實現他們,因為我們很難把它寫好

內建演算法和資料結構

  • collections.deque

  • collections.OrderedDict

  • collection.defaultdict

  • heapq模組操作list(優先佇列):heappush,heappop和nsmallest

    a = []
    heappush(a, 5)
    heappush(a, 3)
    heappush(a, 7)
    heappush(a, 4)
    print(heappop(a), heappop(a), heappop(a), heappop(a))
    # >>>
    # 3 4 5 7
    
    複製程式碼
  • bisect模組:bisect_left可以對有序列表進行高效二分查詢

  • itertools模組(Python2不一定支援):

    • 連線迭代器:chain,cycle,tee和zip_longest
    • 過濾:islice,takewhile,dropwhile,filterfalse
    • 組合不同迭代器:product,permutations和combination

第47 條: 在重視 精確度的場合,應該使用 decimal

備忘錄

  • 高精度要求的使用 Decimal 處理,如對舍入行為要求很嚴的場合,eg: 涉及貨幣計算的場合

第48條: 學會安裝由 Python 開發者社群所構建的模組

  • 在 https://pypi.python.org 查詢通用模組,並且用pip安裝

七、協作開發

第49條: 為每個函式、類和模組編寫文件字串

第50條: 用包來安排模組,並提供穩固的 API

第51條: 為自編的模組定義根異常,以便將呼叫者與 API 相隔離

第52條: 用適當的方式打破迴圈依賴問題

第53條: 用虛擬環境隔離專案,並重建其依賴關係

八、部署

第54條: 考慮用模組級別的程式碼來配置不同的部署環境

第55條: 通過 repr 字串來輸出除錯資訊

備忘錄

  • repr作用於內建型別會產生可列印的字串,eval可以獲得這個字串的原始值
  • __repr__自定義上面輸出的字串

第56條: 用 unittest 來測試全部程式碼

備忘錄

  • 使用unittest編寫測試用例,不光是單元測試,整合測試也很重要
  • 繼承TestCase,並且每個方法名都以test開始

第57條: 考慮用 pdb 實現互動除錯

備忘錄

  • 啟用pdb,然後在配合shell命令除錯 import pdb; pdb.set_trace();

第58條: 先分析效能再優化

  • cProfile 比 profile更精準
    • ncalls:呼叫次數
    • tottime:函式自身耗時,不包括呼叫函式的耗時
    • cumtime:包括呼叫的函式耗時

第59條: 用 tracemaloc 來掌握記憶體的使用及洩露情況

備忘錄

  • gc模組可以知道有哪些物件存在,但是不知道怎麼分配的
  • tracemalloc可以得到記憶體的使用情況,但是隻在Python3.4及其以上版本提供

參考書籍

程式碼

Effective Python(英文版) PDF 密碼: 7v9r

Effecttive Python(中文不完整非掃描版) PDF 密碼: 86bm

Effective Python(中文掃描版) PDF 密碼: dg7w

相關文章